Skip to content
Draft
28 changes: 22 additions & 6 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub mod prelude {
};
}

use bevy_window::{PrimaryWindow, RawHandleWrapper};
use bevy_window::PrimaryWindow;
use globals::GlobalsPlugin;
pub use once_cell;

Expand Down Expand Up @@ -189,12 +189,14 @@ pub struct RenderApp;
impl Plugin for RenderPlugin {
/// Initializes the renderer, sets up the [`RenderSet`](RenderSet) and creates the rendering sub-app.
fn build(&self, app: &mut App) {
use bevy_window::AbstractHandleWrapper;

app.add_asset::<Shader>()
.add_debug_asset::<Shader>()
.init_asset_loader::<ShaderLoader>()
.init_debug_asset_loader::<ShaderLoader>();

let mut system_state: SystemState<Query<&RawHandleWrapper, With<PrimaryWindow>>> =
let mut system_state: SystemState<Query<&AbstractHandleWrapper, With<PrimaryWindow>>> =
SystemState::new(&mut app.world);
let primary_window = system_state.get(&app.world);

Expand All @@ -205,10 +207,24 @@ impl Plugin for RenderPlugin {
});
let surface = primary_window.get_single().ok().map(|wrapper| unsafe {
// SAFETY: Plugins should be set up on the main thread.
let handle = wrapper.get_handle();
instance
.create_surface(&handle)
.expect("Failed to create wgpu surface")
match wrapper {
AbstractHandleWrapper::RawHandle(handle) => instance
.create_surface(&handle.get_handle())
.expect("Failed to create wgpu surface"),
#[cfg(target_arch = "wasm32")]
AbstractHandleWrapper::WebHandle(web_handle) => {
use bevy_window::WebHandle;

match web_handle {
WebHandle::HtmlCanvas(canvas) => {
instance.create_surface_from_canvas(canvas).unwrap()
}
WebHandle::OffscreenCanvas(offscreen_canvas) => instance
.create_surface_from_offscreen_canvas(offscreen_canvas)
.unwrap(),
}
}
}
});

let request_adapter_options = wgpu::RequestAdapterOptions {
Expand Down
34 changes: 28 additions & 6 deletions crates/bevy_render/src/view/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
use bevy_utils::{tracing::debug, HashMap, HashSet};
use bevy_window::{
CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed,
AbstractHandleWrapper, CompositeAlphaMode, PresentMode, PrimaryWindow, Window, WindowClosed,
};
use std::ops::{Deref, DerefMut};
use wgpu::TextureFormat;
Expand Down Expand Up @@ -42,7 +42,7 @@ impl Plugin for WindowRenderPlugin {
pub struct ExtractedWindow {
/// An entity that contains the components in [`Window`].
pub entity: Entity,
pub handle: RawHandleWrapper,
pub handle: AbstractHandleWrapper,
pub physical_width: u32,
pub physical_height: u32,
pub present_mode: PresentMode,
Expand Down Expand Up @@ -76,7 +76,14 @@ impl DerefMut for ExtractedWindows {
fn extract_windows(
mut extracted_windows: ResMut<ExtractedWindows>,
mut closed: Extract<EventReader<WindowClosed>>,
windows: Extract<Query<(Entity, &Window, &RawHandleWrapper, Option<&PrimaryWindow>)>>,
windows: Extract<
Query<(
Entity,
&Window,
&AbstractHandleWrapper,
Option<&PrimaryWindow>,
)>,
>,
) {
for (entity, window, handle, primary) in windows.iter() {
if primary.is_some() {
Expand Down Expand Up @@ -186,9 +193,24 @@ pub fn prepare_windows(
.or_insert_with(|| unsafe {
// NOTE: On some OSes this MUST be called from the main thread.
// As of wgpu 0.15, only failable if the given window is a HTML canvas and obtaining a WebGPU or WebGL2 context fails.
let surface = render_instance
.create_surface(&window.handle.get_handle())
.expect("Failed to create wgpu surface");
let surface = match &window.handle {
AbstractHandleWrapper::RawHandle(handle) => render_instance
.create_surface(&handle.get_handle())
.expect("Failed to create wgpu surface"),
#[cfg(target_arch = "wasm32")]
AbstractHandleWrapper::WebHandle(web_handle) => {
use bevy_window::WebHandle;

match web_handle {
WebHandle::HtmlCanvas(canvas) => render_instance
.create_surface_from_canvas(canvas)
.expect("Failed to create wgpu surface"),
WebHandle::OffscreenCanvas(canvas) => render_instance
.create_surface_from_offscreen_canvas(canvas)
.expect("Failed to create wgpu surface"),
}
}
};
let caps = surface.get_capabilities(&render_adapter);
let formats = caps.formats;
// For future HDR output support, we'll need to request a format that supports HDR,
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_window/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ raw-window-handle = "0.5"

# other
serde = { version = "1.0", features = ["derive"], optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = "0.3"
63 changes: 63 additions & 0 deletions crates/bevy_window/src/raw_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use bevy_ecs::prelude::Component;
use raw_window_handle::{
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle,
};
#[cfg(target_arch = "wasm32")]
use web_sys::{HtmlCanvasElement, OffscreenCanvas};

/// A wrapper over [`RawWindowHandle`] and [`RawDisplayHandle`] that allows us to safely pass it across threads.
///
Expand Down Expand Up @@ -74,3 +76,64 @@ unsafe impl HasRawDisplayHandle for ThreadLockedRawWindowHandleWrapper {
self.0.get_display_handle()
}
}

/// Handle used for creating surfaces in the render plugin
///
/// Either a raw handle to an OS window or some canvas flavor on wasm.
/// For non-web platforms it essentially compiles down to newtype wrapper around `RawHandleWrapper`.
///
/// # Details
///
/// `RawHandleWrapper` is not particularly useful on wasm.
///
/// * `RawDisplayHandle` is entirely ignored as Bevy has no control over
/// where the element is going to be displayed.
/// * `RawWindowHandle::Web` contains a single `u32` as payload.
/// `wgpu` uses that in a css selector to discover canvas element.
///
/// This system is overly rigid and fragile.
/// Regardless of how we specify the target element `wgpu` have to land on `WebGl2RenderingContext`
/// in order to render anything.
/// However that prevents us from directly specifying which element it should use.
/// This is especially bad when Bevy is run from web-worker context:
/// workers don't have access to DOM, so it inevitably leads to panic!
///
/// It is understandable why `RawWindowHandle` doesn't include JS objects,
/// so instead we use `AbstractHandleWrapper` to provide a workaround.
///
/// # Note
///
/// While workable it might be possible to remove this abstraction.
/// At the end of the day interpretation of `RawWindowHandle::Web` payload is up to us.
/// We can intercept it before it makes to `wgpu::Instance` and use it to look up
/// `HtmlCanvasElement` or `OffscreenCanvas` from global memory
/// (which will be different on whether Bevy runs as main or worker)
/// and pass that to `wgpu`.
/// This will require a bunch of extra machinery and will be confusing to users
/// which don't rely on `bevy_winit` but can be an option in case this abstraction is undesirable.
#[derive(Debug, Clone, Component)]
pub enum AbstractHandleWrapper {
/// The window corresponds to an operator system window.
RawHandle(RawHandleWrapper),

/// A handle to JS object containing rendering surface.
#[cfg(target_arch = "wasm32")]
WebHandle(WebHandle),
}

/// A `Send + Sync` wrapper around `HtmlCanvasElement` or `OffscreenCanvas`.
///
/// # Safety
///
/// Only safe to use from the main thread.
#[cfg(target_arch = "wasm32")]
#[derive(Debug, Clone, Component)]
pub enum WebHandle {
HtmlCanvas(HtmlCanvasElement),
OffscreenCanvas(OffscreenCanvas),
}

#[cfg(target_arch = "wasm32")]
unsafe impl Send for WebHandle {}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for WebHandle {}
69 changes: 62 additions & 7 deletions crates/bevy_window/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,37 @@ pub struct Window {
///
/// - iOS / Android / Web / Wayland: Unsupported.
pub window_level: WindowLevel,
/// The "html canvas" element selector.
/// Instructs which web element window should be associated with.
///
/// If set, this selector will be used to find a matching html canvas element,
/// rather than creating a new one.
/// Uses the [CSS selector format](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector).
/// ## Platform-specific
///
/// This value has no effect on non-web platforms.
pub canvas: Option<String>,
/// This field is ignored for non-web platforms.
/// You can safely initialize it to `Default::default()`.
///
/// For web platform the enum determines how `WinitPlugin` is going to discover
/// which web element the window should be associated with.
///
/// ## Panic safety
///
/// On `wasm32` it is important to know *how* Bevy is going to be run.
/// Wasm can be run either as **main** (e.g. on main JS event loop) or as web **worker**.
///
/// * When run as **main**, all web APIs are available so all variants for `WebElement` will work.
/// * When run as **worker** only `WebElement::OffscreenCanvas` is safe, other variants will panic.
///
/// This happens because:
/// * `WebElement::Generate` and `WebElement::CssSelector` require access to DOM which worker doesn't have.
/// * Worker cannot directly interact with WebGL context of `HtmlCanvasElement`.
///
/// For more details on web-worker APIs see [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).
///
/// Note that by default the field is initialized to `Generate` and it will panic for web workers!
///
/// ## Reflection
///
/// On `wasm32` this field contains `js-sys` objects which don't implement `Reflect`.
#[reflect(ignore)]
pub web_element: WebElement,
/// Whether or not to fit the canvas element's size to its parent element's size.
///
/// **Warning**: this will not behave as expected for parents that set their size according to the size of their
Expand Down Expand Up @@ -206,9 +229,9 @@ impl Default for Window {
transparent: false,
focused: true,
window_level: Default::default(),
web_element: Default::default(),
fit_canvas_to_parent: false,
prevent_default_event_handling: true,
canvas: None,
}
}
}
Expand Down Expand Up @@ -826,3 +849,35 @@ pub enum WindowLevel {
/// The window will always be on top of normal windows.
AlwaysOnTop,
}

/// Instructs which web element window should be associated with.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum WebElement {
/// Generate a new `HtmlCanvasElement` and attach it to body.
///
/// This option is good for quick testing/setup,
/// but consider choosing more controllable behavior.
#[default]
Generate,

/// Discover `HtmlCanvasElement` via a css selector.
///
/// # Panic
///
/// This option will panic if the discovered element is not a canvas.
#[cfg(target_arch = "wasm32")]
CssSelector(String),

/// Use specified `HtmlCanvasElement`.
#[cfg(target_arch = "wasm32")]
HtmlCanvas(web_sys::HtmlCanvasElement),

/// Use specified `OffscreenCanvas`.
#[cfg(target_arch = "wasm32")]
OffscreenCanvas(web_sys::OffscreenCanvas),
}

#[cfg(target_arch = "wasm32")]
unsafe impl Send for WebElement {}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for WebElement {}
Loading