| // Copyright 2025 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| //! Native WebGL2 rendering module for the sparse strips CPU/GPU rendering engine. |
| //! |
| //! This module provides identical functionality as the [`render_wgpu`] module, however the graphics |
| //! context is the browser's native [`WebGl2RenderingContext`]. Hence, this module is only available |
| //! when targeting `wasm32` with the "webgl" feature flag active. |
| //! |
| //! The main benefit of this module, is binary size. Omitting `wgpu` saves approximately 3mb of |
| //! binary size (when targeting WebGL2). |
| //! |
| //! Maintaining this backend should be continually re-evaluated, as once the majority of users can |
| //! leverage WebGPU, we can remove this backend without the binary size increasing. |
| //! - WebGPU usage: <https://caniuse.com/webgpu> |
| |
| #![expect( |
| clippy::cast_possible_truncation, |
| reason = "We temporarily ignore those because the casts\ |
| only break in edge cases, and some of them are also only related to conversions from f64 to f32." |
| )] |
| |
| use crate::render::common::IMAGE_PADDING; |
| use crate::{ |
| GpuStrip, RenderError, RenderSettings, RenderSize, Resources, |
| filter::{FilterContext, FilterInstanceData, FilterPassState, FilterPassTarget}, |
| gradient_cache::GradientRampCache, |
| render::{ |
| Config, |
| common::{ |
| GPU_ENCODED_IMAGE_SIZE_TEXELS, GPU_LINEAR_GRADIENT_SIZE_TEXELS, |
| GPU_RADIAL_GRADIENT_SIZE_TEXELS, GPU_SWEEP_GRADIENT_SIZE_TEXELS, GpuEncodedImage, |
| GpuEncodedPaint, GpuLinearGradient, GpuRadialGradient, GpuSweepGradient, |
| normalize_atlas_config, pack_image_offset, pack_image_params, pack_image_size, |
| pack_radial_kind_and_swapped, pack_texture_width_and_extend_mode, pack_tint, |
| }, |
| }, |
| scene::Scene, |
| schedule::{ |
| LoadOp, RendererBackend, RootRenderTarget, Scheduler, SchedulerState, StripPassRenderTarget, |
| }, |
| }; |
| use alloc::sync::Arc; |
| use alloc::vec; |
| use alloc::vec::Vec; |
| use bytemuck::{Pod, Zeroable}; |
| use core::fmt::Debug; |
| #[cfg(feature = "text")] |
| use glifo::{GLYPH_PADDING, PendingClearRect}; |
| use vello_common::image_cache::{ImageCache, ImageResource}; |
| #[cfg(feature = "probe")] |
| use vello_common::multi_atlas::AllocationStrategy; |
| use vello_common::multi_atlas::{AtlasConfig, AtlasId}; |
| #[cfg(feature = "probe")] |
| use vello_common::probe::Probe; |
| use vello_common::render_graph::LayerId; |
| use vello_common::{ |
| coarse::WideTile, |
| encode::{EncodedGradient, EncodedKind, EncodedPaint, MAX_GRADIENT_LUT_SIZE, RadialKind}, |
| paint::{ImageId, ImageSource}, |
| peniko::{self}, |
| pixmap::Pixmap, |
| tile::Tile, |
| }; |
| use vello_sparse_shaders::{ |
| clear_slots, filters, opaque_gradient, opaque_image, opaque_image_untinted, opaque_solid, |
| render_strips, |
| }; |
| use web_sys::wasm_bindgen::{JsCast, JsValue}; |
| use web_sys::{ |
| HtmlCanvasElement, WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram, |
| WebGlTexture, WebGlUniformLocation, WebGlVertexArrayObject, |
| }; |
| |
| /// Placeholder value for uninitialized GPU encoded paints. |
| const GPU_PAINT_PLACEHOLDER: GpuEncodedPaint = GpuEncodedPaint::LinearGradient(GpuLinearGradient { |
| texture_width_and_extend_mode: 0, |
| gradient_start: 0, |
| transform: [0.0; 6], |
| }); |
| |
| /// Query the WebGL context for the max texture size. |
| fn get_max_texture_dimension_2d(gl: &WebGl2RenderingContext) -> u32 { |
| gl.get_parameter(WebGl2RenderingContext::MAX_TEXTURE_SIZE) |
| .unwrap() |
| .as_f64() |
| .unwrap() as u32 |
| } |
| |
| fn get_max_texture_array_layers(gl: &WebGl2RenderingContext) -> u32 { |
| gl.get_parameter(WebGl2RenderingContext::MAX_ARRAY_TEXTURE_LAYERS) |
| .unwrap() |
| .as_f64() |
| .unwrap() as u32 |
| } |
| |
| /// Vello Hybrid's WebGL2 Renderer. |
| #[derive(Debug)] |
| pub struct WebGlRenderer { |
| /// Programs for rendering. |
| programs: WebGlPrograms, |
| /// Scheduler for scheduling draws. |
| scheduler: Scheduler, |
| /// The state used by the scheduler. |
| scheduler_state: SchedulerState, |
| /// WebGL context. |
| gl: WebGl2RenderingContext, |
| /// Encoded paints for storing encoded paints. |
| encoded_paints: Vec<GpuEncodedPaint>, |
| /// Stores the index (offset) of the encoded paints in the encoded paints texture. |
| paint_idxs: Vec<u32>, |
| /// Gradient cache for storing gradient ramps. |
| gradient_cache: GradientRampCache, |
| /// Context for GPU filter effects. |
| filter_context: FilterContext, |
| /// State used for constructing filter passes. |
| filter_pass_state: FilterPassState, |
| dummy_image_cache: Option<ImageCache>, |
| } |
| |
| impl WebGlRenderer { |
| /// Creates a new WebGL2 renderer |
| pub fn new(canvas: &HtmlCanvasElement) -> Self { |
| Self::new_with(canvas, RenderSettings::default()) |
| } |
| |
| /// Creates a new WebGL2 renderer with specific settings. |
| pub fn new_with(canvas: &HtmlCanvasElement, settings: RenderSettings) -> Self { |
| super::common::maybe_warn_about_webgl_feature_conflict(); |
| |
| // We do our own anti-aliasing, so no need to enable it in the WebGL |
| // context. |
| let context_options = js_sys::Object::new(); |
| js_sys::Reflect::set(&context_options, &"antialias".into(), &JsValue::FALSE).unwrap(); |
| // Vello only supports 24+ bit depth buffers. If the hardware falls back to a 16 bit depth buffer, |
| // correctness issues will arise. For all intents and purposes, a device manufactured in the past 10 years |
| // should support 24+ bit depth buffers (certainly those within the realm of what we consider "supported" devices) |
| // but: |
| // |
| // Relevant code for default depth buffer behaviour can be found here: |
| // - Chromium defaults to 24 bit with no fallback: https://github.com/chromium/chromium/blob/86bafb3aab8e999690d310b201d0b5489f512b08/third_party/blink/renderer/platform/graphics/gpu/drawing_buffer.cc#L1376-L1400 |
| // - Firefox defaults to 24 bit with no fallback: https://github.com/mozilla/gecko-dev/blob/5836a062726f715fda621338a17b51aff30d0a8c/gfx/gl/MozFramebuffer.cpp#L155-L161 |
| // - Safari defaults to 24 bit _with 16 bit_ fallback: https://github.com/WebKit/WebKit/blob/a6d6c154bbee0643f5ad1e55c071558c0df9aef7/Source/WebCore/platform/graphics/angle/GraphicsContextGLANGLE.cpp#L393-L416 |
| // |
| // TODO: The above understanding is encoded in a below assertion, but this should be encapsulated within a |
| // "this device can run Vello correctly" check function. |
| js_sys::Reflect::set(&context_options, &"depth".into(), &JsValue::TRUE).unwrap(); |
| |
| let gl = canvas |
| .get_context_with_context_options("webgl2", &context_options) |
| .expect("WebGL2 context to be available") |
| .unwrap() |
| .dyn_into::<WebGl2RenderingContext>() |
| .expect("Context to be a WebGL2 context"); |
| |
| let cloned_gl = gl.clone(); |
| let _state_guard = WebGlStateGuard::with_config( |
| &cloned_gl, |
| WebGlStateConfig { |
| framebuffer: true, |
| ..Default::default() |
| }, |
| ); |
| |
| // Note: It is not entirely clear whether we really _have_ to ensure anti-aliasing is disabled. |
| // This code is inherited from a similar snippet in wgpu |
| // (https://github.com/gfx-rs/wgpu/blob/56e4a389ddd02403e232beef3d3ff305625e6485/wgpu-hal/src/gles/web.rs#L101-L106), |
| // which itself seems to have been copied from the older `gfx` crate, where it was first introduced |
| // in https://github.com/gfx-rs/gfx/pull/2554/changes#diff-a47711d61df7a43fe6dd99c39b936d17ff817cbc2238d7e3ae6698ffde9b88f7R79, |
| // without any comment on why. |
| // From my (Laurenz) testing, tests seem to work even when anti-aliasing is enabled, |
| // but Andrew previously got errors similar to the ones outlined in |
| // https://github.com/gfx-rs/wgpu/issues/5263. Therefore, we just leave it as is for now. |
| #[cfg(debug_assertions)] |
| { |
| // If a WebGL context already exists on this canvas, it will be returned instead of |
| // creating a new one with the correct context_options set. |
| // See this comment for why we still care about non-antialiased context: |
| // https://github.com/linebender/vello/pull/1546/changes#r3008692535 |
| let context_attributes = gl.get_context_attributes().unwrap(); |
| let antialias = js_sys::Reflect::get(&context_attributes, &"antialias".into()) |
| .unwrap() |
| .as_bool() |
| .unwrap(); |
| debug_assert!( |
| !antialias, |
| "WebGL context must be created with `antialias: false` for vello_hybrid to work correctly." |
| ); |
| } |
| |
| let mut settings = settings; |
| let max_texture_dimension_2d = get_max_texture_dimension_2d(&gl); |
| normalize_atlas_config( |
| &mut settings.atlas_config, |
| max_texture_dimension_2d, |
| get_max_texture_array_layers(&gl), |
| 1, |
| ); |
| let total_slots: usize = (max_texture_dimension_2d / u32::from(Tile::HEIGHT)) as usize; |
| assert!( |
| gl.get_parameter(WebGl2RenderingContext::DEPTH_BITS) |
| .unwrap() |
| .as_f64() |
| .unwrap() |
| >= 24.0, |
| "Depth buffer must be at least 24 bits" |
| ); |
| let image_cache = ImageCache::new_with_config(settings.atlas_config); |
| // Estimate the maximum number of gradient cache entries based on the max texture dimension |
| // and the maximum gradient LUT size - worst case scenario. |
| let max_gradient_cache_size = |
| max_texture_dimension_2d * max_texture_dimension_2d / MAX_GRADIENT_LUT_SIZE as u32; |
| let gradient_cache = GradientRampCache::new(max_gradient_cache_size, settings.level); |
| let filter_context = FilterContext::new(settings.atlas_config); |
| |
| Self { |
| programs: WebGlPrograms::new(gl.clone(), &image_cache, &filter_context, total_slots), |
| scheduler: Scheduler::new(total_slots), |
| scheduler_state: SchedulerState::default(), |
| gl, |
| encoded_paints: Vec::new(), |
| paint_idxs: Vec::new(), |
| gradient_cache, |
| filter_context, |
| filter_pass_state: FilterPassState::default(), |
| dummy_image_cache: Some(ImageCache::new_dummy()), |
| } |
| } |
| |
| /// Render `scene` using WebGL2 |
| /// |
| /// This method creates GPU resources as needed and schedules potentially multiple draw calls. |
| pub fn render( |
| &mut self, |
| scene: &Scene, |
| resources: &mut Resources, |
| render_size: &RenderSize, |
| ) -> Result<(), RenderError> { |
| debug_assert_eq!( |
| RenderSize { |
| width: self.gl.drawing_buffer_width() as u32, |
| height: self.gl.drawing_buffer_height() as u32 |
| }, |
| *render_size, |
| "Render size must match drawing buffer size" |
| ); |
| |
| #[cfg(feature = "text")] |
| { |
| resources.before_render( |
| self, |
| |renderer, glyph_renderer, atlas_count, atlas_config, atlas_id| { |
| renderer |
| .render_to_atlas(glyph_renderer, atlas_count, atlas_config, atlas_id) |
| .expect("Failed to render glyphs to atlas"); |
| }, |
| |renderer, image_cache, upload, dst_x, dst_y| { |
| renderer.write_to_atlas( |
| image_cache, |
| upload.image_id, |
| &upload.pixmap, |
| Some([dst_x, dst_y]), |
| ); |
| }, |
| ); |
| } |
| |
| self.render_scene( |
| scene, |
| &mut resources.image_cache, |
| render_size, |
| true, |
| RootRenderTarget::UserSurface, |
| )?; |
| |
| #[cfg(feature = "text")] |
| { |
| resources.after_render(self, |renderer, rect| { |
| clear_atlas_region(renderer, rect); |
| }); |
| } |
| |
| Ok(()) |
| } |
| |
| /// Render a `scene` directly into an atlas layer. |
| /// |
| /// This renders the scene's content into the specified atlas layer, which can then |
| /// be sampled as an image in subsequent render passes. This is useful for rendering |
| /// vector content (e.g., glyphs) into the atlas for later use as cached images. |
| /// |
| /// The scene should be sized to the atlas layer dimensions |
| /// ([`AtlasConfig::atlas_size`]), with content positioned at the allocated offset |
| /// coordinates from `ImageCache::allocate`. |
| /// |
| /// This method creates its own command encoder and submits immediately, |
| /// ensuring atlas content is committed before any subsequent |
| /// [`render`](Self::render) call (the two methods share GPU resources that |
| /// are staged by `queue.write_*` and only applied on the next `queue.submit`). |
| #[doc(hidden)] |
| pub fn render_to_atlas( |
| &mut self, |
| scene: &Scene, |
| atlas_count: u32, |
| atlas_config: AtlasConfig, |
| atlas_id: AtlasId, |
| ) -> Result<(), RenderError> { |
| self.programs |
| .maybe_resize_atlas_texture_array(&self.gl, atlas_count); |
| |
| let (atlas_width, atlas_height) = atlas_config.atlas_size; |
| let atlas_render_size = RenderSize { |
| width: atlas_width, |
| height: atlas_height, |
| }; |
| |
| let atlas_framebuffer = self |
| .programs |
| .resources |
| .atlas_render_framebuffer |
| .take() |
| .unwrap_or_else(|| self.gl.create_framebuffer().unwrap()); |
| self.gl.bind_framebuffer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| Some(&atlas_framebuffer), |
| ); |
| self.gl.framebuffer_texture_layer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| WebGl2RenderingContext::COLOR_ATTACHMENT0, |
| Some(&self.programs.resources.atlas_texture_array.texture), |
| 0, |
| atlas_id.as_u32() as i32, |
| ); |
| |
| // Set the view framebuffer override so the scheduler renders to the |
| // atlas layer instead of the default framebuffer. |
| self.programs.resources.view_framebuffer_override = Some(atlas_framebuffer); |
| |
| // Swap in the stub atlas texture array to avoid binding the real atlas |
| // texture as a shader input while it is also the render target. |
| core::mem::swap( |
| &mut self.programs.resources.atlas_texture_array, |
| &mut self.programs.resources.stub_atlas_texture_array, |
| ); |
| |
| // TODO: Explore using an option instead of a dummy image cache. |
| let mut dummy_image_cache = self |
| .dummy_image_cache |
| .take() |
| .expect("dummy image cache must exist"); |
| let result = self.render_scene( |
| scene, |
| &mut dummy_image_cache, |
| &atlas_render_size, |
| false, |
| RootRenderTarget::AtlasLayer, |
| ); |
| self.dummy_image_cache = Some(dummy_image_cache); |
| |
| // Restore the real atlas texture array. |
| core::mem::swap( |
| &mut self.programs.resources.atlas_texture_array, |
| &mut self.programs.resources.stub_atlas_texture_array, |
| ); |
| |
| // Restore the default view framebuffer and cache the atlas FBO for reuse. |
| self.programs.resources.atlas_render_framebuffer = |
| self.programs.resources.view_framebuffer_override.take(); |
| |
| result |
| } |
| |
| /// Conduct a probing operation. |
| /// |
| /// The WebGL drivers of certain devices are known to be buggy and might therefore not work correctly |
| /// with Vello Hybrid. In the best case, it will simply result in a program crash, but in the worst |
| /// case it can instead result in a silent failure, meaning that no explicit error is |
| /// thrown, but the rendered contents of Vello Hybrid will either be completely empty or look glitchy. |
| /// |
| /// The purpose of this method is to run a sanity check to ensure that running Vello Hybrid on this |
| /// device actually results in visible and correct output. How this achieved is by drawing a selection |
| /// of small elements into a small canvas, and comparing the final output against a reference image. |
| /// |
| /// If certain pixels in the final output deviate too much from the expected image, an error result |
| /// will be returned, containing the expected image and the actual image. If probe rendering itself |
| /// fails, [`Probe::RenderError`] will be returned with the corresponding [`RenderError`]. Otherwise, |
| /// [`Probe::Success`] will be returned, indicating that the device seems to be compatible with |
| /// Vello Hybrid. |
| /// |
| /// **Important:** Note that this method can be expensive to call, as it performs a small rendering |
| /// operation and also does readback of the pixels to the CPU. Expect it to take anywhere from 10ms |
| /// to 100ms+. This should be taken into consideration when deciding when and how to call this method, |
| /// to prevent noticeable stalls on the main thread. |
| #[cfg(feature = "probe")] |
| pub fn probe(&mut self) -> Probe<RenderError> { |
| match self.probe_inner() { |
| Ok(actual) => Probe::from_actual(actual), |
| Err(error) => Probe::RenderError(error), |
| } |
| } |
| |
| #[cfg(feature = "probe")] |
| fn probe_inner(&mut self) -> Result<Pixmap, RenderError> { |
| let (width, height) = vello_common::probe::canvas_size(); |
| let render_size = RenderSize { |
| width: u32::from(width), |
| height: u32::from(height), |
| }; |
| |
| let probe_texture = create_texture(&self.gl); |
| self.gl |
| .tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( |
| WebGl2RenderingContext::TEXTURE_2D, |
| 0, |
| WebGl2RenderingContext::RGBA8 as i32, |
| render_size.width as i32, |
| render_size.height as i32, |
| 0, |
| WebGl2RenderingContext::RGBA, |
| WebGl2RenderingContext::UNSIGNED_BYTE, |
| None, |
| ) |
| .unwrap(); |
| let probe_framebuffer = create_framebuffer_for_texture(&self.gl, &probe_texture); |
| |
| let atlas_config = AtlasConfig { |
| initial_atlas_count: 1, |
| // These should be large enough for the probe scene. |
| atlas_size: (256, 256), |
| max_atlases: 1, |
| auto_grow: true, |
| allocation_strategy: AllocationStrategy::FirstFit, |
| }; |
| let (atlas_width, atlas_height) = atlas_config.atlas_size; |
| |
| let mut probe_image_cache = ImageCache::new_with_config(atlas_config); |
| let mut probe_atlas_texture_array = |
| create_atlas_texture_array(&self.gl, atlas_width, atlas_height, 1); |
| core::mem::swap( |
| &mut self.programs.resources.atlas_texture_array, |
| &mut probe_atlas_texture_array, |
| ); |
| |
| let probe_image = Arc::new(vello_common::probe::probe_image_pixmap()); |
| // Note: No need to destroy the image explicitly in the end, because we discard the image |
| // cache anyway. |
| let probe_image_id = |
| self.upload_image_with(&mut probe_image_cache, &probe_image, IMAGE_PADDING); |
| let mut scene = Scene::new(width, height); |
| vello_common::probe::draw_scene( |
| &mut scene, |
| ImageSource::opaque_id_with_opacity_hint( |
| probe_image_id, |
| probe_image.may_have_opacities(), |
| ), |
| ); |
| |
| let previous_view_framebuffer = self |
| .programs |
| .resources |
| .view_framebuffer_override |
| .replace(probe_framebuffer.clone()); |
| let render_result = self.render_scene( |
| &scene, |
| &mut probe_image_cache, |
| &render_size, |
| true, |
| RootRenderTarget::AtlasLayer, |
| ); |
| self.programs.resources.view_framebuffer_override = previous_view_framebuffer; |
| |
| core::mem::swap( |
| &mut self.programs.resources.atlas_texture_array, |
| &mut probe_atlas_texture_array, |
| ); |
| self.gl |
| .delete_texture(Some(&probe_atlas_texture_array.texture)); |
| |
| let pixels = match render_result { |
| Ok(()) => read_framebuffer_rgba8(&self.gl, &probe_framebuffer, width, height), |
| Err(error) => { |
| self.gl.delete_framebuffer(Some(&probe_framebuffer)); |
| self.gl.delete_texture(Some(&probe_texture)); |
| return Err(error); |
| } |
| }; |
| |
| self.gl.delete_framebuffer(Some(&probe_framebuffer)); |
| self.gl.delete_texture(Some(&probe_texture)); |
| Ok(pixels) |
| } |
| |
| /// Shared render pipeline: prepares GPU resources, runs the scheduler, and |
| /// maintains caches. |
| /// |
| /// When `clear` is true the view framebuffer is cleared to transparent black |
| /// before drawing. This must happen *after* `prepare` (which may create/resize |
| /// the framebuffer attachment). Atlas renders skip the clear so previously |
| /// rendered atlas content is preserved. |
| fn render_scene( |
| &mut self, |
| scene: &Scene, |
| image_cache: &mut ImageCache, |
| render_size: &RenderSize, |
| clear: bool, |
| root_output_target: RootRenderTarget, |
| ) -> Result<(), RenderError> { |
| if !self.filter_context.filter_textures.is_empty() { |
| self.programs.clear_filter_atlas_textures(&self.gl); |
| } |
| |
| self.filter_context |
| .deallocate_all_and_clear_context(image_cache); |
| |
| let mut encoded_paints = scene.encoded_paints.borrow_mut(); |
| let original_scene_paint_count = encoded_paints.len(); |
| |
| self.filter_context |
| .prepare(&scene.render_graph, image_cache, &mut encoded_paints)?; |
| |
| self.prepare_gpu_encoded_paints(&encoded_paints, image_cache); |
| |
| self.programs |
| .maybe_resize_atlas_texture_array(&self.gl, image_cache.atlas_count() as u32); |
| self.programs.maybe_resize_filter_atlas_textures( |
| &self.gl, |
| self.filter_context.image_cache.atlas_count() as u32, |
| ); |
| |
| // TODO: For the time being, we upload the entire alpha buffer as one big chunk. As a future |
| // refinement, we could have a bounded alpha buffer, and break draws when the alpha |
| // buffer fills. |
| self.programs.prepare( |
| &self.gl, |
| &mut self.gradient_cache, |
| &self.encoded_paints, |
| &mut scene.strip_storage.borrow_mut().alphas, |
| render_size, |
| &self.paint_idxs, |
| &self.filter_context, |
| ); |
| |
| if clear { |
| self.programs.clear_view_framebuffer(&self.gl); |
| } |
| self.programs.resources.depth_cleared_this_frame = false; |
| let mut ctx = WebGlRendererContext { |
| programs: &mut self.programs, |
| gl: &self.gl, |
| image_cache, |
| encoded_paints: &encoded_paints, |
| paint_idxs: &self.paint_idxs, |
| filter_context: &self.filter_context, |
| filter_pass_state: &mut self.filter_pass_state, |
| }; |
| self.scheduler.do_scene( |
| &mut self.scheduler_state, |
| &mut ctx, |
| scene, |
| root_output_target, |
| &self.paint_idxs, |
| &self.filter_context, |
| &encoded_paints, |
| )?; |
| |
| // See: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#use_invalidateframebuffer |
| // We want to indicate to the GPU driver that we won't read the depth buffer again |
| // until the next clear. This enables the GPU to avoid storing depth tiles back to VRAM. |
| if self.programs.resources.depth_cleared_this_frame { |
| self.gl.bind_framebuffer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| self.programs.resources.view_framebuffer_override.as_ref(), |
| ); |
| self.gl |
| .invalidate_framebuffer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| &self.programs.resources.depth_attachment_array, |
| ) |
| .unwrap(); |
| } |
| |
| encoded_paints.truncate(original_scene_paint_count); |
| self.gradient_cache.maintain(); |
| |
| Ok(()) |
| } |
| |
| /// Get a reference to the underlying WebGL context. |
| /// |
| /// This allows direct access to WebGL operations for advanced use cases like texture creation. |
| pub fn gl_context(&self) -> &WebGl2RenderingContext { |
| &self.gl |
| } |
| |
| /// Upload image to cache and atlas in one step. Returns the `ImageId`. |
| /// |
| /// This is the WebGL analogue of the wgpu Renderer's `upload_image` method. |
| /// It allocates space in the image cache and uploads the image data to the atlas texture. |
| pub fn upload_image<T: WebGlAtlasWriter>( |
| &mut self, |
| resources: &mut Resources, |
| writer: &T, |
| ) -> ImageId { |
| self.upload_image_with(&mut resources.image_cache, writer, IMAGE_PADDING) |
| } |
| |
| pub(crate) fn upload_image_with<T: WebGlAtlasWriter>( |
| &mut self, |
| image_cache: &mut ImageCache, |
| writer: &T, |
| padding: u16, |
| ) -> ImageId { |
| let width = writer.width(); |
| let height = writer.height(); |
| let image_id = image_cache.allocate(width, height, padding).unwrap(); |
| self.write_to_atlas(image_cache, image_id, writer, None); |
| image_id |
| } |
| |
| /// Write pixel data to an existing atlas allocation. |
| /// |
| /// Unlike [`upload_image`](Self::upload_image), this does not allocate space in the image |
| /// cache. The `image_id` must have been previously allocated (e.g. via |
| /// `ImageCache::allocate`). This is useful for uploading CPU-side pixel data (such as |
| /// bitmap font glyphs) to a pre-allocated atlas region. |
| /// |
| /// If `offset_override` is `Some`, the provided offset is used instead of the |
| /// allocator-assigned position. Pass `None` to use the default atlas offset. |
| pub(crate) fn write_to_atlas<T: WebGlAtlasWriter>( |
| &mut self, |
| image_cache: &ImageCache, |
| image_id: ImageId, |
| writer: &T, |
| offset_override: Option<[u32; 2]>, |
| ) { |
| let image_resource = image_cache.get(image_id).expect("Image resource not found"); |
| |
| self.programs |
| .maybe_resize_atlas_texture_array(&self.gl, image_cache.atlas_count() as u32); |
| let offset = offset_override.unwrap_or([ |
| image_resource.offset[0] as u32, |
| image_resource.offset[1] as u32, |
| ]); |
| writer.write_to_atlas_layer( |
| &self.gl, |
| &self.programs.resources.atlas_texture_array.texture, |
| image_resource.atlas_id.as_u32(), |
| offset, |
| writer.width(), |
| writer.height(), |
| ); |
| } |
| |
| /// Destroy an image from the cache and clear the allocated slot in the atlas. |
| pub fn destroy_image(&mut self, resources: &mut Resources, image_id: ImageId) { |
| if let Some(image_resource) = resources.image_cache.deallocate(image_id) { |
| let padding = image_resource.padding as u32; |
| self.clear_atlas_region( |
| image_resource.atlas_id, |
| [ |
| image_resource.offset[0] as u32 - padding, |
| image_resource.offset[1] as u32 - padding, |
| ], |
| image_resource.width as u32 + padding * 2, |
| image_resource.height as u32 + padding * 2, |
| ); |
| } |
| } |
| |
| /// Returns a reference to the underlying atlas texture array. |
| /// |
| /// This is a 2D array texture (`TextureViewDimension::D2Array`) containing all |
| /// atlas layers used by the image cache. Each layer holds cached image data |
| /// (e.g., rasterised glyphs) that the renderer samples during draw calls. |
| pub fn atlas_texture(&self) -> &WebGlTexture { |
| &self.programs.resources.atlas_texture_array.texture |
| } |
| |
| /// Clear a specific region of the atlas texture array. |
| fn clear_atlas_region(&mut self, atlas_id: AtlasId, offset: [u32; 2], width: u32, height: u32) { |
| let _state_guard = WebGlStateGuard::for_clear_atlas_region(&self.gl); |
| let temp_framebuffer = self.gl.create_framebuffer().unwrap(); |
| |
| // Bind our temporary framebuffer |
| self.gl |
| .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&temp_framebuffer)); |
| |
| // Attach the specific atlas layer to the framebuffer |
| self.gl.framebuffer_texture_layer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| WebGl2RenderingContext::COLOR_ATTACHMENT0, |
| Some(&self.programs.resources.atlas_texture_array.texture), |
| 0, |
| atlas_id.as_u32() as i32, |
| ); |
| |
| // Set viewport to match the atlas texture dimensions |
| let atlas_size = &self.programs.resources.atlas_texture_array.size; |
| self.gl |
| .viewport(0, 0, atlas_size.width as i32, atlas_size.height as i32); |
| |
| // Enable scissor test and set scissor rectangle to our region |
| self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST); |
| self.gl.scissor( |
| offset[0] as i32, |
| offset[1] as i32, |
| width as i32, |
| height as i32, |
| ); |
| |
| // Clear the region to transparent (0, 0, 0, 0) |
| self.gl.clear_color(0.0, 0.0, 0.0, 0.0); |
| self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); |
| |
| // Clean up temporary framebuffer |
| self.gl.delete_framebuffer(Some(&temp_framebuffer)); |
| } |
| |
| fn prepare_gpu_encoded_paints( |
| &mut self, |
| encoded_paints: &[EncodedPaint], |
| image_cache: &ImageCache, |
| ) { |
| self.encoded_paints |
| .resize_with(encoded_paints.len(), || GPU_PAINT_PLACEHOLDER); |
| self.paint_idxs.resize(encoded_paints.len() + 1, 0); |
| |
| let mut current_idx = 0; |
| for (encoded_paint_idx, paint) in encoded_paints.iter().enumerate() { |
| self.paint_idxs[encoded_paint_idx] = current_idx; |
| match paint { |
| EncodedPaint::Image(img) => { |
| if let ImageSource::OpaqueId { id: image_id, .. } = img.source { |
| let image_resource: Option<&ImageResource> = image_cache.get(image_id); |
| if let Some(image_resource) = image_resource { |
| let gpu_image = self.encode_image_paint(img, image_resource); |
| self.encoded_paints[encoded_paint_idx] = gpu_image; |
| current_idx += GPU_ENCODED_IMAGE_SIZE_TEXELS; |
| } |
| } |
| } |
| EncodedPaint::Gradient(gradient) => { |
| let (gradient_start, gradient_width) = |
| self.gradient_cache.get_or_create_ramp(gradient); |
| let gpu_gradient = |
| self.encode_gradient_paint(gradient, gradient_width, gradient_start); |
| let gradient_size_texels = match &gpu_gradient { |
| GpuEncodedPaint::LinearGradient(_) => GPU_LINEAR_GRADIENT_SIZE_TEXELS, |
| GpuEncodedPaint::RadialGradient(_) => GPU_RADIAL_GRADIENT_SIZE_TEXELS, |
| GpuEncodedPaint::SweepGradient(_) => GPU_SWEEP_GRADIENT_SIZE_TEXELS, |
| _ => unreachable!("encode_gradient_for_gpu only returns gradient types"), |
| }; |
| self.encoded_paints[encoded_paint_idx] = gpu_gradient; |
| current_idx += gradient_size_texels; |
| } |
| EncodedPaint::BlurredRoundedRect(_blurred_rect) => { |
| // TODO: Blurred rounded rectangles are not yet supported |
| log::warn!( |
| "Blurred rounded rectangles are not yet supported in sparse strips hybrid renderer" |
| ); |
| } |
| } |
| } |
| self.paint_idxs[encoded_paints.len()] = current_idx; |
| } |
| |
| fn encode_image_paint( |
| &self, |
| image: &vello_common::encode::EncodedImage, |
| image_resource: &ImageResource, |
| ) -> GpuEncodedPaint { |
| let transform = image.transform.as_coeffs().map(|x| x as f32); |
| let image_size = pack_image_size(image_resource.width, image_resource.height); |
| let image_offset = pack_image_offset(image_resource.offset[0], image_resource.offset[1]); |
| let image_params = pack_image_params( |
| image.sampler.quality as u32, |
| image.sampler.x_extend as u32, |
| image.sampler.y_extend as u32, |
| image_resource.atlas_id.as_u32(), |
| ); |
| let (tint, tint_mode) = pack_tint(image.tint); |
| |
| GpuEncodedPaint::Image(GpuEncodedImage { |
| image_params, |
| image_size, |
| image_offset, |
| transform, |
| tint, |
| tint_mode, |
| image_padding: image_resource.padding as u32, |
| }) |
| } |
| |
| fn encode_gradient_paint( |
| &self, |
| gradient: &EncodedGradient, |
| gradient_width: u32, |
| gradient_start: u32, |
| ) -> GpuEncodedPaint { |
| let transform = gradient.transform.as_coeffs().map(|x| x as f32); |
| let extend_mode = match gradient.extend { |
| peniko::Extend::Pad => 0, |
| peniko::Extend::Repeat => 1, |
| peniko::Extend::Reflect => 2, |
| }; |
| let texture_width_and_extend_mode = |
| pack_texture_width_and_extend_mode(gradient_width, extend_mode); |
| |
| match &gradient.kind { |
| EncodedKind::Linear(_) => GpuEncodedPaint::LinearGradient(GpuLinearGradient { |
| texture_width_and_extend_mode, |
| gradient_start, |
| transform, |
| }), |
| EncodedKind::Radial(radial) => { |
| let (kind, bias, scale, fp0, fp1, fr1, f_focal_x, f_is_swapped, scaled_r0_squared) = |
| match radial { |
| RadialKind::Radial { bias, scale } => { |
| (0, *bias, *scale, 0.0, 0.0, 0.0, 0.0, 0, 0.0) |
| } |
| RadialKind::Strip { scaled_r0_squared } => { |
| (1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0, *scaled_r0_squared) |
| } |
| RadialKind::Focal { |
| focal_data, |
| fp0, |
| fp1, |
| } => ( |
| 2, |
| *fp0, |
| *fp1, |
| *fp0, |
| *fp1, |
| focal_data.fr1, |
| focal_data.f_focal_x, |
| focal_data.f_is_swapped as u32, |
| 0.0, |
| ), |
| }; |
| GpuEncodedPaint::RadialGradient(GpuRadialGradient { |
| texture_width_and_extend_mode, |
| gradient_start, |
| transform, |
| kind_and_f_is_swapped: pack_radial_kind_and_swapped(kind, f_is_swapped), |
| bias, |
| scale, |
| fp0, |
| fp1, |
| fr1, |
| f_focal_x, |
| scaled_r0_squared, |
| }) |
| } |
| EncodedKind::Sweep(sweep) => GpuEncodedPaint::SweepGradient(GpuSweepGradient { |
| texture_width_and_extend_mode, |
| gradient_start, |
| transform, |
| start_angle: sweep.start_angle, |
| inv_angle_delta: sweep.inv_angle_delta, |
| _padding: [0, 0], |
| }), |
| } |
| } |
| } |
| |
| #[cfg(feature = "text")] |
| fn clear_atlas_region(renderer: &mut WebGlRenderer, rect: &PendingClearRect) { |
| // TODO: Similarly to wgpu, maybe this can be done in a more effective |
| // way? |
| let padding = u32::from(GLYPH_PADDING); |
| let offset = [ |
| u32::from(rect.x).saturating_sub(padding), |
| u32::from(rect.y).saturating_sub(padding), |
| ]; |
| let width = u32::from(rect.width) + padding * 2; |
| let height = u32::from(rect.height) + padding * 2; |
| renderer.clear_atlas_region(AtlasId::new(rect.page_index), offset, width, height); |
| } |
| |
| /// Contains the WebGL programs and resources for rendering. |
| #[derive(Debug)] |
| struct WebGlPrograms { |
| /// Program for rendering wide tile commands. |
| strip_program: WebGlProgram, |
| /// Uniform locations for the strip program |
| strip_uniforms: StripUniforms, |
| /// Program for opaque solid strips on the final view. |
| opaque_solid_program: WebGlProgram, |
| /// Uniform locations for the opaque solid program. |
| opaque_solid_uniforms: StripUniforms, |
| /// Program for opaque tinted image strips on the final view. |
| opaque_image_program: WebGlProgram, |
| /// Uniform locations for the opaque tinted image program. |
| opaque_image_uniforms: StripUniforms, |
| /// Program for opaque untinted image strips on the final view. |
| opaque_image_untinted_program: WebGlProgram, |
| /// Uniform locations for the opaque untinted image program. |
| opaque_image_untinted_uniforms: StripUniforms, |
| /// Program for opaque gradient strips on the final view. |
| opaque_gradient_program: WebGlProgram, |
| /// Uniform locations for the opaque gradient program. |
| opaque_gradient_uniforms: StripUniforms, |
| /// Program for clearing slots in slot textures. |
| clear_program: WebGlProgram, |
| /// Uniform locations for the `clear_program`. |
| clear_uniforms: ClearUniforms, |
| /// Program for filter passes. |
| filter_program: WebGlProgram, |
| /// Uniform locations for the filter program. |
| filter_uniforms: FilterPassUniforms, |
| /// WebGL resources for rendering. |
| resources: WebGlResources, |
| /// Dimensions of the rendering target. |
| render_size: RenderSize, |
| /// Whether the last config buffer upload had NDC Y negation enabled. |
| negate_ndc: bool, |
| /// Scratch buffer for staging encoded paints texture data. |
| encoded_paints_data: Vec<u8>, |
| /// Scratch buffer for staging filter data texture data. |
| filter_data: Vec<u8>, |
| } |
| |
| #[derive(Debug)] |
| struct FilterPassUniforms { |
| filter_data: WebGlUniformLocation, |
| in_tex: WebGlUniformLocation, |
| original_tex: WebGlUniformLocation, |
| } |
| |
| /// Uniform locations for `strip_program`. |
| #[derive(Debug)] |
| struct StripUniforms { |
| /// Alphas texture location. |
| alphas_texture: Option<WebGlUniformLocation>, |
| /// Clip input texture location. |
| clip_input_texture: Option<WebGlUniformLocation>, |
| /// Atlas texture location. |
| atlas_texture_array: Option<WebGlUniformLocation>, |
| /// Encoded paints texture location for fragment shader. |
| encoded_paints_texture_fs: Option<WebGlUniformLocation>, |
| /// Encoded paints texture location for vertex shader. |
| encoded_paints_texture_vs: Option<WebGlUniformLocation>, |
| /// Gradient texture location. |
| gradient_texture: Option<WebGlUniformLocation>, |
| } |
| |
| /// Uniform locations for `clear_program`. |
| #[derive(Debug)] |
| struct ClearUniforms { |
| /// Config uniform block index. |
| config_block_index: u32, |
| } |
| |
| /// Contains all WebGL resources needed for rendering. |
| #[derive(Debug)] |
| struct WebGlResources { |
| /// VAO for strip rendering. |
| strip_vao: WebGlVertexArrayObject, |
| /// Buffer for [`GpuStrip`] data. |
| strips_buffer: WebGlBuffer, |
| /// Texture for alpha values (used by both view and slot rendering). |
| alphas_texture: WebGlTexture, |
| /// Height of alpha texture. |
| alpha_texture_height: u32, |
| /// Texture array for atlas data (multiple atlases supported) |
| atlas_texture_array: WebGlTextureArray, |
| /// Encoded paints texture for image metadata. |
| encoded_paints_texture: WebGlTexture, |
| /// Height of encoded paints texture. |
| encoded_paints_texture_height: u32, |
| /// Gradient texture for gradient ramp data. |
| gradient_texture: WebGlTexture, |
| /// Height of gradient texture. |
| gradient_texture_height: u32, |
| |
| /// Config buffer for rendering wide tile commands into the view texture. |
| view_config_buffer: WebGlBuffer, |
| /// Config buffer for rendering wide tile commands into a slot texture. |
| slot_config_buffer: WebGlBuffer, |
| |
| /// Buffer for slot indices used in `clear_slots`. |
| clear_slot_indices_buffer: WebGlBuffer, |
| /// VAO for clear slots program. |
| clear_vao: WebGlVertexArrayObject, |
| /// Config buffer for clear program. |
| clear_config_buffer: WebGlBuffer, |
| |
| view_framebuffer_override: Option<WebGlFramebuffer>, |
| /// Whether the depth buffer has been cleared this frame. |
| depth_cleared_this_frame: bool, |
| /// Pre-allocated JS array for `invalidateFramebuffer` calls. |
| depth_attachment_array: js_sys::Array, |
| |
| /// Slot textures. |
| slot_textures: [WebGlTexture; 2], |
| /// Framebuffers for slot textures. |
| slot_framebuffers: [WebGlFramebuffer; 2], |
| |
| /// Cached result from querying `WebGl2RenderingContext::MAX_TEXTURE_SIZE` which is a blocking |
| /// WebGL call. |
| max_texture_dimension_2d: u32, |
| |
| /// Placeholder 1x1 atlas texture array, used during `render_to_atlas` to avoid |
| /// binding the real atlas texture while it is also the render target. |
| stub_atlas_texture_array: WebGlTextureArray, |
| |
| /// Cached framebuffer for rendering into an atlas layer in `render_to_atlas`. |
| /// Reused to avoid create/delete overhead on every call. |
| atlas_render_framebuffer: Option<WebGlFramebuffer>, |
| |
| /// Cached framebuffer for filter passes that write back to the main atlas. |
| /// Reused to avoid create/delete overhead on every filter application. |
| filter_main_atlas_framebuffer: Option<WebGlFramebuffer>, |
| |
| /// Individual 2D textures for filter intermediate results. |
| filter_atlas_textures: Vec<WebGlTexture>, |
| /// Framebuffers for each filter atlas texture. |
| filter_atlas_framebuffers: Vec<WebGlFramebuffer>, |
| /// RGBA32UI texture storing filter parameters. |
| filter_data_texture: WebGlTexture, |
| /// Current height of filter data texture. |
| filter_data_texture_height: u32, |
| /// Per-instance vertex data buffer for filter draws. |
| filter_instance_buffer: WebGlBuffer, |
| /// VAO for filter rendering. |
| filter_vao: WebGlVertexArrayObject, |
| /// Config buffer for rendering filter layers. |
| filter_config_buffer: WebGlBuffer, |
| /// Cached atlas width for creating new filter atlas textures. |
| filter_atlas_width: u32, |
| /// Cached atlas height for creating new filter atlas textures. |
| filter_atlas_height: u32, |
| } |
| |
| /// Config for the clear slots pipeline. |
| #[repr(C)] |
| #[derive(Debug, Copy, Clone, Pod, Zeroable)] |
| struct ClearSlotsConfig { |
| /// Width of a slot. |
| pub slot_width: u32, |
| /// Height of a slot. |
| pub slot_height: u32, |
| /// Total height of the texture. |
| pub texture_height: u32, |
| /// Padding for alignment. |
| pub _padding: u32, |
| } |
| |
| impl WebGlPrograms { |
| /// Creates programs and initializes resources. |
| fn new( |
| gl: WebGl2RenderingContext, |
| image_cache: &ImageCache, |
| filter_context: &FilterContext, |
| slot_count: usize, |
| ) -> Self { |
| let strip_program = create_shader_program( |
| &gl, |
| render_strips::VERTEX_SOURCE, |
| render_strips::FRAGMENT_SOURCE, |
| ); |
| let opaque_solid_program = create_shader_program( |
| &gl, |
| opaque_solid::VERTEX_SOURCE, |
| opaque_solid::FRAGMENT_SOURCE, |
| ); |
| let opaque_image_program = create_shader_program( |
| &gl, |
| opaque_image::VERTEX_SOURCE, |
| opaque_image::FRAGMENT_SOURCE, |
| ); |
| let opaque_image_untinted_program = create_shader_program( |
| &gl, |
| opaque_image_untinted::VERTEX_SOURCE, |
| opaque_image_untinted::FRAGMENT_SOURCE, |
| ); |
| let opaque_gradient_program = create_shader_program( |
| &gl, |
| opaque_gradient::VERTEX_SOURCE, |
| opaque_gradient::FRAGMENT_SOURCE, |
| ); |
| let clear_program = create_shader_program( |
| &gl, |
| clear_slots::VERTEX_SOURCE, |
| clear_slots::FRAGMENT_SOURCE, |
| ); |
| let filter_program = |
| create_shader_program(&gl, filters::VERTEX_SOURCE, filters::FRAGMENT_SOURCE); |
| let filter_uniforms = get_filter_pass_uniforms(&gl, &filter_program); |
| |
| let strip_uniforms = get_strip_uniforms(&gl, &strip_program); |
| let opaque_solid_uniforms = get_opaque_solid_uniforms(&gl, &opaque_solid_program); |
| let opaque_image_uniforms = get_opaque_image_uniforms(&gl, &opaque_image_program); |
| let opaque_image_untinted_uniforms = |
| get_opaque_image_untinted_uniforms(&gl, &opaque_image_untinted_program); |
| let opaque_gradient_uniforms = get_opaque_gradient_uniforms(&gl, &opaque_gradient_program); |
| let clear_uniforms = get_clear_uniforms(&gl, &clear_program); |
| |
| let resources = create_webgl_resources(&gl, image_cache, filter_context, slot_count); |
| |
| initialize_strip_vao(&gl, &resources); |
| initialize_clear_vao(&gl, &resources); |
| initialize_filter_vao(&gl, &resources); |
| |
| let encoded_paints_data = vec![0; (resources.max_texture_dimension_2d << 4) as usize]; |
| |
| gl.enable(WebGl2RenderingContext::BLEND); |
| gl.blend_func( |
| WebGl2RenderingContext::ONE, |
| WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA, |
| ); |
| |
| Self { |
| strip_program, |
| clear_program, |
| filter_program, |
| filter_uniforms, |
| strip_uniforms, |
| opaque_solid_program, |
| opaque_solid_uniforms, |
| opaque_image_program, |
| opaque_image_uniforms, |
| opaque_image_untinted_program, |
| opaque_image_untinted_uniforms, |
| opaque_gradient_program, |
| opaque_gradient_uniforms, |
| clear_uniforms, |
| resources, |
| render_size: RenderSize { |
| width: 0, |
| height: 0, |
| }, |
| negate_ndc: false, |
| encoded_paints_data, |
| filter_data: Vec::new(), |
| } |
| } |
| |
| /// Prepare resources for rendering. |
| fn prepare( |
| &mut self, |
| gl: &WebGl2RenderingContext, |
| gradient_cache: &mut GradientRampCache, |
| encoded_paints: &[GpuEncodedPaint], |
| alphas: &mut Vec<u8>, |
| render_size: &RenderSize, |
| paint_idxs: &[u32], |
| filter_context: &FilterContext, |
| ) { |
| let max_texture_dimension_2d = self.resources.max_texture_dimension_2d; |
| |
| self.maybe_resize_alphas_tex(max_texture_dimension_2d, alphas.len()); |
| self.maybe_resize_encoded_paints_tex(max_texture_dimension_2d, paint_idxs); |
| self.maybe_resize_filter_data_tex(filter_context); |
| self.maybe_update_config_buffer(gl, max_texture_dimension_2d, render_size); |
| |
| self.upload_alpha_texture(gl, alphas); |
| self.upload_encoded_paints_texture(gl, encoded_paints); |
| self.upload_filter_data_texture(gl, filter_context); |
| |
| if gradient_cache.has_changed() { |
| self.maybe_resize_gradient_tex(gl, max_texture_dimension_2d, gradient_cache); |
| self.upload_gradient_texture(gl, gradient_cache); |
| gradient_cache.mark_synced(); |
| } |
| } |
| |
| /// Resize atlas texture array to accommodate more atlases. |
| fn maybe_resize_atlas_texture_array( |
| &mut self, |
| gl: &WebGl2RenderingContext, |
| required_atlas_count: u32, |
| ) { |
| let WebGlTextureSize { |
| width, |
| height, |
| depth_or_array_layers: current_atlas_count, |
| } = self.resources.atlas_texture_array.size(); |
| if required_atlas_count > current_atlas_count { |
| // Create new texture array with more layers |
| let new_atlas_texture_array = |
| create_atlas_texture_array(gl, width, height, required_atlas_count); |
| |
| // Copy existing atlas data from old texture array to new one |
| self.copy_atlas_texture_data(gl, &new_atlas_texture_array, current_atlas_count); |
| |
| // Replace the old resources |
| self.resources.atlas_texture_array = new_atlas_texture_array; |
| // Cached FBOs were attached to the old texture; drop them so we recreate on next use. |
| if let Some(fb) = self.resources.atlas_render_framebuffer.take() { |
| gl.delete_framebuffer(Some(&fb)); |
| } |
| if let Some(fb) = self.resources.filter_main_atlas_framebuffer.take() { |
| gl.delete_framebuffer(Some(&fb)); |
| } |
| } |
| } |
| |
| /// Copy texture data from the old atlas texture array to a new one. |
| /// This is necessary when resizing the texture array to preserve existing atlas data. |
| fn copy_atlas_texture_data( |
| &self, |
| gl: &WebGl2RenderingContext, |
| new_atlas_texture_array: &WebGlTextureArray, |
| layer_count_to_copy: u32, |
| ) { |
| let WebGlTextureSize { width, height, .. } = self.resources.atlas_texture_array.size(); |
| |
| // Copy each layer from the old atlas to the new one |
| for layer in 0..layer_count_to_copy { |
| copy_to_texture_array_layer( |
| gl, |
| |gl| { |
| // Attach source layer to READ framebuffer |
| gl.framebuffer_texture_layer( |
| WebGl2RenderingContext::READ_FRAMEBUFFER, |
| WebGl2RenderingContext::COLOR_ATTACHMENT0, |
| Some(&self.resources.atlas_texture_array.texture), |
| 0, |
| layer as i32, |
| ); |
| }, |
| &new_atlas_texture_array.texture, |
| layer, |
| [0, 0], |
| [width, height], |
| ); |
| } |
| } |
| |
| fn maybe_resize_filter_data_tex(&mut self, filter_context: &FilterContext) { |
| let max_texture_dimension_2d = self.resources.max_texture_dimension_2d; |
| |
| let Some(required_height) = |
| filter_context.required_filter_data_height(max_texture_dimension_2d) |
| else { |
| return; |
| }; |
| |
| if required_height > self.resources.filter_data_texture_height { |
| let required_size = (max_texture_dimension_2d * required_height) << 4; |
| self.filter_data.resize(required_size as usize, 0); |
| self.resources.filter_data_texture_height = required_height; |
| } |
| } |
| |
| fn upload_filter_data_texture( |
| &mut self, |
| gl: &WebGl2RenderingContext, |
| filter_context: &FilterContext, |
| ) { |
| if filter_context.is_empty() { |
| return; |
| } |
| |
| let width = self.resources.max_texture_dimension_2d; |
| let height = self.resources.filter_data_texture_height; |
| filter_context.serialize_to_buffer(&mut self.filter_data); |
| gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&self.resources.filter_data_texture), |
| ); |
| let data_as_u32 = bytemuck::cast_slice::<u8, u32>(&self.filter_data); |
| let packed_array = js_sys::Uint32Array::from(data_as_u32); |
| gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( |
| WebGl2RenderingContext::TEXTURE_2D, |
| 0, |
| WebGl2RenderingContext::RGBA32UI as i32, |
| width as i32, |
| height as i32, |
| 0, |
| WebGl2RenderingContext::RGBA_INTEGER, |
| WebGl2RenderingContext::UNSIGNED_INT, |
| Some(&packed_array), |
| ) |
| .unwrap(); |
| } |
| |
| fn maybe_resize_filter_atlas_textures( |
| &mut self, |
| gl: &WebGl2RenderingContext, |
| required_count: u32, |
| ) { |
| let current_count = self.resources.filter_atlas_textures.len() as u32; |
| // TODO: Same as wgpu, should we be destroying |
| // textures if they aren't needed anymore? |
| if required_count > current_count { |
| let width = self.resources.filter_atlas_width; |
| let height = self.resources.filter_atlas_height; |
| for _ in current_count..required_count { |
| let tex = create_filter_atlas_texture(gl, width, height); |
| let fb = create_framebuffer_for_texture(gl, &tex); |
| self.resources.filter_atlas_textures.push(tex); |
| self.resources.filter_atlas_framebuffers.push(fb); |
| } |
| } |
| } |
| |
| fn clear_filter_atlas_textures(&self, gl: &WebGl2RenderingContext) { |
| let _state_guard = WebGlStateGuard::with_config( |
| gl, |
| WebGlStateConfig { |
| framebuffer: true, |
| viewport: true, |
| ..Default::default() |
| }, |
| ); |
| let width = self.resources.filter_atlas_width; |
| let height = self.resources.filter_atlas_height; |
| for fb in &self.resources.filter_atlas_framebuffers { |
| gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(fb)); |
| gl.viewport(0, 0, width as i32, height as i32); |
| gl.clear_color(0.0, 0.0, 0.0, 0.0); |
| gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); |
| } |
| } |
| |
| fn upload_filter_instances( |
| &self, |
| gl: &WebGl2RenderingContext, |
| instances: &[FilterInstanceData], |
| ) { |
| gl.bind_buffer( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| Some(&self.resources.filter_instance_buffer), |
| ); |
| gl.buffer_data_with_u8_array( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| bytemuck::cast_slice(instances), |
| WebGl2RenderingContext::DYNAMIC_DRAW, |
| ); |
| } |
| |
| /// Update the alpha texture size if needed. |
| fn maybe_resize_alphas_tex(&mut self, max_texture_dimension_2d: u32, alphas_len: usize) { |
| let required_alpha_height = (alphas_len as u32) |
| // There are 16 1-byte alpha values per texel. |
| .div_ceil(max_texture_dimension_2d << 4); |
| |
| let current_alpha_height = self.resources.alpha_texture_height; |
| if required_alpha_height > current_alpha_height { |
| // We need to resize the alpha texture to fit the new alpha data. |
| assert!( |
| required_alpha_height <= max_texture_dimension_2d, |
| "Alpha texture height exceeds max texture dimensions" |
| ); |
| |
| // Track the new height. |
| self.resources.alpha_texture_height = required_alpha_height; |
| } |
| } |
| |
| /// Update the encoded paints texture size if needed. |
| fn maybe_resize_encoded_paints_tex( |
| &mut self, |
| max_texture_dimension_2d: u32, |
| paint_idxs: &[u32], |
| ) { |
| let required_texels = paint_idxs.last().unwrap(); |
| let required_encoded_paints_height = required_texels.div_ceil(max_texture_dimension_2d); |
| let current_encoded_paints_height = self.resources.encoded_paints_texture_height; |
| if required_encoded_paints_height > current_encoded_paints_height { |
| assert!( |
| required_encoded_paints_height <= max_texture_dimension_2d, |
| "Encoded paints texture height exceeds max texture dimensions" |
| ); |
| |
| let required_encoded_paints_size = |
| (max_texture_dimension_2d * required_encoded_paints_height) << 4; |
| self.encoded_paints_data |
| .resize(required_encoded_paints_size as usize, 0); |
| self.resources.encoded_paints_texture_height = required_encoded_paints_height; |
| } |
| } |
| |
| /// Update the gradient texture size if needed. |
| fn maybe_resize_gradient_tex( |
| &mut self, |
| _gl: &WebGl2RenderingContext, |
| max_texture_dimension_2d: u32, |
| gradient_cache: &GradientRampCache, |
| ) { |
| if gradient_cache.is_empty() { |
| return; |
| } |
| |
| let gradient_data_size = gradient_cache.luts_size(); |
| // Each texel is RGBA8, so 4 bytes per texel |
| let required_gradient_height = |
| (gradient_data_size as u32).div_ceil(max_texture_dimension_2d * 4); |
| |
| let current_gradient_height = self.resources.gradient_texture_height; |
| if required_gradient_height > current_gradient_height { |
| assert!( |
| required_gradient_height <= max_texture_dimension_2d, |
| "Gradient texture height exceeds max texture dimensions" |
| ); |
| |
| self.resources.gradient_texture_height = required_gradient_height; |
| } |
| } |
| |
| /// Update config buffer if dimensions changed. |
| fn maybe_update_config_buffer( |
| &mut self, |
| gl: &WebGl2RenderingContext, |
| max_texture_dimension_2d: u32, |
| new_render_size: &RenderSize, |
| ) { |
| // Only negate if we are rendering to the main frame buffer. |
| let negate_ndc = self.resources.view_framebuffer_override.is_none(); |
| |
| // TODO: Collect all attributes that influence the config buffer into a |
| // single struct and compare that, such that we cannot forget to update the |
| // condition in case we add new fields in the future. |
| if self.render_size != *new_render_size || self.negate_ndc != negate_ndc { |
| // Update view config buffer |
| { |
| let config = Config { |
| width: new_render_size.width, |
| height: new_render_size.height, |
| strip_height: u32::from(Tile::HEIGHT), |
| alphas_tex_width_bits: max_texture_dimension_2d.trailing_zeros(), |
| encoded_paints_tex_width_bits: max_texture_dimension_2d.trailing_zeros(), |
| strip_offset_x: 0, |
| strip_offset_y: 0, |
| negate_ndc: u32::from(negate_ndc), |
| }; |
| |
| gl.bind_buffer( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| Some(&self.resources.view_config_buffer), |
| ); |
| let config_data = bytemuck::bytes_of(&config); |
| gl.buffer_data_with_u8_array( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| config_data, |
| WebGl2RenderingContext::STATIC_DRAW, |
| ); |
| } |
| |
| let total_slots = max_texture_dimension_2d / u32::from(Tile::HEIGHT); |
| // Update slot config buffer. |
| { |
| let slot_config = Config { |
| width: u32::from(WideTile::WIDTH), |
| height: u32::from(Tile::HEIGHT) * total_slots, |
| strip_height: u32::from(Tile::HEIGHT), |
| alphas_tex_width_bits: max_texture_dimension_2d.trailing_zeros(), |
| encoded_paints_tex_width_bits: max_texture_dimension_2d.trailing_zeros(), |
| strip_offset_x: 0, |
| strip_offset_y: 0, |
| // Always use y-down when rendering to slots. |
| negate_ndc: 0, |
| }; |
| |
| gl.bind_buffer( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| Some(&self.resources.slot_config_buffer), |
| ); |
| let slot_config_data = bytemuck::bytes_of(&slot_config); |
| gl.buffer_data_with_u8_array( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| slot_config_data, |
| WebGl2RenderingContext::STATIC_DRAW, |
| ); |
| } |
| |
| // Update clear config buffer. |
| // TODO: This can be done once, and doesn't need to be done on every `prepare` call. |
| { |
| let clear_config = ClearSlotsConfig { |
| slot_width: u32::from(WideTile::WIDTH), |
| slot_height: u32::from(Tile::HEIGHT), |
| texture_height: u32::from(Tile::HEIGHT) * total_slots, |
| _padding: 0, |
| }; |
| |
| gl.bind_buffer( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| Some(&self.resources.clear_config_buffer), |
| ); |
| let clear_config_data = bytemuck::bytes_of(&clear_config); |
| gl.buffer_data_with_u8_array( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| clear_config_data, |
| WebGl2RenderingContext::STATIC_DRAW, |
| ); |
| } |
| |
| self.render_size = new_render_size.clone(); |
| self.negate_ndc = negate_ndc; |
| } |
| } |
| |
| /// Upload alpha data to the texture. |
| fn upload_alpha_texture(&mut self, gl: &WebGl2RenderingContext, alphas: &mut Vec<u8>) { |
| if alphas.is_empty() { |
| return; |
| } |
| |
| let alpha_texture_width = self.resources.max_texture_dimension_2d; |
| let alpha_texture_height = self.resources.alpha_texture_height; |
| let total_size = alpha_texture_width as usize * alpha_texture_height as usize * 16; |
| |
| let original_len = alphas.len(); |
| |
| // Temporarily pad the length of the alphas to the texture size before uploading. |
| alphas.resize(total_size, 0); |
| |
| gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&self.resources.alphas_texture), |
| ); |
| |
| upload_data_to_rgba32_texture( |
| gl, |
| bytemuck::cast_slice::<u8, u32>(alphas), |
| alpha_texture_width, |
| alpha_texture_height, |
| ); |
| |
| // Truncate back to the original size. |
| alphas.truncate(original_len); |
| } |
| |
| /// Upload encoded paints to the texture. |
| fn upload_encoded_paints_texture( |
| &mut self, |
| gl: &WebGl2RenderingContext, |
| encoded_paints: &[GpuEncodedPaint], |
| ) { |
| if !encoded_paints.is_empty() { |
| let encoded_paints_texture_width = self.resources.max_texture_dimension_2d; |
| let encoded_paints_texture_height = self.resources.encoded_paints_texture_height; |
| |
| GpuEncodedPaint::serialize_to_buffer(encoded_paints, &mut self.encoded_paints_data); |
| |
| gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&self.resources.encoded_paints_texture), |
| ); |
| |
| upload_data_to_rgba32_texture( |
| gl, |
| bytemuck::cast_slice::<u8, u32>(&self.encoded_paints_data), |
| encoded_paints_texture_width, |
| encoded_paints_texture_height, |
| ); |
| } |
| } |
| |
| /// Upload gradient data to the texture. |
| fn upload_gradient_texture( |
| &mut self, |
| gl: &WebGl2RenderingContext, |
| gradient_cache: &mut GradientRampCache, |
| ) { |
| if gradient_cache.is_empty() { |
| return; |
| } |
| |
| let gradient_texture_width = self.resources.max_texture_dimension_2d; |
| let gradient_texture_height = self.resources.gradient_texture_height; |
| let total_capacity = (gradient_texture_width * gradient_texture_height * 4) as usize; |
| |
| // Take ownership of the luts to avoid copying, then resize for texture padding. |
| let mut luts = gradient_cache.take_luts(); |
| luts.resize(total_capacity, 0); |
| |
| gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&self.resources.gradient_texture), |
| ); |
| |
| gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( |
| WebGl2RenderingContext::TEXTURE_2D, |
| 0, |
| WebGl2RenderingContext::RGBA8 as i32, |
| gradient_texture_width as i32, |
| gradient_texture_height as i32, |
| 0, |
| WebGl2RenderingContext::RGBA, |
| WebGl2RenderingContext::UNSIGNED_BYTE, |
| Some(&luts), |
| ) |
| .unwrap(); |
| |
| // Restore the luts back to the cache. |
| gradient_cache.restore_luts(luts); |
| } |
| |
| /// Clear the view framebuffer. |
| // TODO: Investigate adding tests for the clear_view behavior. |
| fn clear_view_framebuffer(&mut self, gl: &WebGl2RenderingContext) { |
| gl.bind_framebuffer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| self.resources.view_framebuffer_override.as_ref(), |
| ); |
| gl.clear_color(0.0, 0.0, 0.0, 0.0); |
| gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); |
| } |
| |
| /// Uploads a strip slice into the GPU instance buffer. |
| fn upload_strips(&mut self, gl: &WebGl2RenderingContext, strips: &[GpuStrip]) { |
| if strips.is_empty() { |
| return; |
| } |
| |
| gl.bind_buffer( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| Some(&self.resources.strips_buffer), |
| ); |
| |
| let strip_bytes: &[u8] = bytemuck::cast_slice(strips); |
| gl.buffer_data_with_i32( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| strip_bytes.len() as i32, |
| WebGl2RenderingContext::DYNAMIC_DRAW, |
| ); |
| gl.buffer_sub_data_with_i32_and_u8_array( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| 0, |
| strip_bytes, |
| ); |
| } |
| } |
| |
| /// RAII guard for WebGL state management. |
| /// Automatically saves state on creation and restores it on drop. |
| /// Only saves/restores the state specified in the configuration. |
| struct WebGlStateGuard<'a> { |
| gl: &'a WebGl2RenderingContext, |
| config: WebGlStateConfig, |
| original_framebuffer: Option<WebGlFramebuffer>, |
| original_read_framebuffer: Option<WebGlFramebuffer>, |
| original_texture_2d_array: Option<WebGlTexture>, |
| scissor_enabled: bool, |
| viewport: [i32; 4], |
| } |
| |
| impl<'a> WebGlStateGuard<'a> { |
| /// Create a new state guard with custom configuration. |
| fn with_config(gl: &'a WebGl2RenderingContext, config: WebGlStateConfig) -> Self { |
| // Save current framebuffer binding if requested |
| let original_framebuffer = if config.framebuffer { |
| gl.get_parameter(WebGl2RenderingContext::FRAMEBUFFER_BINDING) |
| .ok() |
| .and_then(|v| v.dyn_into::<WebGlFramebuffer>().ok()) |
| } else { |
| None |
| }; |
| |
| // Save current read framebuffer binding if requested |
| let original_read_framebuffer = if config.read_framebuffer { |
| gl.get_parameter(WebGl2RenderingContext::READ_FRAMEBUFFER_BINDING) |
| .ok() |
| .and_then(|v| v.dyn_into::<WebGlFramebuffer>().ok()) |
| } else { |
| None |
| }; |
| |
| // Save current 2D array texture binding if requested |
| let original_texture_2d_array = if config.texture_2d_array { |
| gl.get_parameter(WebGl2RenderingContext::TEXTURE_BINDING_2D_ARRAY) |
| .ok() |
| .and_then(|v| v.dyn_into::<WebGlTexture>().ok()) |
| } else { |
| None |
| }; |
| |
| // Save current scissor test state if requested |
| let scissor_enabled = if config.scissor { |
| gl.get_parameter(WebGl2RenderingContext::SCISSOR_TEST) |
| .unwrap() |
| .as_bool() |
| .unwrap_or(false) |
| } else { |
| false |
| }; |
| |
| // Save current viewport if requested |
| let viewport = if config.viewport { |
| let viewport_js = gl |
| .get_parameter(WebGl2RenderingContext::VIEWPORT) |
| .unwrap() |
| .dyn_into::<js_sys::Int32Array>() |
| .unwrap(); |
| let viewport_vec = viewport_js.to_vec(); |
| [ |
| viewport_vec[0], |
| viewport_vec[1], |
| viewport_vec[2], |
| viewport_vec[3], |
| ] |
| } else { |
| [0, 0, 0, 0] |
| }; |
| |
| Self { |
| gl, |
| config, |
| original_framebuffer, |
| original_read_framebuffer, |
| original_texture_2d_array, |
| scissor_enabled, |
| viewport, |
| } |
| } |
| |
| /// Create a state guard for clearing an atlas region operations. |
| fn for_clear_atlas_region(gl: &'a WebGl2RenderingContext) -> Self { |
| Self::with_config( |
| gl, |
| WebGlStateConfig { |
| framebuffer: true, |
| scissor: true, |
| viewport: true, |
| ..Default::default() |
| }, |
| ) |
| } |
| |
| /// Create a state guard for texture copying operations. |
| fn for_texture_copy(gl: &'a WebGl2RenderingContext) -> Self { |
| Self::with_config( |
| gl, |
| WebGlStateConfig { |
| read_framebuffer: true, |
| texture_2d_array: true, |
| ..Default::default() |
| }, |
| ) |
| } |
| } |
| |
| impl Drop for WebGlStateGuard<'_> { |
| /// Restore WebGL state when the guard goes out of scope. |
| /// Only restores state that was configured to be saved. |
| fn drop(&mut self) { |
| // Restore scissor test state if it was saved |
| if self.config.scissor { |
| if self.scissor_enabled { |
| self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST); |
| } else { |
| self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST); |
| } |
| } |
| |
| // Restore viewport if it was saved |
| if self.config.viewport { |
| self.gl.viewport( |
| self.viewport[0], |
| self.viewport[1], |
| self.viewport[2], |
| self.viewport[3], |
| ); |
| } |
| |
| // Restore original framebuffer if it was saved |
| if self.config.framebuffer { |
| self.gl.bind_framebuffer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| self.original_framebuffer.as_ref(), |
| ); |
| } |
| |
| // Restore original read framebuffer if it was saved |
| if self.config.read_framebuffer { |
| self.gl.bind_framebuffer( |
| WebGl2RenderingContext::READ_FRAMEBUFFER, |
| self.original_read_framebuffer.as_ref(), |
| ); |
| } |
| |
| // Restore original 2D array texture binding if it was saved |
| if self.config.texture_2d_array { |
| self.gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D_ARRAY, |
| self.original_texture_2d_array.as_ref(), |
| ); |
| } |
| } |
| } |
| /// Configuration for which WebGL state to save/restore. |
| #[derive(Debug, Default)] |
| struct WebGlStateConfig { |
| /// Save/restore framebuffer binding (`FRAMEBUFFER_BINDING`) |
| framebuffer: bool, |
| /// Save/restore read framebuffer binding (`READ_FRAMEBUFFER_BINDING`) |
| read_framebuffer: bool, |
| /// Save/restore 2D array texture binding (`TEXTURE_BINDING_2D_ARRAY`) |
| texture_2d_array: bool, |
| /// Save/restore scissor test state |
| scissor: bool, |
| /// Save/restore viewport |
| viewport: bool, |
| } |
| |
| #[cfg(feature = "probe")] |
| fn read_framebuffer_rgba8( |
| gl: &WebGl2RenderingContext, |
| framebuffer: &WebGlFramebuffer, |
| width: u16, |
| height: u16, |
| ) -> Pixmap { |
| let _state_guard = WebGlStateGuard::with_config( |
| gl, |
| WebGlStateConfig { |
| framebuffer: true, |
| ..Default::default() |
| }, |
| ); |
| |
| gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(framebuffer)); |
| let mut pixmap = Pixmap::new(width, height); |
| gl.read_pixels_with_opt_u8_array( |
| 0, |
| 0, |
| i32::from(width), |
| i32::from(height), |
| WebGl2RenderingContext::RGBA, |
| WebGl2RenderingContext::UNSIGNED_BYTE, |
| Some(pixmap.data_as_u8_slice_mut()), |
| ) |
| .unwrap(); |
| pixmap.recompute_may_have_opacities(); |
| pixmap |
| } |
| |
| /// Create a WebGL shader program from vertex and fragment sources. |
| fn create_shader_program( |
| gl: &WebGl2RenderingContext, |
| vertex_src: &str, |
| fragment_src: &str, |
| ) -> WebGlProgram { |
| // Compile vertex shader. |
| let vertex_shader = gl |
| .create_shader(WebGl2RenderingContext::VERTEX_SHADER) |
| .unwrap(); |
| gl.shader_source(&vertex_shader, vertex_src); |
| gl.compile_shader(&vertex_shader); |
| |
| if !gl |
| .get_shader_parameter(&vertex_shader, WebGl2RenderingContext::COMPILE_STATUS) |
| .as_bool() |
| .unwrap_or(false) |
| { |
| let info = gl |
| .get_shader_info_log(&vertex_shader) |
| .unwrap_or_else(|| "Unknown error creating vertex shader".into()); |
| panic!("Failed to compile vertex shader: {info}"); |
| } |
| |
| // Compile fragment shader. |
| let fragment_shader = gl |
| .create_shader(WebGl2RenderingContext::FRAGMENT_SHADER) |
| .unwrap(); |
| gl.shader_source(&fragment_shader, fragment_src); |
| gl.compile_shader(&fragment_shader); |
| |
| if !gl |
| .get_shader_parameter(&fragment_shader, WebGl2RenderingContext::COMPILE_STATUS) |
| .as_bool() |
| .unwrap_or(false) |
| { |
| let info = gl |
| .get_shader_info_log(&fragment_shader) |
| .unwrap_or_else(|| "Unknown error creating fragment shader".into()); |
| panic!("Failed to compile fragment shader: {info}"); |
| } |
| |
| // Create and link the program. |
| let program = gl.create_program().unwrap(); |
| gl.attach_shader(&program, &vertex_shader); |
| gl.attach_shader(&program, &fragment_shader); |
| gl.link_program(&program); |
| |
| if !gl |
| .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS) |
| .as_bool() |
| .unwrap_or(false) |
| { |
| let info = gl |
| .get_program_info_log(&program) |
| .unwrap_or_else(|| "Unknown error creating program".into()); |
| panic!("Failed to link program: {info}"); |
| } |
| |
| gl.delete_shader(Some(&vertex_shader)); |
| gl.delete_shader(Some(&fragment_shader)); |
| |
| program |
| } |
| |
| /// Get the uniform locations for the `render_strips` program. |
| fn get_strip_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> StripUniforms { |
| get_optional_strip_uniforms( |
| gl, |
| program, |
| Some(render_strips::vertex::CONFIG), |
| Some(render_strips::fragment::CONFIG), |
| Some(render_strips::fragment::ALPHAS_TEXTURE), |
| Some(render_strips::fragment::CLIP_INPUT_TEXTURE), |
| Some(render_strips::fragment::ATLAS_TEXTURE_ARRAY), |
| Some(render_strips::fragment::ENCODED_PAINTS_TEXTURE), |
| Some(render_strips::vertex::ENCODED_PAINTS_TEXTURE), |
| Some(render_strips::fragment::GRADIENT_TEXTURE), |
| ) |
| } |
| |
| fn get_opaque_solid_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> StripUniforms { |
| get_optional_strip_uniforms( |
| gl, |
| program, |
| Some(opaque_solid::vertex::CONFIG), |
| None, |
| None, |
| None, |
| None, |
| None, |
| None, |
| None, |
| ) |
| } |
| |
| fn get_opaque_image_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> StripUniforms { |
| get_optional_strip_uniforms( |
| gl, |
| program, |
| Some(opaque_image::vertex::CONFIG), |
| Some(opaque_image::fragment::CONFIG), |
| None, |
| None, |
| Some(opaque_image::fragment::ATLAS_TEXTURE_ARRAY), |
| Some(opaque_image::fragment::ENCODED_PAINTS_TEXTURE), |
| Some(opaque_image::vertex::ENCODED_PAINTS_TEXTURE), |
| None, |
| ) |
| } |
| |
| fn get_opaque_image_untinted_uniforms( |
| gl: &WebGl2RenderingContext, |
| program: &WebGlProgram, |
| ) -> StripUniforms { |
| get_optional_strip_uniforms( |
| gl, |
| program, |
| Some(opaque_image_untinted::vertex::CONFIG), |
| Some(opaque_image_untinted::fragment::CONFIG), |
| None, |
| None, |
| Some(opaque_image_untinted::fragment::ATLAS_TEXTURE_ARRAY), |
| Some(opaque_image_untinted::fragment::ENCODED_PAINTS_TEXTURE), |
| Some(opaque_image_untinted::vertex::ENCODED_PAINTS_TEXTURE), |
| None, |
| ) |
| } |
| |
| fn get_opaque_gradient_uniforms( |
| gl: &WebGl2RenderingContext, |
| program: &WebGlProgram, |
| ) -> StripUniforms { |
| get_optional_strip_uniforms( |
| gl, |
| program, |
| Some(opaque_gradient::vertex::CONFIG), |
| Some(opaque_gradient::fragment::CONFIG), |
| None, |
| None, |
| None, |
| Some(opaque_gradient::fragment::ENCODED_PAINTS_TEXTURE), |
| None, |
| Some(opaque_gradient::fragment::GRADIENT_TEXTURE), |
| ) |
| } |
| |
| #[expect( |
| clippy::too_many_arguments, |
| reason = "Uniform metadata is naturally wide here." |
| )] |
| fn get_optional_strip_uniforms( |
| gl: &WebGl2RenderingContext, |
| program: &WebGlProgram, |
| config_vs_name: Option<&str>, |
| config_fs_name: Option<&str>, |
| alphas_texture_name: Option<&str>, |
| clip_input_texture_name: Option<&str>, |
| atlas_texture_array_name: Option<&str>, |
| encoded_paints_texture_fs_name: Option<&str>, |
| encoded_paints_texture_vs_name: Option<&str>, |
| gradient_texture_name: Option<&str>, |
| ) -> StripUniforms { |
| let _config_vs_block_index = config_vs_name.and_then(|name| { |
| let idx = gl.get_uniform_block_index(program, name); |
| if idx == WebGl2RenderingContext::INVALID_INDEX { |
| None |
| } else { |
| gl.uniform_block_binding(program, idx, 0); |
| Some(idx) |
| } |
| }); |
| |
| let _config_fs_block_index = config_fs_name.and_then(|name| { |
| let idx = gl.get_uniform_block_index(program, name); |
| if idx == WebGl2RenderingContext::INVALID_INDEX { |
| None |
| } else { |
| gl.uniform_block_binding(program, idx, 0); |
| Some(idx) |
| } |
| }); |
| |
| StripUniforms { |
| alphas_texture: alphas_texture_name.and_then(|name| gl.get_uniform_location(program, name)), |
| clip_input_texture: clip_input_texture_name |
| .and_then(|name| gl.get_uniform_location(program, name)), |
| atlas_texture_array: atlas_texture_array_name |
| .and_then(|name| gl.get_uniform_location(program, name)), |
| encoded_paints_texture_fs: encoded_paints_texture_fs_name |
| .and_then(|name| gl.get_uniform_location(program, name)), |
| encoded_paints_texture_vs: encoded_paints_texture_vs_name |
| .and_then(|name| gl.get_uniform_location(program, name)), |
| gradient_texture: gradient_texture_name |
| .and_then(|name| gl.get_uniform_location(program, name)), |
| } |
| } |
| |
| /// Get the uniform locations for the `clear_slots` program. |
| fn get_clear_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> ClearUniforms { |
| let config_name = clear_slots::vertex::CONFIG; |
| let config_block_index = gl.get_uniform_block_index(program, config_name); |
| |
| debug_assert_ne!( |
| config_block_index, |
| WebGl2RenderingContext::INVALID_INDEX, |
| "invalid uniform index" |
| ); |
| |
| // Bind uniform block to binding point. |
| gl.uniform_block_binding(program, config_block_index, 0); |
| |
| ClearUniforms { config_block_index } |
| } |
| |
| fn get_filter_pass_uniforms( |
| gl: &WebGl2RenderingContext, |
| program: &WebGlProgram, |
| ) -> FilterPassUniforms { |
| let filter_data = gl |
| .get_uniform_location(program, filters::fragment::FILTER_DATA) |
| .unwrap(); |
| let in_tex = gl |
| .get_uniform_location(program, filters::fragment::IN_TEX) |
| .unwrap(); |
| let original_tex = gl |
| .get_uniform_location(program, filters::fragment::ORIGINAL_TEX) |
| .unwrap(); |
| FilterPassUniforms { |
| filter_data, |
| in_tex, |
| original_tex, |
| } |
| } |
| |
| fn create_filter_atlas_texture( |
| gl: &WebGl2RenderingContext, |
| width: u32, |
| height: u32, |
| ) -> WebGlTexture { |
| let texture = gl.create_texture().unwrap(); |
| gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture)); |
| gl.tex_parameteri( |
| WebGl2RenderingContext::TEXTURE_2D, |
| WebGl2RenderingContext::TEXTURE_MIN_FILTER, |
| WebGl2RenderingContext::LINEAR as i32, |
| ); |
| gl.tex_parameteri( |
| WebGl2RenderingContext::TEXTURE_2D, |
| WebGl2RenderingContext::TEXTURE_MAG_FILTER, |
| WebGl2RenderingContext::LINEAR as i32, |
| ); |
| gl.tex_parameteri( |
| WebGl2RenderingContext::TEXTURE_2D, |
| WebGl2RenderingContext::TEXTURE_WRAP_S, |
| WebGl2RenderingContext::CLAMP_TO_EDGE as i32, |
| ); |
| gl.tex_parameteri( |
| WebGl2RenderingContext::TEXTURE_2D, |
| WebGl2RenderingContext::TEXTURE_WRAP_T, |
| WebGl2RenderingContext::CLAMP_TO_EDGE as i32, |
| ); |
| gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( |
| WebGl2RenderingContext::TEXTURE_2D, |
| 0, |
| WebGl2RenderingContext::RGBA8 as i32, |
| width as i32, |
| height as i32, |
| 0, |
| WebGl2RenderingContext::RGBA, |
| WebGl2RenderingContext::UNSIGNED_BYTE, |
| None, |
| ) |
| .unwrap(); |
| texture |
| } |
| |
| /// Vertex attribute layout for [`FilterInstanceData`]. |
| const FILTER_ATTRIBS: [(i32, i32); 9] = [ |
| (2, 0), // src_offset |
| (2, 8), // src_size |
| (2, 16), // dest_offset |
| (2, 24), // dest_size |
| (2, 32), // dest_atlas_size |
| (1, 40), // filter_data_offset |
| (2, 44), // original_offset |
| (2, 52), // original_size |
| (1, 60), // pass_kind |
| ]; |
| |
| const FILTER_INSTANCE_STRIDE: i32 = size_of::<FilterInstanceData>() as i32; |
| |
| fn initialize_filter_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) { |
| gl.bind_vertex_array(Some(&resources.filter_vao)); |
| gl.bind_buffer( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| Some(&resources.filter_instance_buffer), |
| ); |
| |
| for (loc, &(components, offset)) in FILTER_ATTRIBS.iter().enumerate() { |
| let loc = loc as u32; |
| gl.enable_vertex_attrib_array(loc); |
| gl.vertex_attrib_i_pointer_with_i32( |
| loc, |
| components, |
| WebGl2RenderingContext::UNSIGNED_INT, |
| FILTER_INSTANCE_STRIDE, |
| offset, |
| ); |
| gl.vertex_attrib_divisor(loc, 1); |
| } |
| |
| gl.bind_vertex_array(None); |
| } |
| |
| /// Create a texture with nearest neighbor sampling and clamp-to-edge wrapping. |
| fn create_texture(gl: &WebGl2RenderingContext) -> WebGlTexture { |
| create_texture_inner(gl, WebGl2RenderingContext::TEXTURE_2D) |
| } |
| |
| /// Create a texture array with nearest neighbor sampling and |
| /// clamp-to-edge wrapping. |
| fn create_texture_array(gl: &WebGl2RenderingContext) -> WebGlTexture { |
| create_texture_inner(gl, WebGl2RenderingContext::TEXTURE_2D_ARRAY) |
| } |
| |
| fn create_texture_inner(gl: &WebGl2RenderingContext, target: u32) -> WebGlTexture { |
| let texture = gl.create_texture().unwrap(); |
| gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| gl.bind_texture(target, Some(&texture)); |
| // The filter and wrap modes are irrelevant because the shader |
| // (`render_strips.wgsl`) exclusively uses `textureLoad`, which bypasses |
| // the sampler entirely. |
| gl.tex_parameteri( |
| target, |
| WebGl2RenderingContext::TEXTURE_MIN_FILTER, |
| WebGl2RenderingContext::NEAREST as i32, |
| ); |
| gl.tex_parameteri( |
| target, |
| WebGl2RenderingContext::TEXTURE_MAG_FILTER, |
| WebGl2RenderingContext::NEAREST as i32, |
| ); |
| gl.tex_parameteri( |
| target, |
| WebGl2RenderingContext::TEXTURE_WRAP_S, |
| WebGl2RenderingContext::CLAMP_TO_EDGE as i32, |
| ); |
| gl.tex_parameteri( |
| target, |
| WebGl2RenderingContext::TEXTURE_WRAP_T, |
| WebGl2RenderingContext::CLAMP_TO_EDGE as i32, |
| ); |
| // Also only to be defensive, in theory this shouldn't be necessary since we use |
| // `NEAREST` for both filters. |
| gl.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_MAX_LEVEL, 0); |
| |
| texture |
| } |
| |
| /// Create all WebGL resources needed for rendering. |
| fn create_webgl_resources( |
| gl: &WebGl2RenderingContext, |
| image_cache: &ImageCache, |
| filter_context: &FilterContext, |
| slot_count: usize, |
| ) -> WebGlResources { |
| let strip_vao = gl.create_vertex_array().unwrap(); |
| let clear_vao = gl.create_vertex_array().unwrap(); |
| let filter_vao = gl.create_vertex_array().unwrap(); |
| let filter_instance_buffer = gl.create_buffer().unwrap(); |
| |
| let strips_buffer = gl.create_buffer().unwrap(); |
| let view_config_buffer = gl.create_buffer().unwrap(); |
| let slot_config_buffer = gl.create_buffer().unwrap(); |
| let clear_slot_indices_buffer = gl.create_buffer().unwrap(); |
| let clear_config_buffer = gl.create_buffer().unwrap(); |
| |
| // Create and configure alpha texture. |
| let alphas_texture = create_texture(gl); |
| |
| let AtlasConfig { |
| atlas_size: (atlas_width, atlas_height), |
| initial_atlas_count, |
| .. |
| } = image_cache.atlas_manager().config(); |
| let atlas_texture_array = |
| create_atlas_texture_array(gl, *atlas_width, *atlas_height, *initial_atlas_count as u32); |
| |
| // Create a 1x1 stub atlas texture array for use during render_to_atlas. |
| // This avoids binding the real atlas as a shader input while it is the render target. |
| let stub_atlas_texture_array = create_atlas_texture_array(gl, 1, 1, 1); |
| |
| // Create and configure encoded paints texture. |
| let encoded_paints_texture = create_texture(gl); |
| |
| // Create and configure gradient texture. |
| let gradient_texture = create_texture(gl); |
| |
| // Create slot textures and framebuffers. |
| let slot_textures: [WebGlTexture; 2] = [ |
| create_slot_texture(gl, slot_count), |
| create_slot_texture(gl, slot_count), |
| ]; |
| |
| let slot_framebuffers: [WebGlFramebuffer; 2] = [ |
| create_framebuffer_for_texture(gl, &slot_textures[0]), |
| create_framebuffer_for_texture(gl, &slot_textures[1]), |
| ]; |
| |
| let max_texture_dimension_2d = get_max_texture_dimension_2d(gl); |
| |
| let filter_data_texture = create_texture(gl); |
| let filter_config_buffer = gl.create_buffer().unwrap(); |
| |
| let AtlasConfig { |
| atlas_size: (filter_atlas_width, filter_atlas_height), |
| .. |
| } = filter_context.image_cache.atlas_manager().config(); |
| |
| WebGlResources { |
| strip_vao, |
| strips_buffer, |
| alphas_texture, |
| alpha_texture_height: 0, |
| atlas_texture_array, |
| encoded_paints_texture, |
| encoded_paints_texture_height: 0, |
| gradient_texture, |
| gradient_texture_height: 0, |
| view_config_buffer, |
| slot_config_buffer, |
| clear_slot_indices_buffer, |
| clear_vao, |
| clear_config_buffer, |
| slot_textures, |
| slot_framebuffers, |
| view_framebuffer_override: None, |
| depth_cleared_this_frame: false, |
| // Note: we use DEPTH (not DEPTH_ATTACHMENT) because we render to the default |
| // framebuffer. If we ever support non-default framebuffers, this must change |
| // to DEPTH_ATTACHMENT. |
| depth_attachment_array: js_sys::Array::of1(&WebGl2RenderingContext::DEPTH.into()), |
| max_texture_dimension_2d, |
| stub_atlas_texture_array, |
| atlas_render_framebuffer: None, |
| filter_main_atlas_framebuffer: None, |
| filter_atlas_textures: Vec::new(), |
| filter_atlas_framebuffers: Vec::new(), |
| filter_data_texture, |
| filter_data_texture_height: 0, |
| filter_instance_buffer, |
| filter_vao, |
| filter_config_buffer, |
| filter_atlas_width: *filter_atlas_width, |
| filter_atlas_height: *filter_atlas_height, |
| } |
| } |
| |
| /// Create an atlas texture array. |
| fn create_atlas_texture_array( |
| gl: &WebGl2RenderingContext, |
| width: u32, |
| height: u32, |
| layer_count: u32, |
| ) -> WebGlTextureArray { |
| let atlas_texture = create_texture_array(gl); |
| |
| // Initialize with empty texture array data |
| gl.tex_image_3d_with_opt_u8_array( |
| WebGl2RenderingContext::TEXTURE_2D_ARRAY, |
| 0, |
| WebGl2RenderingContext::RGBA8 as i32, |
| width as i32, |
| height as i32, |
| layer_count as i32, |
| 0, |
| WebGl2RenderingContext::RGBA, |
| WebGl2RenderingContext::UNSIGNED_BYTE, |
| None, |
| ) |
| .unwrap(); |
| |
| WebGlTextureArray::new(atlas_texture, width, height, layer_count) |
| } |
| |
| /// Create a texture for slot rendering. |
| fn create_slot_texture(gl: &WebGl2RenderingContext, slot_count: usize) -> WebGlTexture { |
| let texture = create_texture(gl); |
| |
| gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( |
| WebGl2RenderingContext::TEXTURE_2D, |
| 0, |
| WebGl2RenderingContext::RGBA8 as i32, |
| u32::from(WideTile::WIDTH) as i32, |
| (u32::from(Tile::HEIGHT) * slot_count as u32) as i32, |
| 0, |
| WebGl2RenderingContext::RGBA, |
| WebGl2RenderingContext::UNSIGNED_BYTE, |
| None, |
| ) |
| .unwrap(); |
| |
| texture |
| } |
| |
| /// Create a framebuffer for a texture. |
| fn create_framebuffer_for_texture( |
| gl: &WebGl2RenderingContext, |
| texture: &WebGlTexture, |
| ) -> WebGlFramebuffer { |
| let framebuffer = gl.create_framebuffer().unwrap(); |
| gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&framebuffer)); |
| |
| gl.framebuffer_texture_2d( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| WebGl2RenderingContext::COLOR_ATTACHMENT0, |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(texture), |
| 0, |
| ); |
| |
| framebuffer |
| } |
| |
| const STRIP_STRIDE: i32 = size_of::<GpuStrip>() as i32; |
| const STRIP_ATTR_COUNT: i32 = STRIP_STRIDE / 4; |
| const _: () = assert!( |
| STRIP_STRIDE == 24, |
| "GpuStrip layout must match strip vertex stride" |
| ); |
| |
| /// Initialize strip VAO. |
| fn initialize_strip_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) { |
| gl.bind_vertex_array(Some(&resources.strip_vao)); |
| gl.bind_buffer( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| Some(&resources.strips_buffer), |
| ); |
| |
| for i in 0..STRIP_ATTR_COUNT { |
| let location = i as u32; |
| let offset = i * 4; |
| |
| gl.enable_vertex_attrib_array(location); |
| gl.vertex_attrib_i_pointer_with_i32( |
| location, |
| 1, |
| WebGl2RenderingContext::UNSIGNED_INT, |
| STRIP_STRIDE, |
| offset, |
| ); |
| |
| gl.vertex_attrib_divisor(location, 1); |
| } |
| |
| gl.bind_vertex_array(None); |
| } |
| |
| /// Initialize clear VAO. |
| fn initialize_clear_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) { |
| gl.bind_vertex_array(Some(&resources.clear_vao)); |
| gl.bind_buffer( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| Some(&resources.clear_slot_indices_buffer), |
| ); |
| |
| // Configure attributes. |
| let slot_idx_loc = 0; |
| gl.enable_vertex_attrib_array(slot_idx_loc); |
| gl.vertex_attrib_i_pointer_with_i32( |
| slot_idx_loc, |
| 1, |
| WebGl2RenderingContext::UNSIGNED_INT, |
| 4, |
| 0, |
| ); |
| gl.vertex_attrib_divisor(slot_idx_loc, 1); |
| |
| gl.bind_vertex_array(None); |
| } |
| |
| /// Context for WebGL rendering operations. |
| // TODO: Improve buffer management. Currently a single buffer is used per resource, which means that |
| // the GPU must finish drawing before the next `upload_strip_pair` can be executed (effectively pausing |
| // execution). Investigate a buffer pool or creating a new buffer per pass. |
| struct WebGlRendererContext<'a> { |
| programs: &'a mut WebGlPrograms, |
| gl: &'a WebGl2RenderingContext, |
| image_cache: &'a ImageCache, |
| encoded_paints: &'a [EncodedPaint], |
| paint_idxs: &'a [u32], |
| filter_context: &'a FilterContext, |
| filter_pass_state: &'a mut FilterPassState, |
| } |
| |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| enum OpaqueBucket { |
| ImageUntinted, |
| ImageTinted, |
| Solid, |
| Gradient, |
| } |
| |
| impl OpaqueBucket { |
| fn from_strip(strip: &GpuStrip, encoded_paints: &[EncodedPaint], paint_idxs: &[u32]) -> Self { |
| const COLOR_SOURCE_PAYLOAD: u32 = 0; |
| const PAINT_TYPE_SOLID: u32 = 0; |
| const PAINT_TYPE_IMAGE: u32 = 1; |
| const PAINT_TYPE_LINEAR_GRADIENT: u32 = 2; |
| const PAINT_TYPE_RADIAL_GRADIENT: u32 = 3; |
| const PAINT_TYPE_SWEEP_GRADIENT: u32 = 4; |
| |
| let paint_and_rect_flag = strip.paint_and_rect_flag; |
| let color_source = (paint_and_rect_flag >> 29) & 0x3; |
| debug_assert_eq!( |
| color_source, COLOR_SOURCE_PAYLOAD, |
| "only payload-backed strips may be emitted as opaque" |
| ); |
| |
| match (paint_and_rect_flag >> 26) & 0x7 { |
| PAINT_TYPE_SOLID => Self::Solid, |
| PAINT_TYPE_IMAGE => { |
| let paint_tex_idx = paint_and_rect_flag & 0x03FF_FFFF; |
| let encoded_paint_idx = paint_idxs[..encoded_paints.len()] |
| .binary_search(&paint_tex_idx) |
| .expect("opaque image strip must reference a valid encoded paint"); |
| let EncodedPaint::Image(img) = &encoded_paints[encoded_paint_idx] else { |
| panic!("opaque image strip must reference an encoded image paint"); |
| }; |
| if img.tint.is_some() { |
| Self::ImageTinted |
| } else { |
| Self::ImageUntinted |
| } |
| } |
| PAINT_TYPE_LINEAR_GRADIENT | PAINT_TYPE_RADIAL_GRADIENT | PAINT_TYPE_SWEEP_GRADIENT => { |
| Self::Gradient |
| } |
| paint_type => panic!("unexpected opaque paint type {paint_type}"), |
| } |
| } |
| } |
| |
| fn bind_strip_program( |
| gl: &WebGl2RenderingContext, |
| program: &WebGlProgram, |
| uniforms: &StripUniforms, |
| resources: &WebGlResources, |
| clip_texture_idx: usize, |
| ) { |
| gl.use_program(Some(program)); |
| |
| if let Some(location) = &uniforms.alphas_texture { |
| gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&resources.alphas_texture), |
| ); |
| gl.uniform1i(Some(location), 0); |
| } |
| |
| if let Some(location) = &uniforms.clip_input_texture { |
| gl.active_texture(WebGl2RenderingContext::TEXTURE1); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&resources.slot_textures[clip_texture_idx]), |
| ); |
| gl.uniform1i(Some(location), 1); |
| } |
| |
| if let Some(location) = &uniforms.atlas_texture_array { |
| gl.active_texture(WebGl2RenderingContext::TEXTURE2); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D_ARRAY, |
| Some(&resources.atlas_texture_array.texture), |
| ); |
| gl.uniform1i(Some(location), 2); |
| } |
| |
| if let Some(location) = &uniforms.encoded_paints_texture_fs { |
| gl.active_texture(WebGl2RenderingContext::TEXTURE3); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&resources.encoded_paints_texture), |
| ); |
| gl.uniform1i(Some(location), 3); |
| } |
| |
| if let Some(location) = &uniforms.encoded_paints_texture_vs { |
| gl.active_texture(WebGl2RenderingContext::TEXTURE3); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&resources.encoded_paints_texture), |
| ); |
| gl.uniform1i(Some(location), 3); |
| } |
| |
| if let Some(location) = &uniforms.gradient_texture { |
| gl.active_texture(WebGl2RenderingContext::TEXTURE4); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&resources.gradient_texture), |
| ); |
| gl.uniform1i(Some(location), 4); |
| } |
| } |
| |
| fn set_strip_instance_offset(gl: &WebGl2RenderingContext, base_instance: i32) { |
| let base_byte_offset = base_instance * STRIP_STRIDE; |
| for i in 0..STRIP_ATTR_COUNT { |
| gl.vertex_attrib_i_pointer_with_i32( |
| i as u32, |
| 1, |
| WebGl2RenderingContext::UNSIGNED_INT, |
| STRIP_STRIDE, |
| i * 4 + base_byte_offset, |
| ); |
| } |
| } |
| |
| impl WebGlRendererContext<'_> { |
| /// Render strips to the specified render target. |
| fn do_strip_render_pass( |
| &mut self, |
| opaque_strips: &[GpuStrip], |
| alpha_strips: &[GpuStrip], |
| target: StripPassRenderTarget, |
| load: LoadOp, |
| ) { |
| if opaque_strips.is_empty() && alpha_strips.is_empty() { |
| return; |
| } |
| |
| let scissor_rect = match &target { |
| StripPassRenderTarget::FilterLayer(layer_id) => { |
| let image_id = self |
| .filter_context |
| .filter_textures |
| .get(layer_id) |
| .unwrap() |
| .initial_image_id; |
| let resources = self.filter_context.image_cache.get(image_id).unwrap(); |
| let atlas_idx = resources.atlas_id.as_u32() as usize; |
| let fb = &self.programs.resources.filter_atlas_framebuffers[atlas_idx]; |
| |
| self.gl |
| .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(fb)); |
| |
| let atlas_width = self.programs.resources.filter_atlas_width; |
| let atlas_height = self.programs.resources.filter_atlas_height; |
| self.gl |
| .viewport(0, 0, atlas_width as i32, atlas_height as i32); |
| |
| let filter_textures = self.filter_context.filter_textures.get(layer_id).unwrap(); |
| let strip_offset_x = resources.offset[0] as i32 |
| - (filter_textures.bbox.x0() * WideTile::WIDTH) as i32; |
| let strip_offset_y = |
| resources.offset[1] as i32 - (filter_textures.bbox.y0() * Tile::HEIGHT) as i32; |
| |
| let config = Config { |
| width: atlas_width, |
| height: atlas_height, |
| strip_height: u32::from(Tile::HEIGHT), |
| alphas_tex_width_bits: self |
| .programs |
| .resources |
| .max_texture_dimension_2d |
| .trailing_zeros(), |
| encoded_paints_tex_width_bits: self |
| .programs |
| .resources |
| .max_texture_dimension_2d |
| .trailing_zeros(), |
| strip_offset_x, |
| strip_offset_y, |
| negate_ndc: 0, |
| }; |
| let buf = &self.programs.resources.filter_config_buffer; |
| self.gl |
| .bind_buffer(WebGl2RenderingContext::UNIFORM_BUFFER, Some(buf)); |
| self.gl.buffer_data_with_u8_array( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| bytemuck::bytes_of(&config), |
| WebGl2RenderingContext::DYNAMIC_DRAW, |
| ); |
| self.gl |
| .bind_buffer_base(WebGl2RenderingContext::UNIFORM_BUFFER, 0, Some(buf)); |
| |
| Some([ |
| resources.offset[0] as i32, |
| resources.offset[1] as i32, |
| resources.width as i32, |
| resources.height as i32, |
| ]) |
| } |
| StripPassRenderTarget::Root(_) => { |
| self.gl.bind_framebuffer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| self.programs.resources.view_framebuffer_override.as_ref(), |
| ); |
| let width = self.programs.render_size.width; |
| let height = self.programs.render_size.height; |
| self.gl.viewport(0, 0, width as i32, height as i32); |
| |
| self.gl.bind_buffer_base( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| 0, |
| Some(&self.programs.resources.view_config_buffer), |
| ); |
| |
| None |
| } |
| StripPassRenderTarget::SlotTexture(ix) => { |
| self.gl.bind_framebuffer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| Some(&self.programs.resources.slot_framebuffers[*ix as usize]), |
| ); |
| // Set viewport to match slot framebuffer. |
| // TODO: Remove the slot height texture calculation. |
| let total_slots: usize = (self.programs.resources.max_texture_dimension_2d |
| / u32::from(Tile::HEIGHT)) as usize; |
| // Set viewport to match slot texture. |
| let height = u32::from(Tile::HEIGHT) * total_slots as u32; |
| self.gl |
| .viewport(0, 0, i32::from(WideTile::WIDTH), height as i32); |
| |
| // Use slot config buffer for rendering to a slot texture. |
| self.gl.bind_buffer_base( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| 0, |
| Some(&self.programs.resources.slot_config_buffer), |
| ); |
| |
| None |
| } |
| }; |
| |
| if let Some([x, y, width, height]) = scissor_rect { |
| self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST); |
| self.gl.scissor(x, y, width, height); |
| } else { |
| self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST); |
| } |
| |
| // Clear framebuffer if requested. |
| if matches!(load, LoadOp::Clear) { |
| self.gl.clear_color(0.0, 0.0, 0.0, 0.0); |
| self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT); |
| } |
| |
| let clip_texture_idx = match &target { |
| StripPassRenderTarget::SlotTexture(1) => 0, |
| _ => 1, |
| }; |
| |
| // Set up attributes. |
| self.gl |
| .bind_vertex_array(Some(&self.programs.resources.strip_vao)); |
| self.gl.bind_buffer( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| Some(&self.programs.resources.strips_buffer), |
| ); |
| |
| // TODO: Today, we only support early-z rejection on the final view. If we wanted to support |
| // intermediate layers, we would require separate depth buffers for each target. We can explore |
| // that possibility in the future. |
| let is_final_view = matches!( |
| target, |
| StripPassRenderTarget::Root(RootRenderTarget::UserSurface) |
| ); |
| let mut image_untinted_opaque = Vec::new(); |
| let mut image_tinted_opaque = Vec::new(); |
| let mut solid_opaque = Vec::new(); |
| let mut gradient_opaque = Vec::new(); |
| let mut uploaded_strips = Vec::new(); |
| let (alpha_start, alpha_count) = if is_final_view { |
| image_untinted_opaque.reserve(opaque_strips.len()); |
| image_tinted_opaque.reserve(opaque_strips.len()); |
| solid_opaque.reserve(opaque_strips.len()); |
| gradient_opaque.reserve(opaque_strips.len()); |
| for strip in opaque_strips { |
| match OpaqueBucket::from_strip(strip, self.encoded_paints, self.paint_idxs) { |
| OpaqueBucket::ImageUntinted => image_untinted_opaque.push(*strip), |
| OpaqueBucket::ImageTinted => image_tinted_opaque.push(*strip), |
| OpaqueBucket::Solid => solid_opaque.push(*strip), |
| OpaqueBucket::Gradient => gradient_opaque.push(*strip), |
| } |
| } |
| |
| uploaded_strips.reserve( |
| image_untinted_opaque.len() |
| + image_tinted_opaque.len() |
| + solid_opaque.len() |
| + gradient_opaque.len() |
| + alpha_strips.len(), |
| ); |
| uploaded_strips.extend_from_slice(&image_untinted_opaque); |
| uploaded_strips.extend_from_slice(&image_tinted_opaque); |
| uploaded_strips.extend_from_slice(&solid_opaque); |
| uploaded_strips.extend_from_slice(&gradient_opaque); |
| let alpha_start = uploaded_strips.len() as i32; |
| uploaded_strips.extend_from_slice(alpha_strips); |
| (alpha_start, alpha_strips.len() as i32) |
| } else { |
| debug_assert!( |
| opaque_strips.is_empty(), |
| "opaque strips should only be emitted for the user surface" |
| ); |
| uploaded_strips.extend_from_slice(alpha_strips); |
| (0, alpha_strips.len() as i32) |
| }; |
| |
| let image_untinted_start = 0_i32; |
| let image_untinted_count = image_untinted_opaque.len() as i32; |
| let image_tinted_start = image_untinted_count; |
| let image_tinted_count = image_tinted_opaque.len() as i32; |
| let solid_start = image_tinted_start + image_tinted_count; |
| let solid_count = solid_opaque.len() as i32; |
| let gradient_start = solid_start + solid_count; |
| let gradient_count = gradient_opaque.len() as i32; |
| |
| self.programs.upload_strips(self.gl, &uploaded_strips); |
| set_strip_instance_offset(self.gl, 0); |
| |
| if is_final_view { |
| self.gl.enable(WebGl2RenderingContext::DEPTH_TEST); |
| self.gl.depth_func(WebGl2RenderingContext::LEQUAL); |
| |
| // Clear depth buffer on first use per frame. |
| if !self.programs.resources.depth_cleared_this_frame { |
| self.programs.resources.depth_cleared_this_frame = true; |
| self.gl.clear_depth(1.0); |
| self.gl.clear(WebGl2RenderingContext::DEPTH_BUFFER_BIT); |
| } |
| |
| // Opaque pass: front-to-back, depth test ON, depth write ON, blend OFF. |
| if image_untinted_count + image_tinted_count + solid_count + gradient_count > 0 { |
| self.gl.depth_mask(true); |
| self.gl.disable(WebGl2RenderingContext::BLEND); |
| } |
| if image_untinted_count > 0 { |
| bind_strip_program( |
| self.gl, |
| &self.programs.opaque_image_untinted_program, |
| &self.programs.opaque_image_untinted_uniforms, |
| &self.programs.resources, |
| clip_texture_idx, |
| ); |
| set_strip_instance_offset(self.gl, image_untinted_start); |
| self.gl.draw_arrays_instanced( |
| WebGl2RenderingContext::TRIANGLE_STRIP, |
| 0, |
| 4, |
| image_untinted_count, |
| ); |
| } |
| if image_tinted_count > 0 { |
| bind_strip_program( |
| self.gl, |
| &self.programs.opaque_image_program, |
| &self.programs.opaque_image_uniforms, |
| &self.programs.resources, |
| clip_texture_idx, |
| ); |
| set_strip_instance_offset(self.gl, image_tinted_start); |
| self.gl.draw_arrays_instanced( |
| WebGl2RenderingContext::TRIANGLE_STRIP, |
| 0, |
| 4, |
| image_tinted_count, |
| ); |
| } |
| if solid_count > 0 { |
| bind_strip_program( |
| self.gl, |
| &self.programs.opaque_solid_program, |
| &self.programs.opaque_solid_uniforms, |
| &self.programs.resources, |
| clip_texture_idx, |
| ); |
| set_strip_instance_offset(self.gl, solid_start); |
| self.gl.draw_arrays_instanced( |
| WebGl2RenderingContext::TRIANGLE_STRIP, |
| 0, |
| 4, |
| solid_count, |
| ); |
| } |
| if gradient_count > 0 { |
| bind_strip_program( |
| self.gl, |
| &self.programs.opaque_gradient_program, |
| &self.programs.opaque_gradient_uniforms, |
| &self.programs.resources, |
| clip_texture_idx, |
| ); |
| set_strip_instance_offset(self.gl, gradient_start); |
| self.gl.draw_arrays_instanced( |
| WebGl2RenderingContext::TRIANGLE_STRIP, |
| 0, |
| 4, |
| gradient_count, |
| ); |
| } |
| |
| // Alpha pass: back-to-front, depth test ON, depth write OFF, blend ON. |
| if alpha_count > 0 { |
| bind_strip_program( |
| self.gl, |
| &self.programs.strip_program, |
| &self.programs.strip_uniforms, |
| &self.programs.resources, |
| clip_texture_idx, |
| ); |
| set_strip_instance_offset(self.gl, alpha_start); |
| self.gl.depth_mask(false); |
| self.gl.enable(WebGl2RenderingContext::BLEND); |
| self.gl.draw_arrays_instanced( |
| WebGl2RenderingContext::TRIANGLE_STRIP, |
| 0, |
| 4, |
| alpha_count, |
| ); |
| } |
| |
| // Restore state. |
| self.gl.disable(WebGl2RenderingContext::DEPTH_TEST); |
| self.gl.depth_mask(true); |
| self.gl.enable(WebGl2RenderingContext::BLEND); |
| set_strip_instance_offset(self.gl, 0); |
| } else { |
| // Slot texture / intermediate: single draw with blending, no depth. |
| bind_strip_program( |
| self.gl, |
| &self.programs.strip_program, |
| &self.programs.strip_uniforms, |
| &self.programs.resources, |
| clip_texture_idx, |
| ); |
| self.gl.draw_arrays_instanced( |
| WebGl2RenderingContext::TRIANGLE_STRIP, |
| 0, |
| 4, |
| alpha_count, |
| ); |
| } |
| |
| // Clean up. |
| self.gl.bind_vertex_array(None); |
| } |
| |
| /// Clear specific slots from a slot texture. |
| fn do_clear_slots_render_pass(&mut self, ix: usize, slot_indices: &[u32]) { |
| if slot_indices.is_empty() { |
| return; |
| } |
| |
| // No blending needed for clearing: we want to completely overwrite existing slot data |
| // (matches wgpu implementation) |
| self.gl.disable(WebGl2RenderingContext::BLEND); |
| |
| // Upload slot indices. |
| self.gl.bind_buffer( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| Some(&self.programs.resources.clear_slot_indices_buffer), |
| ); |
| let slot_indices_data = bytemuck::cast_slice(slot_indices); |
| self.gl.buffer_data_with_u8_array( |
| WebGl2RenderingContext::ARRAY_BUFFER, |
| slot_indices_data, |
| WebGl2RenderingContext::STATIC_DRAW, |
| ); |
| |
| // Bind framebuffer and setup viewport. |
| self.gl.bind_framebuffer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| Some(&self.programs.resources.slot_framebuffers[ix]), |
| ); |
| // TODO: Remove the slot height texture calculation. |
| let total_slots: usize = |
| (self.programs.resources.max_texture_dimension_2d / u32::from(Tile::HEIGHT)) as usize; |
| let height = u32::from(Tile::HEIGHT) * total_slots as u32; |
| self.gl |
| .viewport(0, 0, i32::from(WideTile::WIDTH), height as i32); |
| |
| // Setup clear program. |
| self.gl.use_program(Some(&self.programs.clear_program)); |
| |
| // Set up attributes. |
| self.gl |
| .bind_vertex_array(Some(&self.programs.resources.clear_vao)); |
| |
| // Set up clear config. |
| self.gl.bind_buffer_base( |
| WebGl2RenderingContext::UNIFORM_BUFFER, |
| self.programs.clear_uniforms.config_block_index, |
| Some(&self.programs.resources.clear_config_buffer), |
| ); |
| |
| // Draw. |
| self.gl.draw_arrays_instanced( |
| WebGl2RenderingContext::TRIANGLE_STRIP, |
| 0, |
| 4, |
| slot_indices.len() as i32, |
| ); |
| |
| self.gl.enable(WebGl2RenderingContext::BLEND); |
| |
| // Clean up. |
| self.gl.bind_vertex_array(None); |
| } |
| } |
| |
| impl RendererBackend for WebGlRendererContext<'_> { |
| /// Clear specific slots in a texture |
| fn clear_slots(&mut self, texture_index: usize, slots: &[u32]) { |
| self.do_clear_slots_render_pass(texture_index, slots); |
| } |
| |
| /// Execute a render pass for strips. |
| fn render_strips( |
| &mut self, |
| opaque_strips: &[GpuStrip], |
| alpha_strips: &[GpuStrip], |
| target: StripPassRenderTarget, |
| load_op: LoadOp, |
| ) { |
| self.do_strip_render_pass(opaque_strips, alpha_strips, target, load_op); |
| } |
| |
| fn apply_filter(&mut self, layer_id: LayerId) { |
| let filter_atlas_width = self.programs.resources.filter_atlas_width; |
| let filter_atlas_height = self.programs.resources.filter_atlas_height; |
| |
| self.filter_context.build_filter_passes( |
| self.filter_pass_state, |
| &layer_id, |
| self.image_cache, |
| |_atlas_idx| [filter_atlas_width, filter_atlas_height], |
| || { |
| [ |
| self.programs.resources.atlas_texture_array.size.width, |
| self.programs.resources.atlas_texture_array.size.height, |
| ] |
| }, |
| ); |
| |
| let filter_passes = self.filter_pass_state.filter_passes(); |
| if filter_passes.is_empty() { |
| return; |
| } |
| |
| self.gl.disable(WebGl2RenderingContext::BLEND); |
| self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST); |
| |
| let instances = self.filter_pass_state.instances(); |
| self.programs.upload_filter_instances(self.gl, instances); |
| |
| self.gl.use_program(Some(&self.programs.filter_program)); |
| self.gl |
| .bind_vertex_array(Some(&self.programs.resources.filter_vao)); |
| |
| self.gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| self.gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(&self.programs.resources.filter_data_texture), |
| ); |
| self.gl |
| .uniform1i(Some(&self.programs.filter_uniforms.filter_data), 0); |
| |
| for (i, pass) in filter_passes.iter().enumerate() { |
| // Base points to the correct offset for that specific filter pass. |
| let base = (i as i32) * FILTER_INSTANCE_STRIDE; |
| |
| for (loc, &(components, offset)) in FILTER_ATTRIBS.iter().enumerate() { |
| self.gl.vertex_attrib_i_pointer_with_i32( |
| loc as u32, |
| components, |
| WebGl2RenderingContext::UNSIGNED_INT, |
| FILTER_INSTANCE_STRIDE, |
| base + offset, |
| ); |
| } |
| |
| let (target_width, target_height) = match &pass.output { |
| FilterPassTarget::FilterAtlas(idx) => { |
| let fb = &self.programs.resources.filter_atlas_framebuffers[*idx as usize]; |
| self.gl |
| .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(fb)); |
| self.gl |
| .viewport(0, 0, filter_atlas_width as i32, filter_atlas_height as i32); |
| (filter_atlas_width, filter_atlas_height) |
| } |
| FilterPassTarget::MainAtlas(idx) => { |
| let fb = self |
| .programs |
| .resources |
| .filter_main_atlas_framebuffer |
| .get_or_insert_with(|| self.gl.create_framebuffer().unwrap()); |
| self.gl |
| .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(fb)); |
| self.gl.framebuffer_texture_layer( |
| WebGl2RenderingContext::FRAMEBUFFER, |
| WebGl2RenderingContext::COLOR_ATTACHMENT0, |
| Some(&self.programs.resources.atlas_texture_array.texture), |
| 0, |
| *idx as i32, |
| ); |
| let width = self.programs.resources.atlas_texture_array.size.width; |
| let height = self.programs.resources.atlas_texture_array.size.height; |
| self.gl.viewport(0, 0, width as i32, height as i32); |
| (width, height) |
| } |
| }; |
| |
| let instance = &instances[i]; |
| let [x, y, width, height] = instance.scissor_rect([target_width, target_height]); |
| self.gl |
| .scissor(x as i32, y as i32, width as i32, height as i32); |
| |
| let input_tex = |
| &self.programs.resources.filter_atlas_textures[pass.input_atlas_idx as usize]; |
| self.gl.active_texture(WebGl2RenderingContext::TEXTURE1); |
| self.gl |
| .bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(input_tex)); |
| self.gl |
| .uniform1i(Some(&self.programs.filter_uniforms.in_tex), 1); |
| |
| let original_tex_idx = pass.original_atlas_idx.unwrap_or(pass.input_atlas_idx); |
| let original_tex = |
| &self.programs.resources.filter_atlas_textures[original_tex_idx as usize]; |
| self.gl.active_texture(WebGl2RenderingContext::TEXTURE2); |
| self.gl |
| .bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(original_tex)); |
| self.gl |
| .uniform1i(Some(&self.programs.filter_uniforms.original_tex), 2); |
| |
| self.gl |
| .draw_arrays_instanced(WebGl2RenderingContext::TRIANGLE_STRIP, 0, 4, 1); |
| } |
| self.gl.bind_vertex_array(None); |
| self.gl.disable(WebGl2RenderingContext::SCISSOR_TEST); |
| self.gl.enable(WebGl2RenderingContext::BLEND); |
| } |
| } |
| |
| /// Trait for types that can write image data directly to the atlas texture in WebGL. |
| /// |
| /// This allows efficient uploading from different sources: |
| /// - `Pixmap`: Direct upload using raw pixel data |
| /// - `ImageData`: Browser `ImageData` objects |
| /// - Custom implementations for other image sources |
| pub trait WebGlAtlasWriter { |
| /// Get the width of the image. |
| fn width(&self) -> u32; |
| /// Get the height of the image. |
| fn height(&self) -> u32; |
| |
| /// Write image data to a specific layer of an atlas texture array at the specified offset. |
| fn write_to_atlas_layer( |
| &self, |
| gl: &WebGl2RenderingContext, |
| atlas_texture_array: &WebGlTexture, |
| layer: u32, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ); |
| } |
| |
| /// Implementation for `Pixmap` - direct upload using raw pixel data. |
| impl WebGlAtlasWriter for Pixmap { |
| fn width(&self) -> u32 { |
| self.width() as u32 |
| } |
| |
| fn height(&self) -> u32 { |
| self.height() as u32 |
| } |
| |
| fn write_to_atlas_layer( |
| &self, |
| gl: &WebGl2RenderingContext, |
| atlas_texture_array: &WebGlTexture, |
| layer: u32, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ) { |
| // Bind the atlas texture array |
| gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D_ARRAY, |
| Some(atlas_texture_array), |
| ); |
| |
| // Convert pixmap data to the format expected by WebGL |
| let rgba_data = self.data_as_u8_slice(); |
| |
| // Upload the image data to the specific layer and region of the atlas texture array |
| gl.tex_sub_image_3d_with_opt_u8_array( |
| WebGl2RenderingContext::TEXTURE_2D_ARRAY, |
| 0, |
| offset[0] as i32, |
| offset[1] as i32, |
| layer as i32, |
| width as i32, |
| height as i32, |
| 1, |
| WebGl2RenderingContext::RGBA, |
| WebGl2RenderingContext::UNSIGNED_BYTE, |
| Some(rgba_data), |
| ) |
| .unwrap(); |
| } |
| } |
| |
| /// Implementation for `Arc<Pixmap>`. |
| impl WebGlAtlasWriter for Arc<Pixmap> { |
| fn width(&self) -> u32 { |
| self.as_ref().width() as u32 |
| } |
| |
| fn height(&self) -> u32 { |
| self.as_ref().height() as u32 |
| } |
| |
| fn write_to_atlas_layer( |
| &self, |
| gl: &WebGl2RenderingContext, |
| atlas_texture_array: &WebGlTexture, |
| layer: u32, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ) { |
| self.as_ref() |
| .write_to_atlas_layer(gl, atlas_texture_array, layer, offset, width, height); |
| } |
| } |
| |
| /// Implementation for `WebGlTexture` - texture-to-texture copy. |
| impl WebGlAtlasWriter for WebGlTexture { |
| fn width(&self) -> u32 { |
| // WebGL textures don't expose their dimensions directly |
| // This is a limitation - in practice, you'd need to track dimensions separately |
| // For now, we'll require the caller to provide correct width/height parameters |
| unreachable!("WebGlTexture width must be provided by caller") |
| } |
| |
| fn height(&self) -> u32 { |
| // WebGL textures don't expose their dimensions directly |
| // This is a limitation - in practice, you'd need to track dimensions separately |
| // For now, we'll require the caller to provide correct width/height parameters |
| unreachable!("WebGlTexture height must be provided by caller") |
| } |
| |
| fn write_to_atlas_layer( |
| &self, |
| gl: &WebGl2RenderingContext, |
| atlas_texture_array: &WebGlTexture, |
| layer: u32, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ) { |
| copy_to_texture_array_layer( |
| gl, |
| |gl| { |
| // Attach source texture to read framebuffer |
| gl.framebuffer_texture_2d( |
| WebGl2RenderingContext::READ_FRAMEBUFFER, |
| WebGl2RenderingContext::COLOR_ATTACHMENT0, |
| WebGl2RenderingContext::TEXTURE_2D, |
| Some(self), |
| 0, |
| ); |
| }, |
| atlas_texture_array, |
| layer, |
| offset, |
| [width, height], |
| ); |
| } |
| } |
| |
| /// Wrapper for `WebGlTexture` with known dimensions. |
| #[derive(Debug)] |
| pub struct WebGlTextureWithDimensions { |
| /// The WebGL texture. |
| pub texture: WebGlTexture, |
| /// The width of the texture. |
| pub width: u32, |
| /// The height of the texture. |
| pub height: u32, |
| } |
| |
| impl WebGlAtlasWriter for WebGlTextureWithDimensions { |
| fn width(&self) -> u32 { |
| self.width |
| } |
| |
| fn height(&self) -> u32 { |
| self.height |
| } |
| |
| fn write_to_atlas_layer( |
| &self, |
| gl: &WebGl2RenderingContext, |
| atlas_texture_array: &WebGlTexture, |
| layer: u32, |
| offset: [u32; 2], |
| width: u32, |
| height: u32, |
| ) { |
| self.texture |
| .write_to_atlas_layer(gl, atlas_texture_array, layer, offset, width, height); |
| } |
| } |
| |
| /// Wrapper for `WebGlTexture` array with known dimensions. |
| #[derive(Debug)] |
| struct WebGlTextureArray { |
| /// The WebGL texture array. |
| texture: WebGlTexture, |
| /// The size of the texture array. |
| size: WebGlTextureSize, |
| } |
| |
| impl WebGlTextureArray { |
| /// Create a new WebGL texture array wrapper. |
| fn new(texture: WebGlTexture, width: u32, height: u32, depth_or_array_layers: u32) -> Self { |
| Self { |
| texture, |
| size: WebGlTextureSize { |
| width, |
| height, |
| depth_or_array_layers, |
| }, |
| } |
| } |
| |
| /// Get the size of the texture array, similar to WGPU's `texture.size()`. |
| fn size(&self) -> WebGlTextureSize { |
| self.size |
| } |
| } |
| |
| /// Size information for WebGL texture arrays, similar to WGPU's `Extent3d`. |
| #[derive(Debug, Clone, Copy)] |
| struct WebGlTextureSize { |
| /// The width of the texture. |
| width: u32, |
| /// The height of the texture. |
| height: u32, |
| /// The number of layers in the texture array. |
| depth_or_array_layers: u32, |
| } |
| |
| /// Helper function to copy from a source texture/framebuffer to a destination texture array layer. |
| fn copy_to_texture_array_layer( |
| gl: &WebGl2RenderingContext, |
| source_setup: impl FnOnce(&WebGl2RenderingContext), |
| dest_texture_array: &WebGlTexture, |
| dest_layer: u32, |
| dest_offset: [u32; 2], |
| copy_size: [u32; 2], |
| ) { |
| let _state_guard = WebGlStateGuard::for_texture_copy(gl); |
| let read_framebuffer = gl.create_framebuffer().unwrap(); |
| |
| // Bind destination texture array |
| gl.active_texture(WebGl2RenderingContext::TEXTURE0); |
| gl.bind_texture( |
| WebGl2RenderingContext::TEXTURE_2D_ARRAY, |
| Some(dest_texture_array), |
| ); |
| |
| // Bind the READ framebuffer |
| gl.bind_framebuffer( |
| WebGl2RenderingContext::READ_FRAMEBUFFER, |
| Some(&read_framebuffer), |
| ); |
| |
| // Let the caller set up the source (attach texture/layer to read framebuffer) |
| source_setup(gl); |
| |
| // Copy from READ framebuffer to destination array layer |
| gl.copy_tex_sub_image_3d( |
| WebGl2RenderingContext::TEXTURE_2D_ARRAY, |
| 0, |
| dest_offset[0] as i32, |
| dest_offset[1] as i32, |
| dest_layer as i32, |
| 0, |
| 0, |
| copy_size[0] as i32, |
| copy_size[1] as i32, |
| ); |
| |
| // Clean up |
| gl.delete_framebuffer(Some(&read_framebuffer)); |
| } |
| |
| // Upload the data to the currently bound texture assuming a RGBA32UI format. |
| fn upload_data_to_rgba32_texture( |
| gl: &WebGl2RenderingContext, |
| data: &[u32], |
| texture_width: u32, |
| texture_height: u32, |
| ) { |
| // Safety: This calling `Uint32Array::view` is unsafe because it provides a view into |
| // WASM linear memory, and any additional allocations might invalidate that view. |
| // In our case, this is not an issue because we only use this view once for uploading |
| // data to the GPU below, and no allocations happen between that. |
| // The `tex_image_2d` method is synchronous in the sense that once it returns, it is guaranteed |
| // that all necessary data has already been read, so any allocations that happen |
| // after this block don't affect this anymore. |
| // |
| // See also: https://wikis.khronos.org/opengl/Synchronization |
| // >> There are several OpenGL functions that can pull data directly from client-side memory, |
| // >> or push data directly into client-side memory. Functions like `glTexSubImage2D`, |
| // >> `glReadPixels`, `glBufferSubData` and so forth. |
| // |
| // >> Because OpenGL is defined to be synchronous, when any of these functions have |
| // >> returned, they must have finished with the client memory. When `glReadPixels` returns, |
| // >> the pixel data is in your client memory (unless you are reading into a buffer object). |
| // >> When `glBufferSubData` returns, you can immediately modify or delete whatever memory |
| // >> pointer you gave it, as OpenGL has already read as much as it wants. |
| let packed_array = unsafe { js_sys::Uint32Array::view(data) }; |
| |
| gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( |
| WebGl2RenderingContext::TEXTURE_2D, |
| 0, |
| WebGl2RenderingContext::RGBA32UI as i32, |
| texture_width as i32, |
| texture_height as i32, |
| 0, |
| WebGl2RenderingContext::RGBA_INTEGER, |
| WebGl2RenderingContext::UNSIGNED_INT, |
| Some(&packed_array), |
| ) |
| .unwrap(); |
| } |