Whitespace only; the fork has no own rustfmt.toml so the Oak root policy applies.
2674 lines
80 KiB
Rust
2674 lines
80 KiB
Rust
use crate::{CompositorGpuHint, WgpuAtlas, WgpuContext, WgpuDeviceRequirements};
|
||
use bytemuck::{Pod, Zeroable};
|
||
use gpui::{
|
||
AtlasTextureId, BackdropFilter, Background, Bounds, DevicePixels, FilterBoundary, GpuSpecs,
|
||
MonochromeSprite, PaintSurface, Path, Point, PolychromeSprite, PrimitiveBatch, Quad,
|
||
ScaledFilter, ScaledPixels, Scene, Shadow, Size, SubpixelSprite, Underline,
|
||
get_gamma_correction_ratios,
|
||
};
|
||
use log::warn;
|
||
|
||
/// The largest blur radius in a scene-space filter chain, in device pixels — used to size the
|
||
/// blur kernel and the dilated region the blur passes are scissored to.
|
||
///
|
||
/// The `match` is exhaustive on purpose: adding a [`ScaledFilter`] variant breaks it here,
|
||
/// forcing this backend to handle (or deliberately ignore) the new filter rather than silently
|
||
/// dropping it.
|
||
fn max_blur_radius(filters: &[ScaledFilter]) -> f32 {
|
||
filters.iter().fold(0.0, |acc, filter| match filter {
|
||
ScaledFilter::Blur(radius) => acc.max(radius.0),
|
||
})
|
||
}
|
||
#[cfg(not(target_family = "wasm"))]
|
||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||
use std::cell::RefCell;
|
||
use std::num::NonZeroU64;
|
||
use std::rc::Rc;
|
||
use std::sync::{Arc, Mutex};
|
||
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||
struct GlobalParams {
|
||
viewport_size: [f32; 2],
|
||
premultiplied_alpha: u32,
|
||
pad: u32,
|
||
}
|
||
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Default, Pod, Zeroable)]
|
||
struct PodBounds {
|
||
origin: [f32; 2],
|
||
size: [f32; 2],
|
||
}
|
||
|
||
impl From<Bounds<ScaledPixels>> for PodBounds {
|
||
fn from(bounds: Bounds<ScaledPixels>) -> Self {
|
||
Self {
|
||
origin: [bounds.origin.x.0, bounds.origin.y.0],
|
||
size: [bounds.size.width.0, bounds.size.height.0],
|
||
}
|
||
}
|
||
}
|
||
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||
struct SurfaceParams {
|
||
bounds: PodBounds,
|
||
content_mask: PodBounds,
|
||
}
|
||
|
||
/// Uniform passed to the blur pipelines. The same struct drives the downsample, separable
|
||
/// gaussian, and composite passes; fields not relevant to a given pass are left zero.
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Default, Pod, Zeroable)]
|
||
struct BlurParams {
|
||
/// Composite target rectangle, in device pixels (composite pass only).
|
||
bounds: PodBounds,
|
||
/// Clip rectangle, in device pixels (composite pass only).
|
||
content_mask: PodBounds,
|
||
/// Rounded-corner radii (tl, tr, br, bl), in device pixels (composite pass only).
|
||
corner_radii: [f32; 4],
|
||
/// Per-tap sampling step in UV space (gaussian passes only): (1/width, 0) or (0, 1/height).
|
||
direction: [f32; 2],
|
||
/// Gaussian sigma, in the (half-resolution) blur texture's pixels.
|
||
sigma: f32,
|
||
/// Element opacity, multiplied into the composited result.
|
||
opacity: f32,
|
||
/// Number of taps to each side of center (gaussian passes only).
|
||
tap_count: f32,
|
||
/// Spacing between taps in pixels; >1 lets `tap_count` taps span very large radii without
|
||
/// truncating the gaussian (see #6 in review).
|
||
tap_step: f32,
|
||
/// 1.0 to clip the composite to the rounded rect (backdrop — the panel has a defined shape),
|
||
/// 0.0 to let the blurred result fade out on its own (content `filter` — it bleeds past the
|
||
/// element bounds like CSS, so the fade isn't sharply truncated at the box edge).
|
||
clip_rounded: f32,
|
||
/// 1.0 = snapped 2:1 box downsample (anchor the half-res grid to a fixed 2px grid at the
|
||
/// origin, so a stationary element blurs identically at every window size); 0.0 = 1:1 copy
|
||
/// (the scene blit, which must not downsample). Downsample pass only.
|
||
downsample: f32,
|
||
}
|
||
|
||
#[repr(C)]
|
||
#[derive(Clone, Copy, Pod, Zeroable)]
|
||
struct GammaParams {
|
||
gamma_ratios: [f32; 4],
|
||
grayscale_enhanced_contrast: f32,
|
||
subpixel_enhanced_contrast: f32,
|
||
is_bgr: u32,
|
||
_pad: u32,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
#[repr(C)]
|
||
struct PathSprite {
|
||
bounds: Bounds<ScaledPixels>,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
#[repr(C)]
|
||
struct PathRasterizationVertex {
|
||
xy_position: Point<ScaledPixels>,
|
||
st_position: Point<f32>,
|
||
color: Background,
|
||
bounds: Bounds<ScaledPixels>,
|
||
}
|
||
|
||
pub struct WgpuSurfaceConfig {
|
||
pub size: Size<DevicePixels>,
|
||
pub transparent: bool,
|
||
/// Preferred presentation mode. When `Some`, the renderer will use this
|
||
/// mode if supported by the surface, falling back to `Fifo`.
|
||
/// When `None`, defaults to `Fifo` (VSync).
|
||
///
|
||
/// Mobile platforms may prefer `Mailbox` (triple-buffering) to avoid
|
||
/// blocking in `get_current_texture()` during lifecycle transitions.
|
||
pub preferred_present_mode: Option<wgpu::PresentMode>,
|
||
}
|
||
|
||
struct WgpuPipelines {
|
||
quads: wgpu::RenderPipeline,
|
||
shadows: wgpu::RenderPipeline,
|
||
path_rasterization: wgpu::RenderPipeline,
|
||
paths: wgpu::RenderPipeline,
|
||
underlines: wgpu::RenderPipeline,
|
||
mono_sprites: wgpu::RenderPipeline,
|
||
subpixel_sprites: Option<wgpu::RenderPipeline>,
|
||
poly_sprites: wgpu::RenderPipeline,
|
||
#[allow(dead_code)]
|
||
surfaces: wgpu::RenderPipeline,
|
||
/// Copies a source texture into the (smaller) target with one bilinear tap. Used both to
|
||
/// downsample the scene into the half-resolution blur texture and to blit the offscreen
|
||
/// scene into the swapchain at the end of the frame.
|
||
blur_downsample: wgpu::RenderPipeline,
|
||
/// One axis of a separable gaussian blur; direction is supplied per draw via [`BlurParams`].
|
||
blur: wgpu::RenderPipeline,
|
||
/// Composites a blurred texture into a rounded rectangle (with clip + opacity).
|
||
blur_composite: wgpu::RenderPipeline,
|
||
}
|
||
|
||
struct WgpuBindGroupLayouts {
|
||
globals: wgpu::BindGroupLayout,
|
||
instances: wgpu::BindGroupLayout,
|
||
instances_with_texture: wgpu::BindGroupLayout,
|
||
surfaces: wgpu::BindGroupLayout,
|
||
blur: wgpu::BindGroupLayout,
|
||
}
|
||
|
||
/// Shared GPU context reference, used to coordinate device recovery across multiple windows.
|
||
pub type GpuContext = Rc<RefCell<Option<WgpuContext>>>;
|
||
|
||
/// GPU resources that must be dropped together during device recovery.
|
||
struct WgpuResources {
|
||
device: Arc<wgpu::Device>,
|
||
queue: Arc<wgpu::Queue>,
|
||
surface: wgpu::Surface<'static>,
|
||
pipelines: WgpuPipelines,
|
||
bind_group_layouts: WgpuBindGroupLayouts,
|
||
atlas_sampler: wgpu::Sampler,
|
||
surface_sampler: wgpu::Sampler,
|
||
#[allow(dead_code)]
|
||
surface_uniform_buffer: wgpu::Buffer,
|
||
/// One reused uniform buffer holding [`BlurParams`] for every blur pass in a frame, each at a
|
||
/// distinct (alignment-strided) offset. Avoids allocating a buffer per pass; distinct offsets
|
||
/// mean `write_buffer`'s last-write-at-submit semantics don't clobber earlier passes.
|
||
blur_params_buffer: wgpu::Buffer,
|
||
globals_buffer: wgpu::Buffer,
|
||
globals_bind_group: wgpu::BindGroup,
|
||
path_globals_bind_group: wgpu::BindGroup,
|
||
instance_buffer: wgpu::Buffer,
|
||
path_intermediate_texture: Option<wgpu::Texture>,
|
||
path_intermediate_view: Option<wgpu::TextureView>,
|
||
path_msaa_texture: Option<wgpu::Texture>,
|
||
path_msaa_view: Option<wgpu::TextureView>,
|
||
/// Blur offscreen targets. Allocated lazily (only when a frame actually uses a blur filter)
|
||
/// so apps that never blur pay no extra VRAM. `None`/empty until first use.
|
||
///
|
||
/// Full-resolution offscreen color target the scene is rendered into so that blur passes
|
||
/// can sample already-painted content; blitted to the swapchain at the end of the frame.
|
||
scene_color_texture: Option<wgpu::Texture>,
|
||
scene_color_view: Option<wgpu::TextureView>,
|
||
/// Half-resolution ping/pong targets for the downsample + separable gaussian passes.
|
||
blur_ping_texture: Option<wgpu::Texture>,
|
||
blur_ping_view: Option<wgpu::TextureView>,
|
||
blur_pong_texture: Option<wgpu::Texture>,
|
||
blur_pong_view: Option<wgpu::TextureView>,
|
||
/// Full-resolution offscreen targets a content-filter (`filter`) group renders into before
|
||
/// being blurred and composited back. One per nesting level (indexed by depth) so nested
|
||
/// content blurs isolate correctly, up to [`MAX_FILTER_DEPTH`]; deeper nests render inline.
|
||
group_textures: Vec<wgpu::Texture>,
|
||
group_views: Vec<wgpu::TextureView>,
|
||
}
|
||
|
||
impl WgpuResources {
|
||
fn invalidate_intermediate_textures(&mut self) {
|
||
self.path_intermediate_texture = None;
|
||
self.path_intermediate_view = None;
|
||
self.path_msaa_texture = None;
|
||
self.path_msaa_view = None;
|
||
self.scene_color_texture = None;
|
||
self.scene_color_view = None;
|
||
self.blur_ping_texture = None;
|
||
self.blur_ping_view = None;
|
||
self.blur_pong_texture = None;
|
||
self.blur_pong_view = None;
|
||
self.group_textures.clear();
|
||
self.group_views.clear();
|
||
}
|
||
}
|
||
|
||
/// Number of content-filter (`filter`) nesting levels that get their own isolated group texture.
|
||
/// Two covers the realistic "a blurred element inside another blurred element" case; deeper nests
|
||
/// render inline (unblurred at the inner level) rather than allocating unbounded VRAM.
|
||
const MAX_FILTER_DEPTH: usize = 2;
|
||
|
||
/// Number of [`BlurParams`] slots in the shared blur-params buffer (one per blur pass per frame).
|
||
/// Each frame uses 4 passes per backdrop/group plus one blit; 256 covers dozens of filters.
|
||
const BLUR_PARAMS_SLOTS: u64 = 256;
|
||
|
||
pub struct WgpuRenderer {
|
||
/// Shared GPU context for device recovery coordination (unused on WASM).
|
||
#[allow(dead_code)]
|
||
context: Option<GpuContext>,
|
||
/// Compositor GPU hint for adapter selection (unused on WASM).
|
||
#[allow(dead_code)]
|
||
compositor_gpu: Option<CompositorGpuHint>,
|
||
/// Application-requested extra wgpu features/limits, stored for device recovery.
|
||
#[allow(dead_code)]
|
||
extra_requirements: Option<WgpuDeviceRequirements>,
|
||
resources: Option<WgpuResources>,
|
||
surface_config: wgpu::SurfaceConfiguration,
|
||
atlas: Arc<WgpuAtlas>,
|
||
path_globals_offset: u64,
|
||
gamma_offset: u64,
|
||
instance_buffer_capacity: u64,
|
||
max_buffer_size: u64,
|
||
storage_buffer_alignment: u64,
|
||
/// Stride between [`BlurParams`] slots in `blur_params_buffer`, and a per-frame bump cursor
|
||
/// (in slots) handed out to blur passes. Cell so the `&self` blur helpers can advance it.
|
||
blur_params_stride: u64,
|
||
blur_params_slot: std::cell::Cell<u64>,
|
||
rendering_params: RenderingParameters,
|
||
is_bgr: bool,
|
||
dual_source_blending: bool,
|
||
adapter_info: wgpu::AdapterInfo,
|
||
transparent_alpha_mode: wgpu::CompositeAlphaMode,
|
||
opaque_alpha_mode: wgpu::CompositeAlphaMode,
|
||
max_texture_size: u32,
|
||
last_error: Arc<Mutex<Option<String>>>,
|
||
failed_frame_count: u32,
|
||
device_lost: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||
surface_configured: bool,
|
||
needs_redraw: bool,
|
||
}
|
||
|
||
impl WgpuRenderer {
|
||
fn resources(&self) -> &WgpuResources {
|
||
self.resources
|
||
.as_ref()
|
||
.expect("GPU resources not available")
|
||
}
|
||
|
||
fn resources_mut(&mut self) -> &mut WgpuResources {
|
||
self.resources
|
||
.as_mut()
|
||
.expect("GPU resources not available")
|
||
}
|
||
|
||
/// Creates a new WgpuRenderer from raw window handles.
|
||
///
|
||
/// The `gpu_context` is a shared reference that coordinates GPU context across
|
||
/// multiple windows. The first window to create a renderer will initialize the
|
||
/// context; subsequent windows will share it.
|
||
///
|
||
/// # Safety
|
||
/// The caller must ensure that the window handle remains valid for the lifetime
|
||
/// of the returned renderer.
|
||
#[cfg(not(target_family = "wasm"))]
|
||
pub fn new<W>(
|
||
gpu_context: GpuContext,
|
||
window: &W,
|
||
config: WgpuSurfaceConfig,
|
||
compositor_gpu: Option<CompositorGpuHint>,
|
||
extra_requirements: Option<WgpuDeviceRequirements>,
|
||
) -> anyhow::Result<Self>
|
||
where
|
||
W: HasWindowHandle + HasDisplayHandle + std::fmt::Debug + Send + Sync + Clone + 'static,
|
||
{
|
||
let window_handle = window
|
||
.window_handle()
|
||
.map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?;
|
||
|
||
let target = wgpu::SurfaceTargetUnsafe::RawHandle {
|
||
// Fall back to the display handle already provided via InstanceDescriptor::display.
|
||
raw_display_handle: None,
|
||
raw_window_handle: window_handle.as_raw(),
|
||
};
|
||
|
||
// Use the existing context's instance if available, otherwise create a new one.
|
||
// The surface must be created with the same instance that will be used for
|
||
// adapter selection, otherwise wgpu will panic.
|
||
let instance = gpu_context
|
||
.borrow()
|
||
.as_ref()
|
||
.map(|ctx| ctx.instance.clone())
|
||
.unwrap_or_else(|| WgpuContext::instance(Box::new(window.clone())));
|
||
|
||
// Safety: The caller guarantees that the window handle is valid for the
|
||
// lifetime of this renderer. In practice, the RawWindow struct is created
|
||
// from the native window handles and the surface is dropped before the window.
|
||
let surface = unsafe {
|
||
instance
|
||
.create_surface_unsafe(target)
|
||
.map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?
|
||
};
|
||
|
||
let mut ctx_ref = gpu_context.borrow_mut();
|
||
let context = match ctx_ref.as_mut() {
|
||
Some(context) => {
|
||
context.check_compatible_with_surface(&surface)?;
|
||
context
|
||
}
|
||
None => ctx_ref.insert(WgpuContext::new(
|
||
instance,
|
||
&surface,
|
||
compositor_gpu,
|
||
extra_requirements.as_ref(),
|
||
)?),
|
||
};
|
||
|
||
let atlas = Arc::new(WgpuAtlas::from_context(context));
|
||
|
||
Self::new_internal(
|
||
Some(Rc::clone(&gpu_context)),
|
||
context,
|
||
surface,
|
||
config,
|
||
compositor_gpu,
|
||
extra_requirements,
|
||
atlas,
|
||
)
|
||
}
|
||
|
||
#[cfg(target_family = "wasm")]
|
||
pub fn new_from_canvas(
|
||
context: &WgpuContext,
|
||
canvas: &web_sys::HtmlCanvasElement,
|
||
config: WgpuSurfaceConfig,
|
||
) -> anyhow::Result<Self> {
|
||
let surface = context
|
||
.instance
|
||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
|
||
.map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?;
|
||
|
||
let atlas = Arc::new(WgpuAtlas::from_context(context));
|
||
|
||
Self::new_internal(None, context, surface, config, None, None, atlas)
|
||
}
|
||
|
||
fn new_internal(
|
||
gpu_context: Option<GpuContext>,
|
||
context: &WgpuContext,
|
||
surface: wgpu::Surface<'static>,
|
||
config: WgpuSurfaceConfig,
|
||
compositor_gpu: Option<CompositorGpuHint>,
|
||
extra_requirements: Option<WgpuDeviceRequirements>,
|
||
atlas: Arc<WgpuAtlas>,
|
||
) -> anyhow::Result<Self> {
|
||
let surface_caps = surface.get_capabilities(&context.adapter);
|
||
let preferred_formats = [
|
||
wgpu::TextureFormat::Bgra8Unorm,
|
||
wgpu::TextureFormat::Rgba8Unorm,
|
||
];
|
||
let surface_format = preferred_formats
|
||
.iter()
|
||
.find(|f| surface_caps.formats.contains(f))
|
||
.copied()
|
||
.or_else(|| surface_caps.formats.iter().find(|f| !f.is_srgb()).copied())
|
||
.or_else(|| surface_caps.formats.first().copied())
|
||
.ok_or_else(|| {
|
||
anyhow::anyhow!(
|
||
"Surface reports no supported texture formats for adapter {:?}",
|
||
context.adapter.get_info().name
|
||
)
|
||
})?;
|
||
|
||
let pick_alpha_mode =
|
||
|preferences: &[wgpu::CompositeAlphaMode]| -> anyhow::Result<wgpu::CompositeAlphaMode> {
|
||
preferences
|
||
.iter()
|
||
.find(|p| surface_caps.alpha_modes.contains(p))
|
||
.copied()
|
||
.or_else(|| surface_caps.alpha_modes.first().copied())
|
||
.ok_or_else(|| {
|
||
anyhow::anyhow!(
|
||
"Surface reports no supported alpha modes for adapter {:?}",
|
||
context.adapter.get_info().name
|
||
)
|
||
})
|
||
};
|
||
|
||
let transparent_alpha_mode = pick_alpha_mode(&[
|
||
wgpu::CompositeAlphaMode::PreMultiplied,
|
||
wgpu::CompositeAlphaMode::Inherit,
|
||
])?;
|
||
|
||
let opaque_alpha_mode = pick_alpha_mode(&[
|
||
wgpu::CompositeAlphaMode::Opaque,
|
||
wgpu::CompositeAlphaMode::Inherit,
|
||
])?;
|
||
|
||
let alpha_mode = if config.transparent {
|
||
transparent_alpha_mode
|
||
} else {
|
||
opaque_alpha_mode
|
||
};
|
||
|
||
let device = Arc::clone(&context.device);
|
||
let max_texture_size = device.limits().max_texture_dimension_2d;
|
||
|
||
let requested_width = config.size.width.0 as u32;
|
||
let requested_height = config.size.height.0 as u32;
|
||
let clamped_width = requested_width.min(max_texture_size);
|
||
let clamped_height = requested_height.min(max_texture_size);
|
||
|
||
if clamped_width != requested_width || clamped_height != requested_height {
|
||
warn!(
|
||
"Requested surface size ({}, {}) exceeds maximum texture dimension {}. \
|
||
Clamping to ({}, {}). Window content may not fill the entire window.",
|
||
requested_width, requested_height, max_texture_size, clamped_width, clamped_height
|
||
);
|
||
}
|
||
|
||
let surface_config = wgpu::SurfaceConfiguration {
|
||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||
format: surface_format,
|
||
width: clamped_width.max(1),
|
||
height: clamped_height.max(1),
|
||
present_mode: config
|
||
.preferred_present_mode
|
||
.filter(|mode| surface_caps.present_modes.contains(mode))
|
||
.unwrap_or(wgpu::PresentMode::Fifo),
|
||
desired_maximum_frame_latency: 2,
|
||
alpha_mode,
|
||
view_formats: vec![],
|
||
};
|
||
// Configure the surface immediately. The adapter selection process already validated
|
||
// that this adapter can successfully configure this surface.
|
||
surface.configure(&context.device, &surface_config);
|
||
|
||
let queue = Arc::clone(&context.queue);
|
||
let dual_source_blending = context.supports_dual_source_blending();
|
||
|
||
let rendering_params = RenderingParameters::new(&context.adapter, surface_format);
|
||
let bind_group_layouts = Self::create_bind_group_layouts(&device);
|
||
let pipelines = Self::create_pipelines(
|
||
&device,
|
||
&bind_group_layouts,
|
||
surface_format,
|
||
alpha_mode,
|
||
rendering_params.path_sample_count,
|
||
dual_source_blending,
|
||
);
|
||
|
||
let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||
label: Some("atlas_sampler"),
|
||
mag_filter: wgpu::FilterMode::Linear,
|
||
min_filter: wgpu::FilterMode::Linear,
|
||
..Default::default()
|
||
});
|
||
|
||
let surface_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||
label: Some("surface_sampler"),
|
||
mag_filter: wgpu::FilterMode::Linear,
|
||
min_filter: wgpu::FilterMode::Linear,
|
||
..Default::default()
|
||
});
|
||
|
||
let surface_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("surface_uniform_buffer"),
|
||
size: std::mem::size_of::<SurfaceParams>() as u64,
|
||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
|
||
let uniform_alignment = device.limits().min_uniform_buffer_offset_alignment as u64;
|
||
// Shared blur-params buffer: BLUR_PARAMS_SLOTS slots, each one alignment stride apart.
|
||
let blur_params_stride =
|
||
(std::mem::size_of::<BlurParams>() as u64).next_multiple_of(uniform_alignment);
|
||
let blur_params_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("blur_params_buffer"),
|
||
size: blur_params_stride * BLUR_PARAMS_SLOTS,
|
||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
|
||
let globals_size = std::mem::size_of::<GlobalParams>() as u64;
|
||
let gamma_size = std::mem::size_of::<GammaParams>() as u64;
|
||
let path_globals_offset = globals_size.next_multiple_of(uniform_alignment);
|
||
let gamma_offset = (path_globals_offset + globals_size).next_multiple_of(uniform_alignment);
|
||
|
||
let globals_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("globals_buffer"),
|
||
size: gamma_offset + gamma_size,
|
||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
|
||
let max_buffer_size = device.limits().max_buffer_size;
|
||
let storage_buffer_alignment = device.limits().min_storage_buffer_offset_alignment as u64;
|
||
let initial_instance_buffer_capacity = 2 * 1024 * 1024;
|
||
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("instance_buffer"),
|
||
size: initial_instance_buffer_capacity,
|
||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
|
||
let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("globals_bind_group"),
|
||
layout: &bind_group_layouts.globals,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||
buffer: &globals_buffer,
|
||
offset: 0,
|
||
size: Some(NonZeroU64::new(globals_size).unwrap()),
|
||
}),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||
buffer: &globals_buffer,
|
||
offset: gamma_offset,
|
||
size: Some(NonZeroU64::new(gamma_size).unwrap()),
|
||
}),
|
||
},
|
||
],
|
||
});
|
||
|
||
let path_globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("path_globals_bind_group"),
|
||
layout: &bind_group_layouts.globals,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||
buffer: &globals_buffer,
|
||
offset: path_globals_offset,
|
||
size: Some(NonZeroU64::new(globals_size).unwrap()),
|
||
}),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||
buffer: &globals_buffer,
|
||
offset: gamma_offset,
|
||
size: Some(NonZeroU64::new(gamma_size).unwrap()),
|
||
}),
|
||
},
|
||
],
|
||
});
|
||
|
||
let adapter_info = context.adapter.get_info();
|
||
|
||
let last_error: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||
let last_error_clone = Arc::clone(&last_error);
|
||
device.on_uncaptured_error(Arc::new(move |error| {
|
||
let mut guard = last_error_clone.lock().unwrap();
|
||
*guard = Some(error.to_string());
|
||
}));
|
||
|
||
let resources = WgpuResources {
|
||
device,
|
||
queue,
|
||
surface,
|
||
pipelines,
|
||
bind_group_layouts,
|
||
atlas_sampler,
|
||
surface_sampler,
|
||
surface_uniform_buffer,
|
||
blur_params_buffer,
|
||
globals_buffer,
|
||
globals_bind_group,
|
||
path_globals_bind_group,
|
||
instance_buffer,
|
||
// Defer intermediate texture creation to first draw call via ensure_intermediate_textures().
|
||
// This avoids panics when the device/surface is in an invalid state during initialization.
|
||
path_intermediate_texture: None,
|
||
path_intermediate_view: None,
|
||
path_msaa_texture: None,
|
||
path_msaa_view: None,
|
||
scene_color_texture: None,
|
||
scene_color_view: None,
|
||
blur_ping_texture: None,
|
||
blur_ping_view: None,
|
||
blur_pong_texture: None,
|
||
blur_pong_view: None,
|
||
group_textures: Vec::new(),
|
||
group_views: Vec::new(),
|
||
};
|
||
|
||
Ok(Self {
|
||
context: gpu_context,
|
||
compositor_gpu,
|
||
extra_requirements,
|
||
resources: Some(resources),
|
||
surface_config,
|
||
atlas,
|
||
path_globals_offset,
|
||
gamma_offset,
|
||
instance_buffer_capacity: initial_instance_buffer_capacity,
|
||
max_buffer_size,
|
||
storage_buffer_alignment,
|
||
blur_params_stride,
|
||
blur_params_slot: std::cell::Cell::new(0),
|
||
rendering_params,
|
||
is_bgr: false,
|
||
dual_source_blending,
|
||
adapter_info,
|
||
transparent_alpha_mode,
|
||
opaque_alpha_mode,
|
||
max_texture_size,
|
||
last_error,
|
||
failed_frame_count: 0,
|
||
device_lost: context.device_lost_flag(),
|
||
surface_configured: true,
|
||
needs_redraw: false,
|
||
})
|
||
}
|
||
|
||
fn create_bind_group_layouts(device: &wgpu::Device) -> WgpuBindGroupLayouts {
|
||
let globals = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("globals_layout"),
|
||
entries: &[
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 0,
|
||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer {
|
||
ty: wgpu::BufferBindingType::Uniform,
|
||
has_dynamic_offset: false,
|
||
min_binding_size: NonZeroU64::new(
|
||
std::mem::size_of::<GlobalParams>() as u64
|
||
),
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 1,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer {
|
||
ty: wgpu::BufferBindingType::Uniform,
|
||
has_dynamic_offset: false,
|
||
min_binding_size: NonZeroU64::new(std::mem::size_of::<GammaParams>() as u64),
|
||
},
|
||
count: None,
|
||
},
|
||
],
|
||
});
|
||
|
||
let storage_buffer_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
|
||
binding,
|
||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer {
|
||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||
has_dynamic_offset: false,
|
||
min_binding_size: None,
|
||
},
|
||
count: None,
|
||
};
|
||
|
||
let instances = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("instances_layout"),
|
||
entries: &[storage_buffer_entry(0)],
|
||
});
|
||
|
||
let instances_with_texture =
|
||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("instances_with_texture_layout"),
|
||
entries: &[
|
||
storage_buffer_entry(0),
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 1,
|
||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||
ty: wgpu::BindingType::Texture {
|
||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||
view_dimension: wgpu::TextureViewDimension::D2,
|
||
multisampled: false,
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 2,
|
||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||
count: None,
|
||
},
|
||
],
|
||
});
|
||
|
||
let surfaces = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("surfaces_layout"),
|
||
entries: &[
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 0,
|
||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer {
|
||
ty: wgpu::BufferBindingType::Uniform,
|
||
has_dynamic_offset: false,
|
||
min_binding_size: NonZeroU64::new(
|
||
std::mem::size_of::<SurfaceParams>() as u64
|
||
),
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 1,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Texture {
|
||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||
view_dimension: wgpu::TextureViewDimension::D2,
|
||
multisampled: false,
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 2,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||
count: None,
|
||
},
|
||
],
|
||
});
|
||
|
||
let blur = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||
label: Some("blur_layout"),
|
||
entries: &[
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 0,
|
||
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
||
ty: wgpu::BindingType::Buffer {
|
||
ty: wgpu::BufferBindingType::Uniform,
|
||
has_dynamic_offset: false,
|
||
min_binding_size: NonZeroU64::new(std::mem::size_of::<BlurParams>() as u64),
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 1,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Texture {
|
||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||
view_dimension: wgpu::TextureViewDimension::D2,
|
||
multisampled: false,
|
||
},
|
||
count: None,
|
||
},
|
||
wgpu::BindGroupLayoutEntry {
|
||
binding: 2,
|
||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||
count: None,
|
||
},
|
||
],
|
||
});
|
||
|
||
WgpuBindGroupLayouts {
|
||
globals,
|
||
instances,
|
||
instances_with_texture,
|
||
surfaces,
|
||
blur,
|
||
}
|
||
}
|
||
|
||
fn create_pipelines(
|
||
device: &wgpu::Device,
|
||
layouts: &WgpuBindGroupLayouts,
|
||
surface_format: wgpu::TextureFormat,
|
||
alpha_mode: wgpu::CompositeAlphaMode,
|
||
path_sample_count: u32,
|
||
dual_source_blending: bool,
|
||
) -> WgpuPipelines {
|
||
// Diagnostic guard: verify the device actually has
|
||
// DUAL_SOURCE_BLENDING. We have a crash report (ZED-5G1) where a
|
||
// feature mismatch caused a wgpu-hal abort, but we haven't
|
||
// identified the code path that produces the mismatch. This
|
||
// guard prevents the crash and logs more evidence.
|
||
// Remove this check once:
|
||
// a) We find and fix the root cause, or
|
||
// b) There are no reports of this warning appearing for some time.
|
||
let device_has_feature = device
|
||
.features()
|
||
.contains(wgpu::Features::DUAL_SOURCE_BLENDING);
|
||
if dual_source_blending && !device_has_feature {
|
||
log::error!(
|
||
"BUG: dual_source_blending flag is true but device does not \
|
||
have DUAL_SOURCE_BLENDING enabled (device features: {:?}). \
|
||
Falling back to mono text rendering. Please report this at \
|
||
https://github.com/zed-industries/zed/issues",
|
||
device.features(),
|
||
);
|
||
}
|
||
let dual_source_blending = dual_source_blending && device_has_feature;
|
||
|
||
let base_shader_source = include_str!("shaders.wgsl");
|
||
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("gpui_shaders"),
|
||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(base_shader_source)),
|
||
});
|
||
|
||
let subpixel_shader_source = include_str!("shaders_subpixel.wgsl");
|
||
let subpixel_shader_module = if dual_source_blending {
|
||
let combined = format!(
|
||
"enable dual_source_blending;\n{base_shader_source}\n{subpixel_shader_source}"
|
||
);
|
||
Some(device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||
label: Some("gpui_subpixel_shaders"),
|
||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Owned(combined)),
|
||
}))
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let blend_mode = match alpha_mode {
|
||
wgpu::CompositeAlphaMode::PreMultiplied => {
|
||
wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING
|
||
}
|
||
_ => wgpu::BlendState::ALPHA_BLENDING,
|
||
};
|
||
|
||
let color_target = wgpu::ColorTargetState {
|
||
format: surface_format,
|
||
blend: Some(blend_mode),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
};
|
||
|
||
let create_pipeline = |name: &str,
|
||
vs_entry: &str,
|
||
fs_entry: &str,
|
||
globals_layout: &wgpu::BindGroupLayout,
|
||
data_layout: &wgpu::BindGroupLayout,
|
||
topology: wgpu::PrimitiveTopology,
|
||
color_targets: &[Option<wgpu::ColorTargetState>],
|
||
sample_count: u32,
|
||
module: &wgpu::ShaderModule| {
|
||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||
label: Some(&format!("{name}_layout")),
|
||
bind_group_layouts: &[Some(globals_layout), Some(data_layout)],
|
||
immediate_size: 0,
|
||
});
|
||
|
||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||
label: Some(name),
|
||
layout: Some(&pipeline_layout),
|
||
vertex: wgpu::VertexState {
|
||
module,
|
||
entry_point: Some(vs_entry),
|
||
buffers: &[],
|
||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||
},
|
||
fragment: Some(wgpu::FragmentState {
|
||
module,
|
||
entry_point: Some(fs_entry),
|
||
targets: color_targets,
|
||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||
}),
|
||
primitive: wgpu::PrimitiveState {
|
||
topology,
|
||
strip_index_format: None,
|
||
front_face: wgpu::FrontFace::Ccw,
|
||
cull_mode: None,
|
||
polygon_mode: wgpu::PolygonMode::Fill,
|
||
unclipped_depth: false,
|
||
conservative: false,
|
||
},
|
||
depth_stencil: None,
|
||
multisample: wgpu::MultisampleState {
|
||
count: sample_count,
|
||
mask: !0,
|
||
alpha_to_coverage_enabled: false,
|
||
},
|
||
multiview_mask: None,
|
||
cache: None,
|
||
})
|
||
};
|
||
|
||
let quads = create_pipeline(
|
||
"quads",
|
||
"vs_quad",
|
||
"fs_quad",
|
||
&layouts.globals,
|
||
&layouts.instances,
|
||
wgpu::PrimitiveTopology::TriangleStrip,
|
||
&[Some(color_target.clone())],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
let shadows = create_pipeline(
|
||
"shadows",
|
||
"vs_shadow",
|
||
"fs_shadow",
|
||
&layouts.globals,
|
||
&layouts.instances,
|
||
wgpu::PrimitiveTopology::TriangleStrip,
|
||
&[Some(color_target.clone())],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
let path_rasterization = create_pipeline(
|
||
"path_rasterization",
|
||
"vs_path_rasterization",
|
||
"fs_path_rasterization",
|
||
&layouts.globals,
|
||
&layouts.instances,
|
||
wgpu::PrimitiveTopology::TriangleList,
|
||
&[Some(wgpu::ColorTargetState {
|
||
format: surface_format,
|
||
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
})],
|
||
path_sample_count,
|
||
&shader_module,
|
||
);
|
||
|
||
let paths_blend = wgpu::BlendState {
|
||
color: wgpu::BlendComponent {
|
||
src_factor: wgpu::BlendFactor::One,
|
||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||
operation: wgpu::BlendOperation::Add,
|
||
},
|
||
alpha: wgpu::BlendComponent {
|
||
src_factor: wgpu::BlendFactor::One,
|
||
dst_factor: wgpu::BlendFactor::One,
|
||
operation: wgpu::BlendOperation::Add,
|
||
},
|
||
};
|
||
|
||
let paths = create_pipeline(
|
||
"paths",
|
||
"vs_path",
|
||
"fs_path",
|
||
&layouts.globals,
|
||
&layouts.instances_with_texture,
|
||
wgpu::PrimitiveTopology::TriangleStrip,
|
||
&[Some(wgpu::ColorTargetState {
|
||
format: surface_format,
|
||
blend: Some(paths_blend),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
})],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
let underlines = create_pipeline(
|
||
"underlines",
|
||
"vs_underline",
|
||
"fs_underline",
|
||
&layouts.globals,
|
||
&layouts.instances,
|
||
wgpu::PrimitiveTopology::TriangleStrip,
|
||
&[Some(color_target.clone())],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
let mono_sprites = create_pipeline(
|
||
"mono_sprites",
|
||
"vs_mono_sprite",
|
||
"fs_mono_sprite",
|
||
&layouts.globals,
|
||
&layouts.instances_with_texture,
|
||
wgpu::PrimitiveTopology::TriangleStrip,
|
||
&[Some(color_target.clone())],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
let subpixel_sprites = if let Some(subpixel_module) = &subpixel_shader_module {
|
||
let subpixel_blend = wgpu::BlendState {
|
||
color: wgpu::BlendComponent {
|
||
src_factor: wgpu::BlendFactor::Src1,
|
||
dst_factor: wgpu::BlendFactor::OneMinusSrc1,
|
||
operation: wgpu::BlendOperation::Add,
|
||
},
|
||
alpha: wgpu::BlendComponent {
|
||
src_factor: wgpu::BlendFactor::One,
|
||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||
operation: wgpu::BlendOperation::Add,
|
||
},
|
||
};
|
||
|
||
Some(create_pipeline(
|
||
"subpixel_sprites",
|
||
"vs_subpixel_sprite",
|
||
"fs_subpixel_sprite",
|
||
&layouts.globals,
|
||
&layouts.instances_with_texture,
|
||
wgpu::PrimitiveTopology::TriangleStrip,
|
||
&[Some(wgpu::ColorTargetState {
|
||
format: surface_format,
|
||
blend: Some(subpixel_blend),
|
||
write_mask: wgpu::ColorWrites::COLOR,
|
||
})],
|
||
1,
|
||
subpixel_module,
|
||
))
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let poly_sprites = create_pipeline(
|
||
"poly_sprites",
|
||
"vs_poly_sprite",
|
||
"fs_poly_sprite",
|
||
&layouts.globals,
|
||
&layouts.instances_with_texture,
|
||
wgpu::PrimitiveTopology::TriangleStrip,
|
||
&[Some(color_target.clone())],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
let surfaces = create_pipeline(
|
||
"surfaces",
|
||
"vs_surface",
|
||
"fs_surface",
|
||
&layouts.globals,
|
||
&layouts.surfaces,
|
||
wgpu::PrimitiveTopology::TriangleStrip,
|
||
&[Some(color_target)],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
// Blur pipelines all sample one texture into another; the downsample and gaussian passes
|
||
// overwrite their (intermediate) target, while the composite blends over the scene.
|
||
let no_blend_target = wgpu::ColorTargetState {
|
||
format: surface_format,
|
||
blend: None,
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
};
|
||
|
||
let blur_downsample = create_pipeline(
|
||
"blur_downsample",
|
||
"vs_blur_fullscreen",
|
||
"fs_blur_downsample",
|
||
&layouts.globals,
|
||
&layouts.blur,
|
||
wgpu::PrimitiveTopology::TriangleList,
|
||
&[Some(no_blend_target.clone())],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
let blur = create_pipeline(
|
||
"blur",
|
||
"vs_blur_fullscreen",
|
||
"fs_blur",
|
||
&layouts.globals,
|
||
&layouts.blur,
|
||
wgpu::PrimitiveTopology::TriangleList,
|
||
&[Some(no_blend_target)],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
// The blurred sample is premultiplied (blurring against the transparent, rgb=0 region
|
||
// around the source scales rgb with the fading alpha), so the composite outputs
|
||
// premultiplied and blends premultiplied — straight alpha blending would multiply rgb by
|
||
// alpha a second time and darken the faded edges. Independent of the window's alpha mode.
|
||
let premultiplied_target = wgpu::ColorTargetState {
|
||
format: surface_format,
|
||
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
|
||
write_mask: wgpu::ColorWrites::ALL,
|
||
};
|
||
let blur_composite = create_pipeline(
|
||
"blur_composite",
|
||
"vs_blur_composite",
|
||
"fs_blur_composite",
|
||
&layouts.globals,
|
||
&layouts.blur,
|
||
wgpu::PrimitiveTopology::TriangleStrip,
|
||
&[Some(premultiplied_target)],
|
||
1,
|
||
&shader_module,
|
||
);
|
||
|
||
WgpuPipelines {
|
||
quads,
|
||
shadows,
|
||
path_rasterization,
|
||
paths,
|
||
underlines,
|
||
mono_sprites,
|
||
subpixel_sprites,
|
||
poly_sprites,
|
||
surfaces,
|
||
blur_downsample,
|
||
blur,
|
||
blur_composite,
|
||
}
|
||
}
|
||
|
||
fn create_path_intermediate(
|
||
device: &wgpu::Device,
|
||
format: wgpu::TextureFormat,
|
||
width: u32,
|
||
height: u32,
|
||
) -> (wgpu::Texture, wgpu::TextureView) {
|
||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some("path_intermediate"),
|
||
size: wgpu::Extent3d {
|
||
width: width.max(1),
|
||
height: height.max(1),
|
||
depth_or_array_layers: 1,
|
||
},
|
||
mip_level_count: 1,
|
||
sample_count: 1,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format,
|
||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
|
||
view_formats: &[],
|
||
});
|
||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||
(texture, view)
|
||
}
|
||
|
||
fn create_msaa_if_needed(
|
||
device: &wgpu::Device,
|
||
format: wgpu::TextureFormat,
|
||
width: u32,
|
||
height: u32,
|
||
sample_count: u32,
|
||
) -> Option<(wgpu::Texture, wgpu::TextureView)> {
|
||
if sample_count <= 1 {
|
||
return None;
|
||
}
|
||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||
label: Some("path_msaa"),
|
||
size: wgpu::Extent3d {
|
||
width: width.max(1),
|
||
height: height.max(1),
|
||
depth_or_array_layers: 1,
|
||
},
|
||
mip_level_count: 1,
|
||
sample_count,
|
||
dimension: wgpu::TextureDimension::D2,
|
||
format,
|
||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||
view_formats: &[],
|
||
});
|
||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||
Some((texture, view))
|
||
}
|
||
|
||
pub fn update_drawable_size(&mut self, size: Size<DevicePixels>) {
|
||
let width = size.width.0 as u32;
|
||
let height = size.height.0 as u32;
|
||
|
||
if width != self.surface_config.width || height != self.surface_config.height {
|
||
let clamped_width = width.min(self.max_texture_size);
|
||
let clamped_height = height.min(self.max_texture_size);
|
||
|
||
if clamped_width != width || clamped_height != height {
|
||
warn!(
|
||
"Requested surface size ({}, {}) exceeds maximum texture dimension {}. \
|
||
Clamping to ({}, {}). Window content may not fill the entire window.",
|
||
width, height, self.max_texture_size, clamped_width, clamped_height
|
||
);
|
||
}
|
||
|
||
self.surface_config.width = clamped_width.max(1);
|
||
self.surface_config.height = clamped_height.max(1);
|
||
let surface_config = self.surface_config.clone();
|
||
|
||
// GPU resources may not exist yet, skip rather than panicking
|
||
let Some(resources) = self.resources.as_mut() else {
|
||
return;
|
||
};
|
||
|
||
// Wait for any in-flight GPU work to complete before destroying textures
|
||
if let Err(e) = resources.device.poll(wgpu::PollType::Wait {
|
||
submission_index: None,
|
||
timeout: None,
|
||
}) {
|
||
warn!("Failed to poll device during resize: {e:?}");
|
||
}
|
||
|
||
// Destroy old textures before allocating new ones to avoid GPU memory spikes
|
||
if let Some(ref texture) = resources.path_intermediate_texture {
|
||
texture.destroy();
|
||
}
|
||
if let Some(ref texture) = resources.path_msaa_texture {
|
||
texture.destroy();
|
||
}
|
||
for texture in [
|
||
&resources.scene_color_texture,
|
||
&resources.blur_ping_texture,
|
||
&resources.blur_pong_texture,
|
||
]
|
||
.into_iter()
|
||
.flatten()
|
||
{
|
||
texture.destroy();
|
||
}
|
||
for texture in &resources.group_textures {
|
||
texture.destroy();
|
||
}
|
||
|
||
resources
|
||
.surface
|
||
.configure(&resources.device, &surface_config);
|
||
|
||
// Invalidate intermediate textures - they will be lazily recreated
|
||
// in draw() after we confirm the surface is healthy. This avoids
|
||
// panics when the device/surface is in an invalid state during resize.
|
||
resources.invalidate_intermediate_textures();
|
||
}
|
||
}
|
||
|
||
fn ensure_intermediate_textures(&mut self) {
|
||
if self.resources().path_intermediate_texture.is_some() {
|
||
return;
|
||
}
|
||
|
||
let format = self.surface_config.format;
|
||
let width = self.surface_config.width;
|
||
let height = self.surface_config.height;
|
||
let path_sample_count = self.rendering_params.path_sample_count;
|
||
let resources = self.resources_mut();
|
||
|
||
let (t, v) = Self::create_path_intermediate(&resources.device, format, width, height);
|
||
resources.path_intermediate_texture = Some(t);
|
||
resources.path_intermediate_view = Some(v);
|
||
|
||
let (path_msaa_texture, path_msaa_view) = Self::create_msaa_if_needed(
|
||
&resources.device,
|
||
format,
|
||
width,
|
||
height,
|
||
path_sample_count,
|
||
)
|
||
.map(|(t, v)| (Some(t), Some(v)))
|
||
.unwrap_or((None, None));
|
||
resources.path_msaa_texture = path_msaa_texture;
|
||
resources.path_msaa_view = path_msaa_view;
|
||
}
|
||
|
||
/// Lazily allocate the blur offscreen targets — the full-res scene texture, half-res
|
||
/// ping/pong, and one full-res group texture per nesting level. Called only on frames that
|
||
/// actually use a blur filter, so non-blurring apps never pay this VRAM. A no-op once
|
||
/// allocated (invalidated alongside the path intermediates on resize / device loss).
|
||
fn ensure_blur_textures(&mut self) {
|
||
if self.resources().scene_color_texture.is_some() {
|
||
return;
|
||
}
|
||
let format = self.surface_config.format;
|
||
let width = self.surface_config.width;
|
||
let height = self.surface_config.height;
|
||
let blur_width = (width / 2).max(1);
|
||
let blur_height = (height / 2).max(1);
|
||
let resources = self.resources_mut();
|
||
|
||
let (t, v) = Self::create_path_intermediate(&resources.device, format, width, height);
|
||
resources.scene_color_texture = Some(t);
|
||
resources.scene_color_view = Some(v);
|
||
let (t, v) =
|
||
Self::create_path_intermediate(&resources.device, format, blur_width, blur_height);
|
||
resources.blur_ping_texture = Some(t);
|
||
resources.blur_ping_view = Some(v);
|
||
let (t, v) =
|
||
Self::create_path_intermediate(&resources.device, format, blur_width, blur_height);
|
||
resources.blur_pong_texture = Some(t);
|
||
resources.blur_pong_view = Some(v);
|
||
|
||
for _ in 0..MAX_FILTER_DEPTH {
|
||
let (t, v) = Self::create_path_intermediate(&resources.device, format, width, height);
|
||
resources.group_textures.push(t);
|
||
resources.group_views.push(v);
|
||
}
|
||
}
|
||
|
||
pub fn set_subpixel_layout(&mut self, is_bgr: bool) {
|
||
self.is_bgr = is_bgr;
|
||
}
|
||
|
||
pub fn update_transparency(&mut self, transparent: bool) {
|
||
let new_alpha_mode = if transparent {
|
||
self.transparent_alpha_mode
|
||
} else {
|
||
self.opaque_alpha_mode
|
||
};
|
||
|
||
if new_alpha_mode != self.surface_config.alpha_mode {
|
||
self.surface_config.alpha_mode = new_alpha_mode;
|
||
let surface_config = self.surface_config.clone();
|
||
let path_sample_count = self.rendering_params.path_sample_count;
|
||
let dual_source_blending = self.dual_source_blending;
|
||
let resources = self.resources_mut();
|
||
resources
|
||
.surface
|
||
.configure(&resources.device, &surface_config);
|
||
resources.pipelines = Self::create_pipelines(
|
||
&resources.device,
|
||
&resources.bind_group_layouts,
|
||
surface_config.format,
|
||
surface_config.alpha_mode,
|
||
path_sample_count,
|
||
dual_source_blending,
|
||
);
|
||
}
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
pub fn viewport_size(&self) -> Size<DevicePixels> {
|
||
Size {
|
||
width: DevicePixels(self.surface_config.width as i32),
|
||
height: DevicePixels(self.surface_config.height as i32),
|
||
}
|
||
}
|
||
|
||
pub fn sprite_atlas(&self) -> &Arc<WgpuAtlas> {
|
||
&self.atlas
|
||
}
|
||
|
||
pub fn supports_dual_source_blending(&self) -> bool {
|
||
self.dual_source_blending
|
||
}
|
||
|
||
pub fn gpu_context(&self) -> (Arc<wgpu::Device>, Arc<wgpu::Queue>) {
|
||
let resources = self.resources();
|
||
(resources.device.clone(), resources.queue.clone())
|
||
}
|
||
|
||
pub fn gpu_specs(&self) -> GpuSpecs {
|
||
GpuSpecs {
|
||
is_software_emulated: self.adapter_info.device_type == wgpu::DeviceType::Cpu,
|
||
device_name: self.adapter_info.name.clone(),
|
||
driver_name: self.adapter_info.driver.clone(),
|
||
driver_info: self.adapter_info.driver_info.clone(),
|
||
}
|
||
}
|
||
|
||
pub fn max_texture_size(&self) -> u32 {
|
||
self.max_texture_size
|
||
}
|
||
|
||
pub fn draw(&mut self, scene: &Scene) -> bool {
|
||
// Bail out early if the surface has been unconfigured (e.g. during
|
||
// Android background/rotation transitions). Attempting to acquire
|
||
// a texture from an unconfigured surface can block indefinitely on
|
||
// some drivers (Adreno).
|
||
if !self.surface_configured {
|
||
return false;
|
||
}
|
||
|
||
let last_error = self.last_error.lock().unwrap().take();
|
||
if let Some(error) = last_error {
|
||
self.failed_frame_count += 1;
|
||
log::error!(
|
||
"GPU error during frame (failure {} of 10): {error}",
|
||
self.failed_frame_count
|
||
);
|
||
|
||
// TBD. Does retrying more actually help?
|
||
if self.failed_frame_count > 10 {
|
||
panic!("Too many consecutive GPU errors. Last error: {error}");
|
||
} else if self.failed_frame_count > 5 {
|
||
if let Some(res) = self.resources.as_mut() {
|
||
res.invalidate_intermediate_textures();
|
||
}
|
||
self.atlas.clear();
|
||
self.needs_redraw = true;
|
||
self.failed_frame_count = 0;
|
||
return false;
|
||
}
|
||
} else {
|
||
self.failed_frame_count = 0;
|
||
}
|
||
|
||
self.atlas.before_frame();
|
||
|
||
let frame = match self.resources().surface.get_current_texture() {
|
||
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
|
||
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
|
||
// Textures must be destroyed before the surface can be reconfigured.
|
||
drop(frame);
|
||
let surface_config = self.surface_config.clone();
|
||
let resources = self.resources_mut();
|
||
resources
|
||
.surface
|
||
.configure(&resources.device, &surface_config);
|
||
return false;
|
||
}
|
||
wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
|
||
let surface_config = self.surface_config.clone();
|
||
let resources = self.resources_mut();
|
||
resources
|
||
.surface
|
||
.configure(&resources.device, &surface_config);
|
||
return false;
|
||
}
|
||
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
|
||
return false;
|
||
}
|
||
wgpu::CurrentSurfaceTexture::Validation => {
|
||
*self.last_error.lock().unwrap() =
|
||
Some("Surface texture validation error".to_string());
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// Now that we know the surface is healthy, ensure intermediate textures exist
|
||
self.ensure_intermediate_textures();
|
||
|
||
// Blur is the only thing that needs the offscreen scene texture; allocate it (and the
|
||
// ping/pong/group targets) lazily so non-blurring apps pay no extra VRAM or blit.
|
||
let use_offscreen =
|
||
!scene.backdrop_filters.is_empty() || !scene.filter_boundaries.is_empty();
|
||
if use_offscreen {
|
||
self.ensure_blur_textures();
|
||
}
|
||
|
||
let frame_view = frame
|
||
.texture
|
||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||
|
||
let gamma_params = GammaParams {
|
||
gamma_ratios: self.rendering_params.gamma_ratios,
|
||
grayscale_enhanced_contrast: self.rendering_params.grayscale_enhanced_contrast,
|
||
subpixel_enhanced_contrast: self.rendering_params.subpixel_enhanced_contrast,
|
||
is_bgr: self.is_bgr as u32,
|
||
_pad: 0,
|
||
};
|
||
|
||
let globals = GlobalParams {
|
||
viewport_size: [
|
||
self.surface_config.width as f32,
|
||
self.surface_config.height as f32,
|
||
],
|
||
premultiplied_alpha: if self.surface_config.alpha_mode
|
||
== wgpu::CompositeAlphaMode::PreMultiplied
|
||
{
|
||
1
|
||
} else {
|
||
0
|
||
},
|
||
pad: 0,
|
||
};
|
||
|
||
let path_globals = GlobalParams {
|
||
premultiplied_alpha: 0,
|
||
..globals
|
||
};
|
||
|
||
{
|
||
let resources = self.resources();
|
||
resources.queue.write_buffer(
|
||
&resources.globals_buffer,
|
||
0,
|
||
bytemuck::bytes_of(&globals),
|
||
);
|
||
resources.queue.write_buffer(
|
||
&resources.globals_buffer,
|
||
self.path_globals_offset,
|
||
bytemuck::bytes_of(&path_globals),
|
||
);
|
||
resources.queue.write_buffer(
|
||
&resources.globals_buffer,
|
||
self.gamma_offset,
|
||
bytemuck::bytes_of(&gamma_params),
|
||
);
|
||
}
|
||
|
||
loop {
|
||
let mut instance_offset: u64 = 0;
|
||
// Reset the blur-params bump cursor each (re)render of the scene.
|
||
self.blur_params_slot.set(0);
|
||
let mut overflow = false;
|
||
|
||
let mut encoder =
|
||
self.resources()
|
||
.device
|
||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||
label: Some("main_encoder"),
|
||
});
|
||
|
||
// When the scene contains blur filters, render into the offscreen scene texture (so
|
||
// filters can sample already-painted content mid-frame) and blit to the swapchain at
|
||
// the end; otherwise render straight to the swapchain. `use_offscreen` and the blur
|
||
// textures were computed/allocated above.
|
||
let scene_color_view = if use_offscreen {
|
||
Some(
|
||
self.resources()
|
||
.scene_color_view
|
||
.as_ref()
|
||
.expect("scene_color_view allocated by ensure_blur_textures")
|
||
.clone(),
|
||
)
|
||
} else {
|
||
None
|
||
};
|
||
// The active render target. While inside a content-filter (`filter`) group it points
|
||
// at a group texture so the group renders in isolation.
|
||
let mut current_target = match &scene_color_view {
|
||
Some(view) => view.clone(),
|
||
None => frame_view.clone(),
|
||
};
|
||
// One group texture per nesting depth; empty when not blurring.
|
||
let group_views = if use_offscreen {
|
||
self.resources().group_views.clone()
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
// (boundary, parent target to composite back into, whether this level is isolated).
|
||
let mut filter_stack: Vec<(FilterBoundary, wgpu::TextureView, bool)> = Vec::new();
|
||
|
||
{
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("main_pass"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: ¤t_target,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
depth_slice: None,
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
|
||
for batch in scene.batches() {
|
||
let ok = match batch {
|
||
PrimitiveBatch::Quads(range) => {
|
||
self.draw_quads(&scene.quads[range], &mut instance_offset, &mut pass)
|
||
}
|
||
PrimitiveBatch::Shadows(range) => self.draw_shadows(
|
||
&scene.shadows[range],
|
||
&mut instance_offset,
|
||
&mut pass,
|
||
),
|
||
PrimitiveBatch::Paths(range) => {
|
||
let paths = &scene.paths[range];
|
||
if paths.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
drop(pass);
|
||
|
||
let did_draw = self.draw_paths_to_intermediate(
|
||
&mut encoder,
|
||
paths,
|
||
&mut instance_offset,
|
||
);
|
||
|
||
pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("main_pass_continued"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: ¤t_target,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Load,
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
depth_slice: None,
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
|
||
if did_draw {
|
||
self.draw_paths_from_intermediate(
|
||
paths,
|
||
&mut instance_offset,
|
||
&mut pass,
|
||
)
|
||
} else {
|
||
false
|
||
}
|
||
}
|
||
PrimitiveBatch::Underlines(range) => self.draw_underlines(
|
||
&scene.underlines[range],
|
||
&mut instance_offset,
|
||
&mut pass,
|
||
),
|
||
PrimitiveBatch::MonochromeSprites { texture_id, range } => self
|
||
.draw_monochrome_sprites(
|
||
&scene.monochrome_sprites[range],
|
||
texture_id,
|
||
&mut instance_offset,
|
||
&mut pass,
|
||
),
|
||
PrimitiveBatch::SubpixelSprites { texture_id, range } => self
|
||
.draw_subpixel_sprites(
|
||
&scene.subpixel_sprites[range],
|
||
texture_id,
|
||
&mut instance_offset,
|
||
&mut pass,
|
||
),
|
||
PrimitiveBatch::PolychromeSprites { texture_id, range } => self
|
||
.draw_polychrome_sprites(
|
||
&scene.polychrome_sprites[range],
|
||
texture_id,
|
||
&mut instance_offset,
|
||
&mut pass,
|
||
),
|
||
PrimitiveBatch::Surfaces(range) => {
|
||
self.draw_surfaces(&scene.surfaces[range], &mut pass)
|
||
}
|
||
PrimitiveBatch::BackdropFilters(range) => {
|
||
// Interrupt the current pass, blur the content painted so far behind
|
||
// each backdrop's rounded rect, then resume drawing on top.
|
||
drop(pass);
|
||
for filter in &scene.backdrop_filters[range] {
|
||
self.draw_backdrop_filter(&mut encoder, filter, ¤t_target);
|
||
}
|
||
pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("main_pass_continued"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: ¤t_target,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Load,
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
depth_slice: None,
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
true
|
||
}
|
||
PrimitiveBatch::FilterBoundary(ix) => {
|
||
let boundary = scene.filter_boundaries[ix].clone();
|
||
if boundary.is_start {
|
||
// Each isolated nesting level uses its own group texture from the
|
||
// pool (indexed by current isolation depth). Beyond the pool size
|
||
// (MAX_FILTER_DEPTH) deeper filters render inline without isolation
|
||
// rather than corrupting an outer group.
|
||
let depth = filter_stack.iter().filter(|entry| entry.2).count();
|
||
if depth < group_views.len() {
|
||
drop(pass);
|
||
let parent = current_target.clone();
|
||
current_target = group_views[depth].clone();
|
||
filter_stack.push((boundary, parent, true));
|
||
pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("filter_group"),
|
||
color_attachments: &[Some(
|
||
wgpu::RenderPassColorAttachment {
|
||
view: ¤t_target,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(
|
||
wgpu::Color::TRANSPARENT,
|
||
),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
depth_slice: None,
|
||
},
|
||
)],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
} else {
|
||
filter_stack.push((boundary, current_target.clone(), false));
|
||
}
|
||
} else if let Some((boundary, parent, isolated)) = filter_stack.pop() {
|
||
if isolated {
|
||
drop(pass);
|
||
self.blur_and_composite(
|
||
&mut encoder,
|
||
¤t_target,
|
||
&parent,
|
||
boundary.bounds,
|
||
boundary.content_mask.bounds,
|
||
[
|
||
boundary.corner_radii.top_left.0,
|
||
boundary.corner_radii.top_right.0,
|
||
boundary.corner_radii.bottom_right.0,
|
||
boundary.corner_radii.bottom_left.0,
|
||
],
|
||
max_blur_radius(&boundary.filters),
|
||
boundary.opacity,
|
||
false,
|
||
);
|
||
current_target = parent;
|
||
pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("main_pass_continued"),
|
||
color_attachments: &[Some(
|
||
wgpu::RenderPassColorAttachment {
|
||
view: ¤t_target,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Load,
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
depth_slice: None,
|
||
},
|
||
)],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
}
|
||
}
|
||
true
|
||
}
|
||
};
|
||
if !ok {
|
||
overflow = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if overflow {
|
||
drop(encoder);
|
||
if self.instance_buffer_capacity >= self.max_buffer_size {
|
||
log::error!(
|
||
"instance buffer size grew too large: {}",
|
||
self.instance_buffer_capacity
|
||
);
|
||
frame.present();
|
||
return true;
|
||
}
|
||
self.grow_instance_buffer();
|
||
continue;
|
||
}
|
||
|
||
// Present the offscreen scene by copying it into the swapchain texture. Skipped when
|
||
// rendering went straight to the swapchain (no filters this frame).
|
||
if let Some(scene_color_view) = &scene_color_view {
|
||
self.blit_to_frame(&mut encoder, scene_color_view, &frame_view);
|
||
}
|
||
|
||
self.resources()
|
||
.queue
|
||
.submit(std::iter::once(encoder.finish()));
|
||
frame.present();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
fn draw_quads(
|
||
&self,
|
||
quads: &[Quad],
|
||
instance_offset: &mut u64,
|
||
pass: &mut wgpu::RenderPass<'_>,
|
||
) -> bool {
|
||
let data = unsafe { Self::instance_bytes(quads) };
|
||
self.draw_instances(
|
||
data,
|
||
quads.len() as u32,
|
||
&self.resources().pipelines.quads,
|
||
instance_offset,
|
||
pass,
|
||
)
|
||
}
|
||
|
||
fn draw_shadows(
|
||
&self,
|
||
shadows: &[Shadow],
|
||
instance_offset: &mut u64,
|
||
pass: &mut wgpu::RenderPass<'_>,
|
||
) -> bool {
|
||
let data = unsafe { Self::instance_bytes(shadows) };
|
||
self.draw_instances(
|
||
data,
|
||
shadows.len() as u32,
|
||
&self.resources().pipelines.shadows,
|
||
instance_offset,
|
||
pass,
|
||
)
|
||
}
|
||
|
||
fn draw_underlines(
|
||
&self,
|
||
underlines: &[Underline],
|
||
instance_offset: &mut u64,
|
||
pass: &mut wgpu::RenderPass<'_>,
|
||
) -> bool {
|
||
let data = unsafe { Self::instance_bytes(underlines) };
|
||
self.draw_instances(
|
||
data,
|
||
underlines.len() as u32,
|
||
&self.resources().pipelines.underlines,
|
||
instance_offset,
|
||
pass,
|
||
)
|
||
}
|
||
|
||
fn draw_monochrome_sprites(
|
||
&self,
|
||
sprites: &[MonochromeSprite],
|
||
texture_id: AtlasTextureId,
|
||
instance_offset: &mut u64,
|
||
pass: &mut wgpu::RenderPass<'_>,
|
||
) -> bool {
|
||
let tex_info = self.atlas.get_texture_info(texture_id);
|
||
let data = unsafe { Self::instance_bytes(sprites) };
|
||
self.draw_instances_with_texture(
|
||
data,
|
||
sprites.len() as u32,
|
||
&tex_info.view,
|
||
&self.resources().pipelines.mono_sprites,
|
||
instance_offset,
|
||
pass,
|
||
)
|
||
}
|
||
|
||
fn draw_subpixel_sprites(
|
||
&self,
|
||
sprites: &[SubpixelSprite],
|
||
texture_id: AtlasTextureId,
|
||
instance_offset: &mut u64,
|
||
pass: &mut wgpu::RenderPass<'_>,
|
||
) -> bool {
|
||
let tex_info = self.atlas.get_texture_info(texture_id);
|
||
let data = unsafe { Self::instance_bytes(sprites) };
|
||
let resources = self.resources();
|
||
let pipeline = resources
|
||
.pipelines
|
||
.subpixel_sprites
|
||
.as_ref()
|
||
.unwrap_or(&resources.pipelines.mono_sprites);
|
||
self.draw_instances_with_texture(
|
||
data,
|
||
sprites.len() as u32,
|
||
&tex_info.view,
|
||
pipeline,
|
||
instance_offset,
|
||
pass,
|
||
)
|
||
}
|
||
|
||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||
fn draw_surfaces(&self, surfaces: &[PaintSurface], pass: &mut wgpu::RenderPass<'_>) -> bool {
|
||
let resources = self.resources();
|
||
for surface in surfaces {
|
||
let Some(wgpu_texture) = surface.texture.downcast_ref::<wgpu::Texture>() else {
|
||
continue;
|
||
};
|
||
|
||
let texture_view = wgpu_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||
|
||
let params = SurfaceParams {
|
||
bounds: surface.bounds.into(),
|
||
content_mask: surface.content_mask.bounds.into(),
|
||
};
|
||
|
||
resources.queue.write_buffer(
|
||
&resources.surface_uniform_buffer,
|
||
0,
|
||
bytemuck::bytes_of(¶ms),
|
||
);
|
||
|
||
let bind_group = resources
|
||
.device
|
||
.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("surface_bind_group"),
|
||
layout: &resources.bind_group_layouts.surfaces,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: resources.surface_uniform_buffer.as_entire_binding(),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(&texture_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&resources.surface_sampler),
|
||
},
|
||
],
|
||
});
|
||
|
||
pass.set_pipeline(&resources.pipelines.surfaces);
|
||
pass.set_bind_group(0, &resources.globals_bind_group, &[]);
|
||
pass.set_bind_group(1, &bind_group, &[]);
|
||
pass.draw(0..4, 0..1);
|
||
}
|
||
true
|
||
}
|
||
|
||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||
fn draw_surfaces(&self, _surfaces: &[PaintSurface], _pass: &mut wgpu::RenderPass<'_>) -> bool {
|
||
true
|
||
}
|
||
|
||
/// Build a bind group for a blur pass. Writes `params` into the next slot of the shared
|
||
/// `blur_params_buffer` (no per-pass allocation) and references that slot, the source texture,
|
||
/// and the filtering sampler. Distinct per-pass offsets keep `write_buffer`'s
|
||
/// last-write-at-submit semantics from clobbering earlier passes within a frame.
|
||
fn make_blur_bind_group(
|
||
&self,
|
||
params: BlurParams,
|
||
source: &wgpu::TextureView,
|
||
) -> wgpu::BindGroup {
|
||
let resources = self.resources();
|
||
let slot = self.blur_params_slot.get() % BLUR_PARAMS_SLOTS;
|
||
self.blur_params_slot.set(slot + 1);
|
||
let offset = slot * self.blur_params_stride;
|
||
resources.queue.write_buffer(
|
||
&resources.blur_params_buffer,
|
||
offset,
|
||
bytemuck::bytes_of(¶ms),
|
||
);
|
||
resources
|
||
.device
|
||
.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("blur_bind_group"),
|
||
layout: &resources.bind_group_layouts.blur,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||
buffer: &resources.blur_params_buffer,
|
||
offset,
|
||
size: NonZeroU64::new(std::mem::size_of::<BlurParams>() as u64),
|
||
}),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(source),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&resources.surface_sampler),
|
||
},
|
||
],
|
||
})
|
||
}
|
||
|
||
/// Run a full-screen (3-vertex) blur pass that overwrites `target` by sampling `source`.
|
||
/// `scissor` (x, y, w, h, in `target` pixels) limits fragment work to the region that
|
||
/// actually feeds the composite — the element bounds dilated by the kernel radius.
|
||
fn run_blur_pass(
|
||
&self,
|
||
encoder: &mut wgpu::CommandEncoder,
|
||
label: &str,
|
||
pipeline: &wgpu::RenderPipeline,
|
||
target: &wgpu::TextureView,
|
||
source: &wgpu::TextureView,
|
||
params: BlurParams,
|
||
scissor: [u32; 4],
|
||
) {
|
||
let bind_group = self.make_blur_bind_group(params, source);
|
||
let resources = self.resources();
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some(label),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: target,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
depth_slice: None,
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
pass.set_pipeline(pipeline);
|
||
pass.set_bind_group(0, &resources.globals_bind_group, &[]);
|
||
pass.set_bind_group(1, &bind_group, &[]);
|
||
pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]);
|
||
pass.draw(0..3, 0..1);
|
||
}
|
||
|
||
/// Blur `source` (full-resolution) and composite the result into `target`, clipped to
|
||
/// `bounds`/`corner_radii`/`content_mask` and modulated by `opacity`. Shared by the backdrop
|
||
/// and content-filter paths. Uses the half-resolution ping/pong textures as scratch.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn blur_and_composite(
|
||
&self,
|
||
encoder: &mut wgpu::CommandEncoder,
|
||
source: &wgpu::TextureView,
|
||
target: &wgpu::TextureView,
|
||
bounds: Bounds<ScaledPixels>,
|
||
content_mask: Bounds<ScaledPixels>,
|
||
corner_radii: [f32; 4],
|
||
blur_radius: f32,
|
||
opacity: f32,
|
||
// Backdrop clips to the rounded rect; content (`filter`) bleeds past its bounds.
|
||
clip_rounded: bool,
|
||
) {
|
||
// Sigma is halved because the blur runs at half resolution.
|
||
let sigma = (blur_radius * 0.5).max(0.0);
|
||
if sigma <= 0.0 {
|
||
return;
|
||
}
|
||
// Span ±3σ. If that needs more than 32 taps, spread the taps apart (tap_step > 1) rather
|
||
// than truncating the kernel — keeps very large radii from clipping (review #6).
|
||
let ideal_taps = (3.0 * sigma).ceil();
|
||
let tap_count = ideal_taps.clamp(1.0, 32.0);
|
||
let tap_step = (ideal_taps / tap_count).max(1.0);
|
||
let full_w = self.surface_config.width;
|
||
let full_h = self.surface_config.height;
|
||
let blur_width = (full_w / 2).max(1) as f32;
|
||
let blur_height = (full_h / 2).max(1) as f32;
|
||
|
||
// Limit the half-res passes to the element bounds dilated by the kernel radius (3·sigma,
|
||
// full-res) — outside that the composite never samples, so there's no reason to blur it.
|
||
let dilation = 3.0 * blur_radius;
|
||
let hw = (full_w / 2).max(1);
|
||
let hh = (full_h / 2).max(1);
|
||
let x0 = (((bounds.origin.x.0 - dilation) * 0.5).floor().max(0.0) as u32).min(hw);
|
||
let y0 = (((bounds.origin.y.0 - dilation) * 0.5).floor().max(0.0) as u32).min(hh);
|
||
let x1 = ((((bounds.origin.x.0 + bounds.size.width.0 + dilation) * 0.5)
|
||
.ceil()
|
||
.max(0.0) as u32)
|
||
.min(hw))
|
||
.max(x0);
|
||
let y1 = ((((bounds.origin.y.0 + bounds.size.height.0 + dilation) * 0.5)
|
||
.ceil()
|
||
.max(0.0) as u32)
|
||
.min(hh))
|
||
.max(y0);
|
||
let scissor = [x0, y0, x1 - x0, y1 - y0];
|
||
if scissor[2] == 0 || scissor[3] == 0 {
|
||
return;
|
||
}
|
||
|
||
// Owned handles so the passes below don't borrow `self`.
|
||
let (ping, pong) = {
|
||
let resources = self.resources();
|
||
match (
|
||
resources.blur_ping_view.as_ref(),
|
||
resources.blur_pong_view.as_ref(),
|
||
) {
|
||
(Some(ping), Some(pong)) => (ping.clone(), pong.clone()),
|
||
_ => return,
|
||
}
|
||
};
|
||
|
||
// Downsample source -> ping, then separable gaussian ping -> pong -> ping.
|
||
self.run_blur_pass(
|
||
encoder,
|
||
"blur_downsample",
|
||
&self.resources().pipelines.blur_downsample,
|
||
&ping,
|
||
source,
|
||
BlurParams {
|
||
downsample: 1.0,
|
||
..Default::default()
|
||
},
|
||
scissor,
|
||
);
|
||
self.run_blur_pass(
|
||
encoder,
|
||
"blur_horizontal",
|
||
&self.resources().pipelines.blur,
|
||
&pong,
|
||
&ping,
|
||
BlurParams {
|
||
direction: [1.0 / blur_width, 0.0],
|
||
sigma,
|
||
tap_count,
|
||
tap_step,
|
||
..Default::default()
|
||
},
|
||
scissor,
|
||
);
|
||
self.run_blur_pass(
|
||
encoder,
|
||
"blur_vertical",
|
||
&self.resources().pipelines.blur,
|
||
&ping,
|
||
&pong,
|
||
BlurParams {
|
||
direction: [0.0, 1.0 / blur_height],
|
||
sigma,
|
||
tap_count,
|
||
tap_step,
|
||
..Default::default()
|
||
},
|
||
scissor,
|
||
);
|
||
|
||
// Composite the blurred result into the target (loads existing content). For content blur
|
||
// the quad covers the dilated region so the blur can fade out past the element box (no
|
||
// sharp clip); for backdrop the quad is the element bounds and the shader clips to the
|
||
// rounded rect.
|
||
let composite_bounds = if clip_rounded {
|
||
bounds
|
||
} else {
|
||
bounds.dilate(ScaledPixels(dilation))
|
||
};
|
||
let params = BlurParams {
|
||
bounds: composite_bounds.into(),
|
||
content_mask: content_mask.into(),
|
||
corner_radii,
|
||
opacity,
|
||
clip_rounded: if clip_rounded { 1.0 } else { 0.0 },
|
||
..Default::default()
|
||
};
|
||
let bind_group = self.make_blur_bind_group(params, &ping);
|
||
let resources = self.resources();
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("blur_composite"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: target,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Load,
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
depth_slice: None,
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
pass.set_pipeline(&resources.pipelines.blur_composite);
|
||
pass.set_bind_group(0, &resources.globals_bind_group, &[]);
|
||
pass.set_bind_group(1, &bind_group, &[]);
|
||
pass.draw(0..4, 0..1);
|
||
}
|
||
|
||
/// Blur the scene painted so far behind `filter.bounds` and composite it back as frosted glass.
|
||
fn draw_backdrop_filter(
|
||
&self,
|
||
encoder: &mut wgpu::CommandEncoder,
|
||
filter: &BackdropFilter,
|
||
scene_color_view: &wgpu::TextureView,
|
||
) {
|
||
self.blur_and_composite(
|
||
encoder,
|
||
scene_color_view,
|
||
scene_color_view,
|
||
filter.bounds,
|
||
filter.content_mask.bounds,
|
||
[
|
||
filter.corner_radii.top_left.0,
|
||
filter.corner_radii.top_right.0,
|
||
filter.corner_radii.bottom_right.0,
|
||
filter.corner_radii.bottom_left.0,
|
||
],
|
||
max_blur_radius(&filter.filters),
|
||
filter.opacity,
|
||
true,
|
||
);
|
||
}
|
||
|
||
/// Copy the offscreen scene texture into the swapchain texture.
|
||
fn blit_to_frame(
|
||
&self,
|
||
encoder: &mut wgpu::CommandEncoder,
|
||
source: &wgpu::TextureView,
|
||
frame_view: &wgpu::TextureView,
|
||
) {
|
||
let bind_group = self.make_blur_bind_group(BlurParams::default(), source);
|
||
let resources = self.resources();
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("scene_blit"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: frame_view,
|
||
resolve_target: None,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
depth_slice: None,
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
pass.set_pipeline(&resources.pipelines.blur_downsample);
|
||
pass.set_bind_group(0, &resources.globals_bind_group, &[]);
|
||
pass.set_bind_group(1, &bind_group, &[]);
|
||
pass.draw(0..3, 0..1);
|
||
}
|
||
|
||
fn draw_polychrome_sprites(
|
||
&self,
|
||
sprites: &[PolychromeSprite],
|
||
texture_id: AtlasTextureId,
|
||
instance_offset: &mut u64,
|
||
pass: &mut wgpu::RenderPass<'_>,
|
||
) -> bool {
|
||
let tex_info = self.atlas.get_texture_info(texture_id);
|
||
let data = unsafe { Self::instance_bytes(sprites) };
|
||
self.draw_instances_with_texture(
|
||
data,
|
||
sprites.len() as u32,
|
||
&tex_info.view,
|
||
&self.resources().pipelines.poly_sprites,
|
||
instance_offset,
|
||
pass,
|
||
)
|
||
}
|
||
|
||
fn draw_instances(
|
||
&self,
|
||
data: &[u8],
|
||
instance_count: u32,
|
||
pipeline: &wgpu::RenderPipeline,
|
||
instance_offset: &mut u64,
|
||
pass: &mut wgpu::RenderPass<'_>,
|
||
) -> bool {
|
||
if instance_count == 0 {
|
||
return true;
|
||
}
|
||
let Some((offset, size)) = self.write_to_instance_buffer(instance_offset, data) else {
|
||
return false;
|
||
};
|
||
let resources = self.resources();
|
||
let bind_group = resources
|
||
.device
|
||
.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: None,
|
||
layout: &resources.bind_group_layouts.instances,
|
||
entries: &[wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: self.instance_binding(offset, size),
|
||
}],
|
||
});
|
||
pass.set_pipeline(pipeline);
|
||
pass.set_bind_group(0, &resources.globals_bind_group, &[]);
|
||
pass.set_bind_group(1, &bind_group, &[]);
|
||
pass.draw(0..4, 0..instance_count);
|
||
true
|
||
}
|
||
|
||
fn draw_instances_with_texture(
|
||
&self,
|
||
data: &[u8],
|
||
instance_count: u32,
|
||
texture_view: &wgpu::TextureView,
|
||
pipeline: &wgpu::RenderPipeline,
|
||
instance_offset: &mut u64,
|
||
pass: &mut wgpu::RenderPass<'_>,
|
||
) -> bool {
|
||
if instance_count == 0 {
|
||
return true;
|
||
}
|
||
let Some((offset, size)) = self.write_to_instance_buffer(instance_offset, data) else {
|
||
return false;
|
||
};
|
||
let resources = self.resources();
|
||
let bind_group = resources
|
||
.device
|
||
.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: None,
|
||
layout: &resources.bind_group_layouts.instances_with_texture,
|
||
entries: &[
|
||
wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: self.instance_binding(offset, size),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 1,
|
||
resource: wgpu::BindingResource::TextureView(texture_view),
|
||
},
|
||
wgpu::BindGroupEntry {
|
||
binding: 2,
|
||
resource: wgpu::BindingResource::Sampler(&resources.atlas_sampler),
|
||
},
|
||
],
|
||
});
|
||
pass.set_pipeline(pipeline);
|
||
pass.set_bind_group(0, &resources.globals_bind_group, &[]);
|
||
pass.set_bind_group(1, &bind_group, &[]);
|
||
pass.draw(0..4, 0..instance_count);
|
||
true
|
||
}
|
||
|
||
unsafe fn instance_bytes<T>(instances: &[T]) -> &[u8] {
|
||
unsafe {
|
||
std::slice::from_raw_parts(
|
||
instances.as_ptr() as *const u8,
|
||
std::mem::size_of_val(instances),
|
||
)
|
||
}
|
||
}
|
||
|
||
fn draw_paths_from_intermediate(
|
||
&self,
|
||
paths: &[Path<ScaledPixels>],
|
||
instance_offset: &mut u64,
|
||
pass: &mut wgpu::RenderPass<'_>,
|
||
) -> bool {
|
||
let first_path = &paths[0];
|
||
let sprites: Vec<PathSprite> = if paths.last().map(|p| &p.order) == Some(&first_path.order)
|
||
{
|
||
paths
|
||
.iter()
|
||
.map(|p| PathSprite {
|
||
bounds: p.clipped_bounds(),
|
||
})
|
||
.collect()
|
||
} else {
|
||
let mut bounds = first_path.clipped_bounds();
|
||
for path in paths.iter().skip(1) {
|
||
bounds = bounds.union(&path.clipped_bounds());
|
||
}
|
||
vec![PathSprite { bounds }]
|
||
};
|
||
|
||
let resources = self.resources();
|
||
let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else {
|
||
return true;
|
||
};
|
||
|
||
let sprite_data = unsafe { Self::instance_bytes(&sprites) };
|
||
self.draw_instances_with_texture(
|
||
sprite_data,
|
||
sprites.len() as u32,
|
||
path_intermediate_view,
|
||
&resources.pipelines.paths,
|
||
instance_offset,
|
||
pass,
|
||
)
|
||
}
|
||
|
||
fn draw_paths_to_intermediate(
|
||
&self,
|
||
encoder: &mut wgpu::CommandEncoder,
|
||
paths: &[Path<ScaledPixels>],
|
||
instance_offset: &mut u64,
|
||
) -> bool {
|
||
let mut vertices = Vec::new();
|
||
for path in paths {
|
||
let bounds = path.clipped_bounds();
|
||
vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex {
|
||
xy_position: v.xy_position,
|
||
st_position: v.st_position,
|
||
color: path.color,
|
||
bounds,
|
||
}));
|
||
}
|
||
|
||
if vertices.is_empty() {
|
||
return true;
|
||
}
|
||
|
||
let vertex_data = unsafe { Self::instance_bytes(&vertices) };
|
||
let Some((vertex_offset, vertex_size)) =
|
||
self.write_to_instance_buffer(instance_offset, vertex_data)
|
||
else {
|
||
return false;
|
||
};
|
||
|
||
let resources = self.resources();
|
||
let data_bind_group = resources
|
||
.device
|
||
.create_bind_group(&wgpu::BindGroupDescriptor {
|
||
label: Some("path_rasterization_bind_group"),
|
||
layout: &resources.bind_group_layouts.instances,
|
||
entries: &[wgpu::BindGroupEntry {
|
||
binding: 0,
|
||
resource: self.instance_binding(vertex_offset, vertex_size),
|
||
}],
|
||
});
|
||
|
||
let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else {
|
||
return true;
|
||
};
|
||
|
||
let (target_view, resolve_target) = if let Some(ref msaa_view) = resources.path_msaa_view {
|
||
(msaa_view, Some(path_intermediate_view))
|
||
} else {
|
||
(path_intermediate_view, None)
|
||
};
|
||
|
||
{
|
||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||
label: Some("path_rasterization_pass"),
|
||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||
view: target_view,
|
||
resolve_target,
|
||
ops: wgpu::Operations {
|
||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||
store: wgpu::StoreOp::Store,
|
||
},
|
||
depth_slice: None,
|
||
})],
|
||
depth_stencil_attachment: None,
|
||
..Default::default()
|
||
});
|
||
|
||
pass.set_pipeline(&resources.pipelines.path_rasterization);
|
||
pass.set_bind_group(0, &resources.path_globals_bind_group, &[]);
|
||
pass.set_bind_group(1, &data_bind_group, &[]);
|
||
pass.draw(0..vertices.len() as u32, 0..1);
|
||
}
|
||
|
||
true
|
||
}
|
||
|
||
fn grow_instance_buffer(&mut self) {
|
||
let new_capacity = (self.instance_buffer_capacity * 2).min(self.max_buffer_size);
|
||
log::info!("increased instance buffer size to {}", new_capacity);
|
||
let resources = self.resources_mut();
|
||
resources.instance_buffer = resources.device.create_buffer(&wgpu::BufferDescriptor {
|
||
label: Some("instance_buffer"),
|
||
size: new_capacity,
|
||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||
mapped_at_creation: false,
|
||
});
|
||
self.instance_buffer_capacity = new_capacity;
|
||
}
|
||
|
||
fn write_to_instance_buffer(
|
||
&self,
|
||
instance_offset: &mut u64,
|
||
data: &[u8],
|
||
) -> Option<(u64, NonZeroU64)> {
|
||
let offset = (*instance_offset).next_multiple_of(self.storage_buffer_alignment);
|
||
let size = (data.len() as u64).max(16);
|
||
if offset + size > self.instance_buffer_capacity {
|
||
return None;
|
||
}
|
||
let resources = self.resources();
|
||
resources
|
||
.queue
|
||
.write_buffer(&resources.instance_buffer, offset, data);
|
||
*instance_offset = offset + size;
|
||
Some((offset, NonZeroU64::new(size).expect("size is at least 16")))
|
||
}
|
||
|
||
fn instance_binding(&self, offset: u64, size: NonZeroU64) -> wgpu::BindingResource<'_> {
|
||
wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||
buffer: &self.resources().instance_buffer,
|
||
offset,
|
||
size: Some(size),
|
||
})
|
||
}
|
||
|
||
/// Mark the surface as unconfigured so rendering is skipped until a new
|
||
/// surface is provided via [`replace_surface`](Self::replace_surface).
|
||
///
|
||
/// This does **not** drop the renderer — the device, queue, atlas, and
|
||
/// pipelines stay alive. Use this when the native window is destroyed
|
||
/// (e.g. Android `TerminateWindow`) but you intend to re-create the
|
||
/// surface later without losing cached atlas textures.
|
||
pub fn unconfigure_surface(&mut self) {
|
||
self.surface_configured = false;
|
||
// Drop intermediate textures since they reference the old surface size.
|
||
if let Some(res) = self.resources.as_mut() {
|
||
res.invalidate_intermediate_textures();
|
||
}
|
||
}
|
||
|
||
/// Replace the wgpu surface with a new one (e.g. after Android destroys
|
||
/// and recreates the native window). Keeps the device, queue, atlas, and
|
||
/// all pipelines intact so cached `AtlasTextureId`s remain valid.
|
||
///
|
||
/// The `instance` **must** be the same [`wgpu::Instance`] that was used to
|
||
/// create the adapter and device (i.e. from the [`WgpuContext`]). Using a
|
||
/// different instance will cause a "Device does not exist" panic because
|
||
/// the wgpu device is bound to its originating instance.
|
||
#[cfg(not(target_family = "wasm"))]
|
||
pub fn replace_surface<W: HasWindowHandle>(
|
||
&mut self,
|
||
window: &W,
|
||
config: WgpuSurfaceConfig,
|
||
instance: &wgpu::Instance,
|
||
) -> anyhow::Result<()> {
|
||
let window_handle = window
|
||
.window_handle()
|
||
.map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?;
|
||
|
||
let surface = create_surface(instance, window_handle.as_raw())?;
|
||
|
||
let width = (config.size.width.0 as u32).max(1);
|
||
let height = (config.size.height.0 as u32).max(1);
|
||
|
||
let alpha_mode = if config.transparent {
|
||
self.transparent_alpha_mode
|
||
} else {
|
||
self.opaque_alpha_mode
|
||
};
|
||
|
||
self.surface_config.width = width;
|
||
self.surface_config.height = height;
|
||
self.surface_config.alpha_mode = alpha_mode;
|
||
if let Some(mode) = config.preferred_present_mode {
|
||
self.surface_config.present_mode = mode;
|
||
}
|
||
|
||
{
|
||
let res = self
|
||
.resources
|
||
.as_mut()
|
||
.expect("GPU resources not available");
|
||
surface.configure(&res.device, &self.surface_config);
|
||
res.surface = surface;
|
||
|
||
// Invalidate intermediate textures — they'll be recreated lazily.
|
||
res.invalidate_intermediate_textures();
|
||
}
|
||
|
||
self.surface_configured = true;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
pub fn destroy(&mut self) {
|
||
// Release surface-bound GPU resources eagerly so the underlying native
|
||
// window can be destroyed before the renderer itself is dropped.
|
||
self.resources.take();
|
||
}
|
||
|
||
/// Returns true if the GPU device was lost and recovery is needed.
|
||
pub fn device_lost(&self) -> bool {
|
||
self.device_lost.load(std::sync::atomic::Ordering::SeqCst)
|
||
}
|
||
|
||
/// Returns true if a redraw is needed because GPU state was cleared.
|
||
/// Calling this method clears the flag.
|
||
pub fn needs_redraw(&mut self) -> bool {
|
||
std::mem::take(&mut self.needs_redraw)
|
||
}
|
||
|
||
/// Recovers from a lost GPU device by recreating the renderer with a new context.
|
||
///
|
||
/// Call this after detecting `device_lost()` returns true.
|
||
///
|
||
/// This method coordinates recovery across multiple windows:
|
||
/// - The first window to call this will recreate the shared context
|
||
/// - Subsequent windows will adopt the already-recovered context
|
||
#[cfg(not(target_family = "wasm"))]
|
||
pub fn recover<W>(&mut self, window: &W) -> anyhow::Result<()>
|
||
where
|
||
W: HasWindowHandle + HasDisplayHandle + std::fmt::Debug + Send + Sync + Clone + 'static,
|
||
{
|
||
let gpu_context = self.context.as_ref().expect("recover requires gpu_context");
|
||
|
||
// Check if another window already recovered the context
|
||
let needs_new_context = gpu_context
|
||
.borrow()
|
||
.as_ref()
|
||
.is_none_or(|ctx| ctx.device_lost());
|
||
|
||
let window_handle = window
|
||
.window_handle()
|
||
.map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?;
|
||
|
||
let surface = if needs_new_context {
|
||
log::warn!("GPU device lost, recreating context...");
|
||
|
||
// Drop old resources to release Arc<Device>/Arc<Queue> and GPU resources
|
||
self.resources = None;
|
||
*gpu_context.borrow_mut() = None;
|
||
|
||
// Wait briefly for the GPU driver to stabilize, then try to
|
||
// recreate the context without software renderers. If this fails
|
||
// the caller should request another frame and retry — the real GPU
|
||
// may need more time to come back (e.g. after suspend/resume).
|
||
std::thread::sleep(std::time::Duration::from_millis(350));
|
||
|
||
let instance = WgpuContext::instance(Box::new(window.clone()));
|
||
let surface = create_surface(&instance, window_handle.as_raw())?;
|
||
let new_context = WgpuContext::new_rejecting_software(
|
||
instance,
|
||
&surface,
|
||
self.compositor_gpu,
|
||
self.extra_requirements.as_ref(),
|
||
)?;
|
||
*gpu_context.borrow_mut() = Some(new_context);
|
||
surface
|
||
} else {
|
||
let ctx_ref = gpu_context.borrow();
|
||
let instance = &ctx_ref.as_ref().unwrap().instance;
|
||
create_surface(instance, window_handle.as_raw())?
|
||
};
|
||
|
||
let config = WgpuSurfaceConfig {
|
||
size: gpui::Size {
|
||
width: gpui::DevicePixels(self.surface_config.width as i32),
|
||
height: gpui::DevicePixels(self.surface_config.height as i32),
|
||
},
|
||
transparent: self.surface_config.alpha_mode != wgpu::CompositeAlphaMode::Opaque,
|
||
preferred_present_mode: Some(self.surface_config.present_mode),
|
||
};
|
||
let gpu_context = Rc::clone(gpu_context);
|
||
let ctx_ref = gpu_context.borrow();
|
||
let context = ctx_ref.as_ref().expect("context should exist");
|
||
|
||
self.resources = None;
|
||
self.atlas.handle_device_lost(context);
|
||
|
||
let extra_reqs = self.extra_requirements.clone();
|
||
*self = Self::new_internal(
|
||
Some(gpu_context.clone()),
|
||
context,
|
||
surface,
|
||
config,
|
||
self.compositor_gpu,
|
||
extra_reqs,
|
||
self.atlas.clone(),
|
||
)?;
|
||
|
||
log::info!("GPU recovery complete");
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[cfg(not(target_family = "wasm"))]
|
||
fn create_surface(
|
||
instance: &wgpu::Instance,
|
||
raw_window_handle: raw_window_handle::RawWindowHandle,
|
||
) -> anyhow::Result<wgpu::Surface<'static>> {
|
||
unsafe {
|
||
instance
|
||
.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
|
||
// Fall back to the display handle already provided via InstanceDescriptor::display.
|
||
raw_display_handle: None,
|
||
raw_window_handle,
|
||
})
|
||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||
}
|
||
}
|
||
|
||
struct RenderingParameters {
|
||
path_sample_count: u32,
|
||
gamma_ratios: [f32; 4],
|
||
grayscale_enhanced_contrast: f32,
|
||
subpixel_enhanced_contrast: f32,
|
||
}
|
||
|
||
impl RenderingParameters {
|
||
fn new(adapter: &wgpu::Adapter, surface_format: wgpu::TextureFormat) -> Self {
|
||
use std::env;
|
||
|
||
let format_features = adapter.get_texture_format_features(surface_format);
|
||
let path_sample_count = [4, 2, 1]
|
||
.into_iter()
|
||
.find(|&n| format_features.flags.sample_count_supported(n))
|
||
.unwrap_or(1);
|
||
|
||
let gamma = env::var("ZED_FONTS_GAMMA")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(1.8_f32)
|
||
.clamp(1.0, 2.2);
|
||
let gamma_ratios = get_gamma_correction_ratios(gamma);
|
||
|
||
let grayscale_enhanced_contrast = env::var("ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(1.0_f32)
|
||
.max(0.0);
|
||
|
||
let subpixel_enhanced_contrast = env::var("ZED_FONTS_SUBPIXEL_ENHANCED_CONTRAST")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(0.5_f32)
|
||
.max(0.0);
|
||
|
||
Self {
|
||
path_sample_count,
|
||
gamma_ratios,
|
||
grayscale_enhanced_contrast,
|
||
subpixel_enhanced_contrast,
|
||
}
|
||
}
|
||
}
|