diff --git a/egui_web/CHANGELOG.md b/egui_web/CHANGELOG.md index c5494bb3..93c99f9d 100644 --- a/egui_web/CHANGELOG.md +++ b/egui_web/CHANGELOG.md @@ -7,6 +7,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +### Added ⭐ + +* WebGL2 is now supported, with improved texture sampler. WebGL1 will be used as a fallback. + +### Changed + +* Slightly improved alpha-blending (work-around for non-existing linear-space blending). + ## 0.7.0 - 2021-01-04 diff --git a/egui_web/Cargo.toml b/egui_web/Cargo.toml index 3c025587..0886694b 100644 --- a/egui_web/Cargo.toml +++ b/egui_web/Cargo.toml @@ -58,6 +58,7 @@ features = [ "Touch", "TouchEvent", "TouchList", + "WebGl2RenderingContext", "WebGlBuffer", "WebGlProgram", "WebGlRenderingContext", diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index c0ed8731..51de6c82 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -6,7 +6,7 @@ pub use egui::{pos2, Color32}; pub struct WebBackend { ctx: egui::CtxRef, - painter: webgl::Painter, + painter: Box, previous_frame_time: Option, frame_start: Option, } @@ -14,9 +14,19 @@ pub struct WebBackend { impl WebBackend { pub fn new(canvas_id: &str) -> Result { let ctx = egui::CtxRef::default(); + + let painter: Box = + if let Ok(webgl2_painter) = webgl2::WebGl2Painter::new(canvas_id) { + console_log("Using WebGL2 backend"); + Box::new(webgl2_painter) + } else { + console_log("Falling back to WebGL1 backend"); + Box::new(webgl1::WebGlPainter::new(canvas_id)?) + }; + Ok(Self { ctx, - painter: webgl::Painter::new(canvas_id)?, + painter, previous_frame_time: None, frame_start: None, }) @@ -52,12 +62,10 @@ impl WebBackend { clear_color: egui::Rgba, paint_jobs: egui::PaintJobs, ) -> Result<(), JsValue> { - self.painter.paint_jobs( - clear_color, - paint_jobs, - &self.ctx.texture(), - self.ctx.pixels_per_point(), - ) + self.painter.upload_egui_texture(&self.ctx.texture()); + self.painter.clear(clear_color); + self.painter + .paint_jobs(paint_jobs, self.ctx.pixels_per_point()) } pub fn painter_debug_info(&self) -> String { @@ -65,22 +73,6 @@ impl WebBackend { } } -impl epi::TextureAllocator for webgl::Painter { - fn alloc_srgba_premultiplied( - &mut self, - size: (usize, usize), - srgba_pixels: &[Color32], - ) -> egui::TextureId { - let id = self.alloc_user_texture(); - self.set_user_texture(id, size, srgba_pixels); - id - } - - fn free(&mut self, id: egui::TextureId) { - self.free_user_texture(id) - } -} - // ---------------------------------------------------------------------------- /// Data gathered between frames. @@ -207,7 +199,7 @@ impl AppRunner { seconds_since_midnight: Some(seconds_since_midnight()), native_pixels_per_point: Some(native_pixels_per_point()), }, - tex_allocator: Some(&mut self.web_backend.painter), + tex_allocator: Some(self.web_backend.painter.as_tex_allocator()), #[cfg(feature = "http")] http: self.http.clone(), output: &mut app_output, diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 1d610501..a5edf9e2 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -11,14 +11,17 @@ pub mod backend; #[cfg(feature = "http")] pub mod http; -pub mod webgl; +mod painter; +pub mod webgl1; +pub mod webgl2; pub use backend::*; +use egui::mutex::Mutex; pub use wasm_bindgen; pub use web_sys; -use egui::mutex::Mutex; +pub use painter::Painter; use std::sync::Arc; use wasm_bindgen::prelude::*; diff --git a/egui_web/src/painter.rs b/egui_web/src/painter.rs new file mode 100644 index 00000000..1c86fede --- /dev/null +++ b/egui_web/src/painter.rs @@ -0,0 +1,16 @@ +use wasm_bindgen::prelude::JsValue; + +pub trait Painter { + fn as_tex_allocator(&mut self) -> &mut dyn epi::TextureAllocator; + + fn debug_info(&self) -> String; + + /// id of the canvas html element containing the rendering + fn canvas_id(&self) -> &str; + + fn upload_egui_texture(&mut self, texture: &egui::Texture); + + fn clear(&mut self, lear_color: egui::Rgba); + + fn paint_jobs(&mut self, jobs: egui::PaintJobs, pixels_per_point: f32) -> Result<(), JsValue>; +} diff --git a/egui_web/src/webgl.rs b/egui_web/src/webgl1.rs similarity index 89% rename from egui_web/src/webgl.rs rename to egui_web/src/webgl1.rs index 50ab1dfe..fadfd9ff 100644 --- a/egui_web/src/webgl.rs +++ b/egui_web/src/webgl1.rs @@ -77,10 +77,14 @@ const FRAGMENT_SHADER_SOURCE: &str = r#" void main() { vec4 texture_rgba = linear_from_srgba(texture2D(u_sampler, v_tc) * 255.0); gl_FragColor = srgba_from_linear(v_rgba * texture_rgba) / 255.0; + + // WebGL doesn't support linear blending in the framebuffer, + // so we apply this hack to at least get a bit closer to the desired blending: + gl_FragColor.a = pow(gl_FragColor.a, 1.6); // Empiric nonsense } "#; -pub struct Painter { +pub struct WebGlPainter { canvas_id: String, canvas: web_sys::HtmlCanvasElement, gl: WebGlRenderingContext, @@ -108,24 +112,13 @@ struct UserTexture { gl_texture: Option, } -impl Painter { - pub fn debug_info(&self) -> String { - format!( - "Stored canvas size: {} x {}\n\ - gl context size: {} x {}", - self.canvas.width(), - self.canvas.height(), - self.gl.drawing_buffer_width(), - self.gl.drawing_buffer_height(), - ) - } - - pub fn new(canvas_id: &str) -> Result { +impl WebGlPainter { + pub fn new(canvas_id: &str) -> Result { let canvas = crate::canvas_element_or_die(canvas_id); let gl = canvas .get_context("webgl")? - .unwrap() + .ok_or_else(|| JsValue::from("Failed to get WebGl context"))? .dyn_into::()?; // -------------------------------------------------------------------- @@ -146,7 +139,7 @@ impl Painter { let tc_buffer = gl.create_buffer().ok_or("failed to create tc_buffer")?; let color_buffer = gl.create_buffer().ok_or("failed to create color_buffer")?; - Ok(Painter { + Ok(WebGlPainter { canvas_id: canvas_id.to_owned(), canvas, gl, @@ -161,53 +154,48 @@ impl Painter { }) } - /// id of the canvas html element containing the rendering - pub fn canvas_id(&self) -> &str { - &self.canvas_id - } - - pub fn alloc_user_texture(&mut self) -> egui::TextureId { - for (i, tex) in self.user_textures.iter_mut().enumerate() { + fn alloc_user_texture_index(&mut self) -> usize { + for (index, tex) in self.user_textures.iter_mut().enumerate() { if tex.is_none() { *tex = Some(Default::default()); - return egui::TextureId::User(i as u64); + return index; } } - let id = egui::TextureId::User(self.user_textures.len() as u64); + let index = self.user_textures.len(); self.user_textures.push(Some(Default::default())); - id + index } - pub fn set_user_texture( + fn alloc_user_texture( &mut self, - id: egui::TextureId, size: (usize, usize), srgba_pixels: &[Color32], - ) { + ) -> egui::TextureId { + let index = self.alloc_user_texture_index(); assert_eq!(size.0 * size.1, srgba_pixels.len()); - if let egui::TextureId::User(id) = id { - if let Some(user_texture) = self.user_textures.get_mut(id as usize) { - if let Some(user_texture) = user_texture { - let mut pixels: Vec = Vec::with_capacity(srgba_pixels.len() * 4); - for srgba in srgba_pixels { - pixels.push(srgba.r()); - pixels.push(srgba.g()); - pixels.push(srgba.b()); - pixels.push(srgba.a()); - } - - *user_texture = UserTexture { - size, - pixels, - gl_texture: None, - }; + if let Some(user_texture) = self.user_textures.get_mut(index) { + if let Some(user_texture) = user_texture { + let mut pixels: Vec = Vec::with_capacity(srgba_pixels.len() * 4); + for srgba in srgba_pixels { + pixels.push(srgba.r()); + pixels.push(srgba.g()); + pixels.push(srgba.b()); + pixels.push(srgba.a()); } + + *user_texture = UserTexture { + size, + pixels, + gl_texture: None, + }; } } + + egui::TextureId::User(index as u64) } - pub fn free_user_texture(&mut self, id: egui::TextureId) { + fn free_user_texture(&mut self, id: egui::TextureId) { if let egui::TextureId::User(id) = id { let index = id as usize; if index < self.user_textures.len() { @@ -228,45 +216,6 @@ impl Painter { } } - fn upload_egui_texture(&mut self, texture: &Texture) { - if self.egui_texture_version == Some(texture.version) { - return; // No change - } - - let mut pixels: Vec = Vec::with_capacity(texture.pixels.len() * 4); - for srgba in texture.srgba_pixels() { - pixels.push(srgba.r()); - pixels.push(srgba.g()); - pixels.push(srgba.b()); - pixels.push(srgba.a()); - } - - let gl = &self.gl; - gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture)); - - // TODO: https://developer.mozilla.org/en-US/docs/Web/API/EXT_sRGB - // https://www.khronos.org/registry/webgl/extensions/EXT_sRGB/ - let level = 0; - let internal_format = Gl::RGBA; - let border = 0; - let src_format = Gl::RGBA; - let src_type = Gl::UNSIGNED_BYTE; - gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( - Gl::TEXTURE_2D, - level, - internal_format as i32, - texture.width as i32, - texture.height as i32, - border, - src_format, - src_type, - Some(&pixels), - ) - .unwrap(); - - self.egui_texture_version = Some(texture.version); - } - fn upload_user_textures(&mut self) { let gl = &self.gl; @@ -309,93 +258,6 @@ impl Painter { } } - pub fn paint_jobs( - &mut self, - clear_color: egui::Rgba, - jobs: PaintJobs, - egui_texture: &Texture, - pixels_per_point: f32, - ) -> Result<(), JsValue> { - self.upload_egui_texture(egui_texture); - self.upload_user_textures(); - - let gl = &self.gl; - - gl.disable(Gl::SCISSOR_TEST); - gl.viewport( - 0, - 0, - self.canvas.width() as i32, - self.canvas.height() as i32, - ); - let clear_color: Color32 = clear_color.into(); - gl.clear_color( - clear_color[0] as f32 / 255.0, - clear_color[1] as f32 / 255.0, - clear_color[2] as f32 / 255.0, - clear_color[3] as f32 / 255.0, - ); - gl.clear(Gl::COLOR_BUFFER_BIT); - - gl.enable(Gl::SCISSOR_TEST); - gl.disable(Gl::CULL_FACE); // Egui is not strict about winding order. - gl.enable(Gl::BLEND); - gl.blend_func(Gl::ONE, Gl::ONE_MINUS_SRC_ALPHA); // premultiplied alpha - gl.use_program(Some(&self.program)); - gl.active_texture(Gl::TEXTURE0); - - let u_screen_size_loc = gl - .get_uniform_location(&self.program, "u_screen_size") - .unwrap(); - let screen_size_pixels = vec2(self.canvas.width() as f32, self.canvas.height() as f32); - let screen_size_points = screen_size_pixels / pixels_per_point; - gl.uniform2f( - Some(&u_screen_size_loc), - screen_size_points.x, - screen_size_points.y, - ); - - let u_sampler_loc = gl.get_uniform_location(&self.program, "u_sampler").unwrap(); - gl.uniform1i(Some(&u_sampler_loc), 0); - - for (clip_rect, triangles) in jobs { - if let Some(gl_texture) = self.get_texture(triangles.texture_id) { - gl.bind_texture(Gl::TEXTURE_2D, Some(gl_texture)); - - let clip_min_x = pixels_per_point * clip_rect.min.x; - let clip_min_y = pixels_per_point * clip_rect.min.y; - let clip_max_x = pixels_per_point * clip_rect.max.x; - let clip_max_y = pixels_per_point * clip_rect.max.y; - let clip_min_x = clamp(clip_min_x, 0.0..=screen_size_pixels.x); - let clip_min_y = clamp(clip_min_y, 0.0..=screen_size_pixels.y); - let clip_max_x = clamp(clip_max_x, clip_min_x..=screen_size_pixels.x); - let clip_max_y = clamp(clip_max_y, clip_min_y..=screen_size_pixels.y); - let clip_min_x = clip_min_x.round() as i32; - let clip_min_y = clip_min_y.round() as i32; - let clip_max_x = clip_max_x.round() as i32; - let clip_max_y = clip_max_y.round() as i32; - - // scissor Y coordinate is from the bottom - gl.scissor( - clip_min_x, - self.canvas.height() as i32 - clip_max_y, - clip_max_x - clip_min_x, - clip_max_y - clip_min_y, - ); - - for triangles in triangles.split_to_u16() { - self.paint_triangles(&triangles)?; - } - } else { - crate::console_warn(format!( - "WebGL: Failed to find texture {:?}", - triangles.texture_id - )); - } - } - Ok(()) - } - fn paint_triangles(&self, triangles: &Triangles) -> Result<(), JsValue> { debug_assert!(triangles.is_valid()); let indices: Vec = triangles.indices.iter().map(|idx| *idx as u16).collect(); @@ -516,6 +378,165 @@ impl Painter { } } +impl epi::TextureAllocator for WebGlPainter { + fn alloc_srgba_premultiplied( + &mut self, + size: (usize, usize), + srgba_pixels: &[egui::Color32], + ) -> egui::TextureId { + self.alloc_user_texture(size, srgba_pixels) + } + + fn free(&mut self, id: egui::TextureId) { + self.free_user_texture(id) + } +} + +impl crate::Painter for WebGlPainter { + fn as_tex_allocator(&mut self) -> &mut dyn epi::TextureAllocator { + self + } + + fn debug_info(&self) -> String { + format!( + "Stored canvas size: {} x {}\n\ + gl context size: {} x {}", + self.canvas.width(), + self.canvas.height(), + self.gl.drawing_buffer_width(), + self.gl.drawing_buffer_height(), + ) + } + + /// id of the canvas html element containing the rendering + fn canvas_id(&self) -> &str { + &self.canvas_id + } + + fn upload_egui_texture(&mut self, texture: &Texture) { + if self.egui_texture_version == Some(texture.version) { + return; // No change + } + + let mut pixels: Vec = Vec::with_capacity(texture.pixels.len() * 4); + for srgba in texture.srgba_pixels() { + pixels.push(srgba.r()); + pixels.push(srgba.g()); + pixels.push(srgba.b()); + pixels.push(srgba.a()); + } + + let gl = &self.gl; + gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture)); + + // TODO: https://developer.mozilla.org/en-US/docs/Web/API/EXT_sRGB + // https://www.khronos.org/registry/webgl/extensions/EXT_sRGB/ + let level = 0; + let internal_format = Gl::RGBA; + let border = 0; + let src_format = Gl::RGBA; + let src_type = Gl::UNSIGNED_BYTE; + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + level, + internal_format as i32, + texture.width as i32, + texture.height as i32, + border, + src_format, + src_type, + Some(&pixels), + ) + .unwrap(); + + self.egui_texture_version = Some(texture.version); + } + + fn clear(&mut self, clear_color: egui::Rgba) { + let gl = &self.gl; + + gl.disable(Gl::SCISSOR_TEST); + gl.viewport( + 0, + 0, + self.canvas.width() as i32, + self.canvas.height() as i32, + ); + let clear_color: Color32 = clear_color.into(); + gl.clear_color( + clear_color[0] as f32 / 255.0, + clear_color[1] as f32 / 255.0, + clear_color[2] as f32 / 255.0, + clear_color[3] as f32 / 255.0, + ); + gl.clear(Gl::COLOR_BUFFER_BIT); + } + + fn paint_jobs(&mut self, jobs: PaintJobs, pixels_per_point: f32) -> Result<(), JsValue> { + self.upload_user_textures(); + + let gl = &self.gl; + + gl.enable(Gl::SCISSOR_TEST); + gl.disable(Gl::CULL_FACE); // Egui is not strict about winding order. + gl.enable(Gl::BLEND); + gl.blend_func(Gl::ONE, Gl::ONE_MINUS_SRC_ALPHA); // premultiplied alpha + gl.use_program(Some(&self.program)); + gl.active_texture(Gl::TEXTURE0); + + let u_screen_size_loc = gl + .get_uniform_location(&self.program, "u_screen_size") + .unwrap(); + let screen_size_pixels = vec2(self.canvas.width() as f32, self.canvas.height() as f32); + let screen_size_points = screen_size_pixels / pixels_per_point; + gl.uniform2f( + Some(&u_screen_size_loc), + screen_size_points.x, + screen_size_points.y, + ); + + let u_sampler_loc = gl.get_uniform_location(&self.program, "u_sampler").unwrap(); + gl.uniform1i(Some(&u_sampler_loc), 0); + + for (clip_rect, triangles) in jobs { + if let Some(gl_texture) = self.get_texture(triangles.texture_id) { + gl.bind_texture(Gl::TEXTURE_2D, Some(gl_texture)); + + let clip_min_x = pixels_per_point * clip_rect.min.x; + let clip_min_y = pixels_per_point * clip_rect.min.y; + let clip_max_x = pixels_per_point * clip_rect.max.x; + let clip_max_y = pixels_per_point * clip_rect.max.y; + let clip_min_x = clamp(clip_min_x, 0.0..=screen_size_pixels.x); + let clip_min_y = clamp(clip_min_y, 0.0..=screen_size_pixels.y); + let clip_max_x = clamp(clip_max_x, clip_min_x..=screen_size_pixels.x); + let clip_max_y = clamp(clip_max_y, clip_min_y..=screen_size_pixels.y); + let clip_min_x = clip_min_x.round() as i32; + let clip_min_y = clip_min_y.round() as i32; + let clip_max_x = clip_max_x.round() as i32; + let clip_max_y = clip_max_y.round() as i32; + + // scissor Y coordinate is from the bottom + gl.scissor( + clip_min_x, + self.canvas.height() as i32 - clip_max_y, + clip_max_x - clip_min_x, + clip_max_y - clip_min_y, + ); + + for triangles in triangles.split_to_u16() { + self.paint_triangles(&triangles)?; + } + } else { + crate::console_warn(format!( + "WebGL: Failed to find texture {:?}", + triangles.texture_id + )); + } + } + Ok(()) + } +} + fn compile_shader( gl: &WebGlRenderingContext, shader_type: u32, diff --git a/egui_web/src/webgl2.rs b/egui_web/src/webgl2.rs new file mode 100644 index 00000000..bd397603 --- /dev/null +++ b/egui_web/src/webgl2.rs @@ -0,0 +1,579 @@ +//! Mostly a carbon-copy of `webgl1.rs`. + +use { + js_sys::WebAssembly, + wasm_bindgen::{prelude::*, JsCast}, + web_sys::{WebGl2RenderingContext, WebGlBuffer, WebGlProgram, WebGlShader, WebGlTexture}, +}; + +use egui::{ + math::clamp, + paint::{Color32, PaintJobs, Texture, Triangles}, + vec2, +}; + +type Gl = WebGl2RenderingContext; + +const VERTEX_SHADER_SOURCE: &str = r#" + precision mediump float; + uniform vec2 u_screen_size; + attribute vec2 a_pos; + attribute vec2 a_tc; + attribute vec4 a_srgba; + varying vec4 v_rgba; + varying vec2 v_tc; + + // 0-1 linear from 0-255 sRGB + vec3 linear_from_srgb(vec3 srgb) { + bvec3 cutoff = lessThan(srgb, vec3(10.31475)); + vec3 lower = srgb / vec3(3294.6); + vec3 higher = pow((srgb + vec3(14.025)) / vec3(269.025), vec3(2.4)); + return mix(higher, lower, vec3(cutoff)); + } + + vec4 linear_from_srgba(vec4 srgba) { + return vec4(linear_from_srgb(srgba.rgb), srgba.a / 255.0); + } + + void main() { + gl_Position = vec4( + 2.0 * a_pos.x / u_screen_size.x - 1.0, + 1.0 - 2.0 * a_pos.y / u_screen_size.y, + 0.0, + 1.0); + v_rgba = linear_from_srgba(a_srgba); + v_tc = a_tc; + } +"#; + +const FRAGMENT_SHADER_SOURCE: &str = r#" + precision mediump float; + uniform sampler2D u_sampler; + varying vec4 v_rgba; + varying vec2 v_tc; + + // 0-255 sRGB from 0-1 linear + vec3 srgb_from_linear(vec3 rgb) { + bvec3 cutoff = lessThan(rgb, vec3(0.0031308)); + vec3 lower = rgb * vec3(3294.6); + vec3 higher = vec3(269.025) * pow(rgb, vec3(1.0 / 2.4)) - vec3(14.025); + return mix(higher, lower, vec3(cutoff)); + } + + vec4 srgba_from_linear(vec4 rgba) { + return vec4(srgb_from_linear(rgba.rgb), 255.0 * rgba.a); + } + + void main() { + vec4 texture_rgba = texture2D(u_sampler, v_tc); + // gl_FragColor = v_rgba * texture_rgba; + gl_FragColor = srgba_from_linear(v_rgba * texture_rgba) / 255.0; + + // WebGL doesn't support linear blending in the framebuffer, + // so we apply this hack to at least get a bit closer to the desired blending: + gl_FragColor.a = pow(gl_FragColor.a, 1.6); // Empiric nonsense + } +"#; + +pub struct WebGl2Painter { + canvas_id: String, + canvas: web_sys::HtmlCanvasElement, + gl: WebGl2RenderingContext, + program: WebGlProgram, + index_buffer: WebGlBuffer, + pos_buffer: WebGlBuffer, + tc_buffer: WebGlBuffer, + color_buffer: WebGlBuffer, + + egui_texture: WebGlTexture, + egui_texture_version: Option, + + /// `None` means unallocated (freed) slot. + user_textures: Vec>, +} + +#[derive(Default)] +struct UserTexture { + size: (usize, usize), + + /// Pending upload (will be emptied later). + pixels: Vec, + + /// Lazily uploaded + gl_texture: Option, +} + +impl WebGl2Painter { + pub fn new(canvas_id: &str) -> Result { + let canvas = crate::canvas_element_or_die(canvas_id); + + let gl = canvas + .get_context("webgl2")? + .ok_or_else(|| JsValue::from("Failed to get WebGl2 context"))? + .dyn_into::()?; + + // -------------------------------------------------------------------- + + let egui_texture = gl.create_texture().unwrap(); + gl.bind_texture(Gl::TEXTURE_2D, Some(&egui_texture)); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::LINEAR as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as i32); + + let vert_shader = compile_shader(&gl, Gl::VERTEX_SHADER, VERTEX_SHADER_SOURCE)?; + let frag_shader = compile_shader(&gl, Gl::FRAGMENT_SHADER, FRAGMENT_SHADER_SOURCE)?; + + let program = link_program(&gl, [vert_shader, frag_shader].iter())?; + let index_buffer = gl.create_buffer().ok_or("failed to create index_buffer")?; + let pos_buffer = gl.create_buffer().ok_or("failed to create pos_buffer")?; + let tc_buffer = gl.create_buffer().ok_or("failed to create tc_buffer")?; + let color_buffer = gl.create_buffer().ok_or("failed to create color_buffer")?; + + Ok(WebGl2Painter { + canvas_id: canvas_id.to_owned(), + canvas, + gl, + program, + index_buffer, + pos_buffer, + tc_buffer, + color_buffer, + egui_texture, + egui_texture_version: None, + user_textures: Default::default(), + }) + } + + fn alloc_user_texture_index(&mut self) -> usize { + for (index, tex) in self.user_textures.iter_mut().enumerate() { + if tex.is_none() { + *tex = Some(Default::default()); + return index; + } + } + let index = self.user_textures.len(); + self.user_textures.push(Some(Default::default())); + index + } + + fn alloc_user_texture( + &mut self, + size: (usize, usize), + srgba_pixels: &[Color32], + ) -> egui::TextureId { + let index = self.alloc_user_texture_index(); + assert_eq!(size.0 * size.1, srgba_pixels.len()); + + if let Some(user_texture) = self.user_textures.get_mut(index) { + if let Some(user_texture) = user_texture { + let mut pixels: Vec = Vec::with_capacity(srgba_pixels.len() * 4); + for srgba in srgba_pixels { + pixels.push(srgba.r()); + pixels.push(srgba.g()); + pixels.push(srgba.b()); + pixels.push(srgba.a()); + } + + *user_texture = UserTexture { + size, + pixels, + gl_texture: None, + }; + } + } + + egui::TextureId::User(index as u64) + } + + fn free_user_texture(&mut self, id: egui::TextureId) { + if let egui::TextureId::User(id) = id { + let index = id as usize; + if index < self.user_textures.len() { + self.user_textures[index] = None; + } + } + } + + fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> { + match texture_id { + egui::TextureId::Egui => Some(&self.egui_texture), + egui::TextureId::User(id) => self + .user_textures + .get(id as usize)? + .as_ref()? + .gl_texture + .as_ref(), + } + } + + fn upload_user_textures(&mut self) { + let gl = &self.gl; + + for user_texture in &mut self.user_textures { + if let Some(user_texture) = user_texture { + if user_texture.gl_texture.is_none() { + let pixels = std::mem::take(&mut user_texture.pixels); + + let gl_texture = gl.create_texture().unwrap(); + gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_S, Gl::CLAMP_TO_EDGE as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_WRAP_T, Gl::CLAMP_TO_EDGE as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MIN_FILTER, Gl::LINEAR as i32); + gl.tex_parameteri(Gl::TEXTURE_2D, Gl::TEXTURE_MAG_FILTER, Gl::LINEAR as i32); + + gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); + + let level = 0; + let internal_format = Gl::SRGB8_ALPHA8; + let border = 0; + let src_format = Gl::RGBA; + let src_type = Gl::UNSIGNED_BYTE; + gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1); + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + level, + internal_format as i32, + user_texture.size.0 as i32, + user_texture.size.1 as i32, + border, + src_format, + src_type, + Some(&pixels), + ) + .unwrap(); + + user_texture.gl_texture = Some(gl_texture); + } + } + } + } + + fn paint_triangles(&self, triangles: &Triangles) -> Result<(), JsValue> { + debug_assert!(triangles.is_valid()); + let indices: Vec = triangles.indices.iter().map(|idx| *idx as u16).collect(); + + let mut positions: Vec = Vec::with_capacity(2 * triangles.vertices.len()); + let mut tex_coords: Vec = Vec::with_capacity(2 * triangles.vertices.len()); + for v in &triangles.vertices { + positions.push(v.pos.x); + positions.push(v.pos.y); + tex_coords.push(v.uv.x); + tex_coords.push(v.uv.y); + } + + let mut colors: Vec = Vec::with_capacity(4 * triangles.vertices.len()); + for v in &triangles.vertices { + colors.push(v.color[0]); + colors.push(v.color[1]); + colors.push(v.color[2]); + colors.push(v.color[3]); + } + + // -------------------------------------------------------------------- + + let gl = &self.gl; + + let indices_memory_buffer = wasm_bindgen::memory() + .dyn_into::()? + .buffer(); + let indices_ptr = indices.as_ptr() as u32 / 2; + let indices_array = js_sys::Int16Array::new(&indices_memory_buffer) + .subarray(indices_ptr, indices_ptr + indices.len() as u32); + + gl.bind_buffer(Gl::ELEMENT_ARRAY_BUFFER, Some(&self.index_buffer)); + gl.buffer_data_with_array_buffer_view( + Gl::ELEMENT_ARRAY_BUFFER, + &indices_array, + Gl::STREAM_DRAW, + ); + + // -------------------------------------------------------------------- + + let pos_memory_buffer = wasm_bindgen::memory() + .dyn_into::()? + .buffer(); + let pos_ptr = positions.as_ptr() as u32 / 4; + let pos_array = js_sys::Float32Array::new(&pos_memory_buffer) + .subarray(pos_ptr, pos_ptr + positions.len() as u32); + + gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&self.pos_buffer)); + gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &pos_array, Gl::STREAM_DRAW); + + let a_pos_loc = gl.get_attrib_location(&self.program, "a_pos"); + assert!(a_pos_loc >= 0); + let a_pos_loc = a_pos_loc as u32; + + let normalize = false; + let stride = 0; + let offset = 0; + gl.vertex_attrib_pointer_with_i32(a_pos_loc, 2, Gl::FLOAT, normalize, stride, offset); + gl.enable_vertex_attrib_array(a_pos_loc); + + // -------------------------------------------------------------------- + + let tc_memory_buffer = wasm_bindgen::memory() + .dyn_into::()? + .buffer(); + let tc_ptr = tex_coords.as_ptr() as u32 / 4; + let tc_array = js_sys::Float32Array::new(&tc_memory_buffer) + .subarray(tc_ptr, tc_ptr + tex_coords.len() as u32); + + gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&self.tc_buffer)); + gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &tc_array, Gl::STREAM_DRAW); + + let a_tc_loc = gl.get_attrib_location(&self.program, "a_tc"); + assert!(a_tc_loc >= 0); + let a_tc_loc = a_tc_loc as u32; + + let normalize = false; + let stride = 0; + let offset = 0; + gl.vertex_attrib_pointer_with_i32(a_tc_loc, 2, Gl::FLOAT, normalize, stride, offset); + gl.enable_vertex_attrib_array(a_tc_loc); + + // -------------------------------------------------------------------- + + let colors_memory_buffer = wasm_bindgen::memory() + .dyn_into::()? + .buffer(); + let colors_ptr = colors.as_ptr() as u32; + let colors_array = js_sys::Uint8Array::new(&colors_memory_buffer) + .subarray(colors_ptr, colors_ptr + colors.len() as u32); + + gl.bind_buffer(Gl::ARRAY_BUFFER, Some(&self.color_buffer)); + gl.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &colors_array, Gl::STREAM_DRAW); + + let a_srgba_loc = gl.get_attrib_location(&self.program, "a_srgba"); + assert!(a_srgba_loc >= 0); + let a_srgba_loc = a_srgba_loc as u32; + + let normalize = false; + let stride = 0; + let offset = 0; + gl.vertex_attrib_pointer_with_i32( + a_srgba_loc, + 4, + Gl::UNSIGNED_BYTE, + normalize, + stride, + offset, + ); + gl.enable_vertex_attrib_array(a_srgba_loc); + + // -------------------------------------------------------------------- + + gl.draw_elements_with_i32(Gl::TRIANGLES, indices.len() as i32, Gl::UNSIGNED_SHORT, 0); + + Ok(()) + } +} + +impl epi::TextureAllocator for WebGl2Painter { + fn alloc_srgba_premultiplied( + &mut self, + size: (usize, usize), + srgba_pixels: &[egui::Color32], + ) -> egui::TextureId { + self.alloc_user_texture(size, srgba_pixels) + } + + fn free(&mut self, id: egui::TextureId) { + self.free_user_texture(id) + } +} + +impl crate::Painter for WebGl2Painter { + fn as_tex_allocator(&mut self) -> &mut dyn epi::TextureAllocator { + self + } + + fn debug_info(&self) -> String { + format!( + "Stored canvas size: {} x {}\n\ + gl context size: {} x {}", + self.canvas.width(), + self.canvas.height(), + self.gl.drawing_buffer_width(), + self.gl.drawing_buffer_height(), + ) + } + + /// id of the canvas html element containing the rendering + fn canvas_id(&self) -> &str { + &self.canvas_id + } + + fn upload_egui_texture(&mut self, texture: &Texture) { + if self.egui_texture_version == Some(texture.version) { + return; // No change + } + + let mut pixels: Vec = Vec::with_capacity(texture.pixels.len() * 4); + for srgba in texture.srgba_pixels() { + pixels.push(srgba.r()); + pixels.push(srgba.g()); + pixels.push(srgba.b()); + pixels.push(srgba.a()); + } + + let gl = &self.gl; + gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture)); + + // TODO: https://developer.mozilla.org/en-US/docs/Web/API/EXT_sRGB + // https://www.khronos.org/registry/webgl/extensions/EXT_sRGB/ + let level = 0; + let internal_format = Gl::SRGB8_ALPHA8; + let border = 0; + let src_format = Gl::RGBA; + let src_type = Gl::UNSIGNED_BYTE; + gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1); + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( + Gl::TEXTURE_2D, + level, + internal_format as i32, + texture.width as i32, + texture.height as i32, + border, + src_format, + src_type, + Some(&pixels), + ) + .unwrap(); + + self.egui_texture_version = Some(texture.version); + } + + fn clear(&mut self, clear_color: egui::Rgba) { + let gl = &self.gl; + + gl.disable(Gl::SCISSOR_TEST); + gl.viewport( + 0, + 0, + self.canvas.width() as i32, + self.canvas.height() as i32, + ); + let clear_color: Color32 = clear_color.into(); + gl.clear_color( + clear_color[0] as f32 / 255.0, + clear_color[1] as f32 / 255.0, + clear_color[2] as f32 / 255.0, + clear_color[3] as f32 / 255.0, + ); + gl.clear(Gl::COLOR_BUFFER_BIT); + } + + fn paint_jobs(&mut self, jobs: PaintJobs, pixels_per_point: f32) -> Result<(), JsValue> { + self.upload_user_textures(); + + let gl = &self.gl; + + gl.enable(Gl::SCISSOR_TEST); + gl.disable(Gl::CULL_FACE); // Egui is not strict about winding order. + gl.enable(Gl::BLEND); + gl.blend_func(Gl::ONE, Gl::ONE_MINUS_SRC_ALPHA); // premultiplied alpha + gl.use_program(Some(&self.program)); + gl.active_texture(Gl::TEXTURE0); + + let u_screen_size_loc = gl + .get_uniform_location(&self.program, "u_screen_size") + .unwrap(); + let screen_size_pixels = vec2(self.canvas.width() as f32, self.canvas.height() as f32); + let screen_size_points = screen_size_pixels / pixels_per_point; + gl.uniform2f( + Some(&u_screen_size_loc), + screen_size_points.x, + screen_size_points.y, + ); + + let u_sampler_loc = gl.get_uniform_location(&self.program, "u_sampler").unwrap(); + gl.uniform1i(Some(&u_sampler_loc), 0); + + for (clip_rect, triangles) in jobs { + if let Some(gl_texture) = self.get_texture(triangles.texture_id) { + gl.bind_texture(Gl::TEXTURE_2D, Some(gl_texture)); + + let clip_min_x = pixels_per_point * clip_rect.min.x; + let clip_min_y = pixels_per_point * clip_rect.min.y; + let clip_max_x = pixels_per_point * clip_rect.max.x; + let clip_max_y = pixels_per_point * clip_rect.max.y; + let clip_min_x = clamp(clip_min_x, 0.0..=screen_size_pixels.x); + let clip_min_y = clamp(clip_min_y, 0.0..=screen_size_pixels.y); + let clip_max_x = clamp(clip_max_x, clip_min_x..=screen_size_pixels.x); + let clip_max_y = clamp(clip_max_y, clip_min_y..=screen_size_pixels.y); + let clip_min_x = clip_min_x.round() as i32; + let clip_min_y = clip_min_y.round() as i32; + let clip_max_x = clip_max_x.round() as i32; + let clip_max_y = clip_max_y.round() as i32; + + // scissor Y coordinate is from the bottom + gl.scissor( + clip_min_x, + self.canvas.height() as i32 - clip_max_y, + clip_max_x - clip_min_x, + clip_max_y - clip_min_y, + ); + + for triangles in triangles.split_to_u16() { + self.paint_triangles(&triangles)?; + } + } else { + crate::console_warn(format!( + "WebGL: Failed to find texture {:?}", + triangles.texture_id + )); + } + } + Ok(()) + } +} + +fn compile_shader( + gl: &WebGl2RenderingContext, + shader_type: u32, + source: &str, +) -> Result { + let shader = gl + .create_shader(shader_type) + .ok_or_else(|| String::from("Unable to create shader object"))?; + gl.shader_source(&shader, source); + gl.compile_shader(&shader); + + if gl + .get_shader_parameter(&shader, Gl::COMPILE_STATUS) + .as_bool() + .unwrap_or(false) + { + Ok(shader) + } else { + Err(gl + .get_shader_info_log(&shader) + .unwrap_or_else(|| "Unknown error creating shader".into())) + } +} + +fn link_program<'a, T: IntoIterator>( + gl: &WebGl2RenderingContext, + shaders: T, +) -> Result { + let program = gl + .create_program() + .ok_or_else(|| String::from("Unable to create shader object"))?; + for shader in shaders { + gl.attach_shader(&program, shader) + } + gl.link_program(&program); + + if gl + .get_program_parameter(&program, Gl::LINK_STATUS) + .as_bool() + .unwrap_or(false) + { + Ok(program) + } else { + Err(gl + .get_program_info_log(&program) + .unwrap_or_else(|| "Unknown error creating program object".into())) + } +}