Skip to content

Commit

Permalink
eframe support for wgpu on the web (#2107)
Browse files Browse the repository at this point in the history
* basic working wgpu @ webgl on websys

* fix glow compile error

* introduced WebPainter trait, provide wgpu renderstate

* WebPainterWgpu destroy implemented

* make custom3d demo work on wgpu backend

* changelog entry for wgpu support eframe wasm

* remove temporary logging hack

* stop using pollster for web
we're actually not allowed to block - this only worked because wgpu on webgl doesn't actually cause anything blocking. However, when trying webgpu this became an issue

* revert cargo update

* compile error if neither glow nor wgpu features are enabled

* code cleanup

* Error handling

* Update changelog with link

* Make sure --all-features work

* Select best framebuffer format from the available ones

* update to wasm-bindgen 0.2.83

* Fix typo

* Clean up Cargo.toml

* Log about using the wgpu painter

* fixup wgpu labels

* fix custom3d_wgpu_shader ub padding

* remove duplicated uniforms struct in wgsl shader for custom3d

* Update docs: add async/await to the web 'start' function

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
  • Loading branch information
Wumpf and emilk authored Oct 5, 2022
1 parent c441f33 commit c2a37f4
Show file tree
Hide file tree
Showing 20 changed files with 432 additions and 211 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 11 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/eframe/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 8 additions & 7 deletions crates/eframe/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand Down Expand Up @@ -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"] }
2 changes: 2 additions & 0 deletions crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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,
}
}
Expand Down
12 changes: 6 additions & 6 deletions crates/eframe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
//! /// Call this once from the HTML.
//! #[cfg(target_arch = "wasm32")]
//! #[wasm_bindgen]
//! pub fn start(canvas_id: &str) -> Result<AppRunnerRef, eframe::wasm_bindgen::JsValue> {
//! pub async fn start(canvas_id: &str) -> Result<AppRunnerRef, 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
//! }
//! ```
//!
Expand Down Expand Up @@ -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<AppRunnerRef>, eframe::wasm_bindgen::JsValue> {
/// pub async fn start(canvas_id: &str) -> Result<AppRunnerRef>, 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<AppRunnerRef, wasm_bindgen::JsValue> {
let handle = web::start(canvas_id, web_options, app_creator)?;
let handle = web::start(canvas_id, web_options, app_creator).await?;

Ok(handle)
}
Expand Down
36 changes: 20 additions & 16 deletions crates/eframe/src/web/backend.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use super::{WebPainter, *};

use super::{web_painter::WebPainter, *};
use crate::epi;

use egui::{
Expand Down Expand Up @@ -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<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
Expand All @@ -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<Self, JsValue> {
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()
Expand Down Expand Up @@ -216,19 +216,27 @@ 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,
});

let frame = epi::Frame {
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,
};

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<AppRunnerRef, JsValue> {
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)
}
Expand Down
1 change: 0 additions & 1 deletion crates/eframe/src/web/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 15 additions & 46 deletions crates/eframe/src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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
}
30 changes: 30 additions & 0 deletions crates/eframe/src/web/web_painter.rs
Original file line number Diff line number Diff line change
@@ -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<Self, String>
// 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);
}
Loading

0 comments on commit c2a37f4

Please sign in to comment.