Browse Source

eframe support for wgpu on the web (#2107)

* 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>
pull/2110/head
Andreas Reich 2 years ago
committed by GitHub
parent
commit
c2a37f4bd8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/rust.yml
  2. 21
      Cargo.lock
  3. 1
      crates/eframe/CHANGELOG.md
  4. 15
      crates/eframe/Cargo.toml
  5. 2
      crates/eframe/src/epi.rs
  6. 12
      crates/eframe/src/lib.rs
  7. 36
      crates/eframe/src/web/backend.rs
  8. 1
      crates/eframe/src/web/events.rs
  9. 61
      crates/eframe/src/web/mod.rs
  10. 30
      crates/eframe/src/web/web_painter.rs
  11. 120
      crates/eframe/src/web/web_painter_glow.rs
  12. 196
      crates/eframe/src/web/web_painter_wgpu.rs
  13. 27
      crates/egui-wgpu/src/lib.rs
  14. 51
      crates/egui-wgpu/src/renderer.rs
  15. 26
      crates/egui-wgpu/src/winit.rs
  16. 1
      crates/egui_demo_app/Cargo.toml
  17. 30
      crates/egui_demo_app/src/apps/custom3d_wgpu.rs
  18. 2
      crates/egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl
  19. 7
      crates/egui_demo_app/src/lib.rs
  20. 2
      sh/setup_web.sh

2
.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:

21
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"

1
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

15
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"] }

2
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,
}
}

12
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<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
//! }
//! ```
//!
@ -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)
}

36
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<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
@ -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()
@ -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<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)
}

1
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

61
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
}

30
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<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);
}

120
crates/eframe/src/web/web_glow_painter.rs → 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<Self, String> {
impl WebPainterGlow {
pub fn gl(&self) -> &std::sync::Arc<glow::Context> {
self.painter.gl()
}
pub async fn new(canvas_id: &str, options: &WebOptions) -> Result<Self, String> {
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<glow::Context> {
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::<web_sys::WebGlRenderingContext>()
.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
}

196
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<RenderState>,
}
impl WebPainterWgpu {
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub fn render_state(&self) -> Option<RenderState> {
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<Self, String> {
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;
}
}

27
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<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub target_format: wgpu::TextureFormat,
pub renderer: Arc<RwLock<Renderer>>,
}
/// 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
}

51
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<PrepareCallback>,
paint: Box<PaintCallback>,
@ -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::<UniformBuffer>() 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 {

26
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<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
pub target_format: wgpu::TextureFormat,
pub renderer: Arc<RwLock<Renderer>>,
}
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
}

1
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"

30
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>) {

2
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)

7
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<WebHandle, wasm_bindgen::JsValue> {
pub async fn start_separate(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
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<WebHandle, wasm_bindgen::JsValu
/// 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<WebHandle, wasm_bindgen::JsValue> {
pub async fn start(canvas_id: &str) -> Result<WebHandle, wasm_bindgen::JsValue> {
init_wasm_hooks();
start_separate(canvas_id)
start_separate(canvas_id).await
}

2
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

Loading…
Cancel
Save