gpui: Webgl backend (#62165)

Currently gpui only supports webgpu as a backend when compiling for the
web. However, webgpu support on Linux browsers is spotty (without
setting experimental flags):
- chromium has is enabled for nvidia and recent intel chips
- firefox does not enable it

This PR adds a WebGL backend, which is much more widely supported

---

Release Notes:

- N/A or Added/Fixed/Improved ...

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
Cameron McloughlinandLukas Wirth authored and GitHub committed 2026-08-04 15:53:09 +00:00
1 parent 6f67d7b6a7
commit e96c624624
14 files changed
+1215 -222

No files matched your search

Generated
+11
View File
@@ -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"
+14 -3
View File
@@ -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 {
+1
View File
@@ -54,6 +54,7 @@ web-sys = { version = "0.3", features = [
"IdleDeadline",
"IdleRequestOptions",
"KeyboardEvent",
"Location",
"MediaQueryList",
"MediaQueryListEvent",
"MouseEvent",
@@ -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"
+22 -1
View File
@@ -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 {
+7 -1
View File
@@ -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;
+218 -26
View File
@@ -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<dyn PlatformTextSystem>,
active_window: RefCell<Option<AnyWindowHandle>>,
active_window: Rc<RefCell<Option<AnyWindowHandle>>>,
active_display: Rc<dyn PlatformDisplay>,
callbacks: RefCell<WebPlatformCallbacks>,
backend_preference: WebBackendPreference,
wgpu_context: Rc<RefCell<Option<WgpuContext>>>,
prepared_window: Rc<RefCell<Option<PreparedWebWindow>>>,
window_lifecycle: Rc<Cell<WebWindowLifecycle>>,
cursor_visible: Rc<Cell<bool>>,
last_cursor_css: Rc<Cell<&'static str>>,
_cursor_restore_listeners: Vec<EventListenerHandle>,
}
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<Box<dyn FnMut(Vec<String>)>>,
@@ -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<dyn 'static + FnOnce()>) {
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<Box<dyn PlatformWindow>> {
// 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 &params.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();
}
+57 -44
View File
@@ -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<WebWindowInner>,
display: Rc<dyn PlatformDisplay>,
lifecycle: Rc<Cell<WebWindowLifecycle>>,
active_window: Rc<RefCell<Option<AnyWindowHandle>>>,
_raf_closure: Closure<dyn FnMut()>,
_resize_observer: Option<web_sys::ResizeObserver>,
_resize_observer_closure: Closure<dyn FnMut(js_sys::Array)>,
@@ -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<Self> {
pub(crate) fn prepare_canvas(
browser_window: &web_sys::Window,
) -> anyhow::Result<web_sys::HtmlCanvasElement> {
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<Cell<WebWindowLifecycle>>,
active_window: Rc<RefCell<Option<AnyWindowHandle>>>,
) -> anyhow::Result<Self> {
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<dyn PlatformDisplay> = 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);
}
}
+2
View File
@@ -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"
+17 -19
View File
@@ -526,7 +526,6 @@ struct Quad {
corner_radii: Corners,
border_widths: Edges,
}
@group(1) @binding(0) var<storage, read> b_quads: array<Quad>;
struct QuadVarying {
@builtin(position) position: vec4<f32>,
@@ -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>(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<f32> {
return vec4<f32>(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<storage, read> b_shadows: array<Shadow>;
struct ShadowVarying {
@builtin(position) position: vec4<f32>,
@@ -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>(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<f32> {
return vec4<f32>(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<storage, read> b_path_vertices: array<PathRasterizationVertex>;
struct PathRasterizationVarying {
@builtin(position) position: vec4<f32>,
@@ -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<f
return vec4<f32>(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<f
struct PathSprite {
bounds: Bounds,
}
@group(1) @binding(0) var<storage, read> b_path_sprites: array<PathSprite>;
struct PathVarying {
@builtin(position) position: vec4<f32>,
@@ -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>(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<storage, read> b_underlines: array<Underline>;
struct UnderlineVarying {
@builtin(position) position: vec4<f32>,
@@ -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>(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<f32> {
return vec4<f32>(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<storage, read> b_mono_sprites: array<MonochromeSprite>;
struct MonoSpriteVarying {
@builtin(position) position: vec4<f32>,
@@ -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>(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<storage, read> b_poly_sprites: array<PolychromeSprite>;
struct PolySpriteVarying {
@builtin(position) position: vec4<f32>,
@@ -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>(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<f32> {
return vec4<f32>(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;
+42
View File
@@ -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<storage, read> b_quads: array<Quad>;
@group(1) @binding(0) var<storage, read> b_shadows: array<Shadow>;
@group(1) @binding(0) var<storage, read> b_path_vertices: array<PathRasterizationVertex>;
@group(1) @binding(0) var<storage, read> b_path_sprites: array<PathSprite>;
@group(1) @binding(0) var<storage, read> b_underlines: array<Underline>;
@group(1) @binding(0) var<storage, read> b_mono_sprites: array<MonochromeSprite>;
@group(1) @binding(0) var<storage, read> b_poly_sprites: array<PolychromeSprite>;
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];
}
+221
View File
@@ -0,0 +1,221 @@
@group(1) @binding(0) var t_instances: texture_2d<u32>;
// 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<u32>,
width: u32,
}
fn fetch_instance_texel(texel_index: u32, width: u32) -> vec4<u32> {
let coordinate = vec2<i32>(
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<function, InstanceCursor>) -> 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<function, InstanceCursor>) -> f32 {
return bitcast<f32>(read_word(cursor));
}
fn read_i32(cursor: ptr<function, InstanceCursor>) -> i32 {
return bitcast<i32>(read_word(cursor));
}
fn read_vec2_f32(cursor: ptr<function, InstanceCursor>) -> vec2<f32> {
return vec2<f32>(read_f32(cursor), read_f32(cursor));
}
fn read_vec2_i32(cursor: ptr<function, InstanceCursor>) -> vec2<i32> {
return vec2<i32>(read_i32(cursor), read_i32(cursor));
}
fn read_hsla(cursor: ptr<function, InstanceCursor>) -> Hsla {
return Hsla(
read_f32(cursor),
read_f32(cursor),
read_f32(cursor),
read_f32(cursor),
);
}
fn read_bounds(cursor: ptr<function, InstanceCursor>) -> Bounds {
return Bounds(read_vec2_f32(cursor), read_vec2_f32(cursor));
}
fn read_corners(cursor: ptr<function, InstanceCursor>) -> Corners {
return Corners(
read_f32(cursor),
read_f32(cursor),
read_f32(cursor),
read_f32(cursor),
);
}
fn read_edges(cursor: ptr<function, InstanceCursor>) -> Edges {
return Edges(
read_f32(cursor),
read_f32(cursor),
read_f32(cursor),
read_f32(cursor),
);
}
fn read_color_stop(cursor: ptr<function, InstanceCursor>) -> LinearColorStop {
return LinearColorStop(read_hsla(cursor), read_f32(cursor));
}
fn read_background(cursor: ptr<function, InstanceCursor>) -> Background {
return Background(
read_word(cursor),
read_word(cursor),
read_hsla(cursor),
read_f32(cursor),
array<LinearColorStop, 2>(
read_color_stop(cursor),
read_color_stop(cursor),
),
read_word(cursor),
);
}
fn read_atlas_tile(cursor: ptr<function, InstanceCursor>) -> 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<function, InstanceCursor>) -> TransformationMatrix {
return TransformationMatrix(
mat2x2<f32>(
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),
);
}
+139 -20
View File
@@ -11,11 +11,51 @@ pub struct WgpuContext {
pub adapter: wgpu::Adapter,
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
backend: WgpuBackend,
dual_source_blending: bool,
color_texture_format: wgpu::TextureFormat,
device_lost: Arc<AtomicBool>,
}
#[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::DisplayHandle<'_>, 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<Self> {
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<PreparedWebGraphics> {
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<PreparedWebGraphics> {
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<wgpu::TextureFormat> {
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
}
+463 -108
View File
@@ -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<RefCell<Option<WgpuContext>>>;
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<wgpu::Device>,
@@ -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<wgpu::Texture>,
path_intermediate_view: Option<wgpu::TextureView>,
path_msaa_texture: Option<wgpu::Texture>,
@@ -155,9 +215,10 @@ pub struct WgpuRenderer {
atlas: Arc<WgpuAtlas>,
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<Self> {
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<InstanceBindings> {
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<u32>,
range: Range<u32>,
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<u32>,
range: Range<u32>,
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<T>(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<T>(
&mut self,
label: &str,
instance_offset: &mut u64,
data: &[u8],
) -> Result<wgpu::BindGroup> {
instances: &[T],
) -> Result<InstanceBinding> {
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::<T>() 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<storage"));
validate_wgsl(WEBGL_SHADERS, naga::valid::Capabilities::empty());
}
#[test]
fn storage_buffer_shader_is_valid_wgsl() {
validate_wgsl(STORAGE_BUFFER_SHADERS, naga::valid::Capabilities::empty());
}
#[test]
fn subpixel_shader_is_valid_wgsl() {
validate_wgsl(
SUBPIXEL_SHADERS,
naga::valid::Capabilities::DUAL_SOURCE_BLENDING,
);
}
fn validate_wgsl(source: &str, capabilities: naga::valid::Capabilities) {
let module = naga::front::wgsl::parse_str(source).expect("shader should parse");
naga::valid::Validator::new(naga::valid::ValidationFlags::all(), capabilities)
.validate(&module)
.expect("shader should validate");
}
#[test]
fn webgl_record_sizes_match_shader_word_strides() {
assert_eq!(std::mem::size_of::<Quad>(), 40 * 4);
assert_eq!(std::mem::size_of::<Shadow>(), 28 * 4);
assert_eq!(std::mem::size_of::<PathRasterizationVertex>(), 26 * 4);
assert_eq!(std::mem::size_of::<PathSprite>(), 4 * 4);
assert_eq!(std::mem::size_of::<Underline>(), 16 * 4);
assert_eq!(std::mem::size_of::<MonochromeSprite>(), 28 * 4);
assert_eq!(std::mem::size_of::<SubpixelSprite>(), 28 * 4);
assert_eq!(std::mem::size_of::<PolychromeSprite>(), 24 * 4);
}
}