diff --git a/Cargo.lock b/Cargo.lock index e81a871f48..1c50600e16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8099,6 +8099,7 @@ dependencies = [ "itertools 0.14.0", "js-sys", "log", + "naga", "parking_lot", "pollster 0.4.0", "profiling", @@ -21357,6 +21358,7 @@ dependencies = [ "thiserror 2.0.17", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-naga-bridge", @@ -21381,6 +21383,15 @@ dependencies = [ "wgpu-hal", ] +[[package]] +name = "wgpu-core-deps-wasm" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059" +dependencies = [ + "wgpu-hal", +] + [[package]] name = "wgpu-core-deps-windows-linux-android" version = "29.0.4" diff --git a/crates/gpui_platform/src/gpui_platform.rs b/crates/gpui_platform/src/gpui_platform.rs index 09b66bb8fc..91d336ad16 100644 --- a/crates/gpui_platform/src/gpui_platform.rs +++ b/crates/gpui_platform/src/gpui_platform.rs @@ -13,9 +13,7 @@ pub fn background_executor() -> gpui::BackgroundExecutor { pub fn application() -> gpui::Application { #[cfg(target_family = "wasm")] { - let platform = Rc::new(gpui_web::WebPlatform::new(true)); - let http_client = std::sync::Arc::new(platform.fetch_http_client()); - gpui::Application::with_platform(platform).with_http_client(http_client) + application_with_web_backend(gpui_web::WebBackendPreference::Auto) } #[cfg(not(target_family = "wasm"))] @@ -26,6 +24,19 @@ pub fn headless() -> gpui::Application { gpui::Application::with_platform(current_platform(true)) } +#[cfg(target_family = "wasm")] +pub use gpui_web::WebBackendPreference; + +#[cfg(target_family = "wasm")] +pub fn application_with_web_backend(backend_preference: WebBackendPreference) -> gpui::Application { + let platform = Rc::new(gpui_web::WebPlatform::new_with_backend( + true, + backend_preference, + )); + let http_client = std::sync::Arc::new(platform.fetch_http_client()); + gpui::Application::with_platform(platform).with_http_client(http_client) +} + /// Unlike `application`, this function returns a single-threaded web application. #[cfg(target_family = "wasm")] pub fn single_threaded_web() -> gpui::Application { diff --git a/crates/gpui_web/Cargo.toml b/crates/gpui_web/Cargo.toml index 2ec64e6968..917bc14267 100644 --- a/crates/gpui_web/Cargo.toml +++ b/crates/gpui_web/Cargo.toml @@ -54,6 +54,7 @@ web-sys = { version = "0.3", features = [ "IdleDeadline", "IdleRequestOptions", "KeyboardEvent", + "Location", "MediaQueryList", "MediaQueryListEvent", "MouseEvent", diff --git a/crates/gpui_web/examples/hello_web/Cargo.toml b/crates/gpui_web/examples/hello_web/Cargo.toml index 7f8c5cf137..925f47c71e 100644 --- a/crates/gpui_web/examples/hello_web/Cargo.toml +++ b/crates/gpui_web/examples/hello_web/Cargo.toml @@ -13,4 +13,5 @@ path = "main.rs" [dependencies] gpui = { path = "../../../gpui" } gpui_platform = { path = "../../../gpui_platform" } +web-sys = { version = "0.3", features = ["Location", "Window"] } web-time = "1" diff --git a/crates/gpui_web/examples/hello_web/main.rs b/crates/gpui_web/examples/hello_web/main.rs index a6ff103e64..605fdf090e 100644 --- a/crates/gpui_web/examples/hello_web/main.rs +++ b/crates/gpui_web/examples/hello_web/main.rs @@ -405,9 +405,30 @@ impl Render for HelloWeb { // Entry point // --------------------------------------------------------------------------- +fn requested_backend() -> gpui_platform::WebBackendPreference { + let search = web_sys::window() + .and_then(|window| window.location().search().ok()) + .unwrap_or_default(); + if search + .trim_start_matches('?') + .split('&') + .any(|parameter| parameter == "backend=webgpu") + { + gpui_platform::WebBackendPreference::WebGpu + } else if search + .trim_start_matches('?') + .split('&') + .any(|parameter| parameter == "backend=webgl") + { + gpui_platform::WebBackendPreference::WebGl + } else { + gpui_platform::WebBackendPreference::Auto + } +} + fn main() { gpui_platform::web_init(); - gpui_platform::application().run(|cx: &mut App| { + gpui_platform::application_with_web_backend(requested_backend()).run(|cx: &mut App| { let bounds = Bounds::centered(None, size(px(640.), px(560.)), cx); cx.open_window( WindowOptions { diff --git a/crates/gpui_web/src/gpui_web.rs b/crates/gpui_web/src/gpui_web.rs index dd9c394816..72c56f95d9 100644 --- a/crates/gpui_web/src/gpui_web.rs +++ b/crates/gpui_web/src/gpui_web.rs @@ -1,5 +1,10 @@ #![cfg(target_family = "wasm")] +//! GPUI's browser platform uses one document-owned canvas and supports one top-level window. +//! Browser WebGPU is preferred by default, with an automatic WebGL2 fallback. Applications can +//! force either backend with [`WebBackendPreference`]. Opening a second top-level window, or +//! reopening one after it closes, returns [`WebWindowError`]. + mod dispatcher; mod display; mod events; @@ -11,8 +16,9 @@ mod window; pub use dispatcher::WebDispatcher; pub use display::WebDisplay; +pub use gpui_wgpu::WebBackendPreference; pub use http_client::{FetchCredentials, FetchHttpClient}; pub use keyboard::WebKeyboardLayout; pub use logging::init_logging; -pub use platform::WebPlatform; +pub use platform::{WebPlatform, WebWindowError}; pub use window::WebWindow; diff --git a/crates/gpui_web/src/platform.rs b/crates/gpui_web/src/platform.rs index 33af331e30..ecf2736cee 100644 --- a/crates/gpui_web/src/platform.rs +++ b/crates/gpui_web/src/platform.rs @@ -12,7 +12,7 @@ use gpui::{ PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Task, ThermalState, WindowAppearance, WindowKind, WindowParams, popup::PopupNotSupportedError, }; -use gpui_wgpu::WgpuContext; +use gpui_wgpu::{PreparedWebGraphics, WebBackendPreference, WgpuContext, wgpu}; use std::{ borrow::Cow, cell::{Cell, RefCell}, @@ -39,15 +39,69 @@ pub struct WebPlatform { background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, text_system: Arc, - active_window: RefCell>, + active_window: Rc>>, active_display: Rc, callbacks: RefCell, + backend_preference: WebBackendPreference, wgpu_context: Rc>>, + prepared_window: Rc>>, + window_lifecycle: Rc>, cursor_visible: Rc>, last_cursor_css: Rc>, _cursor_restore_listeners: Vec, } +struct PreparedWebWindow { + canvas: web_sys::HtmlCanvasElement, + surface: wgpu::Surface<'static>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum WebWindowLifecycle { + Available, + Open, + Closed, + Unavailable, +} + +#[derive(Debug)] +pub enum WebWindowError { + AlreadyOpen, + ReopeningUnsupported, + UnsupportedWindowKind(&'static str), + /// Graphics initialization has not completed yet; retrying after it + /// finishes (e.g. from the `Platform::run` callback) can succeed. + GraphicsInitializationPending, + /// Graphics initialization or an earlier window creation failed; + /// retrying cannot succeed. + GraphicsUnavailable, +} + +impl std::fmt::Display for WebWindowError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AlreadyOpen => formatter.write_str( + "GPUI web supports only one top-level window; a window is already open", + ), + Self::ReopeningUnsupported => formatter.write_str( + "reopening the GPUI web top-level window after it closes is not supported", + ), + Self::UnsupportedWindowKind(kind) => write!( + formatter, + "GPUI web does not support {kind} as a separate top-level window; render it inside the normal window instead" + ), + Self::GraphicsInitializationPending => formatter.write_str( + "browser graphics initialization has not completed yet; open windows from the callback passed to Platform::run", + ), + Self::GraphicsUnavailable => formatter.write_str( + "browser graphics are unavailable because graphics initialization or an earlier window creation failed", + ), + } + } +} + +impl std::error::Error for WebWindowError {} + #[derive(Default)] struct WebPlatformCallbacks { open_urls: Option)>>, @@ -62,6 +116,13 @@ struct WebPlatformCallbacks { impl WebPlatform { pub fn new(allow_multi_threading: bool) -> Self { + Self::new_with_backend(allow_multi_threading, WebBackendPreference::Auto) + } + + pub fn new_with_backend( + allow_multi_threading: bool, + backend_preference: WebBackendPreference, + ) -> Self { let browser_window = web_sys::window().expect("must be running in a browser window context"); let dispatcher = Arc::new(WebDispatcher::new( @@ -98,10 +159,13 @@ impl WebPlatform { background_executor, foreground_executor, text_system, - active_window: RefCell::new(None), + active_window: Rc::new(RefCell::new(None)), active_display, callbacks: RefCell::new(WebPlatformCallbacks::default()), + backend_preference, wgpu_context: Rc::new(RefCell::new(None)), + prepared_window: Rc::new(RefCell::new(None)), + window_lifecycle: Rc::new(Cell::new(WebWindowLifecycle::Available)), cursor_visible, last_cursor_css, _cursor_restore_listeners: cursor_restore_listeners, @@ -122,6 +186,83 @@ impl WebPlatform { } } +async fn initialize_graphics( + browser_window: &web_sys::Window, + preference: WebBackendPreference, +) -> anyhow::Result<( + web_sys::HtmlCanvasElement, + WgpuContext, + wgpu::Surface<'static>, +)> { + match preference { + WebBackendPreference::Auto => { + let webgpu_canvas = WebWindow::prepare_canvas(browser_window)?; + let webgpu_result = if wgpu::util::is_browser_webgpu_supported().await { + WgpuContext::new_web(&webgpu_canvas, WebBackendPreference::WebGpu).await + } else { + Err(anyhow::anyhow!( + "browser WebGPU probe did not return a usable adapter" + )) + }; + match webgpu_result { + Ok(PreparedWebGraphics { context, surface }) => { + return Ok((webgpu_canvas, context, surface)); + } + Err(webgpu_error) => { + let canvas: &web_sys::Element = webgpu_canvas.as_ref(); + canvas.remove(); + log::warn!( + "WebGPU initialization failed; falling back to WebGL2: {webgpu_error:#}" + ); + + let webgl_canvas = + WebWindow::prepare_canvas(browser_window).map_err(|error| { + anyhow::anyhow!( + "WebGPU initialization failed: {webgpu_error:#}. \ + Failed to prepare a replacement canvas for WebGL2: {error:#}" + ) + })?; + match WgpuContext::new_web(&webgl_canvas, WebBackendPreference::WebGl).await { + Ok(PreparedWebGraphics { context, surface }) => { + Ok((webgl_canvas, context, surface)) + } + Err(webgl_error) => { + let canvas: &web_sys::Element = webgl_canvas.as_ref(); + canvas.remove(); + Err(anyhow::anyhow!( + "No browser graphics backend could be initialized. \ + Tried WebGPU, then WebGL2. \ + WebGPU failure: {webgpu_error:#}. \ + WebGL2 failure: {webgl_error:#}" + )) + } + } + } + } + } + WebBackendPreference::WebGpu | WebBackendPreference::WebGl => { + let backend_name = if preference == WebBackendPreference::WebGpu { + "WebGPU" + } else { + "WebGL2" + }; + let canvas = WebWindow::prepare_canvas(browser_window)?; + match WgpuContext::new_web(&canvas, preference).await { + Ok(PreparedWebGraphics { context, surface }) => Ok((canvas, context, surface)), + Err(error) => { + let canvas: &web_sys::Element = canvas.as_ref(); + canvas.remove(); + Err(anyhow::anyhow!( + "No browser graphics backend could be initialized. \ + Only {backend_name} was tried because the application requested \ + it explicitly. {backend_name} failure: {error:#}" + )) + } + } + } + } +} + impl Platform for WebPlatform { fn background_executor(&self) -> BackgroundExecutor { self.background_executor.clone() @@ -137,20 +278,25 @@ impl Platform for WebPlatform { fn run(&self, on_finish_launching: Box) { let wgpu_context = self.wgpu_context.clone(); + let prepared_window = self.prepared_window.clone(); + let window_lifecycle = self.window_lifecycle.clone(); let browser_window = self.browser_window.clone(); + let backend_preference = self.backend_preference; wasm_bindgen_futures::spawn_local(async move { - match WgpuContext::new_web().await { - Ok(context) => { - log::info!("WebGPU context initialized successfully"); + match initialize_graphics(&browser_window, backend_preference).await { + Ok((canvas, context, surface)) => { + log::info!( + "Browser graphics initialized successfully with {:?}", + context.backend() + ); *wgpu_context.borrow_mut() = Some(context); + *prepared_window.borrow_mut() = Some(PreparedWebWindow { canvas, surface }); on_finish_launching(); } - Err(err) => { - // Without a GPU context nothing can ever render, so - // launching the app would only produce confusing - // downstream errors. Leave a message in the page instead. - log::error!("Failed to initialize WebGPU context: {err:#}"); - show_webgpu_unavailable_message(&browser_window); + Err(error) => { + window_lifecycle.set(WebWindowLifecycle::Unavailable); + log::error!("Failed to initialize browser graphics: {error:#}"); + show_graphics_unavailable_message(&browser_window, &error); } } }); @@ -187,20 +333,66 @@ impl Platform for WebPlatform { handle: AnyWindowHandle, params: WindowParams, ) -> anyhow::Result> { - // Native popups are not implemented on the web yet. Rejecting lets callers fall back to - // gpui's in-window popovers. - if let WindowKind::AnchoredPopup(_) = params.kind { - return Err(PopupNotSupportedError.into()); + match ¶ms.kind { + WindowKind::Normal => {} + WindowKind::AnchoredPopup(_) => return Err(PopupNotSupportedError.into()), + WindowKind::PopUp => { + return Err(WebWindowError::UnsupportedWindowKind("popup windows").into()); + } + WindowKind::Floating => { + return Err(WebWindowError::UnsupportedWindowKind("floating windows").into()); + } + WindowKind::Dialog => { + return Err(WebWindowError::UnsupportedWindowKind("dialog windows").into()); + } + } + + match self.window_lifecycle.get() { + WebWindowLifecycle::Open => return Err(WebWindowError::AlreadyOpen.into()), + WebWindowLifecycle::Closed => { + return Err(WebWindowError::ReopeningUnsupported.into()); + } + WebWindowLifecycle::Unavailable => { + return Err(WebWindowError::GraphicsUnavailable.into()); + } + WebWindowLifecycle::Available => {} } let context_ref = self.wgpu_context.borrow(); - let context = context_ref.as_ref().ok_or_else(|| { - anyhow::anyhow!("WebGPU context not initialized. Was Platform::run() called?") - })?; + let context = context_ref + .as_ref() + .ok_or(WebWindowError::GraphicsInitializationPending)?; + let prepared_window = self + .prepared_window + .borrow_mut() + .take() + .ok_or(WebWindowError::GraphicsInitializationPending)?; + let canvas = prepared_window.canvas; + let canvas_for_cleanup = canvas.clone(); - let window = WebWindow::new(handle, params, context, self.browser_window.clone())?; - *self.active_window.borrow_mut() = Some(handle); - Ok(Box::new(window)) + let window = WebWindow::new( + handle, + params, + context, + canvas, + prepared_window.surface, + self.browser_window.clone(), + self.window_lifecycle.clone(), + self.active_window.clone(), + ); + match window { + Ok(window) => { + self.window_lifecycle.set(WebWindowLifecycle::Open); + *self.active_window.borrow_mut() = Some(handle); + Ok(Box::new(window)) + } + Err(error) => { + let canvas: &web_sys::Element = canvas_for_cleanup.as_ref(); + canvas.remove(); + self.window_lifecycle.set(WebWindowLifecycle::Unavailable); + Err(error) + } + } } fn window_appearance(&self) -> WindowAppearance { @@ -438,7 +630,7 @@ fn cursor_restore_listeners( handles } -fn show_webgpu_unavailable_message(browser_window: &web_sys::Window) { +fn show_graphics_unavailable_message(browser_window: &web_sys::Window, error: &anyhow::Error) { let Some(document) = browser_window.document() else { return; }; @@ -448,9 +640,9 @@ fn show_webgpu_unavailable_message(browser_window: &web_sys::Window) { let Ok(message) = document.create_element("p") else { return; }; - message.set_text_content(Some( - "Failed to initialize WebGPU. This application requires a browser with WebGPU support.", - )); + message.set_text_content(Some(&format!( + "Failed to initialize browser graphics: {error}" + ))); body.append_child(&message).ok(); } diff --git a/crates/gpui_web/src/window.rs b/crates/gpui_web/src/window.rs index 3dbf013a6f..484014681b 100644 --- a/crates/gpui_web/src/window.rs +++ b/crates/gpui_web/src/window.rs @@ -1,5 +1,6 @@ use crate::display::WebDisplay; use crate::events::{ClickState, EventListenerHandle, WebEventListeners, is_mac_platform}; +use crate::platform::WebWindowLifecycle; use std::sync::Arc; use std::{cell::Cell, cell::RefCell, rc::Rc}; @@ -10,7 +11,7 @@ use gpui::{ ResizeEdge, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, }; -use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig}; +use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig, wgpu}; use wasm_bindgen::prelude::*; #[derive(Default)] @@ -63,6 +64,8 @@ pub(crate) struct WebWindowInner { pub struct WebWindow { inner: Rc, display: Rc, + lifecycle: Rc>, + active_window: Rc>>, _raf_closure: Closure, _resize_observer: Option, _resize_observer_closure: Closure, @@ -70,50 +73,69 @@ pub struct WebWindow { } impl WebWindow { - pub fn new( - _handle: AnyWindowHandle, - _params: WindowParams, - context: &WgpuContext, - browser_window: web_sys::Window, - ) -> anyhow::Result { + pub(crate) fn prepare_canvas( + browser_window: &web_sys::Window, + ) -> anyhow::Result { let document = browser_window .document() .ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?; - let canvas: web_sys::HtmlCanvasElement = document .create_element("canvas") - .map_err(|e| anyhow::anyhow!("Failed to create canvas element: {e:?}"))? + .map_err(|error| anyhow::anyhow!("Failed to create canvas element: {error:?}"))? .dyn_into() - .map_err(|e| anyhow::anyhow!("Created element is not a canvas: {e:?}"))?; - - let dpr = browser_window.device_pixel_ratio() as f32; - let max_texture_dimension = context.device.limits().max_texture_dimension_2d; - let has_device_pixel_support = check_device_pixel_support(); - + .map_err(|error| anyhow::anyhow!("Created element is not a canvas: {error:?}"))?; canvas.set_tab_index(-1); let style = canvas.style(); - style - .set_property("width", "100%") - .map_err(|e| anyhow::anyhow!("Failed to set canvas width style: {e:?}"))?; - style - .set_property("height", "100%") - .map_err(|e| anyhow::anyhow!("Failed to set canvas height style: {e:?}"))?; - style - .set_property("display", "block") - .map_err(|e| anyhow::anyhow!("Failed to set canvas display style: {e:?}"))?; - style - .set_property("outline", "none") - .map_err(|e| anyhow::anyhow!("Failed to set canvas outline style: {e:?}"))?; - style - .set_property("touch-action", "none") - .map_err(|e| anyhow::anyhow!("Failed to set touch-action style: {e:?}"))?; + for (property, value) in [ + ("width", "100%"), + ("height", "100%"), + ("display", "block"), + ("outline", "none"), + ("touch-action", "none"), + ] { + style.set_property(property, value).map_err(|error| { + anyhow::anyhow!("Failed to set canvas {property} style: {error:?}") + })?; + } let body = document .body() .ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?; body.append_child(&canvas) - .map_err(|e| anyhow::anyhow!("Failed to append canvas to body: {e:?}"))?; + .map_err(|error| anyhow::anyhow!("Failed to append canvas to body: {error:?}"))?; + Ok(canvas) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + _handle: AnyWindowHandle, + _params: WindowParams, + context: &WgpuContext, + canvas: web_sys::HtmlCanvasElement, + surface: wgpu::Surface<'static>, + browser_window: web_sys::Window, + lifecycle: Rc>, + active_window: Rc>>, + ) -> anyhow::Result { + let document = browser_window + .document() + .ok_or_else(|| anyhow::anyhow!("No `document` found on window"))?; + let body = document + .body() + .ok_or_else(|| anyhow::anyhow!("No `body` found on document"))?; + let dpr = browser_window.device_pixel_ratio() as f32; + let max_texture_dimension = context.device.limits().max_texture_dimension_2d; + let has_device_pixel_support = check_device_pixel_support(); + let renderer_config = WgpuSurfaceConfig { + size: Size { + width: DevicePixels(0), + height: DevicePixels(0), + }, + transparent: false, + preferred_present_mode: None, + }; + let renderer = WgpuRenderer::new_from_surface(context, surface, renderer_config)?; let input_element: web_sys::HtmlInputElement = document .create_element("input") @@ -131,19 +153,6 @@ impl WebWindow { .map_err(|e| anyhow::anyhow!("Failed to append input to body: {e:?}"))?; input_element.focus().ok(); - let device_size = Size { - width: DevicePixels(0), - height: DevicePixels(0), - }; - - let renderer_config = WgpuSurfaceConfig { - size: device_size, - transparent: false, - preferred_present_mode: None, - }; - - let renderer = WgpuRenderer::new_from_canvas(context, &canvas, renderer_config)?; - let display: Rc = Rc::new(WebDisplay::new(browser_window.clone())); let initial_bounds = Bounds { @@ -203,6 +212,8 @@ impl WebWindow { Ok(Self { inner, display, + lifecycle, + active_window, _raf_closure: raf_closure, _resize_observer: resize_observer, _resize_observer_closure: resize_observer_closure, @@ -515,6 +526,8 @@ impl Drop for WebWindow { canvas.remove(); let input_element: &web_sys::Element = self.inner.input_element.as_ref(); input_element.remove(); + self.active_window.borrow_mut().take(); + self.lifecycle.set(WebWindowLifecycle::Closed); } } diff --git a/crates/gpui_wgpu/Cargo.toml b/crates/gpui_wgpu/Cargo.toml index 03aea7f7d2..280c64ddf8 100644 --- a/crates/gpui_wgpu/Cargo.toml +++ b/crates/gpui_wgpu/Cargo.toml @@ -46,9 +46,11 @@ wasm-bindgen.workspace = true wasm-bindgen-futures.workspace = true web-sys = { version = "0.3", features = ["HtmlCanvasElement"] } js-sys.workspace = true +wgpu = { workspace = true, features = ["webgl"] } [dev-dependencies] criterion.workspace = true +naga = { version = "29.0.4", features = ["wgsl-in"] } [[bench]] name = "layout_line" diff --git a/crates/gpui_wgpu/src/shaders.wgsl b/crates/gpui_wgpu/src/shaders.wgsl index c009ed9807..718cb7595d 100644 --- a/crates/gpui_wgpu/src/shaders.wgsl +++ b/crates/gpui_wgpu/src/shaders.wgsl @@ -526,7 +526,6 @@ struct Quad { corner_radii: Corners, border_widths: Edges, } -@group(1) @binding(0) var b_quads: array; struct QuadVarying { @builtin(position) position: vec4, @@ -542,7 +541,7 @@ struct QuadVarying { @vertex fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying { let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let quad = b_quads[instance_id]; + let quad = load_quad(instance_id); var out = QuadVarying(); out.position = to_device_position(unit_vertex, quad.bounds); @@ -569,7 +568,7 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4 { return vec4(0.0); } - let quad = b_quads[input.quad_id]; + let quad = load_quad(input.quad_id); let background_color = gradient_color(quad.background, input.position.xy, quad.bounds, input.background_solid, input.background_color0, input.background_color1); @@ -964,7 +963,6 @@ struct Shadow { inset: u32, pad: u32, // align to 8 bytes } -@group(1) @binding(0) var b_shadows: array; struct ShadowVarying { @builtin(position) position: vec4, @@ -977,7 +975,7 @@ struct ShadowVarying { @vertex fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying { let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - var shadow = b_shadows[instance_id]; + var shadow = load_shadow(instance_id); var geometry: Bounds; if (shadow.inset != 0u) { @@ -1005,7 +1003,7 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4 { return vec4(0.0); } - let shadow = b_shadows[input.shadow_id]; + let shadow = load_shadow(input.shadow_id); let half_size = shadow.bounds.size / 2.0; let center = shadow.bounds.origin + half_size; let center_to_point = input.position.xy - center; @@ -1056,7 +1054,7 @@ struct PathRasterizationVertex { bounds: Bounds, } -@group(1) @binding(0) var b_path_vertices: array; + struct PathRasterizationVarying { @builtin(position) position: vec4, @@ -1068,7 +1066,7 @@ struct PathRasterizationVarying { @vertex fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying { - let v = b_path_vertices[vertex_id]; + let v = load_path_vertex(vertex_id); var out = PathRasterizationVarying(); out.position = to_device_position_impl(v.xy_position); @@ -1086,7 +1084,7 @@ fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) vec4(0.0); } - let v = b_path_vertices[input.vertex_id]; + let v = load_path_vertex(input.vertex_id); let background = v.color; let bounds = v.bounds; @@ -1116,7 +1114,7 @@ fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) vec4 b_path_sprites: array; + struct PathVarying { @builtin(position) position: vec4, @@ -1126,7 +1124,7 @@ struct PathVarying { @vertex fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying { let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let sprite = b_path_sprites[instance_id]; + let sprite = load_path_sprite(instance_id); // Don't apply content mask because it was already accounted for when rasterizing the path. let device_position = to_device_position(unit_vertex, sprite.bounds); // For screen-space intermediate texture, convert screen position to texture coordinates @@ -1157,7 +1155,7 @@ struct Underline { thickness: f32, wavy: u32, } -@group(1) @binding(0) var b_underlines: array; + struct UnderlineVarying { @builtin(position) position: vec4, @@ -1170,7 +1168,7 @@ struct UnderlineVarying { @vertex fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying { let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let underline = b_underlines[instance_id]; + let underline = load_underline(instance_id); var out = UnderlineVarying(); out.position = to_device_position(unit_vertex, underline.bounds); @@ -1190,7 +1188,7 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4 { return vec4(0.0); } - let underline = b_underlines[input.underline_id]; + let underline = load_underline(input.underline_id); if (underline.wavy == 0u) { return blend_color(input.color, input.color.a); @@ -1223,7 +1221,7 @@ struct MonochromeSprite { tile: AtlasTile, transformation: TransformationMatrix, } -@group(1) @binding(0) var b_mono_sprites: array; + struct MonoSpriteVarying { @builtin(position) position: vec4, @@ -1235,7 +1233,7 @@ struct MonoSpriteVarying { @vertex fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying { let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let sprite = b_mono_sprites[instance_id]; + let sprite = load_mono_sprite(instance_id); var out = MonoSpriteVarying(); out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation); @@ -1271,7 +1269,7 @@ struct PolychromeSprite { corner_radii: Corners, tile: AtlasTile, } -@group(1) @binding(0) var b_poly_sprites: array; + struct PolySpriteVarying { @builtin(position) position: vec4, @@ -1283,7 +1281,7 @@ struct PolySpriteVarying { @vertex fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying { let unit_vertex = vec2(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); - let sprite = b_poly_sprites[instance_id]; + let sprite = load_poly_sprite(instance_id); var out = PolySpriteVarying(); out.position = to_device_position(unit_vertex, sprite.bounds); @@ -1301,7 +1299,7 @@ fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4 { return vec4(0.0); } - let sprite = b_poly_sprites[input.sprite_id]; + let sprite = load_poly_sprite(input.sprite_id); let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii); var color = sample; diff --git a/crates/gpui_wgpu/src/shaders_storage.wgsl b/crates/gpui_wgpu/src/shaders_storage.wgsl new file mode 100644 index 0000000000..55b80802e6 --- /dev/null +++ b/crates/gpui_wgpu/src/shaders_storage.wgsl @@ -0,0 +1,42 @@ +// Storage-buffer instance transport. Concatenated after `shaders.wgsl` on backends +// with storage buffer support; `shaders_webgl.wgsl` is the WebGL2 counterpart that +// decodes the same records from a texture instead. +// +// All buffers share `@group(1) @binding(0)` because each pipeline binds only the +// buffer its own entry points read. + +@group(1) @binding(0) var b_quads: array; +@group(1) @binding(0) var b_shadows: array; +@group(1) @binding(0) var b_path_vertices: array; +@group(1) @binding(0) var b_path_sprites: array; +@group(1) @binding(0) var b_underlines: array; +@group(1) @binding(0) var b_mono_sprites: array; +@group(1) @binding(0) var b_poly_sprites: array; + +fn load_quad(instance_id: u32) -> Quad { + return b_quads[instance_id]; +} + +fn load_shadow(instance_id: u32) -> Shadow { + return b_shadows[instance_id]; +} + +fn load_path_vertex(vertex_id: u32) -> PathRasterizationVertex { + return b_path_vertices[vertex_id]; +} + +fn load_path_sprite(instance_id: u32) -> PathSprite { + return b_path_sprites[instance_id]; +} + +fn load_underline(instance_id: u32) -> Underline { + return b_underlines[instance_id]; +} + +fn load_mono_sprite(instance_id: u32) -> MonochromeSprite { + return b_mono_sprites[instance_id]; +} + +fn load_poly_sprite(instance_id: u32) -> PolychromeSprite { + return b_poly_sprites[instance_id]; +} diff --git a/crates/gpui_wgpu/src/shaders_webgl.wgsl b/crates/gpui_wgpu/src/shaders_webgl.wgsl new file mode 100644 index 0000000000..0c9c217a69 --- /dev/null +++ b/crates/gpui_wgpu/src/shaders_webgl.wgsl @@ -0,0 +1,221 @@ +@group(1) @binding(0) var t_instances: texture_2d; + +// Each texel of `t_instances` packs four 32-bit words of instance data. Records +// are read strictly front to back, so all readers share a cursor that keeps the +// most recently fetched texel and only touches the texture again when the next +// word crosses a texel boundary. This fetches each texel exactly once per +// record load instead of once per word. The `read_*` functions must therefore +// consume their words in struct declaration order; skipping or re-reading words +// would still return correct data (the cursor re-fetches whenever the texel +// index changes) but would forfeit the single-fetch-per-texel guarantee. +struct InstanceCursor { + word_index: u32, + texel_index: u32, + texel: vec4, + width: u32, +} + +fn fetch_instance_texel(texel_index: u32, width: u32) -> vec4 { + let coordinate = vec2( + i32(texel_index % width), + i32(texel_index / width), + ); + return textureLoad(t_instances, coordinate, 0); +} + +fn instance_cursor(word_index: u32) -> InstanceCursor { + let width = textureDimensions(t_instances).x; + let texel_index = word_index / 4u; + return InstanceCursor( + word_index, + texel_index, + fetch_instance_texel(texel_index, width), + width, + ); +} + +fn read_word(cursor: ptr) -> u32 { + let word_index = (*cursor).word_index; + let texel_index = word_index / 4u; + if texel_index != (*cursor).texel_index { + (*cursor).texel = fetch_instance_texel(texel_index, (*cursor).width); + (*cursor).texel_index = texel_index; + } + (*cursor).word_index = word_index + 1u; + return (*cursor).texel[word_index % 4u]; +} + +fn read_f32(cursor: ptr) -> f32 { + return bitcast(read_word(cursor)); +} + +fn read_i32(cursor: ptr) -> i32 { + return bitcast(read_word(cursor)); +} + +fn read_vec2_f32(cursor: ptr) -> vec2 { + return vec2(read_f32(cursor), read_f32(cursor)); +} + +fn read_vec2_i32(cursor: ptr) -> vec2 { + return vec2(read_i32(cursor), read_i32(cursor)); +} + +fn read_hsla(cursor: ptr) -> Hsla { + return Hsla( + read_f32(cursor), + read_f32(cursor), + read_f32(cursor), + read_f32(cursor), + ); +} + +fn read_bounds(cursor: ptr) -> Bounds { + return Bounds(read_vec2_f32(cursor), read_vec2_f32(cursor)); +} + +fn read_corners(cursor: ptr) -> Corners { + return Corners( + read_f32(cursor), + read_f32(cursor), + read_f32(cursor), + read_f32(cursor), + ); +} + +fn read_edges(cursor: ptr) -> Edges { + return Edges( + read_f32(cursor), + read_f32(cursor), + read_f32(cursor), + read_f32(cursor), + ); +} + +fn read_color_stop(cursor: ptr) -> LinearColorStop { + return LinearColorStop(read_hsla(cursor), read_f32(cursor)); +} + +fn read_background(cursor: ptr) -> Background { + return Background( + read_word(cursor), + read_word(cursor), + read_hsla(cursor), + read_f32(cursor), + array( + read_color_stop(cursor), + read_color_stop(cursor), + ), + read_word(cursor), + ); +} + +fn read_atlas_tile(cursor: ptr) -> AtlasTile { + return AtlasTile( + AtlasTextureId( + read_word(cursor), + read_word(cursor), + ), + read_word(cursor), + read_word(cursor), + AtlasBounds( + read_vec2_i32(cursor), + read_vec2_i32(cursor), + ), + ); +} + +fn read_transformation(cursor: ptr) -> TransformationMatrix { + return TransformationMatrix( + mat2x2( + read_vec2_f32(cursor), + read_vec2_f32(cursor), + ), + read_vec2_f32(cursor), + ); +} + +fn load_quad(instance_id: u32) -> Quad { + var cursor = instance_cursor(instance_id * 40u); + return Quad( + read_word(&cursor), + read_word(&cursor), + read_bounds(&cursor), + read_bounds(&cursor), + read_background(&cursor), + read_hsla(&cursor), + read_corners(&cursor), + read_edges(&cursor), + ); +} + +fn load_shadow(instance_id: u32) -> Shadow { + var cursor = instance_cursor(instance_id * 28u); + return Shadow( + read_word(&cursor), + read_f32(&cursor), + read_bounds(&cursor), + read_corners(&cursor), + read_bounds(&cursor), + read_hsla(&cursor), + read_bounds(&cursor), + read_corners(&cursor), + read_word(&cursor), + read_word(&cursor), + ); +} + +fn load_path_vertex(vertex_id: u32) -> PathRasterizationVertex { + var cursor = instance_cursor(vertex_id * 26u); + return PathRasterizationVertex( + read_vec2_f32(&cursor), + read_vec2_f32(&cursor), + read_background(&cursor), + read_bounds(&cursor), + ); +} + +fn load_path_sprite(instance_id: u32) -> PathSprite { + var cursor = instance_cursor(instance_id * 4u); + return PathSprite(read_bounds(&cursor)); +} + +fn load_underline(instance_id: u32) -> Underline { + var cursor = instance_cursor(instance_id * 16u); + return Underline( + read_word(&cursor), + read_word(&cursor), + read_bounds(&cursor), + read_bounds(&cursor), + read_hsla(&cursor), + read_f32(&cursor), + read_word(&cursor), + ); +} + +fn load_mono_sprite(instance_id: u32) -> MonochromeSprite { + var cursor = instance_cursor(instance_id * 28u); + return MonochromeSprite( + read_word(&cursor), + read_word(&cursor), + read_bounds(&cursor), + read_bounds(&cursor), + read_hsla(&cursor), + read_atlas_tile(&cursor), + read_transformation(&cursor), + ); +} + +fn load_poly_sprite(instance_id: u32) -> PolychromeSprite { + var cursor = instance_cursor(instance_id * 24u); + return PolychromeSprite( + read_word(&cursor), + read_word(&cursor), + read_word(&cursor), + read_f32(&cursor), + read_bounds(&cursor), + read_bounds(&cursor), + read_corners(&cursor), + read_atlas_tile(&cursor), + ); +} diff --git a/crates/gpui_wgpu/src/wgpu_context.rs b/crates/gpui_wgpu/src/wgpu_context.rs index 9dd5599393..7e744e80d6 100644 --- a/crates/gpui_wgpu/src/wgpu_context.rs +++ b/crates/gpui_wgpu/src/wgpu_context.rs @@ -11,11 +11,51 @@ pub struct WgpuContext { pub adapter: wgpu::Adapter, pub device: Arc, pub queue: Arc, + backend: WgpuBackend, dual_source_blending: bool, color_texture_format: wgpu::TextureFormat, device_lost: Arc, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WgpuBackend { + BrowserWebGpu, + Gl, + Native(wgpu::Backend), +} + +#[cfg(target_family = "wasm")] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum WebBackendPreference { + #[default] + Auto, + WebGpu, + WebGl, +} + +#[cfg(target_family = "wasm")] +pub struct PreparedWebGraphics { + pub context: WgpuContext, + pub surface: wgpu::Surface<'static>, +} + +/// wgpu-core refuses to create a surface when neither the instance nor the surface +/// target carries a display handle, and `SurfaceTarget::Canvas` always passes `None`. +/// The WebGL2 backend never reads the handle (WebGPU bypasses wgpu-core entirely), so +/// a unit web display handle on the instance satisfies the check. +#[cfg(target_family = "wasm")] +#[derive(Debug)] +struct WebDisplaySource; + +#[cfg(target_family = "wasm")] +impl raw_window_handle::HasDisplayHandle for WebDisplaySource { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + Ok(raw_window_handle::DisplayHandle::web()) + } +} + #[derive(Clone, Copy)] pub struct CompositorGpuHint { pub vendor_id: u32, @@ -88,11 +128,13 @@ impl WgpuContext { adapter.get_info().backend ); + let backend = WgpuBackend::Native(adapter.get_info().backend); Ok(Self { instance, adapter, device: Arc::new(device), queue: Arc::new(queue), + backend, dual_source_blending, color_texture_format, device_lost, @@ -100,43 +142,95 @@ impl WgpuContext { } #[cfg(target_family = "wasm")] - pub async fn new_web() -> anyhow::Result { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL, + pub async fn new_web( + canvas: &web_sys::HtmlCanvasElement, + preference: WebBackendPreference, + ) -> anyhow::Result { + Self::new_web_with_backend(canvas, preference).await + } + + #[cfg(target_family = "wasm")] + #[allow(clippy::arc_with_non_send_sync)] + async fn new_web_with_backend( + canvas: &web_sys::HtmlCanvasElement, + preference: WebBackendPreference, + ) -> anyhow::Result { + let backends = match preference { + WebBackendPreference::Auto => wgpu::Backends::BROWSER_WEBGPU | wgpu::Backends::GL, + WebBackendPreference::WebGpu => wgpu::Backends::BROWSER_WEBGPU, + WebBackendPreference::WebGl => wgpu::Backends::GL, + }; + let descriptor = wgpu::InstanceDescriptor { + backends, flags: wgpu::InstanceFlags::default(), backend_options: wgpu::BackendOptions::default(), memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), - display: None, - }); + display: Some(Box::new(WebDisplaySource)), + }; + let instance = if preference == WebBackendPreference::Auto { + wgpu::util::new_instance_with_webgpu_detection(descriptor).await + } else { + wgpu::Instance::new(descriptor) + }; + let surface = instance + .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) + .map_err(|error| { + anyhow::anyhow!("Failed to create browser graphics surface: {error}") + })?; let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: None, + compatible_surface: Some(&surface), force_fallback_adapter: false, }) .await - .map_err(|e| anyhow::anyhow!("Failed to request GPU adapter: {e}"))?; - - log::info!( - "Selected GPU adapter: {:?} ({:?})", - adapter.get_info().name, - adapter.get_info().backend - ); + .map_err(|error| { + anyhow::anyhow!( + "Failed to request a {preference:?} adapter compatible with the canvas: {error}" + ) + })?; + let adapter_info = adapter.get_info(); + let backend = match adapter_info.backend { + wgpu::Backend::BrowserWebGpu => WgpuBackend::BrowserWebGpu, + wgpu::Backend::Gl => WgpuBackend::Gl, + backend => { + anyhow::bail!( + "Browser graphics initialization selected unexpected backend {backend:?}" + ) + } + }; let device_lost = Arc::new(AtomicBool::new(false)); let (device, queue, dual_source_blending, color_texture_format) = Self::create_device(&adapter).await?; + device.set_device_lost_callback({ + let device_lost = Arc::clone(&device_lost); + move |reason, message| { + log::error!("wgpu device lost: reason={reason:?}, message={message}"); + if reason != wgpu::DeviceLostReason::Destroyed { + device_lost.store(true, Ordering::Relaxed); + } + } + }); + log::info!( + "Browser graphics initialized: requested={preference:?}, selected={backend:?}, \ + adapter={:?}, limits={:?}, dual_source_blending={dual_source_blending}", + adapter_info.name, + device.limits(), + ); - Ok(Self { + let context = Self { instance, adapter, device: Arc::new(device), queue: Arc::new(queue), + backend, dual_source_blending, color_texture_format, device_lost, - }) + }; + Ok(PreparedWebGraphics { context, surface }) } async fn create_device( @@ -157,14 +251,26 @@ impl WgpuContext { } let color_atlas_texture_format = Self::select_color_texture_format(adapter)?; + #[cfg(target_family = "wasm")] + let required_limits = if adapter.get_info().backend == wgpu::Backend::Gl { + wgpu::Limits::downlevel_webgl2_defaults() + .using_resolution(adapter.limits()) + .using_alignment(adapter.limits()) + } else { + wgpu::Limits::downlevel_defaults() + .using_resolution(adapter.limits()) + .using_alignment(adapter.limits()) + }; + #[cfg(not(target_family = "wasm"))] + let required_limits = wgpu::Limits::downlevel_defaults() + .using_resolution(adapter.limits()) + .using_alignment(adapter.limits()); let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor { label: Some("gpui_device"), required_features, - required_limits: wgpu::Limits::downlevel_defaults() - .using_resolution(adapter.limits()) - .using_alignment(adapter.limits()), + required_limits, memory_hints: wgpu::MemoryHints::MemoryUsage, trace: wgpu::Trace::Off, experimental_features: wgpu::ExperimentalFeatures::disabled(), @@ -396,11 +502,16 @@ impl WgpuContext { fn select_color_texture_format(adapter: &wgpu::Adapter) -> anyhow::Result { let required_usages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST; let bgra_features = adapter.get_texture_format_features(wgpu::TextureFormat::Bgra8Unorm); + let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm); + #[cfg(target_family = "wasm")] + if adapter.get_info().backend == wgpu::Backend::Gl + && rgba_features.allowed_usages.contains(required_usages) + { + return Ok(wgpu::TextureFormat::Rgba8Unorm); + } if bgra_features.allowed_usages.contains(required_usages) { return Ok(wgpu::TextureFormat::Bgra8Unorm); } - - let rgba_features = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba8Unorm); if rgba_features.allowed_usages.contains(required_usages) { let info = adapter.get_info(); log::warn!( @@ -426,6 +537,14 @@ impl WgpuContext { rgba_features.allowed_usages, )) } + pub fn backend(&self) -> WgpuBackend { + self.backend + } + + pub fn uses_webgl_instance_data(&self) -> bool { + matches!(self.backend, WgpuBackend::Gl) && cfg!(target_family = "wasm") + } + pub fn supports_dual_source_blending(&self) -> bool { self.dual_source_blending } diff --git a/crates/gpui_wgpu/src/wgpu_renderer.rs b/crates/gpui_wgpu/src/wgpu_renderer.rs index e30ade6e39..b0cd40a767 100644 --- a/crates/gpui_wgpu/src/wgpu_renderer.rs +++ b/crates/gpui_wgpu/src/wgpu_renderer.rs @@ -16,6 +16,43 @@ use std::sync::{Arc, Mutex}; const MAX_INSTANCE_BUFFER_SIZE: u64 = 256 * 1024 * 1024; +const INSTANCE_TEXTURE_TEXEL_SIZE: u64 = 16; + +/// Shader variant for backends with storage buffer support: the shared shader +/// logic plus the storage-buffer instance transport. +const STORAGE_BUFFER_SHADERS: &str = concat!( + include_str!("shaders.wgsl"), + include_str!("shaders_storage.wgsl"), +); + +/// Shader variant for WebGL2, which has no storage buffers: the shared shader +/// logic plus the texture-based instance transport. +const WEBGL_SHADERS: &str = concat!( + include_str!("shaders.wgsl"), + include_str!("shaders_webgl.wgsl"), +); + +/// Subpixel text rendering requires dual-source blending, which WebGL2 lacks, so +/// this variant only ever runs with the storage-buffer transport. The `enable` +/// directive must precede all declarations. +const SUBPIXEL_SHADERS: &str = concat!( + "enable dual_source_blending;\n", + include_str!("shaders.wgsl"), + include_str!("shaders_storage.wgsl"), + include_str!("shaders_subpixel.wgsl"), +); + +fn least_common_multiple(left: u64, right: u64) -> u64 { + let mut first = left; + let mut second = right; + while second != 0 { + let remainder = first % second; + first = second; + second = remainder; + } + left / first * right +} + #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct GlobalParams { @@ -97,13 +134,24 @@ struct WgpuPipelines { surfaces: wgpu::RenderPipeline, } +/// One frame allocation of instance data, ready to bind. +struct InstanceBinding { + bind_group: wgpu::BindGroup, + /// Index of the allocation's first instance within the bound data. Always + /// zero on the storage-buffer path, where the binding offset already + /// positions the array; on the WebGL texture path the shader indexes the + /// shared instance texture absolutely, so draws must offset their + /// instance (or vertex) ranges by this value. + first_instance: u32, +} + struct InstanceBindings { - quads: wgpu::BindGroup, - shadows: wgpu::BindGroup, - underlines: wgpu::BindGroup, - monochrome_sprites: wgpu::BindGroup, - subpixel_sprites: wgpu::BindGroup, - polychrome_sprites: wgpu::BindGroup, + quads: InstanceBinding, + shadows: InstanceBinding, + underlines: InstanceBinding, + monochrome_sprites: InstanceBinding, + subpixel_sprites: InstanceBinding, + polychrome_sprites: InstanceBinding, } struct WgpuBindGroupLayouts { @@ -116,6 +164,18 @@ struct WgpuBindGroupLayouts { /// Shared GPU context reference, used to coordinate device recovery across multiple windows. pub type GpuContext = Rc>>; +enum InstanceData { + Storage(wgpu::Buffer), + // WebGL2 has no storage buffers. A uint texture keeps the records available to both shader + // stages while preserving integer and floating-point bit patterns exactly. + Texture { + texture: wgpu::Texture, + view: wgpu::TextureView, + width: u32, + height: u32, + }, +} + /// GPU resources that must be dropped together during device recovery. struct WgpuResources { device: Arc, @@ -127,7 +187,7 @@ struct WgpuResources { globals_buffer: wgpu::Buffer, globals_bind_group: wgpu::BindGroup, path_globals_bind_group: wgpu::BindGroup, - instance_buffer: wgpu::Buffer, + instance_data: InstanceData, path_intermediate_texture: Option, path_intermediate_view: Option, path_msaa_texture: Option, @@ -155,9 +215,10 @@ pub struct WgpuRenderer { atlas: Arc, path_globals_offset: u64, gamma_offset: u64, - instance_buffer_capacity: u64, - max_buffer_size: u64, - storage_buffer_alignment: u64, + instance_data_capacity: u64, + max_instance_data_size: u64, + instance_data_alignment: u64, + uses_webgl_instance_data: bool, rendering_params: RenderingParameters, is_bgr: bool, dual_source_blending: bool, @@ -263,9 +324,17 @@ impl WgpuRenderer { .instance .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?; + Self::new_from_surface(context, surface, config) + } + #[cfg(target_family = "wasm")] + #[allow(clippy::arc_with_non_send_sync)] + pub fn new_from_surface( + context: &WgpuContext, + surface: wgpu::Surface<'static>, + config: WgpuSurfaceConfig, + ) -> anyhow::Result { let atlas = Arc::new(WgpuAtlas::from_context(context)); - Self::new_internal(None, context, surface, config, None, atlas) } @@ -360,10 +429,11 @@ impl WgpuRenderer { surface.configure(&context.device, &surface_config); let queue = Arc::clone(&context.queue); - let dual_source_blending = context.supports_dual_source_blending(); - let rendering_params = RenderingParameters::new(&context.adapter, surface_format); - let bind_group_layouts = Self::create_bind_group_layouts(&device); + let uses_webgl_instance_data = context.uses_webgl_instance_data(); + let dual_source_blending = + context.supports_dual_source_blending() && !uses_webgl_instance_data; + let bind_group_layouts = Self::create_bind_group_layouts(&device, uses_webgl_instance_data); let pipelines = Self::create_pipelines( &device, &bind_group_layouts, @@ -371,6 +441,7 @@ impl WgpuRenderer { alpha_mode, rendering_params.path_sample_count, dual_source_blending, + uses_webgl_instance_data, ); let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor { @@ -393,21 +464,47 @@ impl WgpuRenderer { mapped_at_creation: false, }); - // Every frame allocation is exposed as one storage-buffer binding, so - // its backing buffer must satisfy both the allocation and binding limits. - let max_buffer_size = device - .limits() - .max_buffer_size - .min(device.limits().max_storage_buffer_binding_size) - .min(MAX_INSTANCE_BUFFER_SIZE); - let storage_buffer_alignment = device.limits().min_storage_buffer_offset_alignment as u64; - let initial_instance_buffer_capacity = (2 * 1024 * 1024).min(max_buffer_size); - let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("instance_buffer"), - size: initial_instance_buffer_capacity, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); + let ( + instance_data, + instance_data_capacity, + max_instance_data_size, + instance_data_alignment, + ) = if uses_webgl_instance_data { + let max_texture_dimension = device.limits().max_texture_dimension_2d; + let max_instance_data_size = (u64::from(max_texture_dimension).pow(2) + * INSTANCE_TEXTURE_TEXEL_SIZE) + .min(MAX_INSTANCE_BUFFER_SIZE); + let initial_capacity = (2 * 1024 * 1024).min(max_instance_data_size); + let (instance_data, capacity) = + Self::create_instance_texture(&device, initial_capacity, max_texture_dimension); + ( + instance_data, + capacity, + max_instance_data_size, + INSTANCE_TEXTURE_TEXEL_SIZE, + ) + } else { + // Every frame allocation is exposed as one storage-buffer binding, so + // its backing buffer must satisfy both the allocation and binding limits. + let max_buffer_size = device + .limits() + .max_buffer_size + .min(device.limits().max_storage_buffer_binding_size) + .min(MAX_INSTANCE_BUFFER_SIZE); + let initial_capacity = (2 * 1024 * 1024).min(max_buffer_size); + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("instance_buffer"), + size: initial_capacity, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + ( + InstanceData::Storage(buffer), + initial_capacity, + max_buffer_size, + device.limits().min_storage_buffer_offset_alignment as u64, + ) + }; let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("globals_bind_group"), @@ -474,7 +571,7 @@ impl WgpuRenderer { globals_buffer, globals_bind_group, path_globals_bind_group, - instance_buffer, + instance_data, // Defer intermediate texture creation to first draw call via ensure_intermediate_textures(). // This avoids panics when the device/surface is in an invalid state during initialization. path_intermediate_texture: None, @@ -491,9 +588,10 @@ impl WgpuRenderer { atlas, path_globals_offset, gamma_offset, - instance_buffer_capacity: initial_instance_buffer_capacity, - max_buffer_size, - storage_buffer_alignment, + instance_data_capacity, + max_instance_data_size, + instance_data_alignment, + uses_webgl_instance_data, rendering_params, is_bgr: false, dual_source_blending, @@ -509,7 +607,10 @@ impl WgpuRenderer { }) } - fn create_bind_group_layouts(device: &wgpu::Device) -> WgpuBindGroupLayouts { + fn create_bind_group_layouts( + device: &wgpu::Device, + uses_webgl_instance_data: bool, + ) -> WgpuBindGroupLayouts { let globals = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("globals_layout"), @@ -541,20 +642,28 @@ impl WgpuRenderer { ], }); - let storage_buffer_entry = |binding: u32| wgpu::BindGroupLayoutEntry { - binding, + let instance_data_entry = wgpu::BindGroupLayoutEntry { + binding: 0, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, + ty: if uses_webgl_instance_data { + wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Uint, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + } + } else { + wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + } }, count: None, }; let instances = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("instances_layout"), - entries: &[storage_buffer_entry(0)], + entries: &[instance_data_entry], }); let texture = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -631,6 +740,44 @@ impl WgpuRenderer { } } + fn create_instance_texture( + device: &wgpu::Device, + requested_capacity: u64, + max_texture_dimension: u32, + ) -> (InstanceData, u64) { + let texel_count = requested_capacity.div_ceil(INSTANCE_TEXTURE_TEXEL_SIZE); + let width = texel_count.min(u64::from(max_texture_dimension)).max(1) as u32; + let height = texel_count + .div_ceil(u64::from(width)) + .min(u64::from(max_texture_dimension)) + .max(1) as u32; + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("instance_texture"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba32Uint, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let capacity = u64::from(width) * u64::from(height) * INSTANCE_TEXTURE_TEXEL_SIZE; + ( + InstanceData::Texture { + texture, + view, + width, + height, + }, + capacity, + ) + } + fn create_pipelines( device: &wgpu::Device, layouts: &WgpuBindGroupLayouts, @@ -638,6 +785,7 @@ impl WgpuRenderer { alpha_mode: wgpu::CompositeAlphaMode, path_sample_count: u32, dual_source_blending: bool, + uses_webgl_instance_data: bool, ) -> WgpuPipelines { // Diagnostic guard: verify the device actually has // DUAL_SOURCE_BLENDING. We have a crash report (ZED-5G1) where a @@ -659,22 +807,23 @@ impl WgpuRenderer { device.features(), ); } - let dual_source_blending = dual_source_blending && device_has_feature; + let dual_source_blending = + dual_source_blending && device_has_feature && !uses_webgl_instance_data; - let base_shader_source = include_str!("shaders.wgsl"); + let shader_source = if uses_webgl_instance_data { + WEBGL_SHADERS + } else { + STORAGE_BUFFER_SHADERS + }; let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("gpui_shaders"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(base_shader_source)), + source: wgpu::ShaderSource::Wgsl(shader_source.into()), }); - let subpixel_shader_source = include_str!("shaders_subpixel.wgsl"); let subpixel_shader_module = if dual_source_blending { - let combined = format!( - "enable dual_source_blending;\n{base_shader_source}\n{subpixel_shader_source}" - ); Some(device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("gpui_subpixel_shaders"), - source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(combined)), + source: wgpu::ShaderSource::Wgsl(SUBPIXEL_SHADERS.into()), })) } else { None @@ -1065,6 +1214,7 @@ impl WgpuRenderer { let surface_config = self.surface_config.clone(); let path_sample_count = self.rendering_params.path_sample_count; let dual_source_blending = self.dual_source_blending; + let uses_webgl_instance_data = self.uses_webgl_instance_data; let Some(resources) = self.resources.as_mut() else { return; }; @@ -1078,6 +1228,7 @@ impl WgpuRenderer { surface_config.alpha_mode, path_sample_count, dual_source_blending, + uses_webgl_instance_data, ); } } @@ -1112,6 +1263,17 @@ impl WgpuRenderer { } pub fn draw(&mut self, scene: &Scene) -> bool { + #[cfg(target_family = "wasm")] + if self.device_lost() { + if self.surface_configured { + log::error!( + "Browser graphics context was lost; rendering has stopped. Reload the page to recover." + ); + self.surface_configured = false; + } + return false; + } + // Bail out early if the surface has been unconfigured (e.g. during // Android background/rotation transitions). Attempting to acquire // a texture from an unconfigured surface can block indefinitely on @@ -1383,33 +1545,35 @@ impl WgpuRenderer { instance_offset: &mut u64, ) -> Result { Ok(InstanceBindings { - quads: self.write_instance_binding("quads_bind_group", instance_offset, unsafe { - Self::instance_bytes(&scene.quads) - })?, + quads: self.write_instance_binding( + "quads_bind_group", + instance_offset, + &scene.quads, + )?, shadows: self.write_instance_binding( "shadows_bind_group", instance_offset, - unsafe { Self::instance_bytes(&scene.shadows) }, + &scene.shadows, )?, underlines: self.write_instance_binding( "underlines_bind_group", instance_offset, - unsafe { Self::instance_bytes(&scene.underlines) }, + &scene.underlines, )?, monochrome_sprites: self.write_instance_binding( "monochrome_sprites_bind_group", instance_offset, - unsafe { Self::instance_bytes(&scene.monochrome_sprites) }, + &scene.monochrome_sprites, )?, subpixel_sprites: self.write_instance_binding( "subpixel_sprites_bind_group", instance_offset, - unsafe { Self::instance_bytes(&scene.subpixel_sprites) }, + &scene.subpixel_sprites, )?, polychrome_sprites: self.write_instance_binding( "polychrome_sprites_bind_group", instance_offset, - unsafe { Self::instance_bytes(&scene.polychrome_sprites) }, + &scene.polychrome_sprites, )?, }) } @@ -1440,29 +1604,32 @@ impl WgpuRenderer { fn draw_instances( &self, - bind_group: &wgpu::BindGroup, + instances: &InstanceBinding, pipeline: &wgpu::RenderPipeline, - instances: Range, + range: Range, pass: &mut wgpu::RenderPass<'_>, ) { - if instances.is_empty() { + if range.is_empty() { return; } pass.set_pipeline(pipeline); pass.set_bind_group(0, &self.resources().globals_bind_group, &[]); - pass.set_bind_group(1, bind_group, &[]); - pass.draw(0..4, instances); + pass.set_bind_group(1, &instances.bind_group, &[]); + pass.draw( + 0..4, + instances.first_instance + range.start..instances.first_instance + range.end, + ); } fn draw_sprites( &self, - sprite_instances: &wgpu::BindGroup, + sprite_instances: &InstanceBinding, texture_id: AtlasTextureId, pipeline: &wgpu::RenderPipeline, - instances: Range, + range: Range, pass: &mut wgpu::RenderPass<'_>, ) { - if instances.is_empty() { + if range.is_empty() { return; } let texture_info = self.atlas.get_texture_info(texture_id); @@ -1470,9 +1637,13 @@ impl WgpuRenderer { self.create_texture_bind_group("atlas_texture_bind_group", &texture_info.view); pass.set_pipeline(pipeline); pass.set_bind_group(0, &self.resources().globals_bind_group, &[]); - pass.set_bind_group(1, sprite_instances, &[]); + pass.set_bind_group(1, &sprite_instances.bind_group, &[]); pass.set_bind_group(2, &texture, &[]); - pass.draw(0..4, instances); + pass.draw( + 0..4, + sprite_instances.first_instance + range.start + ..sprite_instances.first_instance + range.end, + ); } unsafe fn instance_bytes(instances: &[T]) -> &[u8] { @@ -1511,9 +1682,7 @@ impl WgpuRenderer { return Ok(()); }; let instances = - self.write_instance_binding("path_sprites_bind_group", instance_offset, unsafe { - Self::instance_bytes(&sprites) - })?; + self.write_instance_binding("path_sprites_bind_group", instance_offset, &sprites)?; let texture = self.create_texture_bind_group( "path_intermediate_texture_bind_group", &path_intermediate_view, @@ -1521,9 +1690,12 @@ impl WgpuRenderer { let resources = self.resources(); pass.set_pipeline(&resources.pipelines.paths); pass.set_bind_group(0, &resources.globals_bind_group, &[]); - pass.set_bind_group(1, &instances, &[]); + pass.set_bind_group(1, &instances.bind_group, &[]); pass.set_bind_group(2, &texture, &[]); - pass.draw(0..4, 0..sprites.len() as u32); + pass.draw( + 0..4, + instances.first_instance..instances.first_instance + sprites.len() as u32, + ); Ok(()) } @@ -1548,10 +1720,10 @@ impl WgpuRenderer { return Ok(false); } - let data_bind_group = self.write_instance_binding( + let vertex_binding = self.write_instance_binding( "path_rasterization_bind_group", instance_offset, - unsafe { Self::instance_bytes(&vertices) }, + &vertices, )?; let resources = self.resources(); @@ -1583,34 +1755,65 @@ impl WgpuRenderer { pass.set_pipeline(&resources.pipelines.path_rasterization); pass.set_bind_group(0, &resources.path_globals_bind_group, &[]); - pass.set_bind_group(1, &data_bind_group, &[]); - pass.draw(0..vertices.len() as u32, 0..1); + pass.set_bind_group(1, &vertex_binding.bind_group, &[]); + // The path rasterization shader loads records by vertex index + // rather than instance index, so the allocation's base shifts the + // vertex range here. + pass.draw( + vertex_binding.first_instance + ..vertex_binding.first_instance + vertices.len() as u32, + 0..1, + ); } Ok(true) } - fn write_instance_binding( + fn write_instance_binding( &mut self, label: &str, instance_offset: &mut u64, - data: &[u8], - ) -> Result { + instances: &[T], + ) -> Result { + let data = unsafe { Self::instance_bytes(instances) }; // wgpu rejects zero-sized bindings, so empty primitive arrays still // reserve the 16-byte minimum. let size = (data.len() as u64).max(16); - let mut offset = (*instance_offset).next_multiple_of(self.storage_buffer_alignment.max(1)); - if offset + size > self.instance_buffer_capacity { - self.grow_instance_buffer(size)?; + let stride = (std::mem::size_of::() as u64).max(1); + let (alignment, allocation_size) = if self.uses_webgl_instance_data { + // The texture transport has no binding offset: the shader indexes + // the instance texture absolutely, so each allocation must start on + // a whole instance (a stride multiple) and a whole texel, and must + // end on a texel boundary so the zero padding of its final partial + // texel cannot overlap the next allocation. + ( + least_common_multiple(self.instance_data_alignment, stride), + size.next_multiple_of(INSTANCE_TEXTURE_TEXEL_SIZE), + ) + } else { + (self.instance_data_alignment.max(1), size) + }; + let mut offset = (*instance_offset).next_multiple_of(alignment); + if offset + allocation_size > self.instance_data_capacity { + self.grow_instance_data(allocation_size)?; offset = 0; } - *instance_offset = offset + size; + *instance_offset = offset + allocation_size; + + let first_instance = if self.uses_webgl_instance_data { + u32::try_from(offset / stride).context("instance index exceeds u32 range")? + } else { + 0 + }; let resources = self.resources(); if !data.is_empty() { - resources - .queue - .write_buffer(&resources.instance_buffer, offset, data); + match &resources.instance_data { + InstanceData::Storage(buffer) => resources.queue.write_buffer(buffer, offset, data), + InstanceData::Texture { .. } => { + Self::write_instance_texture(resources, offset, data) + } + } } let bind_group = resources .device @@ -1619,42 +1822,150 @@ impl WgpuRenderer { layout: &resources.bind_group_layouts.instances, entries: &[wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &resources.instance_buffer, - offset, - size: NonZeroU64::new(size), - }), + resource: match &resources.instance_data { + InstanceData::Storage(buffer) => { + wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer, + offset, + size: NonZeroU64::new(size), + }) + } + InstanceData::Texture { view, .. } => { + wgpu::BindingResource::TextureView(view) + } + }, }], }); - Ok(bind_group) + Ok(InstanceBinding { + bind_group, + first_instance, + }) } - fn grow_instance_buffer(&mut self, required: u64) -> Result<()> { - let capacity = (self.instance_buffer_capacity * 2) + fn write_instance_texture(resources: &WgpuResources, offset: u64, data: &[u8]) { + let InstanceData::Texture { + texture, + width, + height, + .. + } = &resources.instance_data + else { + return; + }; + let mut byte_offset = 0usize; + let mut texel_offset = offset / INSTANCE_TEXTURE_TEXEL_SIZE; + while byte_offset < data.len() { + let x = (texel_offset % u64::from(*width)) as u32; + let y = (texel_offset / u64::from(*width)) as u32; + if y >= *height { + // The capacity check in write_instance_binding should make this + // unreachable. Truncating silently would leave stale bytes in the + // texture and draw garbage for the remaining instances. + debug_assert!( + false, + "instance texture write out of bounds: row {y} >= height {}", + *height + ); + log::error!( + "instance texture write out of bounds; dropping {} bytes of instance data", + data.len() - byte_offset + ); + return; + } + let available_texels = u64::from(*width - x); + let remaining_bytes = data.len() - byte_offset; + let complete_texels = remaining_bytes as u64 / INSTANCE_TEXTURE_TEXEL_SIZE; + let texels = complete_texels.min(available_texels); + if texels > 0 { + let byte_count = (texels * INSTANCE_TEXTURE_TEXEL_SIZE) as usize; + resources.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d { x, y, z: 0 }, + aspect: wgpu::TextureAspect::All, + }, + &data[byte_offset..byte_offset + byte_count], + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(byte_count as u32), + rows_per_image: None, + }, + wgpu::Extent3d { + width: texels as u32, + height: 1, + depth_or_array_layers: 1, + }, + ); + byte_offset += byte_count; + texel_offset += texels; + continue; + } + + let mut final_texel = [0; INSTANCE_TEXTURE_TEXEL_SIZE as usize]; + final_texel[..remaining_bytes].copy_from_slice(&data[byte_offset..]); + resources.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d { x, y, z: 0 }, + aspect: wgpu::TextureAspect::All, + }, + &final_texel, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(INSTANCE_TEXTURE_TEXEL_SIZE as u32), + rows_per_image: None, + }, + wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + ); + break; + } + } + + fn grow_instance_data(&mut self, required: u64) -> Result<()> { + let capacity = (self.instance_data_capacity * 2) .max(required.next_power_of_two()) - .min(self.max_buffer_size); + .min(self.max_instance_data_size); anyhow::ensure!( capacity >= required, - "instance buffer needs {required} bytes, above the device maximum of {}", - self.max_buffer_size + "instance data needs {required} bytes, above the maximum of {}", + self.max_instance_data_size ); anyhow::ensure!( - capacity > self.instance_buffer_capacity, + capacity > self.instance_data_capacity, "frame instance data exceeds the {}-byte maximum", - self.max_buffer_size + self.max_instance_data_size ); log::debug!( - "instance buffer grown from {} to {capacity}", - self.instance_buffer_capacity + "instance data grown from {} to {capacity}", + self.instance_data_capacity ); + // Bind groups created earlier in the frame keep the previous buffer or + // texture alive, so allocations written before the grow remain valid; + // only subsequent writes land in the new allocation. + let uses_webgl_instance_data = self.uses_webgl_instance_data; let resources = self.resources_mut(); - resources.instance_buffer = resources.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("instance_buffer"), - size: capacity, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - self.instance_buffer_capacity = capacity; + if uses_webgl_instance_data { + let max_texture_dimension = resources.device.limits().max_texture_dimension_2d; + let (instance_data, actual_capacity) = + Self::create_instance_texture(&resources.device, capacity, max_texture_dimension); + resources.instance_data = instance_data; + self.instance_data_capacity = actual_capacity; + } else { + resources.instance_data = + InstanceData::Storage(resources.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("instance_buffer"), + size: capacity, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + })); + self.instance_data_capacity = capacity; + } Ok(()) } @@ -1886,3 +2197,47 @@ impl RenderingParameters { } } } + +#[cfg(test)] +mod tests { + use super::*; + use gpui::{MonochromeSprite, PolychromeSprite, Quad, Shadow, SubpixelSprite, Underline}; + + #[test] + fn webgl_shader_is_valid_wgsl_without_storage_buffers() { + assert!(!WEBGL_SHADERS.contains("var(), 40 * 4); + assert_eq!(std::mem::size_of::(), 28 * 4); + assert_eq!(std::mem::size_of::(), 26 * 4); + assert_eq!(std::mem::size_of::(), 4 * 4); + assert_eq!(std::mem::size_of::(), 16 * 4); + assert_eq!(std::mem::size_of::(), 28 * 4); + assert_eq!(std::mem::size_of::(), 28 * 4); + assert_eq!(std::mem::size_of::(), 24 * 4); + } +}