diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f681cc8e2..a87808307 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -108,7 +108,7 @@ jobs: - name: wasm-bindgen uses: jetli/wasm-bindgen-action@v0.1.0 with: - version: "0.2.82" + version: "0.2.83" - run: ./sh/wasm_bindgen_check.sh --skip-setup cargo-deny: diff --git a/Cargo.lock b/Cargo.lock index 100325eaa..b5ff210e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1223,6 +1223,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tracing-wasm", + "wasm-bindgen-futures", ] [[package]] @@ -3979,9 +3980,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3989,9 +3990,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", @@ -4016,9 +4017,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4026,9 +4027,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -4039,9 +4040,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "wayland-client" diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 6af5e4976..4b519e0ea 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -13,6 +13,7 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C * Fix: app state is now saved when user presses Cmd-Q on Mac ([#2013](https://github.com/emilk/egui/pull/2013)). * Added `center` to `NativeOptions` and `monitor_size` to `WindowInfo` on desktop ([#2035](https://github.com/emilk/egui/pull/2035)). * Web: you can access your application from JS using `AppRunner::app_mut`. See `crates/egui_demo_app/src/lib.rs`. +* Web: You can now use WebGL on top of `wgpu` by enabling the `wgpu` feature (and disabling `glow` via disabling default features) ([#2107](https://github.com/emilk/egui/pull/2107)). ## 0.19.0 - 2022-08-20 diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 32c456adc..a190c4f07 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -57,7 +57,7 @@ screen_reader = [ ## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)). ## This overrides the `glow` feature. -wgpu = ["dep:wgpu", "egui-wgpu"] +wgpu = ["dep:wgpu", "dep:egui-wgpu"] [dependencies] @@ -72,23 +72,23 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } document-features = { version = "0.2", optional = true } egui_glow = { version = "0.19.0", path = "../egui_glow", optional = true, default-features = false } -egui-wgpu = { version = "0.19.0", path = "../egui-wgpu", optional = true, features = ["winit"] } glow = { version = "0.11", optional = true } ron = { version = "0.8", optional = true, features = ["integer128"] } serde = { version = "1", optional = true, features = ["derive"] } -wgpu = { version = "0.13", optional = true } # ------------------------------------------- # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -dark-light = { version = "0.2.1", optional = true } egui-winit = { version = "0.19.0", path = "../egui-winit", default-features = false, features = ["clipboard", "links"] } glutin = { version = "0.29.0" } winit = "0.27.2" # optional native: -puffin = { version = "0.13", optional = true } +dark-light = { version = "0.2.1", optional = true } directories-next = { version = "2", optional = true } +egui-wgpu = { version = "0.19.0", path = "../egui-wgpu", optional = true, features = ["winit"] } # if wgpu is used, use it with winit +puffin = { version = "0.13", optional = true } +wgpu = { version = "0.13", optional = true } # ------------------------------------------- # web: @@ -142,6 +142,7 @@ web-sys = { version = "0.3.58", features = [ "Window", ] } -# optional -# feature screen_reader +# optional web: +egui-wgpu = { version = "0.19.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit tts = { version = "0.20", optional = true } # Can't use 0.21-0.24 due to compilation problems on linux +wgpu = { version = "0.13", optional = true, features = ["webgl"] } diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index b3cd25180..571eeecc4 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -455,6 +455,7 @@ pub struct WebOptions { /// Which version of WebGl context to select /// /// Default: [`WebGlContextOption::BestFirst`]. + #[cfg(feature = "glow")] pub webgl_context_option: WebGlContextOption, } @@ -464,6 +465,7 @@ impl Default for WebOptions { Self { follow_system_theme: true, default_theme: Theme::Dark, + #[cfg(feature = "glow")] webgl_context_option: WebGlContextOption::BestFirst, } } diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 088382cb5..eb16dc93d 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -48,9 +48,9 @@ //! /// Call this once from the HTML. //! #[cfg(target_arch = "wasm32")] //! #[wasm_bindgen] -//! pub fn start(canvas_id: &str) -> Result { +//! pub async fn start(canvas_id: &str) -> Result { //! let web_options = eframe::WebOptions::default(); -//! eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))) +//! eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await //! } //! ``` //! @@ -103,18 +103,18 @@ pub use web_sys; /// /// You can add more callbacks like this if you want to call in to your code. /// #[cfg(target_arch = "wasm32")] /// #[wasm_bindgen] -/// pub fn start(canvas_id: &str) -> Result, eframe::wasm_bindgen::JsValue> { +/// pub async fn start(canvas_id: &str) -> Result, eframe::wasm_bindgen::JsValue> { /// let web_options = eframe::WebOptions::default(); -/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))) +/// eframe::start_web(canvas_id, web_options, Box::new(|cc| Box::new(MyEguiApp::new(cc)))).await /// } /// ``` #[cfg(target_arch = "wasm32")] -pub fn start_web( +pub async fn start_web( canvas_id: &str, web_options: WebOptions, app_creator: AppCreator, ) -> Result { - let handle = web::start(canvas_id, web_options, app_creator)?; + let handle = web::start(canvas_id, web_options, app_creator).await?; Ok(handle) } diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index ea92b39b6..c6a3d6d7a 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -1,5 +1,4 @@ -use super::{WebPainter, *}; - +use super::{web_painter::WebPainter, *}; use crate::epi; use egui::{ @@ -162,7 +161,7 @@ fn test_parse_query() { pub struct AppRunner { pub(crate) frame: epi::Frame, egui_ctx: egui::Context, - painter: WebPainter, + painter: ActiveWebPainter, pub(crate) input: WebInput, app: Box, pub(crate) needs_repaint: std::sync::Arc, @@ -182,13 +181,14 @@ impl Drop for AppRunner { } impl AppRunner { - pub fn new( + pub async fn new( canvas_id: &str, web_options: crate::WebOptions, app_creator: epi::AppCreator, ) -> Result { - let painter = - WebPainter::new(canvas_id, web_options.webgl_context_option).map_err(JsValue::from)?; // fail early + let painter = ActiveWebPainter::new(canvas_id, &web_options) + .await + .map_err(JsValue::from)?; let system_theme = if web_options.follow_system_theme { super::system_theme() @@ -216,9 +216,13 @@ impl AppRunner { egui_ctx: egui_ctx.clone(), integration_info: info.clone(), storage: Some(&storage), + #[cfg(feature = "glow")] - gl: Some(painter.painter.gl().clone()), - #[cfg(feature = "wgpu")] + gl: Some(painter.gl().clone()), + + #[cfg(all(feature = "wgpu", not(feature = "glow")))] + wgpu_render_state: painter.render_state(), + #[cfg(all(feature = "wgpu", feature = "glow"))] wgpu_render_state: None, }); @@ -226,9 +230,13 @@ impl AppRunner { info, output: Default::default(), storage: Some(Box::new(storage)), + #[cfg(feature = "glow")] gl: Some(painter.gl().clone()), - #[cfg(feature = "wgpu")] + + #[cfg(all(feature = "wgpu", not(feature = "glow")))] + wgpu_render_state: painter.render_state(), + #[cfg(all(feature = "wgpu", feature = "glow"))] wgpu_render_state: None, }; @@ -357,16 +365,12 @@ impl AppRunner { Ok((repaint_after, clipped_primitives)) } - pub fn clear_color_buffer(&self) { - self.painter - .clear(self.app.clear_color(&self.egui_ctx.style().visuals)); - } - /// Paint the results of the last call to [`Self::logic`]. pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> { let textures_delta = std::mem::take(&mut self.textures_delta); self.painter.paint_and_update_textures( + self.app.clear_color(&self.egui_ctx.style().visuals), clipped_primitives, self.egui_ctx.pixels_per_point(), &textures_delta, @@ -512,12 +516,12 @@ impl AppRunnerContainer { /// Install event listeners to register different input events /// and start running the given app. -pub fn start( +pub async fn start( canvas_id: &str, web_options: crate::WebOptions, app_creator: epi::AppCreator, ) -> Result { - let mut runner = AppRunner::new(canvas_id, web_options, app_creator)?; + let mut runner = AppRunner::new(canvas_id, web_options, app_creator).await?; runner.warm_up()?; start_runner(runner) } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index b50d581eb..71d89818e 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -13,7 +13,6 @@ pub fn paint_and_schedule( if !is_destroyed && runner_lock.needs_repaint.when_to_repaint() <= now_sec() { runner_lock.needs_repaint.clear(); - runner_lock.clear_color_buffer(); let (repaint_after, clipped_primitives) = runner_lock.logic()?; runner_lock.paint(&clipped_primitives)?; runner_lock diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index fbd2028f3..18b754408 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -8,12 +8,25 @@ mod input; pub mod screen_reader; pub mod storage; mod text_agent; -mod web_glow_painter; + +#[cfg(not(any(feature = "glow", feature = "wgpu")))] +compile_error!("You must enable either the 'glow' or 'wgpu' feature"); + +mod web_painter; + +#[cfg(feature = "glow")] +mod web_painter_glow; +#[cfg(feature = "glow")] +pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow; + +#[cfg(feature = "wgpu")] +mod web_painter_wgpu; +#[cfg(all(feature = "wgpu", not(feature = "glow")))] +pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu; pub use backend::*; pub use events::*; pub use storage::*; -pub(crate) use web_glow_painter::WebPainter; use std::collections::BTreeMap; use std::sync::{ @@ -244,47 +257,3 @@ pub fn percent_decode(s: &str) -> String { .decode_utf8_lossy() .to_string() } - -// ---------------------------------------------------------------------------- - -pub(crate) fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool { - // See https://github.com/emilk/egui/issues/794 - - // detect WebKitGTK - - // WebKitGTK use WebKit default unmasked vendor and renderer - // but safari use same vendor and renderer - // so exclude "Mac OS X" user-agent. - let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap(); - !user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl) -} - -/// detecting Safari and `webkitGTK`. -/// -/// Safari and `webkitGTK` use unmasked renderer :Apple GPU -/// -/// If we detect safari or `webkitGTKs` returns true. -/// -/// This function used to avoid displaying linear color with `sRGB` supported systems. -fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool { - // This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.") - // but unless we call it we get errors in Chrome when we call `get_parameter` below. - // TODO(emilk): do something smart based on user agent? - if gl - .get_extension("WEBGL_debug_renderer_info") - .unwrap() - .is_some() - { - if let Ok(renderer) = - gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL) - { - if let Some(renderer) = renderer.as_string() { - if renderer.contains("Apple") { - return true; - } - } - } - } - - false -} diff --git a/crates/eframe/src/web/web_painter.rs b/crates/eframe/src/web/web_painter.rs new file mode 100644 index 000000000..4ced22f46 --- /dev/null +++ b/crates/eframe/src/web/web_painter.rs @@ -0,0 +1,30 @@ +use egui::Rgba; +use wasm_bindgen::JsValue; + +/// Renderer for a browser canvas. +/// As of writing we're not allowing to decide on the painter at runtime, +/// therefore this trait is merely there for specifying and documenting the interface. +pub(crate) trait WebPainter { + // Create a new web painter targeting a given canvas. + // fn new(canvas_id: &str, options: &WebOptions) -> Result + // where + // Self: Sized; + + /// Id of the canvas in use. + fn canvas_id(&self) -> &str; + + /// Maximum size of a texture in one direction. + fn max_texture_side(&self) -> usize; + + /// Update all internal textures and paint gui. + fn paint_and_update_textures( + &mut self, + clear_color: Rgba, + clipped_primitives: &[egui::ClippedPrimitive], + pixels_per_point: f32, + textures_delta: &egui::TexturesDelta, + ) -> Result<(), JsValue>; + + /// Destroy all resources. + fn destroy(&mut self); +} diff --git a/crates/eframe/src/web/web_glow_painter.rs b/crates/eframe/src/web/web_painter_glow.rs similarity index 58% rename from crates/eframe/src/web/web_glow_painter.rs rename to crates/eframe/src/web/web_painter_glow.rs index e5e239cc7..a8ba05f14 100644 --- a/crates/eframe/src/web/web_glow_painter.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -1,25 +1,30 @@ use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; -#[cfg(not(target_arch = "wasm32"))] -use web_sys::{WebGl2RenderingContext, WebGlRenderingContext}; -use egui::{ClippedPrimitive, Rgba}; +use egui::Rgba; use egui_glow::glow; -use crate::WebGlContextOption; +use crate::{WebGlContextOption, WebOptions}; -pub(crate) struct WebPainter { - pub(crate) canvas: HtmlCanvasElement, - pub(crate) canvas_id: String, - pub(crate) painter: egui_glow::Painter, +use super::web_painter::WebPainter; + +pub(crate) struct WebPainterGlow { + canvas: HtmlCanvasElement, + canvas_id: String, + painter: egui_glow::Painter, } -impl WebPainter { - pub fn new(canvas_id: &str, options: WebGlContextOption) -> Result { +impl WebPainterGlow { + pub fn gl(&self) -> &std::sync::Arc { + self.painter.gl() + } + + pub async fn new(canvas_id: &str, options: &WebOptions) -> Result { let canvas = super::canvas_element_or_die(canvas_id); - let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options)?; + let (gl, shader_prefix) = + init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; let gl = std::sync::Arc::new(gl); let painter = egui_glow::Painter::new(gl, shader_prefix, None) @@ -33,63 +38,40 @@ impl WebPainter { } } -impl WebPainter { - pub fn gl(&self) -> &std::sync::Arc { - self.painter.gl() - } - - pub fn max_texture_side(&self) -> usize { +impl WebPainter for WebPainterGlow { + fn max_texture_side(&self) -> usize { self.painter.max_texture_side() } - pub fn canvas_id(&self) -> &str { + fn canvas_id(&self) -> &str { &self.canvas_id } - pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { - self.painter.set_texture(tex_id, delta); - } - - pub fn free_texture(&mut self, tex_id: egui::TextureId) { - self.painter.free_texture(tex_id); - } - - pub fn clear(&self, clear_color: Rgba) { - let canvas_dimension = [self.canvas.width(), self.canvas.height()]; - egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color); - } - - pub fn paint_primitives( - &mut self, - clipped_primitives: &[ClippedPrimitive], - pixels_per_point: f32, - ) -> Result<(), JsValue> { - let canvas_dimension = [self.canvas.width(), self.canvas.height()]; - self.painter - .paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives); - Ok(()) - } - - pub fn paint_and_update_textures( + fn paint_and_update_textures( &mut self, + clear_color: Rgba, clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, ) -> Result<(), JsValue> { + let canvas_dimension = [self.canvas.width(), self.canvas.height()]; + for (id, image_delta) in &textures_delta.set { - self.set_texture(*id, image_delta); + self.painter.set_texture(*id, image_delta); } - self.paint_primitives(clipped_primitives, pixels_per_point)?; + egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color); + self.painter + .paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives); for &id in &textures_delta.free { - self.free_texture(id); + self.painter.free_texture(id); } Ok(()) } - pub fn destroy(&mut self) { + fn destroy(&mut self) { self.painter.destroy() } } @@ -131,7 +113,7 @@ fn init_webgl1(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static st .dyn_into::() .unwrap(); - let shader_prefix = if super::webgl1_requires_brightening(&gl1_ctx) { + let shader_prefix = if webgl1_requires_brightening(&gl1_ctx) { tracing::debug!("Enabling webkitGTK brightening workaround."); "#define APPLY_BRIGHTENING_GAMMA" } else { @@ -159,3 +141,45 @@ fn init_webgl2(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static st Some((gl, shader_prefix)) } + +fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool { + // See https://github.com/emilk/egui/issues/794 + + // detect WebKitGTK + + // WebKitGTK use WebKit default unmasked vendor and renderer + // but safari use same vendor and renderer + // so exclude "Mac OS X" user-agent. + let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap(); + !user_agent.contains("Mac OS X") && is_safari_and_webkit_gtk(gl) +} + +/// detecting Safari and `webkitGTK`. +/// +/// Safari and `webkitGTK` use unmasked renderer :Apple GPU +/// +/// If we detect safari or `webkitGTKs` returns true. +/// +/// This function used to avoid displaying linear color with `sRGB` supported systems. +fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool { + // This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.") + // but unless we call it we get errors in Chrome when we call `get_parameter` below. + // TODO(emilk): do something smart based on user agent? + if gl + .get_extension("WEBGL_debug_renderer_info") + .unwrap() + .is_some() + { + if let Ok(renderer) = + gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL) + { + if let Some(renderer) = renderer.as_string() { + if renderer.contains("Apple") { + return true; + } + } + } + } + + false +} diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs new file mode 100644 index 000000000..66bb0e361 --- /dev/null +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -0,0 +1,196 @@ +use std::sync::Arc; + +use wasm_bindgen::JsValue; +use web_sys::HtmlCanvasElement; + +use egui::{mutex::RwLock, Rgba}; +use egui_wgpu::{renderer::ScreenDescriptor, RenderState}; + +use crate::WebOptions; + +use super::web_painter::WebPainter; + +pub(crate) struct WebPainterWgpu { + canvas: HtmlCanvasElement, + canvas_id: String, + surface: wgpu::Surface, + surface_size: [u32; 2], + limits: wgpu::Limits, + render_state: Option, +} + +impl WebPainterWgpu { + #[allow(unused)] // only used if `wgpu` is the only active feature. + pub fn render_state(&self) -> Option { + self.render_state.clone() + } + + #[allow(unused)] // only used if `wgpu` is the only active feature. + pub async fn new(canvas_id: &str, _options: &WebOptions) -> Result { + tracing::debug!("Creating wgpu painter with WebGL backend…"); + + let canvas = super::canvas_element_or_die(canvas_id); + let limits = wgpu::Limits::downlevel_webgl2_defaults(); // TODO(Wumpf): Expose to eframe user + + // TODO(Wumpf): Should be able to switch between WebGL & WebGPU (only) + let backends = wgpu::Backends::GL; //wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all); + let instance = wgpu::Instance::new(backends); + let surface = instance.create_surface_from_canvas(&canvas); + + let adapter = + wgpu::util::initialize_adapter_from_env_or_default(&instance, backends, Some(&surface)) + .await + .ok_or_else(|| "No suitable GPU adapters found on the system".to_owned())?; + + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: Some("egui_webpainter"), + features: wgpu::Features::empty(), + limits: limits.clone(), + }, + None, // No capture exposed so far - unclear how we can expose this in a browser environment (?) + ) + .await + .map_err(|err| format!("Failed to find wgpu device: {}", err))?; + + // TODO(Wumpf): MSAA & depth + + let target_format = + egui_wgpu::preferred_framebuffer_format(&surface.get_supported_formats(&adapter)); + + let renderer = egui_wgpu::Renderer::new(&device, target_format, 1, 0); + let render_state = RenderState { + device: Arc::new(device), + queue: Arc::new(queue), + target_format, + renderer: Arc::new(RwLock::new(renderer)), + }; + + tracing::debug!("wgpu painter initialized."); + + Ok(Self { + canvas, + canvas_id: canvas_id.to_owned(), + render_state: Some(render_state), + surface, + surface_size: [0, 0], + limits, + }) + } +} + +impl WebPainter for WebPainterWgpu { + fn canvas_id(&self) -> &str { + &self.canvas_id + } + + fn max_texture_side(&self) -> usize { + self.limits.max_texture_dimension_2d as _ + } + + fn paint_and_update_textures( + &mut self, + clear_color: Rgba, + clipped_primitives: &[egui::ClippedPrimitive], + pixels_per_point: f32, + textures_delta: &egui::TexturesDelta, + ) -> Result<(), JsValue> { + let render_state = if let Some(render_state) = &self.render_state { + render_state + } else { + return Err(JsValue::from_str( + "Can't paint, wgpu renderer was already disposed", + )); + }; + + // Resize surface if needed + let canvas_size = [self.canvas.width(), self.canvas.height()]; + if canvas_size != self.surface_size { + self.surface.configure( + &render_state.device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: render_state.target_format, + width: canvas_size[0], + height: canvas_size[1], + present_mode: wgpu::PresentMode::Fifo, + }, + ); + self.surface_size = canvas_size.clone(); + } + + let frame = self.surface.get_current_texture().map_err(|err| { + JsValue::from_str(&format!( + "Failed to acquire next swap chain texture: {}", + err + )) + })?; + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = + render_state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("egui_webpainter_paint_and_update_textures"), + }); + + // Upload all resources for the GPU. + let screen_descriptor = ScreenDescriptor { + size_in_pixels: canvas_size, + pixels_per_point, + }; + + { + let mut renderer = render_state.renderer.write(); + for (id, image_delta) in &textures_delta.set { + renderer.update_texture( + &render_state.device, + &render_state.queue, + *id, + image_delta, + ); + } + + renderer.update_buffers( + &render_state.device, + &render_state.queue, + clipped_primitives, + &screen_descriptor, + ); + } + + // Record all render passes. + render_state.renderer.read().render( + &mut encoder, + &view, + clipped_primitives, + &screen_descriptor, + Some(wgpu::Color { + r: clear_color.r() as f64, + g: clear_color.g() as f64, + b: clear_color.b() as f64, + a: clear_color.a() as f64, + }), + ); + + { + let mut renderer = render_state.renderer.write(); + for id in &textures_delta.free { + renderer.free_texture(id); + } + } + + // Submit the commands. + render_state.queue.submit(std::iter::once(encoder.finish())); + frame.present(); + + Ok(()) + } + + fn destroy(&mut self) { + self.render_state = None; + } +} diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 78b07ac0f..7455943bb 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -17,5 +17,28 @@ pub use renderer::Renderer; #[cfg(feature = "winit")] pub mod winit; -#[cfg(feature = "winit")] -pub use crate::winit::RenderState; +use egui::mutex::RwLock; +use std::sync::Arc; + +/// Access to the render state for egui, which can be useful in combination with +/// [`egui::PaintCallback`]s for custom rendering using WGPU. +#[derive(Clone)] +pub struct RenderState { + pub device: Arc, + pub queue: Arc, + pub target_format: wgpu::TextureFormat, + pub renderer: Arc>, +} + +/// Find the framebuffer format that egui prefers +pub fn preferred_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat { + for &format in formats { + if matches!( + format, + wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm + ) { + return format; + } + } + formats[0] // take the first +} diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index a7db573f2..e81b4b0ae 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -1,5 +1,6 @@ #![allow(unsafe_code)] +use std::num::NonZeroU64; use std::{borrow::Cow, collections::HashMap, num::NonZeroU32}; use egui::{epaint::Primitive, PaintCallbackInfo}; @@ -26,7 +27,7 @@ use wgpu::util::DeviceExt as _; /// /// # Example /// -/// See the [`custom3d_glow`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example. +/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example. pub struct CallbackFn { prepare: Box, paint: Box, @@ -149,7 +150,7 @@ impl Renderer { depth_bits: u8, ) -> Self { let shader = wgpu::ShaderModuleDescriptor { - label: Some("egui_shader"), + label: Some("egui"), source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))), }; let module = device.create_shader_module(shader); @@ -175,7 +176,7 @@ impl Renderer { visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { has_dynamic_offset: false, - min_binding_size: None, + min_binding_size: NonZeroU64::new(std::mem::size_of::() as _), ty: wgpu::BufferBindingType::Uniform, }, count: None, @@ -306,8 +307,9 @@ impl Renderer { } pub fn update_depth_texture(&mut self, device: &wgpu::Device, width: u32, height: u32) { + // TODO(wumpf) don't recreate texture if size hasn't changed let texture = device.create_texture(&wgpu::TextureDescriptor { - label: None, + label: Some("egui_depth_texture"), size: wgpu::Extent3d { width, height, @@ -361,7 +363,7 @@ impl Renderer { }, })], depth_stencil_attachment, - label: Some("egui_render_pass"), + label: Some("egui_render"), }); self.render_onto_renderpass(&mut render_pass, paint_jobs, screen_descriptor); @@ -559,9 +561,13 @@ impl Renderer { origin, ); } else { + // TODO(Wumpf): Create only a new texture if we need to // allocate a new texture + // Use same label for all resources associated with this texture id (no point in retyping the type) + let label_str = format!("egui_texid_{:?}", id); + let label = Some(label_str.as_str()); let texture = device.create_texture(&wgpu::TextureDescriptor { - label: None, + label, size, mip_level_count: 1, sample_count: 1, @@ -573,14 +579,15 @@ impl Renderer { egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest, egui::TextureFilter::Linear => wgpu::FilterMode::Linear, }; + // TODO(Wumpf): Reuse this sampler. let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - label: None, + label, mag_filter: filter, min_filter: filter, ..Default::default() }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, + label, layout: &self.texture_bind_group_layout, entries: &[ wgpu::BindGroupEntry { @@ -633,13 +640,7 @@ impl Renderer { device, texture, wgpu::SamplerDescriptor { - label: Some( - format!( - "egui_user_image_{}_texture_sampler", - self.next_user_texture_id - ) - .as_str(), - ), + label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()), mag_filter: texture_filter, min_filter: texture_filter, ..Default::default() @@ -661,13 +662,7 @@ impl Renderer { device, texture, wgpu::SamplerDescriptor { - label: Some( - format!( - "egui_user_image_{}_texture_sampler", - self.next_user_texture_id - ) - .as_str(), - ), + label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()), mag_filter: texture_filter, min_filter: texture_filter, ..Default::default() @@ -698,13 +693,7 @@ impl Renderer { }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some( - format!( - "egui_user_image_{}_texture_bind_group", - self.next_user_texture_id - ) - .as_str(), - ), + label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()), layout: &self.texture_bind_group_layout, entries: &[ wgpu::BindGroupEntry { @@ -748,9 +737,7 @@ impl Renderer { }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some( - format!("egui_user_{}_texture_bind_group", self.next_user_texture_id).as_str(), - ), + label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()), layout: &self.texture_bind_group_layout, entries: &[ wgpu::BindGroupEntry { diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 008c5b231..f822de4af 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -4,17 +4,7 @@ use egui::mutex::RwLock; use tracing::error; use wgpu::{Adapter, Instance, Surface}; -use crate::{renderer, Renderer}; - -/// Access to the render state for egui, which can be useful in combination with -/// [`egui::PaintCallback`]s for custom rendering using WGPU. -#[derive(Clone)] -pub struct RenderState { - pub device: Arc, - pub queue: Arc, - pub target_format: wgpu::TextureFormat, - pub renderer: Arc>, -} +use crate::{renderer, RenderState, Renderer}; struct SurfaceState { surface: Surface, @@ -119,7 +109,7 @@ impl<'a> Painter<'a> { let adapter = self.adapter.as_ref().unwrap(); let swapchain_format = - select_framebuffer_format(&surface.get_supported_formats(adapter)); + crate::preferred_framebuffer_format(&surface.get_supported_formats(adapter)); let rs = pollster::block_on(self.init_render_state(adapter, swapchain_format)); self.render_state = Some(rs); @@ -324,15 +314,3 @@ impl<'a> Painter<'a> { // TODO(emilk): something here? } } - -fn select_framebuffer_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat { - for &format in formats { - if matches!( - format, - wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm - ) { - return format; - } - } - formats[0] // take the first -} diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index af43c7631..5df4a50fc 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -69,3 +69,4 @@ tracing-subscriber = "0.3" [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1.6" tracing-wasm = "0.2" +wasm-bindgen-futures = "0.4" diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index 0ef65f2bf..f3957e227 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{num::NonZeroU64, sync::Arc}; use eframe::{ egui_wgpu::{self, wgpu}, @@ -18,32 +18,32 @@ impl Custom3d { let device = &wgpu_render_state.device; let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: None, + label: Some("custom3d"), source: wgpu::ShaderSource::Wgsl(include_str!("./custom3d_wgpu_shader.wgsl").into()), }); let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, + label: Some("custom3d"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, - min_binding_size: None, + min_binding_size: NonZeroU64::new(16), }, count: None, }], }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: None, + label: Some("custom3d"), bind_group_layouts: &[&bind_group_layout], push_constant_ranges: &[], }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: None, + label: Some("custom3d"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, @@ -62,15 +62,15 @@ impl Custom3d { }); let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: None, - contents: bytemuck::cast_slice(&[0.0]), - usage: wgpu::BufferUsages::COPY_DST - | wgpu::BufferUsages::MAP_WRITE - | wgpu::BufferUsages::UNIFORM, + label: Some("custom3d"), + contents: bytemuck::cast_slice(&[0.0_f32; 4]), // 16 bytes aligned! + // Mapping at creation (as done by the create_buffer_init utility) doesn't require us to to add the MAP_WRITE usage + // (this *happens* to workaround this bug ) + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, + label: Some("custom3d"), layout: &bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, @@ -165,7 +165,11 @@ struct TriangleRenderResources { impl TriangleRenderResources { fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, angle: f32) { // Update our uniform buffer with the angle from the UI - queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[angle])); + queue.write_buffer( + &self.uniform_buffer, + 0, + bytemuck::cast_slice(&[angle, 0.0, 0.0, 0.0]), + ); } fn paint<'rp>(&'rp self, render_pass: &mut wgpu::RenderPass<'rp>) { diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl b/crates/egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl index 140f5f6e1..d8d7ad16d 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl @@ -4,7 +4,7 @@ struct VertexOut { }; struct Uniforms { - angle: f32, + @size(16) angle: f32, // pad to 16 bytes }; @group(0) @binding(0) diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs index aafd53ae2..85d5bd498 100644 --- a/crates/egui_demo_app/src/lib.rs +++ b/crates/egui_demo_app/src/lib.rs @@ -62,13 +62,14 @@ pub fn init_wasm_hooks() { #[cfg(target_arch = "wasm32")] #[wasm_bindgen] -pub fn start_separate(canvas_id: &str) -> Result { +pub async fn start_separate(canvas_id: &str) -> Result { let web_options = eframe::WebOptions::default(); let handle = eframe::start_web( canvas_id, web_options, Box::new(|cc| Box::new(WrapApp::new(cc))), ) + .await .map(|handle| WebHandle { handle }); handle @@ -80,7 +81,7 @@ pub fn start_separate(canvas_id: &str) -> Result Result { +pub async fn start(canvas_id: &str) -> Result { init_wasm_hooks(); - start_separate(canvas_id) + start_separate(canvas_id).await } diff --git a/sh/setup_web.sh b/sh/setup_web.sh index f7ed0bd51..d75343e74 100755 --- a/sh/setup_web.sh +++ b/sh/setup_web.sh @@ -5,4 +5,4 @@ cd "$script_path/.." # Pre-requisites: rustup target add wasm32-unknown-unknown -cargo install wasm-bindgen-cli --version 0.2.82 +cargo install wasm-bindgen-cli --version 0.2.83