Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -0,0 +1,138 @@
use crate::rendering::atlas::{AllocatedRegion, AllocationError};
use pathfinder_geometry::rect::{RectF, RectI};
use pathfinder_geometry::vector::{vec2f, vec2i, Vector2I};
/// The number of pixels of padding that should be applied between elements
/// in an atlas row.
const HORIZONTAL_PADDING: i32 = 1;
/// The number of pixels of padding that should be applied between rows of
/// elements in the atlas.
const VERTICAL_PADDING: i32 = 1;
/// A naive allocator to determine where items should be inserted into an atlas. Items are packed in
/// by using the Shelf-Next Fit algorithm (as described in
/// <https://blog.roomanna.com/09-25-2015/binpacking-shelf>). Items are fit horizontally in the
/// current open row (aka shelf) until a new element does not fit in that row, at which point a new
/// row for elements are created.
/// Visually, this looks like the following:
///
/// ```text
/// (width, height)
/// ┌─────┬─────┬─────┬─────┬─────┐
/// │ 10 │ │ │ │ │ <- Empty spaces; can be filled while
/// │ │ │ │ │ │ element_height < height - row_baseline
/// ├─────┼─────┼─────┼─────┼─────┤
/// │ 5 │ 6 │ 7 │ 8 │ 9 │
/// │ │ │ │ │ │
/// ├─────┼─────┼─────┼─────┴─────┤ <- Row height is tallest element in row; this is
/// │ 1 │ 2 │ 3 │ 4 │ used as the baseline for the following row.
/// │ │ │ │ │ <- Row considered full when next element doesn't
/// └─────┴─────┴─────┴───────────┘ fit in the row.
/// (0, 0) x->
/// ```
#[derive(Debug)]
pub(crate) struct Allocator {
/// Width of atlas.
width: i32,
/// Height of atlas.
height: i32,
/// Left-most free pixel in a row.
///
/// This is called the extent because it is the upper bound of used pixels
/// in a row.
row_extent: i32,
/// Baseline for elements in the current row.
row_baseline: i32,
/// Tallest element in current row.
///
/// This is used as the advance when end of row is reached.
row_tallest: i32,
}
impl Allocator {
pub fn new(size: usize) -> Self {
Self {
width: size as i32,
height: size as i32,
row_extent: 0,
row_baseline: 0,
row_tallest: 0,
}
}
/// Attempts to allocate space for an item of size `element_size` into the atlas. If allocated,
/// returns an [`AllocatedRegion`] that describes the region of the texture that was allocated.
/// Returns an [`AllocationError`] if the item was unable to be inserted into the atlas.
pub fn insert(&mut self, element_size: Vector2I) -> Result<AllocatedRegion, AllocationError> {
if element_size.x() > self.width || element_size.y() > self.height {
return Err(AllocationError::ItemTooLarge);
}
// If there's not enough room in current row, go onto next one.
if !self.room_in_row(element_size) {
self.advance_row()?;
}
// If there's still not room, there's nothing that can be done here.
if !self.room_in_row(element_size) {
return Err(AllocationError::Full);
}
// There appears to be room; allocate space for the iten.
Ok(self.insert_inner(element_size))
}
/// Allocate space for the item without checking for room.
///
/// Internal function for use once atlas has been checked for space.
fn insert_inner(&mut self, element_size: Vector2I) -> AllocatedRegion {
let offset_y = self.row_baseline;
let offset_x = self.row_extent;
let height = element_size.y();
let width = element_size.x();
// Update Atlas state.
self.row_extent = offset_x + width + HORIZONTAL_PADDING;
if height > self.row_tallest {
self.row_tallest = height;
}
// Generate UV coordinates.
let uv_top = offset_y as f32 / self.height as f32;
let uv_left = offset_x as f32 / self.width as f32;
let uv_height = height as f32 / self.height as f32;
let uv_width = width as f32 / self.width as f32;
AllocatedRegion {
uv_region: RectF::new(vec2f(uv_left, uv_top), vec2f(uv_width, uv_height)),
pixel_region: RectI::new(vec2i(offset_x, offset_y), vec2i(width, height)),
}
}
/// Check if there's room in the current row for given element..
fn room_in_row(&self, element_size: Vector2I) -> bool {
let next_extent = self.row_extent + element_size.x();
let enough_width = next_extent <= self.width;
let enough_height = element_size.y() < (self.height - self.row_baseline);
enough_width && enough_height
}
/// Mark current row as finished and prepare to insert into the next row.
fn advance_row(&mut self) -> Result<(), AllocationError> {
let advance_to = self.row_baseline + self.row_tallest + VERTICAL_PADDING;
if self.height - advance_to <= 0 {
return Err(AllocationError::Full);
}
self.row_baseline = advance_to;
self.row_extent = 0;
self.row_tallest = 0;
Ok(())
}
}
@@ -0,0 +1,67 @@
use crate::rendering::atlas::allocator::Allocator;
use crate::rendering::atlas::{AllocatedRegion, AllocationError};
use anyhow::Result;
use pathfinder_geometry::vector::Vector2I;
/// Manager that is responsible for allocating areas into a series of textures atlases.
pub(crate) struct Manager {
current_allocator: Allocator,
current_texture_id: TextureId,
atlas_size: usize,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct TextureId(usize);
impl TextureId {
/// Returns the initial [`TextureId`] value to use in a fresh texture atlas
/// cache.
pub fn initial_value() -> Self {
Self(0)
}
/// Returns the next [`TextureId`] value to use after this one.
pub fn next(&self) -> Self {
Self(self.0 + 1)
}
pub fn as_usize(&self) -> usize {
self.0
}
}
/// An offset into a region of a given texture that has been allocated for an item.
#[derive(Copy, Debug, Clone)]
pub(crate) struct TextureOffset {
/// The unique identifier for the texture.
pub texture_id: TextureId,
/// The region of the texture that was allocated.
pub allocated_region: AllocatedRegion,
}
impl Manager {
pub fn new(atlas_size: usize) -> Self {
Self {
current_allocator: Allocator::new(atlas_size),
current_texture_id: TextureId::initial_value(),
atlas_size,
}
}
/// Allocates a region of `size` into a texture. Returns a [`TextureOffset`] denoting the region
/// that was allocated.
pub fn insert(&mut self, size: Vector2I) -> Result<TextureOffset> {
match self.current_allocator.insert(size) {
Ok(allocated_region) => Ok(TextureOffset {
texture_id: self.current_texture_id,
allocated_region,
}),
Err(AllocationError::Full) => {
self.current_texture_id = self.current_texture_id.next();
self.current_allocator = Allocator::new(self.atlas_size);
self.insert(size)
}
Err(insert_error) => Err(insert_error.into()),
}
}
}
+28
View File
@@ -0,0 +1,28 @@
mod allocator;
mod manager;
pub(crate) use manager::{Manager, TextureId};
use pathfinder_geometry::rect::{RectF, RectI};
use thiserror::Error;
/// A region of an atlas that has been allocated.
#[derive(Copy, Debug, Clone)]
pub(crate) struct AllocatedRegion {
/// The region of the atlas that was allocated in UV (texture) coordinates.
pub uv_region: RectF,
/// The region of the atlas that was allocated in screen coordinates.
pub pixel_region: RectI,
}
/// Error that can happen when attempting to allocate an element into the atlas.
#[derive(Error, Debug)]
pub(crate) enum AllocationError {
/// Texture atlas is full.
#[error("Unable to insert; atlas is full")]
Full,
/// The item cannot fit within a single texture.
#[error("Unable to insert; item is too large to fit into atlas")]
ItemTooLarge,
}
+150
View File
@@ -0,0 +1,150 @@
use crate::fonts::{canvas, RasterizedGlyph};
use crate::rendering::atlas::{self, AllocatedRegion, TextureId};
use crate::{fonts::SubpixelAlignment, rendering, scene::GlyphKey};
use anyhow::Result;
use ordered_float::OrderedFloat;
use pathfinder_geometry::rect::RectI;
use pathfinder_geometry::{
rect::RectF,
vector::{Vector2F, Vector2I},
};
use std::collections::HashMap;
const ATLAS_SIZE: usize = 1024;
/// Callback to create a texture at a given size.
type CreateTextureCallback<'a, T> = dyn Fn(usize) -> T + 'a;
/// Callback to insert [`RasterizedGlyph`] at a region identified by [`AllocatedRegion`] into a
/// texture, `T`.
type InsertIntoTextureCallback<'a, T> = dyn Fn(AllocatedRegion, &RasterizedGlyph, &mut T) + 'a;
/// Callback to compute the bounds of a glyph when rasterized.
pub(crate) type GlyphRasterBoundsFn<'a> =
dyn Fn(GlyphKey, Vector2F, &rendering::GlyphConfig) -> Result<RectI> + 'a;
/// Callback to rasterize a glyph.
pub(crate) type RasterizeGlyphFn<'a> = dyn Fn(
GlyphKey,
Vector2F,
SubpixelAlignment,
&rendering::GlyphConfig,
canvas::RasterFormat,
) -> Result<RasterizedGlyph>
+ 'a;
/// A cache that caches glyphs in a texture atlas.
pub struct GlyphCache<Texture> {
textures: Vec<Texture>,
cache: HashMap<GlyphCacheKey, GlyphTextureOffset>,
glyph_config: rendering::GlyphConfig,
atlas_manager: atlas::Manager,
}
#[derive(Hash, PartialEq, Eq)]
struct GlyphCacheKey {
glyph_key: GlyphKey,
scale_factor: OrderedFloat<f32>,
subpixel_alignment: SubpixelAlignment,
}
impl GlyphCacheKey {
fn new(glyph_key: GlyphKey, scale_factor: f32, subpixel_alignment: SubpixelAlignment) -> Self {
GlyphCacheKey {
glyph_key,
scale_factor: scale_factor.into(),
subpixel_alignment,
}
}
}
/// A glyph within a texture atlas.
#[derive(Copy, Debug, Clone)]
pub(crate) struct GlyphTextureOffset {
pub texture_id: TextureId,
pub allocated_region: AllocatedRegion,
pub raster_bounds: RectF,
pub is_emoji: bool,
}
impl<Texture> GlyphCache<Texture> {
pub(crate) fn new(glyph_config: rendering::GlyphConfig) -> Self {
GlyphCache {
textures: Vec::new(),
cache: HashMap::new(),
glyph_config,
atlas_manager: atlas::Manager::new(ATLAS_SIZE),
}
}
pub(crate) fn update_config(&mut self, glyph_config: &rendering::GlyphConfig) {
// If the glyph rendering configuration has changed, blow away the cache
// and replace ourself with a new one.
if *glyph_config != self.glyph_config {
*self = GlyphCache::new(*glyph_config);
}
}
/// Returns the texture identified by [`TextureId`].
pub(crate) fn texture(&self, texture_id: &TextureId) -> Option<&Texture> {
self.textures.get(texture_id.as_usize())
}
/// Returns a [`GlyphTextureOffset`] identified by [`GlyphKey`]. If the [`GlyphKey`] has not
/// been previously cached, the glyph is rasterized and inserted into the texture via the
/// `insert_into_texture` callback. If a new texture needs to be created (since a previous
/// texture is now fill), the `create_texture` callback is called to construct a new texture
/// atlas.
#[allow(clippy::too_many_arguments)]
pub(crate) fn get(
&mut self,
glyph_key: GlyphKey,
scale_factor: f32,
subpixel_alignment: SubpixelAlignment,
create_texture: &CreateTextureCallback<'_, Texture>,
insert_into_texture: &InsertIntoTextureCallback<'_, Texture>,
raster_bounds_fn: &GlyphRasterBoundsFn<'_>,
rasterize_glyph_fn: &RasterizeGlyphFn<'_>,
) -> Result<Option<GlyphTextureOffset>> {
let cache_key = GlyphCacheKey::new(glyph_key, scale_factor, subpixel_alignment);
match self.cache.get(&cache_key) {
None => {
let bounds =
raster_bounds_fn(glyph_key, Vector2F::splat(scale_factor), &self.glyph_config)?;
if bounds.size() == Vector2I::zero() {
return Ok(None);
}
let rasterized_glyph = rasterize_glyph_fn(
glyph_key,
Vector2F::splat(scale_factor),
subpixel_alignment,
&self.glyph_config,
crate::fonts::canvas::RasterFormat::Rgba32,
)?;
let texture_offset = self.atlas_manager.insert(rasterized_glyph.canvas.size)?;
let idx = texture_offset.texture_id.as_usize();
if idx >= self.textures.len() {
self.textures
.resize_with(idx + 1, || create_texture(ATLAS_SIZE));
}
let texture = &mut self.textures[idx];
insert_into_texture(texture_offset.allocated_region, &rasterized_glyph, texture);
let glyph_texture_offset = GlyphTextureOffset {
texture_id: texture_offset.texture_id,
raster_bounds: bounds.to_f32(),
is_emoji: rasterized_glyph.is_emoji,
allocated_region: texture_offset.allocated_region,
};
self.cache.insert(cache_key, glyph_texture_offset);
Ok(Some(glyph_texture_offset))
}
Some(gto) => Ok(Some(*gto)),
}
}
}
+64
View File
@@ -0,0 +1,64 @@
pub(crate) mod atlas;
pub(crate) mod glyph_cache;
#[cfg(wgpu)]
pub mod wgpu;
pub use warpui_core::rendering::*;
use warpui_core::scene::Dash;
pub(crate) use glyph_cache::{GlyphCache, GlyphRasterBoundsFn, RasterizeGlyphFn};
/// Cache for the result of calling [`is_low_power_gpu_available`], as the
/// check can be expensive.
static LOW_POWER_GPU_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
/// Returns `true` if a low power GPU is available for rendering. Typically, this is true for
/// machines with two GPUs -- a dedicated discrete high-performance GPU and a lower power
/// integrated GPU.
pub fn is_low_power_gpu_available() -> bool {
*LOW_POWER_GPU_AVAILABLE.get_or_init(|| {
cfg_if::cfg_if! {
if #[cfg(target_os = "macos")] {
crate::platform::mac::is_low_power_gpu_available()
} else if #[cfg(wgpu)] {
warpui_core::r#async::block_on(wgpu::is_low_power_gpu_available())
} else {
false
}
}
})
}
/// Returns the gap length between each dash to ensure that the stroke begins and ends with a full dash,
/// minimizing deviation from the target gap length.
// adapted from Blink dashed border rendering code:
// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/blink/renderer/platform/graphics/stroke_data.cc;l=130-147;drc=51e1b713f6da38219910bf8fb93a81262340bf97
pub(crate) fn get_best_dash_gap(
stroke_length: f32,
Dash {
dash_length,
gap_length,
force_consistent_gap_length,
}: Dash,
) -> f32 {
if force_consistent_gap_length {
return gap_length;
}
// If no space for two dashes and a gap between, return gap length 0 (solid border)
if stroke_length < 2. * dash_length + gap_length {
return 0.;
}
let min_num_dashes = (stroke_length / (dash_length + gap_length)).floor();
let max_num_dashes = min_num_dashes + 1.;
let min_num_gaps = min_num_dashes - 1.;
let max_num_gaps = max_num_dashes - 1.;
let min_gap = (stroke_length - min_num_dashes * dash_length) / min_num_gaps;
let max_gap = (stroke_length - max_num_dashes * dash_length) / max_num_gaps;
if max_gap <= 0. || ((min_gap - gap_length).abs() < (max_gap - gap_length).abs()) {
min_gap
} else {
max_gap
}
}
+246
View File
@@ -0,0 +1,246 @@
pub mod renderer;
mod resources;
mod shader_types;
mod texture_with_bind_group;
use std::sync::{Arc, LazyLock, Mutex};
use wgpu::wgt::WgpuHasDisplayHandle;
pub use renderer::Renderer;
pub use resources::{adapter_has_rendering_offset_bug, Resources};
use crate::platform::GraphicsBackend;
#[cfg(not(target_family = "wasm"))]
use crate::{rendering::GPUPowerPreference, windowing};
static WGPU_INSTANCE: LazyLock<Mutex<Option<Arc<wgpu::Instance>>>> = LazyLock::new(Mutex::default);
/// Drops and recreates the global shared [`wgpu::Instance`].
pub fn reset_wgpu_instance(display_handle: Box<dyn wgpu::wgt::WgpuHasDisplayHandle>) {
// Drop the existing wgpu instance.
{
let mut instance = WGPU_INSTANCE
.lock()
.expect("wgpu instance lock should not be poisoned");
let _ = instance.take();
}
// Create a new one.
init_wgpu_instance(display_handle);
}
/// Initializes the global wgpu instance. This MUST be called before [`get_wgpu_instance()`].
pub fn init_wgpu_instance(display_handle: Box<dyn WgpuHasDisplayHandle>) {
// Check whether DirectComposition should be explicitly disabled on Windows.
let disable_dcomp = std::env::var("WARP_USE_DIRECT_COMPOSITION")
.ok()
.is_some_and(|val| {
let val = val.to_lowercase();
val == "0" || val == "false"
});
// A helper function to create a wgpu instance with the appropriate configuration.
let create_instance = move || {
let dx12_shader_compiler = get_dx12_shader_compiler();
Arc::new(wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu_backend_options(),
backend_options: wgpu::BackendOptions {
dx12: wgpu::Dx12BackendOptions {
presentation_system: if disable_dcomp {
wgpu::wgt::Dx12SwapchainKind::DxgiFromHwnd
} else {
wgpu::wgt::Dx12SwapchainKind::DxgiFromVisual
},
shader_compiler: dx12_shader_compiler.unwrap_or(wgpu::Dx12Compiler::Fxc),
..Default::default()
},
..Default::default()
},
flags: wgpu::InstanceFlags::empty(),
memory_budget_thresholds: Default::default(),
display: Some(display_handle),
}))
};
// A helper function for initializing the WGPU_INSTANCE static variable.
//
// If `lock_acquired_tx` is provided, it will be used to signal when the lock has been acquired, allowing
// for asynchronous initialization in a dedicated thread while ensuring that `get_wgpu_instance()` cannot
// race with the initialization.
let init_static_var = |lock_acquired_tx: Option<std::sync::mpsc::Sender<()>>| {
let mut instance_lock_guard = WGPU_INSTANCE
.lock()
.expect("wgpu instance lock should not be poisoned");
if let Some(tx) = lock_acquired_tx {
tx.send(()).expect("Failed to send lock acquired signal");
}
instance_lock_guard.get_or_insert_with(|| {
#[cfg(target_os = "linux")]
{
use crate::windowing::{winit::app::WINDOWING_SYSTEM, WindowingSystem};
// If the user hasn't enabled (and is making use of) native Wayland
// support, due to the fact that we force use of X11 in
// ui/src/windowing/winit/app.rs, we need to make sure wgpu doesn't
// attempt to configure the instance to use Wayland, as that causes
// crashes due to a mismatch between the instance and the window
// handle we pass in later when constructing GPU resources.
if WINDOWING_SYSTEM
.get()
.is_some_and(|windowing_system| *windowing_system == WindowingSystem::X11)
|| std::env::var_os("WAYLAND_DISPLAY").is_none()
{
let old_wayland_display = std::env::var_os("WAYLAND_DISPLAY");
std::env::set_var("WAYLAND_DISPLAY", "");
let instance = create_instance();
match old_wayland_display {
Some(wayland_display) => {
std::env::set_var("WAYLAND_DISPLAY", wayland_display)
}
None => std::env::remove_var("WAYLAND_DISPLAY"),
};
return instance;
}
}
create_instance()
});
};
cfg_if::cfg_if! {
if #[cfg(target_family = "wasm")] {
// On wasm, synchronously initialize the wgpu static variable.
init_static_var(None);
} else {
// On other platforms, initialize the wgpu static variable in a separate thread to parallelize
// wgpu instance initialization with other application initialization. We block until we have
// acquired the lock on the instance, ensuring that this function doesn't return until it is
// safe to call `get_wgpu_instance()`.
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
init_static_var(Some(tx));
});
let _ = rx.recv();
}
}
}
/// Helper function to get a [`wgpu::Instance`].
///
/// This should always be used over [`wgpu::Instance::new`] or
/// [`wgpu::Instance::default`] to ensure that configuration is consistent
/// across the app.
fn get_wgpu_instance() -> Arc<wgpu::Instance> {
WGPU_INSTANCE
.lock()
.expect("wgpu instance lock should not be poisoned")
.as_ref()
.expect("wgpu instance should have been initialized")
.clone()
}
/// Returns the set of wgpu backends that we can select from.
fn wgpu_backend_options() -> wgpu::Backends {
wgpu::Backends::from_env().unwrap_or(wgpu::Backends::all())
}
#[cfg(not(target_family = "wasm"))]
pub async fn print_wgpu_adapters(
gpu_power_preference: GPUPowerPreference,
backend_preference: Option<GraphicsBackend>,
windowing_system: Option<windowing::System>,
) {
let instance = get_wgpu_instance();
let backends = wgpu_backend_options();
let adapters = instance.enumerate_adapters(backends).await;
let sorted = resources::sort_adapters(
adapters,
backend_preference.map(to_wgpu_backend),
&gpu_power_preference,
windowing_system,
// This value is only ever true after failing to render frames, which we never attempt when
// running in this mode.
false, /* downrank_non_nvidia_vulkan_adapters */
);
for adapter in sorted {
let info = adapter.get_info();
let device_type = info.device_type;
let device_name = info.name;
let backend = info.backend;
let driver = if info.driver.is_empty() {
"?"
} else {
&info.driver
};
let driver_info = if info.driver_info.is_empty() {
String::new()
} else {
format!(" ({})", info.driver_info)
};
println!("{device_type:?}: {device_name}\n\tBackend: {backend:?}\n\tDriver: {driver}{driver_info}");
}
}
/// Returns `true` if a low power GPU is available for rendering. Typically, this is true for
/// machines with two GPUs -- a dedicated discrete high-performance GPU and a lower power
/// integrated GPU.
#[cfg(not(target_family = "wasm"))]
pub async fn is_low_power_gpu_available() -> bool {
get_wgpu_instance()
.enumerate_adapters(::wgpu::Backends::all())
.await
.iter()
.any(|adapter| adapter.get_info().device_type == ::wgpu::DeviceType::IntegratedGpu)
}
#[cfg(target_family = "wasm")]
pub async fn is_low_power_gpu_available() -> bool {
// We return false here because we only support WebGL (not WebGPU) on the web and the former
// does not allow configuration of a low or high power GPU.
false
}
#[cfg(windows)]
fn get_dx12_shader_compiler() -> Option<wgpu::Dx12Compiler> {
let dxc_path = crate::platform::windows::DXC_PATH.get()?;
dxc_path
.as_ref()
.map(|dxc_path| wgpu::Dx12Compiler::DynamicDxc {
dxc_path: dxc_path.dxc_path.clone(),
})
}
#[cfg(not(windows))]
fn get_dx12_shader_compiler() -> Option<wgpu::Dx12Compiler> {
None
}
/// Converts a [`wgpu::Backend`] to a [`GraphicsBackend`].
#[cfg_attr(target_os = "macos", expect(dead_code))]
pub(crate) fn from_wgpu_backend(backend: wgpu::Backend) -> GraphicsBackend {
match backend {
wgpu::Backend::Noop => GraphicsBackend::Empty,
wgpu::Backend::Vulkan => GraphicsBackend::Vulkan,
wgpu::Backend::Metal => GraphicsBackend::Metal,
wgpu::Backend::Dx12 => GraphicsBackend::Dx12,
wgpu::Backend::Gl => GraphicsBackend::Gl,
wgpu::Backend::BrowserWebGpu => GraphicsBackend::BrowserWebGpu,
}
}
/// Converts a [`GraphicsBackend`] to a [`wgpu::Backend`].
pub(crate) fn to_wgpu_backend(backend: GraphicsBackend) -> wgpu::Backend {
match backend {
GraphicsBackend::Empty => wgpu::Backend::Noop,
GraphicsBackend::Dx12 => wgpu::Backend::Dx12,
GraphicsBackend::Vulkan => wgpu::Backend::Vulkan,
GraphicsBackend::Gl => wgpu::Backend::Gl,
GraphicsBackend::Metal => wgpu::Backend::Metal,
GraphicsBackend::BrowserWebGpu => wgpu::Backend::BrowserWebGpu,
}
}
@@ -0,0 +1,281 @@
mod frame;
mod glyph;
mod image;
mod rect;
mod util;
use frame::Frame;
use pathfinder_geometry::vector::Vector2F;
use util::with_error_scope;
use warpui_core::platform::CapturedFrame;
use wgpu::wgc::{device::DeviceError, present::SurfaceError};
use crate::r#async::block_on;
use crate::rendering::wgpu::Resources;
use crate::rendering::{GlyphConfig, GlyphRasterBoundsFn, RasterizeGlyphFn};
use crate::Scene;
pub use super::resources::{GetSurfaceTextureError, SurfaceConfigureError};
const ENCODER_DESCRIPTOR: wgpu::CommandEncoderDescriptor = wgpu::CommandEncoderDescriptor {
label: Some("Command encoder"),
};
pub struct Renderer {
rect_pipeline: rect::Pipeline,
glyph_pipeline: glyph::Pipeline,
image_pipeline: image::Pipeline,
}
impl Renderer {
pub fn new(resources: &Resources, glyph_config: GlyphConfig) -> Self {
let Resources { device, .. } = resources;
let format = resources.surface_config.borrow().format;
let color_target = wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::all(),
};
let rect_pipeline = rect::Pipeline::new(
resources.uniform_bind_group_layout(),
device,
color_target.clone(),
);
let glyph_pipeline = glyph::Pipeline::new(
resources.uniform_bind_group_layout(),
device,
color_target.clone(),
glyph_config,
);
let image_pipeline =
image::Pipeline::new(resources.uniform_bind_group_layout(), device, color_target);
Self {
rect_pipeline,
glyph_pipeline,
image_pipeline,
}
}
#[allow(clippy::too_many_arguments)]
pub fn render<'a>(
&mut self,
scene: &Scene,
resources: &Resources,
rasterize_glyph_fn: &RasterizeGlyphFn,
glyph_raster_bounds_fn: &GlyphRasterBoundsFn,
window_size: Vector2F,
pre_present_callback: Option<Box<dyn FnOnce() + 'a>>,
capture_callback: Option<Box<dyn FnOnce(CapturedFrame) + Send + 'static>>,
) -> Result<(), Error> {
let Resources { device, queue, .. } = resources;
// Don't initiate the render if we are trying to render into a
// zero-sized window.
if window_size.is_zero() {
return Ok(());
}
let mut ctx = WGPUContext {
resources,
rasterize_glyph_fn,
glyph_raster_bounds_fn,
};
let frame = match with_error_scope(device, || {
Frame::new(
scene,
&mut ctx,
&self.rect_pipeline,
&mut self.glyph_pipeline,
&mut self.image_pipeline,
)
}) {
(_, Some(error)) => return Err(error),
(frame, _) => frame,
};
let surface_texture = resources.get_surface_texture()?;
let mut encoder = device.create_command_encoder(&ENCODER_DESCRIPTOR);
let (_, error) = with_error_scope(device, || {
frame.draw(resources, &mut encoder, &surface_texture);
queue.submit(Some(encoder.finish()));
});
if let Some(callback) = capture_callback {
if let Err(err) =
capture_surface_texture(device, queue, resources, &surface_texture, callback)
{
log::warn!("Frame capture failed: {err}");
}
}
if let Some(callback) = pre_present_callback {
callback();
}
match error {
Some(error) => Err(error),
None => {
// Only present the surface if there were no errors, otherwise
// wgpu will print out an error that we attempted to present a
// texture without submitting any work to the GPU.
match with_error_scope(device, || {
surface_texture.present();
}) {
(_, None) => Ok(()),
(_, Some(error)) => Err(error),
}
}
}
}
}
/// Errors that can occur while rendering a scene.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Device was lost")]
DeviceLost,
#[error("Failed to acquire surface texture: {0:#}")]
SurfaceError(#[from] GetSurfaceTextureError),
#[error("Failed to configure surface: {0:#}")]
SurfaceConfigureError(#[from] SurfaceConfigureError),
#[error("{0:#}")]
Unknown(#[source] wgpu::Error),
}
impl From<wgpu::Error> for Error {
fn from(value: wgpu::Error) -> Self {
for error in anyhow::Chain::new(&value) {
if let Some(DeviceError::Lost) = error.downcast_ref::<DeviceError>() {
return Error::DeviceLost;
}
// The use of `#[transparent]` for many nested device errors breaks
// error chaining - the call to `source()` gets forwarded to the
// DeviceError::Lost, which returns None (it doesn't wrap an error).
// Ideally, these wrapped errors should use `#[from]` instead, but
// until then, we need to do this to properly catch DeviceError::Lost
// from within a call to present().
if let Some(SurfaceError::Device(DeviceError::Lost)) =
error.downcast_ref::<SurfaceError>()
{
return Error::DeviceLost;
}
}
Error::Unknown(value)
}
}
/// Copies the current surface texture into a `CapturedFrame` and delivers it via `callback`.
///
/// **`callback` is invoked synchronously on the render thread** once the GPU readback
/// completes. It must be lightweight (e.g., move the frame into a shared buffer and return
/// immediately) to avoid stalling frame presentation.
fn capture_surface_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
resources: &Resources,
surface_texture: &wgpu::SurfaceTexture,
callback: Box<dyn FnOnce(CapturedFrame) + Send + 'static>,
) -> Result<(), String> {
let texture = &surface_texture.texture;
let width = texture.width();
let height = texture.height();
if width == 0 || height == 0 {
return Err(format!("Invalid texture dimensions: {width}x{height}"));
}
let format = resources.surface_config.borrow().format;
let bytes_per_pixel = 4u32;
let unpadded_bytes_per_row = width * bytes_per_pixel;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
let buffer_size = (padded_bytes_per_row * height) as u64;
let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Frame capture staging buffer"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Frame capture encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &staging_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: None,
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
queue.submit(Some(encoder.finish()));
let buffer_slice = staging_buffer.slice(..);
let (sender, receiver) = std::sync::mpsc::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = sender.send(result);
});
block_on(async {
let _ = device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
});
});
let map_result = receiver
.recv()
.map_err(|e| format!("Failed to receive map result: {e}"))?
.map_err(|e| format!("Buffer mapping failed: {e}"));
map_result?;
let data = buffer_slice.get_mapped_range();
let mut rgba_data = Vec::with_capacity((width * height * bytes_per_pixel) as usize);
for row in 0..height {
let start = (row * padded_bytes_per_row) as usize;
let end = start + unpadded_bytes_per_row as usize;
rgba_data.extend_from_slice(&data[start..end]);
}
drop(data);
staging_buffer.unmap();
if format == wgpu::TextureFormat::Bgra8Unorm || format == wgpu::TextureFormat::Bgra8UnormSrgb {
for chunk in rgba_data.chunks_exact_mut(4) {
chunk.swap(0, 2);
}
}
callback(CapturedFrame::new(width, height, rgba_data));
Ok(())
}
struct WGPUContext<'a> {
resources: &'a Resources,
rasterize_glyph_fn: &'a RasterizeGlyphFn<'a>,
glyph_raster_bounds_fn: &'a GlyphRasterBoundsFn<'a>,
}
@@ -0,0 +1,194 @@
use crate::rendering::wgpu::renderer::{glyph, image, rect, WGPUContext};
use crate::rendering::wgpu::Resources;
use crate::scene::Layer;
use crate::Scene;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::Vector2F;
use wgpu::{CommandEncoder, RenderPass, SurfaceTexture};
#[derive(Default)]
struct PerFrameState {
rect: rect::PerFrameState,
glyph: glyph::PerFrameState,
image: image::PerFrameState,
}
/// Struct responsible for rendering a frame by issuing draw calls.
pub(super) struct Frame<'a> {
scene: &'a Scene,
layer_state: Vec<LayerState<'a>>,
per_frame_state: PerFrameState,
rect_pipeline: &'a rect::Pipeline,
glyph_pipeline: &'a mut glyph::Pipeline,
image_pipeline: &'a mut image::Pipeline,
}
impl<'a> Frame<'a> {
pub(super) fn new(
scene: &'a Scene,
ctx: &'a mut WGPUContext<'a>,
rect_pipeline: &'a rect::Pipeline,
glyph_pipeline: &'a mut glyph::Pipeline,
image_pipeline: &'a mut image::Pipeline,
) -> Self {
glyph_pipeline.update_config(&scene.rendering_config().glyphs);
let mut layer_state = vec![];
let mut per_frame_state = PerFrameState::default();
for layer in scene.layers() {
let rect_layer_state =
rect_pipeline.initialize_for_layer(layer, scene, &mut per_frame_state.rect);
let glyph_layer_state =
glyph_pipeline.initialize_for_layer(layer, scene, &mut per_frame_state.glyph, ctx);
let image_layer_state =
image_pipeline.initialize_for_layer(layer, scene, &mut per_frame_state.image, ctx);
layer_state.push(LayerState {
layer,
rect_layer_state,
glyph_layer_state,
image_layer_state,
});
}
rect::Pipeline::finalize_per_frame_state(
&mut per_frame_state.rect,
&ctx.resources.device,
&ctx.resources.device_lost,
);
glyph::Pipeline::finalize_per_frame_state(
&mut per_frame_state.glyph,
&ctx.resources.device,
&ctx.resources.device_lost,
);
image::Pipeline::finalize_per_frame_state(
&mut per_frame_state.image,
&ctx.resources.device,
&ctx.resources.device_lost,
);
Self {
scene,
layer_state,
per_frame_state,
rect_pipeline,
glyph_pipeline,
image_pipeline,
}
}
/// Encodes draw calls into the [`wgpu::CommandEncoder`] to render the [`Scene`]. Callers are
/// responsible for finishing the [`wgpu::CommandEncoder`] and actually presenting the current
/// drawable on the screen.
pub(super) fn draw(
self,
resources: &Resources,
encoder: &mut CommandEncoder,
surface_texture: &SurfaceTexture,
) {
let surface_size = Vector2F::new(
surface_texture.texture.width() as f32,
surface_texture.texture.height() as f32,
);
let view = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor {
format: Some(surface_texture.texture.format()),
..Default::default()
});
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
resources.configure_render_pass(&mut render_pass, surface_size);
let device_bounds = RectF::new(Vector2F::zero(), surface_size);
for layer_state in &self.layer_state {
if let Some(bounds) = layer_state.layer.clip_bounds {
// Make sure the scissor rect doesn't extend beyond the boundaries
// of the window.
let bounds = (bounds * self.scene.scale_factor()).intersection(device_bounds);
let Some(intersection) = bounds else {
// The layer's clip bounds don't intersect the window bounds
// at all; we can skip drawing anything in this layer.
continue;
};
Self::set_scissor_rect(&mut render_pass, intersection);
} else {
Self::set_scissor_rect(&mut render_pass, device_bounds);
}
if let Some(rect_layer_state) = &layer_state.rect_layer_state {
self.rect_pipeline.draw(
&mut render_pass,
rect_layer_state,
&self.per_frame_state.rect,
);
}
if let Some(image_layer_state) = &layer_state.image_layer_state {
self.image_pipeline.draw(
&mut render_pass,
image_layer_state,
&self.per_frame_state.image,
);
}
if let Some(glyph_layer_state) = &layer_state.glyph_layer_state {
self.glyph_pipeline.draw(
&mut render_pass,
glyph_layer_state,
&self.per_frame_state.glyph,
);
}
}
}
fn set_scissor_rect(render_pass: &mut RenderPass<'_>, scissor_rect_bounds: RectF) {
// Round the corners independently and derive width/height from those. Rounding origin and
// size independently can produce a rect that extends beyond the surface when the origin
// rounds up and the size also rounds up.
let origin_x = scissor_rect_bounds.origin_x().round() as u32;
let origin_y = scissor_rect_bounds.origin_y().round() as u32;
let max_x = scissor_rect_bounds.max_x().round() as u32;
let max_y = scissor_rect_bounds.max_y().round() as u32;
let width = max_x.saturating_sub(origin_x);
let height = max_y.saturating_sub(origin_y);
// wgpu runtime assertions will fail if a scissor rect is set with a 0 width or height. See
// https://github.com/gfx-rs/wgpu/issues/1750
if height != 0 && width != 0 {
render_pass.set_scissor_rect(origin_x, origin_y, width, height);
}
}
}
impl Drop for Frame<'_> {
fn drop(&mut self) {
// Let the image pipeline know that we've finished the frame so it can
// perform cache cleanup.
self.image_pipeline.end_frame();
}
}
/// State for rendering a given [`Layer`] onto the screen.
struct LayerState<'a> {
layer: &'a Layer,
rect_layer_state: Option<rect::LayerState>,
glyph_layer_state: Option<glyph::LayerState>,
image_layer_state: Option<image::LayerState>,
}
@@ -0,0 +1,348 @@
use crate::fonts::SubpixelAlignment;
use crate::rendering::atlas::TextureId;
use crate::rendering::wgpu::renderer::WGPUContext;
use crate::rendering::wgpu::texture_with_bind_group::TextureWithBindGroup;
use crate::rendering::wgpu::{resources, shader_types};
use crate::rendering::{GlyphCache, GlyphConfig};
use crate::scene::{GlyphFade, Layer};
use crate::Scene;
use pathfinder_geometry::rect::RectF;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::{atomic::AtomicBool, Arc};
use wgpu::util::BufferInitDescriptor;
use wgpu::{
BindGroupLayout, BufferUsages, ColorTargetState, Device, FilterMode, RenderPass,
RenderPipeline, Sampler,
};
use super::util::create_buffer_init;
pub(super) struct Pipeline {
glyph_cache: GlyphCache<TextureWithBindGroup>,
render_pipeline: RenderPipeline,
texture_bind_group_layout: BindGroupLayout,
sampler: Sampler,
}
#[derive(Default)]
pub(super) struct PerFrameState {
glyph_data: Vec<shaders::GlyphInstanceData>,
buffer: Option<wgpu::Buffer>,
}
pub(super) struct LayerState {
textures: Vec<PerTextureState>,
}
pub(super) struct PerTextureState {
texture_id: TextureId,
start_offset: usize,
len: usize,
}
impl Pipeline {
pub(super) fn new(
uniform_bind_group_layout: &BindGroupLayout,
device: &Device,
color_target: ColorTargetState,
glyph_config: GlyphConfig,
) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Glyph Shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"../shaders/glyph_shader.wgsl"
))),
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
// This should match the filterable field of the
// corresponding Texture entry above.
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
label: Some("texture_bind_group_layout"),
});
let glyph_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Glyph pipeline layout"),
bind_group_layouts: &[
Some(uniform_bind_group_layout),
Some(&texture_bind_group_layout),
],
immediate_size: 0,
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Glyph Render pipeline"),
layout: Some(&glyph_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[
shader_types::Vertex::desc(),
shaders::GlyphInstanceData::desc(),
],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(color_target)],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
// Don't use a pipeline cache. Most desktop GPU drivers have their own internal caches,
// so we are unlikely to get much value out of this for the platforms Warp supports.
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..Default::default()
});
Self {
glyph_cache: GlyphCache::new(glyph_config),
render_pipeline,
texture_bind_group_layout,
sampler,
}
}
pub(super) fn update_config(&mut self, glyph_config: &GlyphConfig) {
self.glyph_cache.update_config(glyph_config);
}
pub(super) fn initialize_for_layer(
&mut self,
layer: &Layer,
scene: &Scene,
per_frame_state: &mut PerFrameState,
ctx: &WGPUContext,
) -> Option<LayerState> {
if layer.glyphs.is_empty() {
// There are no glyphs to render, exit early.
return None;
}
let scale_factor = scene.scale_factor();
let mut texture_to_glyph: HashMap<TextureId, Vec<shaders::GlyphInstanceData>> =
HashMap::new();
for glyph in &layer.glyphs {
let glyph_position = glyph.position * scale_factor;
let subpixel_alignment = SubpixelAlignment::new(glyph_position);
match self.glyph_cache.get(
glyph.glyph_key,
scene.scale_factor(),
subpixel_alignment,
&|size| {
TextureWithBindGroup::new(
size,
&ctx.resources.device,
&self.texture_bind_group_layout,
&self.sampler,
)
},
&|region, rasterized_glyph, texture| {
texture.insert_glyph_into_texture(
region,
rasterized_glyph,
&ctx.resources.queue,
)
},
ctx.glyph_raster_bounds_fn,
ctx.rasterize_glyph_fn,
) {
Ok(Some(gto)) => {
let (fade_start, fade_end) = match &glyph.fade {
None => (&0.0, &-1.0),
Some(GlyphFade::Horizontal { start, end }) => (start, end),
};
// Adjust the horizontal position by the subpixel alignment
// so that we only shift the glyph over by the amount that
// isn't accounted for in the subpixel-rasterized glyph.
let glyph_position = glyph_position - subpixel_alignment.to_offset();
// Make sure to pass the glyph size in the atlas
// Not the size of the render bounds (which may be smaller)
// If you pass the render bounds as the size, the shader
// will try to sample from a smaller area than the size
// in the atlas, leading to artifacts.
let glyph_instance_data = shaders::GlyphInstanceData::new(
RectF::new(
glyph_position + gto.raster_bounds.origin(),
gto.allocated_region.pixel_region.size().to_f32(),
),
gto.allocated_region.uv_region,
fade_start * scale_factor,
fade_end * scale_factor,
glyph.color,
gto.is_emoji,
);
texture_to_glyph
.entry(gto.texture_id)
.or_default()
.push(glyph_instance_data);
}
Ok(None) => {}
Err(err) => {
log::warn!("Unable to get glyph out of glyph cache: {err:?}, {glyph:?}");
return None;
}
}
}
if texture_to_glyph.is_empty() {
// Early exit if there are no glyphs to render, as it causes a debug assert
// failure in the metal code to create an empty metal buffer.
return None;
}
let mut start_offset = per_frame_state.glyph_data.len();
let per_texture_data = texture_to_glyph
.into_iter()
.map(|(texture_id, mut glyph_instance_data)| {
let len = glyph_instance_data.len();
per_frame_state.glyph_data.append(&mut glyph_instance_data);
let state = PerTextureState {
texture_id,
start_offset,
len,
};
start_offset += len;
state
})
.collect();
Some(LayerState {
textures: per_texture_data,
})
}
pub(super) fn finalize_per_frame_state(
per_frame_state: &mut PerFrameState,
device: &Device,
device_lost: &Arc<AtomicBool>,
) {
per_frame_state.buffer = create_buffer_init(
device,
device_lost,
&BufferInitDescriptor {
label: Some("Glyph instance buffer"),
contents: bytemuck::cast_slice(&per_frame_state.glyph_data),
usage: BufferUsages::VERTEX,
},
)
.ok();
}
pub(super) fn draw<'a>(
&'a self,
render_pass: &mut RenderPass<'a>,
layer_state: &LayerState,
per_frame_state: &'a PerFrameState,
) {
let Some(buffer) = per_frame_state.buffer.as_ref() else {
return;
};
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_vertex_buffer(1, buffer.slice(..));
for per_texture_state in &layer_state.textures {
let texture_with_view = self
.glyph_cache
.texture(&per_texture_state.texture_id)
.expect("texture ID should be in atlas");
render_pass.set_bind_group(1, texture_with_view.bind_group(), &[]);
let end_offset = per_texture_state.start_offset + per_texture_state.len;
render_pass.draw_indexed(
0..resources::quad::INDICES.len() as u32,
0,
per_texture_state.start_offset as u32..end_offset as u32,
);
}
}
}
mod shaders {
use crate::rendering::wgpu::shader_types::{ColorF, Vector4F};
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GlyphInstanceData {
bounds: Vector4F,
uv_bounds: Vector4F,
fade_start: f32,
fade_end: f32,
color: ColorF,
is_emoji: i32,
}
impl GlyphInstanceData {
const ATTRIBS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
1 => Float32x4, // Bounds
2 => Float32x4, // UV Bounds
3 => Float32, // Fade Start
4 => Float32, // Fade end
5 => Float32x4, // Color
6 => Sint32, // Is Emoji
];
pub(super) fn new(
bounds: RectF,
uv_left: RectF,
fade_start: f32,
fade_end: f32,
color: ColorU,
is_emoji: bool,
) -> Self {
Self {
bounds: bounds.into(),
uv_bounds: uv_left.into(),
fade_start,
fade_end,
color: color.into(),
is_emoji: is_emoji as i32,
}
}
pub(super) fn desc() -> wgpu::VertexBufferLayout<'static> {
use std::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &Self::ATTRIBS,
}
}
}
}
@@ -0,0 +1,370 @@
use crate::image_cache::StaticImage;
use crate::rendering::texture_cache::{TextureCache, TextureCacheIndex};
use crate::rendering::wgpu::{resources, shader_types};
use crate::scene::Layer;
use crate::Scene;
use std::borrow::Cow;
use std::sync::{atomic::AtomicBool, Arc};
use wgpu::util::BufferInitDescriptor;
use wgpu::{
BindGroup, BindGroupDescriptor, BindGroupLayout, ColorTargetState, Device, Extent3d,
FilterMode, RenderPass, RenderPipeline, Sampler, TextureDescriptor, TextureFormat,
TextureUsages,
};
use self::shaders::{ColorModifier, ImageInstanceData};
use super::util::create_buffer_init;
use super::WGPUContext;
pub(super) struct Pipeline {
render_pipeline: RenderPipeline,
texture_cache: TextureCache<TextureInfo>,
texture_bind_group_layout: BindGroupLayout,
sampler: Sampler,
}
#[derive(Default)]
pub(super) struct PerFrameState {
image_data: Vec<shaders::ImageInstanceData>,
buffer: Option<wgpu::Buffer>,
}
pub(super) struct LayerState {
start_offset: usize,
image_textures: Vec<TextureCacheIndex>,
}
impl Pipeline {
pub(super) fn new(
uniform_bind_group_layout: &BindGroupLayout,
device: &Device,
color_target: ColorTargetState,
) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Image Shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"../shaders/image_shader.wgsl"
))),
});
let texture_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
// This should match the filterable field of the
// corresponding Texture entry above.
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
label: Some("texture_bind_group_layout"),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Image pipeline layout"),
bind_group_layouts: &[
Some(uniform_bind_group_layout),
Some(&texture_bind_group_layout),
],
immediate_size: 0,
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Image render pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[shader_types::Vertex::desc(), ImageInstanceData::desc()],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(color_target)],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
// Don't use a pipeline cache. Most desktop GPU drivers have their own internal caches,
// so we are unlikely to get much value out of this for the platforms Warp supports.
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
..Default::default()
});
Self {
render_pipeline,
texture_cache: TextureCache::new(),
texture_bind_group_layout,
sampler,
}
}
pub(super) fn initialize_for_layer(
&mut self,
layer: &Layer,
scene: &Scene,
per_frame_state: &mut PerFrameState,
ctx: &WGPUContext,
) -> Option<LayerState> {
if layer.images.is_empty() && layer.icons.is_empty() {
return None;
}
let start_offset = per_frame_state.image_data.len();
let mut layer_state = LayerState {
start_offset,
image_textures: Vec::with_capacity(layer.images.len() + layer.icons.len()),
};
let scale_factor = scene.scale_factor();
for image in &layer.images {
let bounds = image.bounds * scale_factor;
let min_dimension = f32::min(bounds.height(), bounds.width());
let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius(
image.corner_radius,
scale_factor,
min_dimension,
);
per_frame_state.image_data.push(ImageInstanceData::new(
image.bounds * scale_factor,
ColorModifier::Image {
opacity: (image.opacity * 255.) as u8,
},
corner_radius,
));
let (texture_id, _) =
self.texture_cache
.get_or_insert_by_asset(&image.asset, |asset| {
TextureInfo::new(asset, &self.texture_bind_group_layout, &self.sampler, ctx)
});
layer_state.image_textures.push(texture_id);
}
for icon in &layer.icons {
per_frame_state.image_data.push(ImageInstanceData::new(
icon.bounds * scale_factor,
ColorModifier::Icon { color: icon.color },
crate::rendering::CornerRadius::default(),
));
let (texture_id, _) = self
.texture_cache
.get_or_insert_by_asset(&icon.asset, |asset| {
TextureInfo::new(asset, &self.texture_bind_group_layout, &self.sampler, ctx)
});
layer_state.image_textures.push(texture_id);
}
Some(layer_state)
}
pub(super) fn finalize_per_frame_state(
per_frame_state: &mut PerFrameState,
device: &Device,
device_lost: &Arc<AtomicBool>,
) {
per_frame_state.buffer = create_buffer_init(
device,
device_lost,
&BufferInitDescriptor {
label: Some("Image instance buffer"),
contents: bytemuck::cast_slice(&per_frame_state.image_data),
usage: wgpu::BufferUsages::VERTEX,
},
)
.ok();
}
pub(super) fn draw<'a>(
&'a self,
render_pass: &mut RenderPass<'a>,
layer_state: &LayerState,
per_frame_state: &'a PerFrameState,
) {
let Some(buffer) = per_frame_state.buffer.as_ref() else {
return;
};
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_vertex_buffer(1, buffer.slice(..));
for (index, texture_id) in layer_state.image_textures.iter().enumerate() {
let TextureInfo { bind_group, .. } = self
.texture_cache
.get(*texture_id)
.expect("texture should not leave cache between generating layer data and drawing");
render_pass.set_bind_group(1, bind_group, &[]);
let start_offset = layer_state.start_offset + index;
render_pass.draw_indexed(
0..resources::quad::INDICES.len() as u32,
0,
start_offset as u32..(start_offset + 1) as u32,
);
}
}
pub(super) fn end_frame(&mut self) {
self.texture_cache.end_frame();
}
}
/// A structure containing info about a GPU texture from which we can render
/// a particular static image asset.
struct TextureInfo {
/// A handle to the set of resources that are needed to bind the texture
/// in a shader.
bind_group: BindGroup,
}
impl TextureInfo {
fn new(
asset: &Arc<StaticImage>,
bind_group_layout: &BindGroupLayout,
sampler: &Sampler,
ctx: &WGPUContext,
) -> Self {
let texture_size = Extent3d {
width: asset.width(),
height: asset.height(),
depth_or_array_layers: 1,
};
let desc = TextureDescriptor {
label: Some("Image texture"),
size: texture_size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
};
let texture = ctx.resources.device.create_texture(&desc);
let bytes_per_row: u32 = 4 * asset.width();
ctx.resources.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
asset.rgba_bytes(),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: None,
},
texture_size,
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = ctx
.resources
.device
.create_bind_group(&BindGroupDescriptor {
layout: bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
label: None,
});
Self { bind_group }
}
}
mod shaders {
use crate::rendering::wgpu::shader_types::{vec4f, ColorF, Vector4F};
use crate::rendering::CornerRadius;
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
/// Icons support overriding the color, whereas images only allow setting the opacity.
pub(super) enum ColorModifier {
Icon { color: ColorU },
Image { opacity: u8 },
}
impl From<ColorModifier> for ColorF {
fn from(color_mod: ColorModifier) -> Self {
match color_mod {
ColorModifier::Icon { color } => color.to_f32().into(),
ColorModifier::Image { opacity } => ColorU::new(0, 0, 0, opacity).to_f32().into(),
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct ImageInstanceData {
bounds: Vector4F,
color: ColorF,
is_icon: u32,
corner_radius: Vector4F,
}
impl ImageInstanceData {
const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
1 => Float32x4, // Bounds
2 => Float32x4, // Color
3 => Uint32, // Boolean, image or icon
4 => Float32x4, // Corner radius
];
pub(super) fn new(
bounds: RectF,
color_modifier: ColorModifier,
corner_radius: CornerRadius,
) -> Self {
Self {
bounds: bounds.into(),
is_icon: matches!(color_modifier, ColorModifier::Icon { .. }).into(),
color: color_modifier.into(),
corner_radius: vec4f(
corner_radius.top_left,
corner_radius.top_right,
corner_radius.bottom_left,
corner_radius.bottom_right,
),
}
}
pub(super) fn desc() -> wgpu::VertexBufferLayout<'static> {
use std::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &Self::ATTRIBS,
}
}
}
}
@@ -0,0 +1,231 @@
use crate::rendering::get_best_dash_gap;
use crate::rendering::wgpu::shader_types::BorderWidth;
use crate::rendering::wgpu::{resources, shader_types};
use crate::scene::Layer;
use crate::Scene;
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::vec2f;
use std::borrow::Cow;
use std::sync::{atomic::AtomicBool, Arc};
use wgpu::util::BufferInitDescriptor;
use wgpu::{BindGroupLayout, ColorTargetState, Device, RenderPass, RenderPipeline};
use super::util::create_buffer_init;
pub(super) struct Pipeline {
render_pipeline: RenderPipeline,
}
#[derive(Default)]
pub(super) struct PerFrameState {
rect_data: Vec<shader_types::RectData>,
buffer: Option<wgpu::Buffer>,
}
pub(super) struct LayerState {
start_offset: usize,
len: usize,
}
impl Pipeline {
pub(super) fn new(
uniform_bind_group_layout: &BindGroupLayout,
device: &Device,
color_target: ColorTargetState,
) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Rect Shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"../shaders/rect_shader.wgsl"
))),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Rect pipeline layout"),
bind_group_layouts: &[Some(uniform_bind_group_layout)],
immediate_size: 0,
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Rect render pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[shader_types::Vertex::desc(), shader_types::RectData::desc()],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("rect_fs_main"),
targets: &[Some(color_target)],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
// Don't use a pipeline cache. Most desktop GPU drivers have their own internal caches,
// so we are unlikely to get much value out of this for the platforms Warp supports.
cache: None,
});
Self { render_pipeline }
}
pub(super) fn initialize_for_layer(
&self,
layer: &Layer,
scene: &Scene,
per_frame_state: &mut PerFrameState,
) -> Option<LayerState> {
if layer.rects.is_empty() {
// It's a mac assertion error to create an empty metal buffer, so exit early
return None;
}
let scale_factor = scene.scale_factor();
let mut rect_instance_data = Vec::with_capacity(layer.rects.len());
for rect in &layer.rects {
let bounds = rect.bounds * scale_factor;
if let Some(drop_shadow) = rect.drop_shadow {
let sigma = drop_shadow.blur_radius * scale_factor;
let padding = drop_shadow.spread_radius * scale_factor;
let shadow_origin = bounds.origin() + drop_shadow.offset * scale_factor - padding;
let shadow_size = bounds.size() + vec2f(2. * padding, 2. * padding);
let min_dimension = f32::min(shadow_size.x(), shadow_size.y());
let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius(
rect.corner_radius,
scale_factor,
min_dimension,
);
let bounds = RectF::new(shadow_origin, shadow_size);
let shadow_color = shader_types::Color {
start: vec2f(0., 0.).into(),
start_color: drop_shadow.color.into(),
end: vec2f(1., 0.).into(),
end_color: drop_shadow.color.into(),
};
let border_color = shader_types::Color {
start: vec2f(0., 0.).into(),
start_color: ColorU::transparent_black().into(),
end: vec2f(1., 0.).into(),
end_color: ColorU::transparent_black().into(),
};
rect_instance_data.push(shader_types::RectData::new(
bounds,
shadow_color,
border_color,
corner_radius.clone(),
BorderWidth::default(),
sigma,
padding,
0.,
vec2f(0., 0.),
));
}
let min_dimension = f32::min(bounds.height(), bounds.width());
let corner_radius = crate::rendering::CornerRadius::from_ui_corner_radius(
rect.corner_radius,
scale_factor,
min_dimension,
);
let background_color = shader_types::Color {
start: rect.background.start().into(),
start_color: (rect.background.start_color().into()),
end: rect.background.end().into(),
end_color: (rect.background.end_color().into()),
};
let border_color = shader_types::Color {
start: rect.border.color.start().into(),
start_color: (rect.border.color.start_color().into()),
end: rect.border.color.end().into(),
end_color: (rect.border.color.end_color().into()),
};
let border_width = shader_types::BorderWidth {
top: rect.border.top_width() * scale_factor,
right: rect.border.right_width() * scale_factor,
bottom: rect.border.bottom_width() * scale_factor,
left: rect.border.left_width() * scale_factor,
};
let dash = rect
.border
.dash
.map(|mut dash| {
dash.dash_length *= scale_factor;
dash.gap_length *= scale_factor;
dash
})
.unwrap_or_default();
let horizontal_gap = get_best_dash_gap(bounds.width(), dash);
let vertical_gap = get_best_dash_gap(bounds.height(), dash);
let gap_lengths = vec2f(horizontal_gap, vertical_gap);
let rect_data = shader_types::RectData::new(
bounds,
background_color,
border_color,
corner_radius,
border_width,
0.,
0.,
dash.dash_length,
gap_lengths,
);
rect_instance_data.push(rect_data);
}
let start_offset = per_frame_state.rect_data.len();
let len = rect_instance_data.len();
per_frame_state.rect_data.append(&mut rect_instance_data);
Some(LayerState { start_offset, len })
}
pub(super) fn finalize_per_frame_state(
per_frame_state: &mut PerFrameState,
device: &Device,
device_lost: &Arc<AtomicBool>,
) {
per_frame_state.buffer = create_buffer_init(
device,
device_lost,
&BufferInitDescriptor {
label: Some("Rect instance buffer"),
contents: bytemuck::cast_slice(&per_frame_state.rect_data),
usage: wgpu::BufferUsages::VERTEX,
},
)
.ok();
}
pub(super) fn draw<'a>(
&'a self,
render_pass: &mut RenderPass<'a>,
layer_state: &LayerState,
per_frame_state: &'a PerFrameState,
) {
let Some(buffer) = per_frame_state.buffer.as_ref() else {
return;
};
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_vertex_buffer(1, buffer.slice(..));
let end_offset = layer_state.start_offset + layer_state.len;
render_pass.draw_indexed(
0..resources::quad::INDICES.len() as u32,
0,
layer_state.start_offset as u32..end_offset as u32,
);
}
}
@@ -0,0 +1,103 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use wgpu::{
util::BufferInitDescriptor, Buffer, BufferAddress, BufferDescriptor, Device,
COPY_BUFFER_ALIGNMENT,
};
use super::Error;
/// Calls the provided function, capturing and returning any validation errors
/// detected by wgpu.
#[must_use]
pub fn with_error_scope<T>(
device: &wgpu::Device,
callback: impl FnOnce() -> T,
) -> (T, Option<Error>) {
let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let ret = callback();
// On native platforms, the future returned by `pop_error_scope` resolves
// immediately. On wasm, it may take longer due to asynchronous browser
// APIs, but it's necessary to wait here to know if it is safe to continue.
let error_future = error_scope.pop();
cfg_if::cfg_if! {
if #[cfg(target_family = "wasm")] {
let error = crate::r#async::block_on(error_future);
} else {
use futures::FutureExt;
let error = error_future.now_or_never().expect("always resolves immediately");
}
}
(ret, error.map(Into::into))
}
/// Creates a buffer and initializes it with data, synchronously returning an
/// error if the buffer could not be created successfully.
///
/// This is adapted from [`wgpu::util::DeviceExt::create_buffer_init`], with
/// added logic to check for and return errors from the underlying buffer
/// creation.
pub fn create_buffer_init(
device: &Device,
device_lost: &Arc<AtomicBool>,
descriptor: &BufferInitDescriptor<'_>,
) -> Result<Buffer, super::Error> {
// Skip mapping if the buffer is zero sized
if descriptor.contents.is_empty() {
let wgt_descriptor = BufferDescriptor {
label: descriptor.label,
size: 0,
usage: descriptor.usage,
mapped_at_creation: false,
};
create_buffer(device, &wgt_descriptor)
} else {
let unpadded_size = descriptor.contents.len() as BufferAddress;
// Valid vulkan usage is
// 1. buffer size must be a multiple of COPY_BUFFER_ALIGNMENT.
// 2. buffer size must be greater than 0.
// Therefore we round the value up to the nearest multiple, and ensure it's at least COPY_BUFFER_ALIGNMENT.
let align_mask = COPY_BUFFER_ALIGNMENT - 1;
let padded_size = ((unpadded_size + align_mask) & !align_mask).max(COPY_BUFFER_ALIGNMENT);
let wgt_descriptor = BufferDescriptor {
label: descriptor.label,
size: padded_size,
usage: descriptor.usage,
mapped_at_creation: true,
};
let buffer = create_buffer(device, &wgt_descriptor)?;
if device_lost.load(Ordering::SeqCst) {
return Err(super::Error::DeviceLost);
}
buffer
.slice(..)
.get_mapped_range_mut()
.slice(..unpadded_size as usize)
.copy_from_slice(descriptor.contents);
buffer.unmap();
Ok(buffer)
}
}
/// Creates a buffer using the given device and descriptor, synchronously
/// returning an error if the buffer could not be created successfully.
fn create_buffer(device: &Device, desc: &BufferDescriptor<'_>) -> Result<Buffer, Error> {
let (buffer, error) = with_error_scope(device, || device.create_buffer(desc));
match error {
Some(error) => {
log::warn!("Failed to create wgpu::Buffer: {error:#}");
Err(error)
}
None => Ok(buffer),
}
}
@@ -0,0 +1,917 @@
pub mod quad;
pub mod uniforms;
use std::cell::RefCell;
use std::collections::HashSet;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use crate::rendering::OnGPUDeviceSelected;
use crate::windowing;
use crate::{r#async::block_on, rendering::GPUPowerPreference};
use anyhow::{anyhow, Result};
use itertools::Itertools;
use lazy_static::lazy_static;
use pathfinder_geometry::vector::Vector2F;
use thiserror::Error;
use version_compare::Version;
use warpui_core::rendering::{GPUBackend, GPUDeviceInfo, GPUDeviceType};
use wgpu::{
Adapter, Backend, CompositeAlphaMode, CurrentSurfaceTexture, Device, DeviceType, PresentMode,
Queue, Surface, SurfaceConfiguration,
};
/// A mostly-arbitrary value to use as the height/width of a surface when
/// creating a default surface configuration.
///
/// 4 was chosen here because sometimes drivers care that things are a
/// multiple of 2 or 4, so this seemed like a safe choice, while being
/// small enough that any buffers that get allocated are tiny and quick to
/// create and destroy.
const SURFACE_SIZE_FOR_TESTING: u32 = 4;
lazy_static! {
/// The minimum supported driver version for lavapipe, the Vulkan version
/// of Mesa's llvmpipe software renderer.
///
/// While lavapipe is theoretically Vulkan 1.3 compatible starting in version
/// 22.1.2, in practice, Warp windows don't render properly until 24.0.2.
static ref MIN_SUPPORTED_LAVAPIPE_VERSION: Version<'static> = Version::from("24.0.2")
.expect("should not fail to parse version");
/// The minimum supported driver version for Vulkan-backed Intel UHD integrated graphics.
///
/// Some issues we've seen: PLAT-744 and PLAT-599.
/// Mesa changelog mentions a fix for flickering on Intel UHD:
/// https://docs.mesa3d.org/relnotes/21.3.6.html#:~:text=Flickering%20Intel%20Uhd%20620%20Graphics
static ref MIN_SUPPORTED_INTEL_UHD_VERSION: Version<'static> = Version::from("21.3.6")
.expect("should not fail to parse version");
/// Nvidia drivers version 535 have problems with Wayland window managers, e.g. PLAT-667 and
/// PLAT-674.
static ref MIN_SUPPORTED_NVIDIA_VERSION: Version<'static> = Version::from("545")
.expect("should not fail to parse version");
static ref MAX_SUPPORTED_NVIDIA_VERSION_ON_WINDOWS: Version<'static> = Version::from("572")
.expect("should not fail to parse version");
}
/// Set of resources needed to render using wgpu.
pub struct Resources {
pub device: wgpu::Device,
pub device_lost: Arc<AtomicBool>,
pub queue: Queue,
pub adapter: Adapter,
pub surface: Surface<'static>,
pub surface_config: RefCell<SurfaceConfiguration>,
pub supported_backends: Vec<wgpu::Backend>,
uniforms: uniforms::Uniforms,
quad: quad::Resources,
}
impl Resources {
/// Attempts to construct a new instance of [`Resources`] via the provided `window_handle`.
pub fn new(
window_handle: impl Into<wgpu::SurfaceTarget<'static>> + wgpu::rwh::HasDisplayHandle,
gpu_power_preference: GPUPowerPreference,
backend_preference: Option<wgpu::Backend>,
on_gpu_device_selected: &OnGPUDeviceSelected,
initial_surface_size: Vector2F,
downrank_non_nvidia_vulkan_adapters: bool,
) -> Result<Self> {
let windowing_system = window_handle.display_handle()?.as_raw().try_into().ok();
let instance = super::get_wgpu_instance();
let surface = instance.create_surface(window_handle)?;
let backends = super::wgpu_backend_options();
// All of the WGPU initialization functions are asynchronous. For simplicity while
// prototyping, we just use `block_on` to force them to be synchronous.
block_on(async {
let (adapter, device, queue, surface_config, supported_backends) = select_adapter(
&instance,
&surface,
backends,
backend_preference,
gpu_power_preference,
initial_surface_size,
windowing_system,
downrank_non_nvidia_vulkan_adapters,
)
.await
.ok_or_else(|| anyhow!("No usable wgpu adapter was found"))?;
let adapter_info = adapter.get_info();
log::info!(
"Using {:?} {:?} ({}) for rendering new window.",
adapter_info.backend,
adapter_info.device_type,
adapter_info.name,
);
on_gpu_device_selected(device_info_from_adapter_info(adapter_info));
let uniforms = uniforms::Uniforms::new(&device);
let quad = quad::Resources::new(&device);
let device_lost = Arc::new(AtomicBool::new(false));
let device_lost_clone = device_lost.clone();
device.set_device_lost_callback(move |device_lost_reason, message| {
device_lost_clone.store(true, Ordering::SeqCst);
log::warn!("The current device is lost. Reason: {device_lost_reason:?}. Message: {message}")
});
Ok(Self {
device,
device_lost,
queue,
adapter,
surface,
surface_config: surface_config.into(),
supported_backends: supported_backends.into_iter().collect(),
uniforms,
quad,
})
})
}
pub fn uniform_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
self.uniforms.bind_group_layout()
}
pub fn configure_render_pass<'a>(
&'a self,
render_pass: &mut wgpu::RenderPass<'a>,
drawable_size: Vector2F,
) {
self.uniforms
.configure_render_pass(render_pass, drawable_size, self);
self.quad.configure_render_pass(render_pass);
}
/// Updates the size of the underlying surface.
pub fn update_surface_size(&self, size: Vector2F) -> Result<(), SurfaceConfigureError> {
if size.x() > 0. && size.y() > 0. {
let mut surface_config = self.surface_config.borrow_mut();
surface_config.width = size.x() as u32;
surface_config.height = size.y() as u32;
block_on(configure_surface(
&self.surface,
&self.device,
&surface_config,
))
} else {
Ok(())
}
}
/// Gets the next surface texture to render to.
pub fn get_surface_texture(&self) -> Result<wgpu::SurfaceTexture, GetSurfaceTextureError> {
let Resources {
surface,
device,
surface_config,
..
} = self;
let error = match get_surface_texture(surface) {
Ok(texture) => return Ok(texture),
Err(error) => error,
};
log::warn!("Encountered error while getting the next swap chain texture: {error:#}");
match error {
GetSurfaceTextureError::Timeout
| GetSurfaceTextureError::Validation
| GetSurfaceTextureError::Occluded
| GetSurfaceTextureError::ConfigurationError(_) => {
// Skip this frame and hope it resolves itself by the next one.
log::info!("Skipping rendering the current frame...");
Err(error)
}
GetSurfaceTextureError::Lost | GetSurfaceTextureError::Outdated => {
block_on(configure_surface(surface, device, &surface_config.borrow()))
.map_err(GetSurfaceTextureError::ConfigurationError)?;
match get_surface_texture(surface) {
Ok(texture) => {
log::info!("Successfully recreated the swap chain");
Ok(texture)
}
Err(e) => {
log::warn!("Failed to recreate the swap chain: {e:#}");
Err(e)
}
}
}
}
}
}
fn device_info_from_adapter_info(adapter_info: wgpu::AdapterInfo) -> GPUDeviceInfo {
let device_type = match adapter_info.device_type {
DeviceType::Other => GPUDeviceType::Other,
DeviceType::IntegratedGpu => GPUDeviceType::IntegratedGpu,
DeviceType::DiscreteGpu => GPUDeviceType::DiscreteGpu,
DeviceType::VirtualGpu => GPUDeviceType::VirtualGpu,
DeviceType::Cpu => GPUDeviceType::Cpu,
};
let backend = match adapter_info.backend {
Backend::Noop => GPUBackend::Empty,
Backend::Vulkan => GPUBackend::Vulkan,
Backend::Metal => GPUBackend::Metal,
Backend::Dx12 => GPUBackend::Dx12,
Backend::Gl => GPUBackend::Gl,
Backend::BrowserWebGpu => GPUBackend::BrowserWebGpu,
};
GPUDeviceInfo {
device_type,
device_name: adapter_info.name,
driver_name: adapter_info.driver,
driver_info: adapter_info.driver_info,
backend,
}
}
/// Selects the adapter to use to render to the given surface.
///
/// The adapter is selected from the set of adapters that support the given
/// backends, and priority is determined by the power preference.
///
/// This is inspired by the implementation of `request_adapter` in `wgpu_core`:
/// https://github.com/gfx-rs/wgpu/blob/badb3c88ea29acb159d333e2f60b1cc305bbd512/wgpu-core/src/instance.rs#L857
#[allow(clippy::too_many_arguments)]
#[cfg_attr(target_family = "wasm", allow(unused_variables))]
async fn select_adapter(
instance: &wgpu::Instance,
surface: &wgpu::Surface<'static>,
backends: wgpu::Backends,
backend_preference: Option<wgpu::Backend>,
gpu_power_preference: GPUPowerPreference,
initial_surface_size: Vector2F,
windowing_system: Option<windowing::System>,
downrank_non_nvidia_vulkan_adapters: bool,
) -> Option<(
Adapter,
Device,
Queue,
SurfaceConfiguration,
HashSet<wgpu::Backend>,
)> {
cfg_if::cfg_if! {
if #[cfg(target_family = "wasm")] {
let power_preference = match gpu_power_preference {
GPUPowerPreference::LowPower => wgpu::PowerPreference::LowPower,
GPUPowerPreference::HighPerformance => wgpu::PowerPreference::HighPerformance,
};
let request_adapter_options = wgpu::RequestAdapterOptions {
power_preference,
force_fallback_adapter: false,
compatible_surface: Some(surface),
};
let adapter = instance.request_adapter(&request_adapter_options).await.ok()?;
let adapters = [adapter].into_iter();
} else {
let adapters = instance
.enumerate_adapters(backends)
.await
.into_iter();
}
}
log::info!("Enabled wgpu backends: {backends:?}");
log::info!("Available wgpu adapters (in priority order):");
let sorted_adapters = sort_adapters(
adapters.collect(),
backend_preference,
&gpu_power_preference,
windowing_system,
downrank_non_nvidia_vulkan_adapters,
);
let adapters = sorted_adapters
// Filter out any unsupported adapters and log information about each one.
.filter(|adapter| is_supported_adapter(adapter, surface))
// While we don't strictly need to collect the iterator into a vector,
// this ensures we log adapter information for all adapters. (Omitting
// this means the iterator is lazily evaluated, and we'll only print
// adapter information up until the point where we find a working one.)
.collect_vec();
let supported_backends = adapters
.iter()
.map(|adapter| adapter.get_info().backend)
.collect::<HashSet<_>>();
for adapter in adapters {
if let Some((device, queue, surface_config)) =
initialize_device(&adapter, surface, initial_surface_size).await
{
return Some((adapter, device, queue, surface_config, supported_backends));
}
}
None
}
/// Sorts adapters according to user preference, stability, and performance.
///
/// All sorts performed here should be stable, ensuring that the relative ordering of previous
/// sorting steps is preserved.
pub(super) fn sort_adapters(
adapters: Vec<wgpu::Adapter>,
backend_preference: Option<wgpu::Backend>,
gpu_power_preference: &GPUPowerPreference,
windowing_system: Option<windowing::System>,
downrank_non_nvidia_vulkan_adapters: bool,
) -> impl Iterator<Item = wgpu::Adapter> {
adapters
.into_iter()
// Sort adapters by backend priority.
.sorted_by_cached_key(|adapter| adapter_backend_sort_func(adapter, backend_preference))
.sorted_by_cached_key(adapter_supported_features)
// Sort adapters based on low/high power preferences.
.sorted_by_cached_key(power_preference_adapter_sort_func(gpu_power_preference))
// Sort adapters that we know have some issues towards the end of the list.
.sorted_by_cached_key(|adapter| {
adapter_stability_sort_func(
adapter,
windowing_system,
downrank_non_nvidia_vulkan_adapters,
)
})
}
/// Returns whether or not a particular adapter is supported and can be used
/// for rendering.
fn is_supported_adapter(adapter: &wgpu::Adapter, surface: &wgpu::Surface) -> bool {
let can_present = adapter.is_surface_supported(surface);
let supported_texture_format = surface
.get_default_config(adapter, SURFACE_SIZE_FOR_TESTING, SURFACE_SIZE_FOR_TESTING)
.map(|config| config.format);
let supported_alpha_modes = surface.get_capabilities(adapter).alpha_modes;
// Log information about the adapter (to assist with debugging).
let info = adapter.get_info();
let device_type = &info.device_type;
let device_name = &info.name;
let backend = &info.backend;
let driver = if info.driver.is_empty() {
"Unknown"
} else {
&info.driver
};
let driver_info = if info.driver_info.is_empty() {
String::new()
} else {
format!(" ({})", info.driver_info)
};
log::info!("{device_type:?}: {device_name}\n\tBackend: {backend:?}\n\tDriver: {driver}{driver_info}\n\tCan present: {can_present}\n\tSupported texture format: {supported_texture_format:?}\n\tSupported alpha mode: {supported_alpha_modes:?}");
can_present && supported_texture_format.is_some()
}
/// Encode levels of preference for graphics adapters based on features they enable. This takes
/// precedence under the "GPU power preference".
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
enum AdapterFeatureSet {
/// No features are hindered by what this adapter supports.
Full = 0,
/// Some non-critical features not supported by the adapter.
MissingMinorFeatures = 1,
}
fn adapter_supported_features(adapter: &Adapter) -> AdapterFeatureSet {
if adapter_has_rendering_offset_bug(&adapter.get_info()) {
log::warn!("Deprioritizing OpenGL-backed Intel UHD adapter");
AdapterFeatureSet::MissingMinorFeatures
} else {
AdapterFeatureSet::Full
}
}
fn is_nvidia_adapter(adapter_info: &wgpu::AdapterInfo) -> bool {
adapter_info.driver == "NVIDIA"
}
fn is_vulkan_nvidia_adapter(adapter_info: &wgpu::AdapterInfo) -> bool {
// Only consider Vulkan adapters using the Nvidia driver.
adapter_info.backend == wgpu::Backend::Vulkan && is_nvidia_adapter(adapter_info)
}
/// Returns whether or not the provided adapter is an unsupported Nvidia driver version for warpui
/// to render properly.
fn is_older_nvidia_adapter(adapter_info: &wgpu::AdapterInfo) -> bool {
if !is_vulkan_nvidia_adapter(adapter_info) {
return false;
}
let Some(version) = Version::from(&adapter_info.driver_info) else {
// Log an error so we know this occurred and can improve the logic as-needed.
log::error!(
"Unable to parse Vulkan-backed Nvidia adapter version {:?}; de-prioritizing out of an \
abundance of caution.",
adapter_info.driver_info
);
return true;
};
version < *MIN_SUPPORTED_NVIDIA_VERSION
}
/// Returns whether this adapter is a newer Windows NVIDIA adapter using a non-DX12 backend.
/// On NVIDIA drivers 572 and later, the default value of "auto" for the "Vulkan / OpenGL Present
/// Method" can cause crashes when creating multiple windows, so we downrank it.
fn is_newer_nondx12_nvidia_adapter_on_windows(adapter_info: &wgpu::AdapterInfo) -> bool {
if !cfg!(windows) {
return false;
}
if !is_nvidia_adapter(adapter_info) {
return false;
}
if adapter_info.backend == Backend::Dx12 {
return false;
}
let Some(version) = Version::from(&adapter_info.driver_info) else {
// Log an error so we know this occurred and can improve the logic as-needed.
log::error!(
"Unable to parse Nvidia adapter version {:?} adapter_info.driver_info",
adapter_info.driver_info
);
return false;
};
version >= *MAX_SUPPORTED_NVIDIA_VERSION_ON_WINDOWS
}
/// Returns whether this adapter is the integrated OpenGL driver for Windows running in Parallels.
/// It caused problems with theme background images.
/// https://linear.app/warpdotdev/issue/CORE-3692/background-images-broken-in-parallels
fn is_gl_to_metal_adapter_on_windows_in_parallels(adapter_info: &wgpu::AdapterInfo) -> bool {
cfg!(windows)
&& adapter_info.backend == Backend::Gl
&& adapter_info.device_type == DeviceType::IntegratedGpu
&& adapter_info.driver_info.to_lowercase().contains("metal")
&& adapter_info.name.to_lowercase().starts_with("parallels")
}
/// Returns whether or not the provided adapter is an unsupported Intel UHD Mesa driver version for
/// warpui to render properly. Currently, we limit this to "Intel UHD Graphics 620", but we do have
/// some suspicion that more Intel UHD devices are affected, e.g. PLAT-599 has a "Intel(R) UHD
/// Graphics (TGL GT1)" user seeing the exact same issue.
fn is_older_vulkan_intel_uhd_adapter(adapter_info: &wgpu::AdapterInfo) -> bool {
if adapter_info.backend != wgpu::Backend::Vulkan
|| adapter_info.device_type != wgpu::DeviceType::IntegratedGpu
|| !adapter_info.name.contains("Intel(R) HD Graphics 620")
{
return false;
}
mesa_driver_version_is_below_minimum(
&adapter_info.driver_info,
&MIN_SUPPORTED_INTEL_UHD_VERSION,
)
}
/// Returns true if this is:
/// 1) An Intel UHD 620 Graphics device
/// 2) Using the Vulkan backend
/// 3) On Windows
///
/// We have indication that this specific device is unstable on Windows so we ignore it in the
/// hopes that there is a DX12 or GL version of this adapter that is more stable.
fn is_intel_uhd_620_adapter_on_windows_with_vulkan_backend(
adapter_info: &wgpu::AdapterInfo,
) -> bool {
cfg!(windows)
&& adapter_info.backend == Backend::Vulkan
&& adapter_info.device_type == DeviceType::IntegratedGpu
&& (adapter_info.name.contains("Intel(R) UHD Graphics 620")
|| adapter_info.name.contains("Intel(R) HD Graphics 620"))
}
/// Returns whether the given adapter is known to have a rendering offset bug on Windows.
///
/// Certain Intel integrated GPU drivers using the GL backend render the scene at an offset from
/// the window bounds when window decorations are disabled. The offset matches the size of the
/// window decorations (e.g. title bar height). Enabling native window decorations fixes the
/// alignment.
///
/// See: https://github.com/warpdotdev/Warp/issues/6120
pub fn adapter_has_rendering_offset_bug(adapter_info: &wgpu::AdapterInfo) -> bool {
if !cfg!(windows) {
return false;
}
if adapter_info.backend != Backend::Gl || adapter_info.device_type != DeviceType::IntegratedGpu
{
return false;
}
// Known affected Intel integrated GPU models. This list is based on user reports from
// https://github.com/warpdotdev/Warp/issues/6120.
let affected_models = [
"Intel(R) HD Graphics 4000",
"Intel(R) HD Graphics 4400",
"Intel(R) HD Graphics 4600",
"Intel(R) HD Graphics 5500",
"Intel(R) HD Graphics P4600",
"Intel(R) Iris(TM) Pro Graphics 5200",
"Intel(R) Iris(TM) Graphics 6100",
];
affected_models
.iter()
.any(|model| adapter_info.name.contains(model))
}
/// Checks whether the provided adapter info describes a lavapipe
/// (Vulkan llvmpipe) adapter that may not work properly with warpui.
fn is_older_lavapipe_adapter(adapter_info: &wgpu::AdapterInfo) -> bool {
// Only consider Vulkan adapters using the llvmpipe driver.
if adapter_info.backend != wgpu::Backend::Vulkan || adapter_info.driver != "llvmpipe" {
return false;
}
mesa_driver_version_is_below_minimum(&adapter_info.driver_info, &MIN_SUPPORTED_LAVAPIPE_VERSION)
}
fn mesa_driver_version_is_below_minimum(info_str: &str, min_version: &Version) -> bool {
let &[name, version, ..] = info_str.splitn(3, ' ').collect_vec().as_slice() else {
// Log an error so we know this occurred and can improve the logic as-needed.
log::error!(
"Encountered Mesa driver info {info_str:?} with an unexpected format! (too few parts)"
);
return false;
};
// Perform an extra check that we parsed the driver info string properly.
if name.trim() != "Mesa" {
// Log an error so we know this occurred and can improve the logic as-needed.
log::error!(
"Encountered Mesa driver info {info_str:?} with an unexpected format! (name != Mesa)"
);
return false;
}
let manifest = version_compare::Manifest {
// We only care about major, minor, and patch versions.
max_depth: Some(3),
..Default::default()
};
let Some(version) = Version::from_manifest(version, &manifest) else {
// Log an error so we know this occurred and can improve the logic as-needed.
log::error!(
"Unable to parse Mesa version {version:?}; de-prioritizing out of an abundance of caution."
);
return true;
};
version < *min_version
}
/// Creates a device and command queue for the given adapter that is guaranteed
/// to be able to create a swapchain for the surface.
async fn initialize_device(
adapter: &Adapter,
surface: &Surface<'static>,
initial_surface_size: Vector2F,
) -> Option<(Device, Queue, SurfaceConfiguration)> {
log::info!(
"Verifying adapter \"{}\" is valid...",
adapter.get_info().name
);
// `Limits::downlevel_webgl2_defaults` gives very conservative defaults. We want to keep these
// limits low in order to make sure we remain compatible with lower-end devices. One exception
// to this is sizes of textures. `using_resolution` increases the size limits on textures. We
// need this because users' displays often exceed the downleveled default limits of 2048px.
// Here, we increase that to the ceiling of what this adapter is capable of.
let mut limits = wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits());
// Set a higher minimum number of variables that can be passed between shader stages.
limits.max_inter_stage_shader_variables = 15;
limits.max_mesh_output_layers = 0;
let (device, queue) = match adapter
.request_device(&wgpu::DeviceDescriptor {
// Use the broadest/most permissive device requirements
// so that we can run on as many machines as possible.
// If we use any WGSL features that aren't included in
// these defaults, we can add specific overrides as needed.
required_limits: limits,
..Default::default()
})
.await
{
Ok(device_and_queue) => device_and_queue,
Err(err) => {
log::warn!("Failed to create a logical device: {err:#}");
return None;
}
};
// Ensure that we're able to create a swapchain before we treat the device
// as valid.
let Some(surface_config) = create_surface_config(adapter, surface, initial_surface_size) else {
log::warn!("Failed to get default surface configuration");
return None;
};
match configure_surface(surface, &device, &surface_config).await {
Ok(_) => Some((device, queue, surface_config)),
Err(err) => {
log::warn!("Failed to create swapchain: {err:#}");
None
}
}
}
/// Returns a priority for an adapter based on backend type, to be used as a
/// sort function.
///
/// This matches the order used by wgpu; see:
/// https://github.com/gfx-rs/wgpu/blob/v0.18/wgpu-core/src/instance.rs#L869-L913
#[cfg(not(windows))]
fn adapter_backend_sort_func(
adapter: &wgpu::Adapter,
backend_preference: Option<wgpu::Backend>,
) -> usize {
let backend = adapter.get_info().backend;
if backend_preference.is_some_and(|pref| pref == backend) {
return 0;
}
match backend {
wgpu::Backend::Vulkan => 1,
wgpu::Backend::Metal => 2,
wgpu::Backend::Dx12 => 3,
wgpu::Backend::BrowserWebGpu => 4,
wgpu::Backend::Gl => 5,
wgpu::Backend::Noop => 6,
}
}
/// Returns a priority for an adapter based on backend type, to be used as a
/// sort function.
///
/// This prioritizes DX12 on Windows which is more reliable. See this issue:
/// https://github.com/gfx-rs/wgpu/issues/2719
#[cfg(windows)]
fn adapter_backend_sort_func(
adapter: &wgpu::Adapter,
backend_preference: Option<wgpu::Backend>,
) -> usize {
let backend = adapter.get_info().backend;
if backend_preference.is_some_and(|pref| pref == backend) {
return 0;
}
match backend {
// On Windows, we prefer DirectX 12 over Vulkan. Given that no other
// platform supports DX12 at all, there's no need to condition this
// ranking on OS.
wgpu::Backend::Dx12 => 1,
wgpu::Backend::Vulkan => 2,
wgpu::Backend::Gl => 3,
wgpu::Backend::Metal => 4,
wgpu::Backend::BrowserWebGpu => 5,
wgpu::Backend::Noop => 6,
}
}
/// Returns a priority for an adapter based on our expectations of its
/// stability.
///
/// This should be used to deprioritize adapters where they _may not_
/// work, but we're not so confident that they are broken that we fully filter
/// them out. Ultimately, if the user only has one adapter, it's better for
/// us to attempt to use it than for us to give up without trying.
fn adapter_stability_sort_func(
adapter: &wgpu::Adapter,
windowing_system: Option<windowing::System>,
downrank_non_nvidia_vulkan_adapters: bool,
) -> AdapterSupport {
let adapter_info = adapter.get_info();
let window_server_is_wayland = matches!(
windowing_system,
Some(windowing::System::Wayland) | Some(windowing::System::X11 { is_x_wayland: true })
);
if downrank_non_nvidia_vulkan_adapters
&& adapter_info.backend == Backend::Vulkan
&& !is_vulkan_nvidia_adapter(&adapter_info)
{
log::info!("Deprioritizing non-NVIDIA Vulkan adapter (the PRIME performance profile is likely enabled)");
return AdapterSupport::Unsupported;
}
if is_intel_uhd_620_adapter_on_windows_with_vulkan_backend(&adapter_info) {
log::warn!("Deprioritizing Vulkan-backed Intel UHD 620 adapter");
return AdapterSupport::SupportedWithIssues;
}
if is_older_vulkan_intel_uhd_adapter(&adapter_info) {
log::warn!(
"Deprioritizing Vulkan-backed Intel UHD adapter due to Mesa < {} (unsupported)",
*MIN_SUPPORTED_INTEL_UHD_VERSION
);
AdapterSupport::SupportedWithIssues
}
// Deprioritize older lavapipe adapters where we have evidence that they are less stable.
else if is_older_lavapipe_adapter(&adapter_info) {
log::warn!(
"Deprioritizing Vulkan-backed llvmpipe adapter due to Mesa < {} (unsupported)",
*MIN_SUPPORTED_LAVAPIPE_VERSION
);
AdapterSupport::Unsupported
// Same with Nvidia drivers, though this is only an issue with a Wayland window server.
} else if window_server_is_wayland && is_older_nvidia_adapter(&adapter_info) {
log::warn!(
"Deprioritizing Vulkan-backed Nvidia adapter due to version < {} (unsupported).\nSee \
the \"Graphics\" secion of our docs here: \
https://docs.warp.dev/help/known-issues#linux-1",
*MIN_SUPPORTED_NVIDIA_VERSION
);
AdapterSupport::Unsupported
} else if is_newer_nondx12_nvidia_adapter_on_windows(&adapter_info) {
log::warn!(
"Deprioritizing non DX12 Nvidia adapter due to version > {} (unsupported). Newer NVIDIA \
drivers can crash if multiple windows are created if the `Vulkan / OpenGL Present Method\
NVIDIA setting is set to `Auto` or `Prefer layered on DXGI Swapchain`.",
*MAX_SUPPORTED_NVIDIA_VERSION_ON_WINDOWS
);
AdapterSupport::SupportedWithIssues
} else if is_gl_to_metal_adapter_on_windows_in_parallels(&adapter_info) {
log::warn!("Deprioritizing integrated OpenGL Windows Parallels adapter.");
AdapterSupport::SupportedWithIssues
} else {
AdapterSupport::Supported
}
}
/// Encode levels of preference for graphics adapters based on application stability. This takes
/// precedence over the "GPU power preference". We've seen varying severities of graphics issues on
/// Linux and Windows.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
enum AdapterSupport {
/// The adapter has no known issues.
Supported = 0,
/// The adapter is somewhat usable, but there have been some problems.
SupportedWithIssues = 1,
/// The adapter is basically not viable. Warpui will either crash or not render.
Unsupported = 2,
}
/// Returns a function that computes the priority for an adapter based on
/// device type, to be used as a sort function.
///
/// This matches the order used by wgpu; see:
/// https://github.com/gfx-rs/wgpu/blob/v0.18/wgpu-core/src/instance.rs#L953-L954
fn power_preference_adapter_sort_func(
pref: &GPUPowerPreference,
) -> impl FnMut(&wgpu::Adapter) -> usize {
match pref {
GPUPowerPreference::LowPower => {
|adapter: &wgpu::Adapter| match adapter.get_info().device_type {
wgpu::DeviceType::IntegratedGpu => 0,
wgpu::DeviceType::DiscreteGpu => 1,
wgpu::DeviceType::Other => 2,
wgpu::DeviceType::VirtualGpu => 3,
wgpu::DeviceType::Cpu => 4,
}
}
GPUPowerPreference::HighPerformance => {
|adapter: &wgpu::Adapter| match adapter.get_info().device_type {
wgpu::DeviceType::DiscreteGpu => 0,
wgpu::DeviceType::IntegratedGpu => 1,
wgpu::DeviceType::Other => 2,
wgpu::DeviceType::VirtualGpu => 3,
wgpu::DeviceType::Cpu => 4,
}
}
}
}
fn create_surface_config(
adapter: &Adapter,
surface: &Surface,
initial_surface_size: Vector2F,
) -> Option<SurfaceConfiguration> {
let mut config = surface.get_default_config(
adapter,
initial_surface_size.x() as u32,
initial_surface_size.y() as u32,
)?;
// Make sure we're not using an sRGB format.
config.format = config.format.remove_srgb_suffix();
let caps = surface.get_capabilities(adapter);
// COPY_SRC is only needed to support integration test frame capture via
// request_frame_capture. It is not required for normal rendering.
#[cfg(feature = "integration_tests")]
if caps.usages.contains(wgpu::TextureUsages::COPY_SRC) {
config.usage |= wgpu::TextureUsages::COPY_SRC;
}
// Use a non-vsync presentation mode for reduced input delay. This could
// cause visual tearing on present, but we're ok with paying that cost to
// improve responsiveness.
config.present_mode = PresentMode::AutoNoVsync;
// Explicitly request a non-opaque alpha compositing mode, if available.
// Without this, transparent surfaces don't work on native Wayland.
if caps
.alpha_modes
.contains(&CompositeAlphaMode::PostMultiplied)
&& adapter.get_info().backend != wgpu::Backend::Dx12
{
config.alpha_mode = CompositeAlphaMode::PostMultiplied;
} else if caps
.alpha_modes
.contains(&CompositeAlphaMode::PreMultiplied)
{
config.alpha_mode = CompositeAlphaMode::PreMultiplied;
} else if caps.alpha_modes.contains(&CompositeAlphaMode::Inherit) {
config.alpha_mode = CompositeAlphaMode::Inherit;
} else {
config.alpha_mode = CompositeAlphaMode::Auto;
}
Some(config)
}
#[derive(Error, Debug)]
pub enum GetSurfaceTextureError {
#[error("Timeout while getting next surface texture")]
Timeout,
#[error("Window is occluded and cannot be presented to")]
Occluded,
#[error("Surface configuration outdated")]
Outdated,
#[error("Device lost")]
Lost,
#[error("Validation error")]
Validation,
#[error("Failed to configure surface")]
ConfigurationError(SurfaceConfigureError),
}
fn get_surface_texture(
surface: &Surface<'_>,
) -> Result<wgpu::SurfaceTexture, GetSurfaceTextureError> {
let error = match surface.get_current_texture() {
CurrentSurfaceTexture::Success(texture) | CurrentSurfaceTexture::Suboptimal(texture) => {
return Ok(texture)
}
CurrentSurfaceTexture::Timeout => GetSurfaceTextureError::Timeout,
CurrentSurfaceTexture::Occluded => GetSurfaceTextureError::Occluded,
CurrentSurfaceTexture::Outdated => GetSurfaceTextureError::Outdated,
CurrentSurfaceTexture::Lost => GetSurfaceTextureError::Lost,
CurrentSurfaceTexture::Validation => GetSurfaceTextureError::Validation,
};
Err(error)
}
/// Represents an error that occurred when configuring a surface.
#[derive(Error, Debug)]
pub enum SurfaceConfigureError {
#[error("Failed to configure surface: {source:#}\n\nDesired configuration: {config:#?}")]
Error {
/// The underlying error.
#[source]
source: wgpu::Error,
/// The desired configuration.
config: SurfaceConfiguration,
},
}
/// Configures the provided surface.
async fn configure_surface(
surface: &Surface<'_>,
device: &Device,
surface_config: &SurfaceConfiguration,
) -> Result<(), SurfaceConfigureError> {
let error_scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
surface.configure(device, surface_config);
match error_scope.pop().await {
Some(err) => Err(SurfaceConfigureError::Error {
source: err,
config: surface_config.clone(),
}),
None => Ok(()),
}
}
#[cfg(test)]
#[path = "resources_tests.rs"]
mod tests;
@@ -0,0 +1,61 @@
use wgpu::{
util::{BufferInitDescriptor, DeviceExt},
Buffer, RenderPass,
};
use crate::rendering::wgpu::shader_types;
/// The vertex buffer slot used for quad vertex data.
const VERTEX_BUFFER_SLOT: u32 = 0;
/// Ordered list of indices in the [`VERTICES`] array to be used as part of an index buffer.
pub(in crate::rendering::wgpu) const INDICES: &[u16] = &[0, 1, 2, 2, 3, 1];
/// List of vertex positions in normalized device coordinates (NDC) that are used when rendering.
/// Similar to our metal renderer, we hardcode a list of vertices for each rect we render, and then
/// determine the actual position of the rect in NDC within the vertex shader.
const VERTICES: &[shader_types::Vertex] = &[
shader_types::Vertex {
position: shader_types::vec2f(0.0, 0.0),
},
shader_types::Vertex {
position: shader_types::vec2f(1.0, 0.0),
},
shader_types::Vertex {
position: shader_types::vec2f(0.0, 1.0),
},
shader_types::Vertex {
position: shader_types::vec2f(1.0, 1.0),
},
];
pub(super) struct Resources {
index_buffer: Buffer,
vertex_buffer: Buffer,
}
impl Resources {
pub fn new(device: &wgpu::Device) -> Self {
let index_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("Quad Index Buffer"),
contents: bytemuck::cast_slice(INDICES),
usage: wgpu::BufferUsages::INDEX,
});
let vertex_buffer = device.create_buffer_init(&BufferInitDescriptor {
label: Some("Quad Vertex Buffer"),
contents: bytemuck::cast_slice(VERTICES),
usage: wgpu::BufferUsages::VERTEX,
});
Self {
index_buffer,
vertex_buffer,
}
}
pub fn configure_render_pass<'a>(&'a self, render_pass: &mut RenderPass<'a>) {
render_pass.set_vertex_buffer(VERTEX_BUFFER_SLOT, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
}
}
@@ -0,0 +1,71 @@
use std::mem;
use pathfinder_geometry::vector::Vector2F;
use wgpu::{BindGroup, BindGroupLayout, Buffer};
use crate::rendering::wgpu::{shader_types, Resources};
pub(super) struct Uniforms {
bind_group_layout: BindGroupLayout,
bind_group: BindGroup,
buffer: Buffer,
}
impl Uniforms {
pub fn new(device: &wgpu::Device) -> Self {
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Quad Uniforms Bind Group Layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
mem::size_of::<shader_types::Uniforms>() as wgpu::BufferAddress,
),
},
count: None,
}],
});
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Uniforms buffer"),
size: mem::size_of::<shader_types::Uniforms>() as wgpu::BufferAddress,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Uniforms Bind Group"),
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
});
Self {
bind_group_layout,
bind_group,
buffer,
}
}
pub fn bind_group_layout(&self) -> &BindGroupLayout {
&self.bind_group_layout
}
pub fn configure_render_pass<'a>(
&'a self,
render_pass: &mut wgpu::RenderPass<'a>,
drawable_size: Vector2F,
resources: &Resources,
) {
let uniforms = shader_types::Uniforms::new(drawable_size);
resources
.queue
.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[uniforms]));
render_pass.set_bind_group(0, &self.bind_group, &[]);
}
}
@@ -0,0 +1,126 @@
use super::*;
#[test]
fn test_is_unsupported_llvmpipe_adapter() {
let supported_adapter_info = wgpu::AdapterInfo {
name: "llvmpipe (LLVM 17.0.6, 256 bits)".to_owned(),
// not used
vendor: 0,
// not used
device: 0,
device_type: wgpu::DeviceType::Cpu,
driver: "llvmpipe".to_owned(),
driver_info: "Mesa 24.0.2-arch1.2 (LLVM 17.0.6)".to_owned(),
backend: wgpu::Backend::Vulkan,
device_pci_bus_id: "01:00.0".to_owned(),
subgroup_min_size: wgpu::MINIMUM_SUBGROUP_MIN_SIZE,
subgroup_max_size: wgpu::MAXIMUM_SUBGROUP_MAX_SIZE,
transient_saves_memory: false,
};
assert!(!is_older_lavapipe_adapter(&supported_adapter_info));
let unsupported_adapter_info = wgpu::AdapterInfo {
name: "llvmpipe (LLVM 17.0.6, 256 bits)".to_owned(),
// not used
vendor: 0,
// not used
device: 0,
device_type: wgpu::DeviceType::Cpu,
driver: "llvmpipe".to_owned(),
driver_info: "Mesa 23.2.1-1ubuntu3.1~22.04.2 (LLVM 15.0.7)".to_owned(),
backend: wgpu::Backend::Vulkan,
device_pci_bus_id: "01:00.0".to_owned(),
subgroup_min_size: wgpu::MINIMUM_SUBGROUP_MIN_SIZE,
subgroup_max_size: wgpu::MAXIMUM_SUBGROUP_MAX_SIZE,
transient_saves_memory: false,
};
assert!(is_older_lavapipe_adapter(&unsupported_adapter_info));
}
#[test]
fn test_is_unsupported_intel_uhd_adapter() {
assert!(is_older_vulkan_intel_uhd_adapter(&wgpu::AdapterInfo {
name: String::from("Intel(R) HD Graphics 620 (KBL GT2)"),
vendor: 0,
device: 0,
device_type: wgpu::DeviceType::IntegratedGpu,
driver: String::from("Intel open-source Mesa driver"),
driver_info: String::from("Mesa 21.2.6"),
backend: wgpu::Backend::Vulkan,
device_pci_bus_id: "01:00.0".to_owned(),
subgroup_min_size: wgpu::MINIMUM_SUBGROUP_MIN_SIZE,
subgroup_max_size: wgpu::MAXIMUM_SUBGROUP_MAX_SIZE,
transient_saves_memory: false,
}));
assert!(!is_older_vulkan_intel_uhd_adapter(&wgpu::AdapterInfo {
name: String::from("Intel(R) HD Graphics 620 (KBL GT2)"),
vendor: 0,
device: 0,
device_type: wgpu::DeviceType::IntegratedGpu,
driver: String::from("Intel open-source Mesa driver"),
// Version is recent enough
driver_info: String::from("Mesa 23.2.6"),
backend: wgpu::Backend::Vulkan,
device_pci_bus_id: "01:00.0".to_owned(),
subgroup_min_size: wgpu::MINIMUM_SUBGROUP_MIN_SIZE,
subgroup_max_size: wgpu::MAXIMUM_SUBGROUP_MAX_SIZE,
transient_saves_memory: false,
}));
assert!(!is_older_vulkan_intel_uhd_adapter(&wgpu::AdapterInfo {
name: String::from("Intel(R) HD Graphics 620 (KBL GT2)"),
vendor: 0,
device: 0,
device_type: wgpu::DeviceType::IntegratedGpu,
driver: String::from("Intel open-source Mesa driver"),
// Info string is messed up
driver_info: String::from("Mssa 21.2.6"),
backend: wgpu::Backend::Vulkan,
device_pci_bus_id: "01:00.0".to_owned(),
subgroup_min_size: wgpu::MINIMUM_SUBGROUP_MIN_SIZE,
subgroup_max_size: wgpu::MAXIMUM_SUBGROUP_MAX_SIZE,
transient_saves_memory: false,
}));
assert!(is_older_vulkan_intel_uhd_adapter(&wgpu::AdapterInfo {
name: String::from("Intel(R) HD Graphics 620 (KBL GT2)"),
vendor: 0,
device: 0,
device_type: wgpu::DeviceType::IntegratedGpu,
driver: String::from("Intel open-source Mesa driver"),
// Additional info should be ignored
driver_info: String::from("Mesa 21.2.6 foo bar"),
backend: wgpu::Backend::Vulkan,
device_pci_bus_id: "01:00.0".to_owned(),
subgroup_min_size: wgpu::MINIMUM_SUBGROUP_MIN_SIZE,
subgroup_max_size: wgpu::MAXIMUM_SUBGROUP_MAX_SIZE,
transient_saves_memory: false,
}));
assert!(!is_older_vulkan_intel_uhd_adapter(&wgpu::AdapterInfo {
name: String::from("Intel(R) HD Graphics 620 (KBL GT2)"),
vendor: 0,
device: 0,
device_type: wgpu::DeviceType::IntegratedGpu,
driver: String::from("Intel open-source Mesa driver"),
// No version number
driver_info: String::from("Mesa"),
backend: wgpu::Backend::Vulkan,
device_pci_bus_id: "01:00.0".to_owned(),
subgroup_min_size: wgpu::MINIMUM_SUBGROUP_MIN_SIZE,
subgroup_max_size: wgpu::MAXIMUM_SUBGROUP_MAX_SIZE,
transient_saves_memory: false,
}));
assert!(is_older_vulkan_intel_uhd_adapter(&wgpu::AdapterInfo {
name: String::from("Intel(R) HD Graphics 620 (KBL GT2)"),
vendor: 0,
device: 0,
device_type: wgpu::DeviceType::IntegratedGpu,
driver: String::from("Intel open-source Mesa driver"),
// Nonsense version string
driver_info: String::from("Mesa wtfis&this"),
backend: wgpu::Backend::Vulkan,
device_pci_bus_id: "01:00.0".to_owned(),
subgroup_min_size: wgpu::MINIMUM_SUBGROUP_MIN_SIZE,
subgroup_max_size: wgpu::MAXIMUM_SUBGROUP_MAX_SIZE,
transient_saves_memory: false,
}));
}
@@ -0,0 +1,234 @@
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct ColorF {
r: f32,
g: f32,
b: f32,
a: f32,
}
impl From<ColorU> for ColorF {
fn from(coloru: ColorU) -> Self {
coloru.to_f32().into()
}
}
impl From<pathfinder_color::ColorF> for ColorF {
fn from(color: pathfinder_color::ColorF) -> Self {
Self {
r: color.r(),
g: color.g(),
b: color.b(),
a: color.a(),
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct Vector2F {
x: f32,
y: f32,
}
pub(super) const fn vec2f(x: f32, y: f32) -> Vector2F {
Vector2F { x, y }
}
impl From<crate::geometry::vector::Vector2F> for Vector2F {
fn from(vec2f: pathfinder_geometry::vector::Vector2F) -> Self {
Self {
x: vec2f.x(),
y: vec2f.y(),
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct Vector4F {
x: f32,
y: f32,
z: f32,
w: f32,
}
pub(super) const fn vec4f(x: f32, y: f32, z: f32, w: f32) -> Vector4F {
Vector4F { x, y, z, w }
}
impl From<pathfinder_geometry::vector::Vector4F> for Vector4F {
fn from(vec4f: pathfinder_geometry::vector::Vector4F) -> Self {
Self {
x: vec4f.x(),
y: vec4f.y(),
z: vec4f.z(),
w: vec4f.w(),
}
}
}
impl From<pathfinder_geometry::rect::RectF> for Vector4F {
fn from(rectf: pathfinder_geometry::rect::RectF) -> Self {
Self {
x: rectf.origin_x(),
y: rectf.origin_y(),
z: rectf.width(),
w: rectf.height(),
}
}
}
/// Vertex position in normalized device coordinates (NDC). We don't need to manage padding of
/// this struct to ensure it is a power of two--WGPU does this for us via the call to
/// `create_buffer_init`.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct Vertex {
pub(super) position: Vector2F,
}
impl Vertex {
const ATTRIBS: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x2];
pub(super) fn desc() -> wgpu::VertexBufferLayout<'static> {
use std::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &Self::ATTRIBS,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct Color {
/// The start location of the background in the range [0,1].
pub(super) start: Vector2F,
pub(super) start_color: ColorF,
/// The end location of the background in the range [0,1].
pub(super) end: Vector2F,
pub(super) end_color: ColorF,
}
#[derive(Default)]
pub(super) struct BorderWidth {
pub(super) top: f32,
pub(super) right: f32,
pub(super) bottom: f32,
pub(super) left: f32,
}
/// Data for a rect that is stored per instance. We don't need to manage padding of
/// this struct to ensure it is a power of two--WGPU does this for us via the call to
/// `create_buffer_init`.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct RectData {
bounds: Vector4F,
background_color: Color,
border_width: Vector4F,
border_color: Color,
corner_radius: Vector4F,
/// The amount of blurring for the shadow, i.e. higher value means more spread out. "Sigma"
/// refers to the term in the formula of the Gaussian distribution, which is used in computing
/// the shadow's shading.
drop_shadow_sigma: f32,
/// The shadow usually spans a larger size than its corresponding rect. This value determines
/// that additional distance in px along each direction.
drop_shadow_padding_factor: f32,
dash_length: f32,
gap_lengths: Vector2F,
}
impl RectData {
const ATTRIBS: [wgpu::VertexAttribute; 13] = wgpu::vertex_attr_array![
// Start at location 1 here because the vertex location occupies location 0.
1 => Float32x4, // Bounds
2 => Float32x2, // Background Start
3 => Float32x4, // Background Start Color
4 => Float32x2, // Background End
5 => Float32x4, // Background End Color
6 => Float32x4, // Border
7 => Float32x2, // Border Start
8 => Float32x4, // Border Start Color
9 => Float32x2, // Border End
10 => Float32x4, // Border End Color
11 => Float32x4, // Corner radius
12 => Float32x2, // Drop Shadow Sigma (Blur Radius) and Padding Factor (Spread Radius)
13 => Float32x3, // Dashed border data: dash length and gap length for x and y dimension
];
#[allow(clippy::too_many_arguments)]
pub fn new(
bounds: RectF,
background_color: Color,
border_color: Color,
corner_radius: crate::rendering::CornerRadius,
border_width: BorderWidth,
drop_shadow_sigma: f32,
drop_shadow_padding_factor: f32,
dash_length: f32,
gap_lengths: pathfinder_geometry::vector::Vector2F,
) -> Self {
Self {
bounds: bounds.into(),
background_color,
border_width: vec4f(
border_width.top,
border_width.right,
border_width.bottom,
border_width.left,
),
border_color,
corner_radius: vec4f(
corner_radius.top_left,
corner_radius.top_right,
corner_radius.bottom_left,
corner_radius.bottom_right,
),
drop_shadow_sigma,
drop_shadow_padding_factor,
dash_length,
gap_lengths: gap_lengths.into(),
}
}
pub(super) fn desc() -> wgpu::VertexBufferLayout<'static> {
use std::mem;
wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Self>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &Self::ATTRIBS,
}
}
}
// Uniform buffer objects need to be 16-byte aligned in WGSL, so enforce
// that constraint here.
//
// See: https://www.w3.org/TR/WGSL/#address-space-layout-constraints
#[repr(C, align(16))]
#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
pub(super) struct Uniforms {
viewport_size: Vector2F,
// The shader-side paired struct will automatically be padded as necessary,
// so we add any necessary padding bytes here by adjusting the size of this
// byte array.
_struct_padding_bytes: [u8; 8],
}
impl Uniforms {
pub(super) fn new(size: pathfinder_geometry::vector::Vector2F) -> Self {
Self {
viewport_size: size.into(),
_struct_padding_bytes: Default::default(),
}
}
}
@@ -0,0 +1,123 @@
// Brightness-scaled contrast enhancement for glyph alpha masks.
//
// Linear sRGB blending makes light-on-dark text appear too thin because AA fringe
// pixels blend perceptually darker than expected. Dark-on-light text has the opposite
// problem — it already looks heavier than its geometric coverage.
//
// To compensate, we compute the text color's brightness (k) and use it to boost the
// glyph alpha through enhance_contrast(). Brighter text gets a stronger boost;
// dark text is left unchanged.
//
// enhance_contrast() adapted from DWrite_EnhanceContrast in Windows Terminal's DirectWrite shader:
// https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.hlsl
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
fn glyph_color_brightness(color: vec3<f32>) -> f32 {
// REC. 601 luminance coefficients for perceived brightness.
return dot(color, vec3<f32>(0.30, 0.59, 0.11));
}
fn enhance_contrast(alpha: f32, k: f32) -> f32 {
return alpha * (k + 1.0) / (alpha * k + 1.0);
}
struct Uniforms {
viewport_size: vec2<f32>,
// Padding necessary to ensure that the uniforms is 16 bytes. Some wgpu-supported devices (such as webgl) require
// buffer bindings to be a multiple of 16 bytes.
padding: vec2<f32>
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(1) @binding(0) var glyphAtlasTexture: texture_2d<f32>;
@group(1) @binding(1) var glyphAtlasSampler: sampler;
struct GlyphVertexShaderInput {
// The position of the vertex in normalized device coordinates.
@location(0) vertex_position: vec2<f32>,
@location(1) bounds: vec4<f32>,
@location(2) uv_bounds: vec4<f32>,
@location(3) fade_start: f32,
@location(4) fade_end: f32,
@location(5) color: vec4<f32>,
@location(6) is_emoji: i32,
}
struct GlyphVertexShaderOutput {
@builtin(position) position: vec4<f32>,
@location(0) rect_center: vec2<f32>,
@location(1) rect_corner: vec2<f32>,
@location(2) texture_coordinate: vec2<f32>,
@location(3) fade_alpha: f32,
@location(4) color: vec4<f32>,
@location(5) is_emoji: i32,
}
@vertex
fn vs_main(
glyph: GlyphVertexShaderInput,
) -> GlyphVertexShaderOutput {
var out: GlyphVertexShaderOutput;
var origin: vec2<f32> = glyph.bounds.xy;
var size: vec2<f32> = glyph.bounds.zw;
var pixel_pos: vec2<f32> = glyph.vertex_position * size + origin;
// Use floor here to vertically align the glyph to the pixel grid.
// If it's not aligned to the grid, the fragment shader will do its
// own interpolation, which makes it so we don't use the anti-aliasing
// from core text, which is what we want. We don't force the glyph to a
// horizontal pixel position because we rasterize the glyph at multiple
// subpixel positions, and so the very slight linear interpolation here
// won't produce a fuzzy glyph, just a correctly-positioned one.
pixel_pos = vec2(pixel_pos.x, floor(pixel_pos.y));
// Evaluating the glyphs fade effect. Note that the fade may go in two different directions:
// - Right to left (default) - where the opaque side is on the right, and transparent on the left
// (in this case, the start_fade < end_fade; start is where the fade is transparent)
// - Left to right - where the opaque side is on the left, and it fades towards the right side.
// In this case, start_fade > end_fade, and the opaque side is on the left (end_fade).
// To clarify: fade_start is ALWAYS where the fade is transparent, and fade_end is ALWAYS where
// the opaque part is, this is reflected in how we compute width, dist, and alpha.
var fade_width: f32 = abs(glyph.fade_end - glyph.fade_start);
var fade_dist: f32 = pixel_pos.x - min(glyph.fade_start, glyph.fade_end);
var fade_alpha: f32;
if glyph.fade_end < glyph.fade_start { // left-to-right case
fade_alpha = fade_dist / fade_width;
} else { // right-to-left case
fade_alpha = 1. - fade_dist / fade_width;
}
// Convert the position of the item from screen coordinates into normalized device coordinates
var device_pos: vec2<f32> = pixel_pos / uniforms.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0);
var texture_coordinate: vec2<f32> = glyph.uv_bounds.xy + glyph.vertex_position * glyph.uv_bounds.zw;
out.position = vec4<f32>(device_pos, 0.0, 1.0);
out.rect_corner = size / 2.0;
out.rect_center = origin + out.rect_corner;
out.texture_coordinate = texture_coordinate;
out.fade_alpha = fade_alpha;
out.color = glyph.color;
out.is_emoji = glyph.is_emoji;
return out;
}
@fragment
fn fs_main(in: GlyphVertexShaderOutput) -> @location(0) vec4<f32> {
// Sample the texture to obtain a color.
var tex_color: vec4<f32> = textureSample(glyphAtlasTexture, glyphAtlasSampler, in.texture_coordinate);
// Use the input color for non-emoji, and the sampled color for emoji.
var color: vec4<f32> = mix(in.color, tex_color, f32(in.is_emoji));
// Scale contrast boost by text brightness:
// light text (white=1) gets full boost; dark text (black=0) gets none.
let k = glyph_color_brightness(color.rgb);
let contrasted = enhance_contrast(tex_color.r, k);
color.a *= max(contrasted, f32(in.is_emoji));
// Apply the fade.
color.a *= saturate(in.fade_alpha);
return color;
}
@@ -0,0 +1,118 @@
struct Uniforms {
viewport_size: vec2<f32>,
// Padding necessary to ensure that the uniforms is 16 bytes. Some wgpu-supported devices (such as webgl) require
// buffer bindings to be a multiple of 16 bytes.
padding: vec2<f32>
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(1) @binding(0) var imageTexture: texture_2d<f32>;
@group(1) @binding(1) var imageSampler: sampler;
struct ImageVertexShaderInput {
// The position of the vertex in normalized device coordinates.
@location(0) vertex_position: vec2<f32>,
@location(1) bounds: vec4<f32>,
@location(2) color: vec4<f32>,
// This field is treated as a boolean to indicate how to interpret the preceding `color` field.
// Icons allow overriding their foreground color, so for icons the whole `color` struct is used.
// For images, only the opacity can be set, and so only the alpha channel would be used.
@location(3) is_icon: u32,
// Corner radius in the order top_left, top_right, bottom_left, bottom_right.
@location(4) corner_radius: vec4<f32>,
}
struct ImageVertexShaderOutput {
@builtin(position) position: vec4<f32>,
@location(0) rect_center: vec2<f32>,
@location(1) rect_corner: vec2<f32>,
@location(2) texture_coordinate: vec2<f32>,
@location(4) color: vec4<f32>,
@location(5) is_icon: u32,
@location(6) corner_radius: vec4<f32>,
}
@vertex
fn vs_main(
image: ImageVertexShaderInput,
) -> ImageVertexShaderOutput {
var out: ImageVertexShaderOutput;
var origin: vec2<f32> = image.bounds.xy;
var size: vec2<f32> = image.bounds.zw;
var pixel_pos: vec2<f32> = image.vertex_position * size + origin;
// Convert the position of the item from screen coordinates into normalized device coordinates
var device_pos: vec2<f32> = pixel_pos / uniforms.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0);
out.position = vec4<f32>(device_pos, 0.0, 1.0);
// Re-compute size and origin such that they are clipped by the viewport bounds.
var clipped_origin = max(origin, vec2f(0.0, 0.0));
var clipped_size = max(min(origin + size, uniforms.viewport_size) - clipped_origin, vec2f(0.0, 0.0));
out.rect_corner = clipped_size / 2.0;
out.rect_center = clipped_origin + out.rect_corner;
out.texture_coordinate = image.vertex_position;
out.color = image.color;
out.is_icon = image.is_icon;
out.corner_radius = image.corner_radius;
return out;
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, corner_radius: f32) -> f32 {
var p: vec2<f32> = pixel_pos - rect_center;
var q: vec2<f32> = abs(p) - rect_corner + corner_radius;
return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - corner_radius;
}
@fragment
fn fs_main(in: ImageVertexShaderOutput) -> @location(0) vec4<f32> {
// Sample the texture to obtain a color.
var color_sample: vec4<f32> = textureSample(imageTexture, imageSampler, in.texture_coordinate);
var color: vec4<f32>;
if in.is_icon == 0u {
// For an image, use the image color and just adjust opacity.
color = color_sample;
color.a *= in.color.a;
} else {
// There's a naga bug with wgsl --> hlsl conversion where images are always rendered as red.
// We workaround this by first creating an intermediate color where the alpha channel is actually the
// red channel from `color_sample` and then multiplying that by the desired opacity.
var new_color: vec4<f32> = vec4(color_sample.r, color_sample.g, color_sample.b, color_sample.r);
new_color.a *= in.color.a;
// For an icon, use the specified input color.
color = vec4(in.color.r, in.color.g, in.color.b, new_color.a);
}
var outer_corner_radius: f32;
if in.position.y >= in.rect_center.y {
// Bottom half
if in.position.x >= in.rect_center.x {
// Bottom right quadrant
outer_corner_radius = in.corner_radius.w;
} else {
// Bottom left quadrant
outer_corner_radius = in.corner_radius.z;
}
} else {
// Top half
if in.position.x >= in.rect_center.x {
// Top right quadrant
outer_corner_radius = in.corner_radius.y;
} else {
// Top left quadrant
outer_corner_radius = in.corner_radius.x;
}
}
var outer_distance: f32 = distance_from_rect(in.position.xy, in.rect_center, in.rect_corner, outer_corner_radius);
// If there's a corner radius we need to do some anti aliasing to smooth out the rounded corner effect.
if outer_corner_radius > 0. {
color.a *= 1.0 - saturate(outer_distance + 0.5);
}
return color;
}
@@ -0,0 +1,291 @@
struct Uniforms {
viewport_size: vec2<f32>,
// Padding necessary to ensure that the uniforms is 16 bytes. Some wgpu-supported devices (such as webgl) require
// buffer bindings to be a multiple of 16 bytes.
padding: vec2<f32>
}
const EPSILON: f32 = 0.0000001;
const PI: f32 = 3.141592653589793;
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct RectVertexShaderInput {
// The position of the vertex in normalized device coordinates.
@location(0) vertex_position: vec2<f32>,
// Bounds of the item in screen coordinates. Origin is contained in `xy`, size is contained in `zw`.
@location(1) bounds: vec4<f32>,
@location(2) background_start: vec2<f32>,
@location(3) background_start_color: vec4<f32>,
@location(4) background_end: vec2<f32>,
@location(5) background_end_color: vec4<f32>,
// Width of the border in the order top, left, right, bottom.
@location(6) border_width: vec4<f32>,
@location(7) border_start: vec2<f32>,
@location(8) border_start_color: vec4<f32>,
@location(9) border_end: vec2<f32>,
@location(10) border_end_color: vec4<f32>,
// Corner radius in the order top_left, top_right, bottom_left, bottom_right.
@location(11) corner_radius: vec4<f32>,
// The sigma and padding factor values packed into a single vec2. We pack them together in order
// to reduce the total number of attributes, which maxes out at 16. See here:
// https://docs.rs/wgpu/latest/wgpu/struct.Limits.html#structfield.max_vertex_attributes
@location(12) drop_shadow_data: vec2<f32>,
// The length of the dash and the gaps for the x and y dimensions, packed into a single vec3.
@location(13) dashed_border_data: vec3<f32>,
};
struct RectVertexShaderOutput {
@builtin(position) position: vec4<f32>,
@location(0) background_start: vec2<f32>,
@location(1) background_start_color: vec4<f32>,
@location(2) background_end: vec2<f32>,
@location(3) background_end_color: vec4<f32>,
@location(4) border_width: vec4<f32>,
@location(5) border_start: vec2<f32>,
@location(6) border_start_color: vec4<f32>,
@location(7) border_end: vec2<f32>,
@location(8) border_end_color: vec4<f32>,
@location(9) rect_corner: vec2<f32>,
@location(10) rect_center: vec2<f32>,
@location(11) corner_radius: vec4<f32>,
@location(12) drop_shadow_data: vec2<f32>,
@location(13) dashed_border_data: vec3<f32>,
};
@vertex
fn vs_main(
in: RectVertexShaderInput,
) -> RectVertexShaderOutput {
var out: RectVertexShaderOutput;
var origin: vec2<f32> = in.bounds.xy;
var size: vec2<f32> = in.bounds.zw;
var pixel_pos: vec2<f32> = in.vertex_position * size + origin;
// Convert the position of the item from screen coordinates into normalized device coordinates
var ndc_position: vec2<f32> = pixel_pos / uniforms.viewport_size * vec2(2.0, -2.0) + vec2(-1.0, 1.0);
out.position = vec4<f32>(ndc_position, 0.0, 1.0);
out.background_start = in.background_start * size + origin;
out.background_start_color = in.background_start_color;
out.background_end = in.background_end * size + origin;
out.background_end_color = in.background_end_color;
out.border_start = in.border_start * size + origin;
out.border_start_color = in.border_start_color;
out.border_end = in.border_end * size + origin;
out.border_end_color = in.border_end_color;
out.border_width = in.border_width;
out.corner_radius = in.corner_radius;
out.rect_corner = size / 2.;
out.rect_center = origin + out.rect_corner;
out.drop_shadow_data = in.drop_shadow_data;
out.dashed_border_data = in.dashed_border_data;
return out;
}
@fragment
fn rect_fs_main(in: RectVertexShaderOutput) -> @location(0) vec4<f32> {
var background_color: vec4<f32> = derive_color(
in.position.xy,
in.background_start,
in.background_end,
in.background_start_color,
in.background_end_color
);
var border_color: vec4<f32> = derive_color(
in.position.xy,
in.border_start,
in.border_end,
in.border_start_color,
in.border_end_color
);
// There are actually two different radii at play here - the inner
// (background) and outer (shape) radii. The inner radius is equal to the
// outer radius minus the border width, in order for the two curves to
// maintain a constant distance from each other.
var inner_corner_radius: f32;
var outer_corner_radius: f32;
var border_inner_corner: vec2<f32> = in.rect_corner;
if in.position.y >= in.rect_center.y {
// Bottom half
border_inner_corner.y -= in.border_width.z;
if in.position.x >= in.rect_center.x {
// Bottom right quadrant
border_inner_corner.x -= in.border_width.y;
outer_corner_radius = in.corner_radius.w;
inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.z);
} else {
// Bottom left quadrant
border_inner_corner.x -= in.border_width.w;
outer_corner_radius = in.corner_radius.z;
inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.z);
}
} else {
// Top half
border_inner_corner.y -= in.border_width.x;
if in.position.x >= in.rect_center.x {
// Top right quadrant
border_inner_corner.x -= in.border_width.y;
outer_corner_radius = in.corner_radius.y;
inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.x);
} else {
// Top left quadrant
border_inner_corner.x -= in.border_width.w;
outer_corner_radius = in.corner_radius.x;
inner_corner_radius = max(0.0, outer_corner_radius - in.border_width.x);
}
}
var rect_origin: vec2<f32> = in.rect_center - in.rect_corner;
var outer_distance: f32 = distance_from_rect(in.position.xy, in.rect_center, in.rect_corner, outer_corner_radius);
var inner_distance: f32 = distance_from_rect(in.position.xy, in.rect_center, border_inner_corner, inner_corner_radius);
var drop_shadow_sigma = in.drop_shadow_data.x;
var drop_shadow_padding_factor = in.drop_shadow_data.y;
if drop_shadow_sigma > 0.0 {
var rect_size: vec2<f32> = in.rect_corner * 2.0;
// When we are rendering a drop shadow we need to pass in the positions
// of the original rect, so we figure them out from the padding.
// Note we subtract twice the padding, because the padding is specified
// in terms of padding on a single side.
var shadowed_rect_origin: vec2<f32> = rect_origin + drop_shadow_padding_factor;
var shadowed_rect_size: vec2<f32> = rect_size - 2.0 * drop_shadow_padding_factor;
background_color.a *= rounded_box_shadow(
shadowed_rect_origin,
shadowed_rect_origin + shadowed_rect_size,
in.position.xy,
drop_shadow_sigma,
outer_corner_radius
);
} else {
// Adjust the opacity of the border color based on where the pixel lies
// between the background and the border_width.
border_color.a *= saturate(inner_distance + 0.5);
// Force the alpha value to 0 (fully transparent) if the pixel is
// outside the border_width.
//
// When we are outside the border, outer_distance is a larger positive
// value than inner_distance. When we are inside the border itself,
// outer_distance is negative and inner_distance is positive. When we
// are inside the inner border edge, outer_distance is more negative
// than inner_distance.
border_color.a *= f32(inner_distance > outer_distance);
var rect_bottom_right = in.rect_center + in.rect_corner;
var pos_from_origin = in.position.xy - rect_origin;
// Masks for pixels outside of inner rectangle or on border
var is_horizontal_border = (in.position.y <= rect_origin.y + in.border_width.x) || (in.position.y >= rect_bottom_right.y - in.border_width.z);
var is_vertical_border = (in.position.x <= rect_origin.x + in.border_width.w) || (in.position.x >= rect_bottom_right.x - in.border_width.y);
var dash_length = in.dashed_border_data.x;
var gap_lengths = in.dashed_border_data.yz;
// Get length along the dash and gap segment and determine if pixel is in dash or gap
var length_on_dash_and_gap_segment_x = pos_from_origin.x % (dash_length + gap_lengths.x);
var length_on_dash_and_gap_segment_y = pos_from_origin.y % (dash_length + gap_lengths.y);
var is_horizontal_dash = is_horizontal_border && (length_on_dash_and_gap_segment_x < dash_length);
var is_vertical_dash = is_vertical_border && (length_on_dash_and_gap_segment_y < dash_length);
// Mask out any gaps in the border
border_color.a *= f32(dash_length <= 0.0 || is_horizontal_dash || is_vertical_dash);
// Perform proper alpha blending on the two colors, avoiding a
// divide-by-zero if both colors are fully transparent.
//
// See formula for "over" compositing here: https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
var alpha: f32 = border_color.a + background_color.a * (1.0 - border_color.a);
var new_background_color: vec3<f32> = (border_color.rgb * border_color.a + background_color.rgb * background_color.a * (1.0 - border_color.a)) / (alpha + EPSILON);
background_color = vec4(new_background_color, alpha);
}
// If there's a corner radius we need to do some anti aliasing to smooth out the rounded corner effect.
if outer_corner_radius > 0. {
background_color.a *= 1.0 - saturate(outer_distance + 0.5);
}
return background_color;
}
fn derive_color(
position: vec2<f32>,
start: vec2<f32>,
end: vec2<f32>,
start_color: vec4<f32>,
end_color: vec4<f32>
) -> vec4<f32> {
var adjusted_end: vec2<f32> = end - start;
var h: f32 = dot(position - start, adjusted_end) / dot(adjusted_end, adjusted_end);
return mix(start_color, end_color, h);
}
// Based on the fragement position and the center of the quad, select one of the 4 radi.
// Order matches CSS border radius attribute:
// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left
fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 {
var rx = radi.x;
var ry = radi.y;
rx = select(radi.x, radi.y, position.x > center.x);
ry = select(radi.w, radi.z, position.x > center.x);
rx = select(rx, ry, position.y > center.y);
return rx;
}
fn distance_from_rect(pixel_pos: vec2<f32>, rect_center: vec2<f32>, rect_corner: vec2<f32>, corner_radius: f32) -> f32 {
var p: vec2<f32> = pixel_pos - rect_center;
var q: vec2<f32> = abs(p) - rect_corner + corner_radius;
return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - corner_radius;
}
// Drop shadow code *heavily* inspired by this post:
// http://madebyevan.com/shaders/fast-rounded-rectangle-shadows/
// Return the mask for the shadow of a box from lower to upper
fn rounded_box_shadow(lower: vec2<f32>, upper: vec2<f32>, in_point: vec2<f32>, sigma: f32, corner: f32) -> f32 {
// Center everything to make the math easier
var center: vec2<f32> = (lower + upper) * 0.5;
var half_size: vec2<f32> = (upper - lower) * 0.5;
var point = in_point - center;
// The signal is only non-zero in a limited range, so don't waste samples
var low: f32 = point.y - half_size.y;
var high: f32 = point.y + half_size.y;
var start: f32 = clamp(-3.0 * sigma, low, high);
var end: f32 = clamp(3.0 * sigma, low, high);
// Accumulate samples (we can get away with surprisingly few samples)
var step: f32 = (end - start) / 4.0;
var y: f32 = start + step * 0.5;
var value: f32 = 0.0;
for (var i = 0; i < 4; i++) {
value += rounded_box_shadow_x(point.x, point.y - y, sigma, corner, half_size) * gaussian(y, sigma) * step;
y += step;
}
return value;
}
// Return the blurred mask along the x dimension
fn rounded_box_shadow_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
var delta: f32 = min(half_size.y - corner - abs(y), 0.0);
var curved: f32 = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
var integral: vec2<f32> = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma));
return integral.y - integral.x;
}
// This approximates the error function, needed for the gaussian integral
fn erf(x: vec2<f32>) -> vec2<f32> {
var s = sign(x);
var a = abs(x);
var denom = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
denom *= denom;
return s - s / (denom * denom);
}
// A standard gaussian function, used for weighting samples
fn gaussian(x: f32, sigma: f32) -> f32 {
return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * PI) * sigma);
}
@@ -0,0 +1,97 @@
use crate::fonts::RasterizedGlyph;
use crate::rendering::atlas::AllocatedRegion;
use wgpu::{
BindGroup, BindGroupDescriptor, BindGroupLayout, Extent3d, Queue, Sampler,
TexelCopyBufferLayout, Texture, TextureDescriptor, TextureFormat, TextureUsages,
};
/// Helper struct that includes a [`Texture`] and its corresponding [`BindGroup`] for use in the
/// `GlyphCache`.
pub(super) struct TextureWithBindGroup {
texture: Texture,
/// The [`BindGroup`] associated with the `texture`. We compute this whenever we need to create
/// a new texture as a performance optimization to ensure we don't create it on every render.
bind_group: BindGroup,
}
impl TextureWithBindGroup {
pub(super) fn new(
size: usize,
device: &wgpu::Device,
bind_group_layout: &BindGroupLayout,
sampler: &Sampler,
) -> Self {
let texture = device.create_texture(&TextureDescriptor {
label: Some("Glyph atlas texture"),
size: Extent3d {
width: size as u32,
height: size as u32,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&BindGroupDescriptor {
layout: bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
label: None,
});
Self {
texture,
bind_group,
}
}
pub(super) fn insert_glyph_into_texture(
&mut self,
region: AllocatedRegion,
glyph: &RasterizedGlyph,
queue: &Queue,
) {
let bytes_per_row: u32 = 4 * (glyph.canvas.size.x() as u32);
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: region.pixel_region.origin_x() as u32,
y: region.pixel_region.origin_y() as u32,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
glyph.canvas.pixels.as_slice(),
TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bytes_per_row),
rows_per_image: None,
},
Extent3d {
width: region.pixel_region.width() as u32,
height: region.pixel_region.height() as u32,
depth_or_array_layers: 1,
},
);
}
pub(super) fn bind_group(&self) -> &BindGroup {
&self.bind_group
}
}