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:
1 parent
6f67d7b6a7
commit
e96c624624
14 files changed
+1215
-222
No files matched your search
Generated
+11
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 ¶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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user