Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user