1897 lines
68 KiB
Rust
1897 lines
68 KiB
Rust
use crate::{CompositorGpuHint, WgpuAtlas, WgpuContext};
|
|
use bytemuck::{Pod, Zeroable};
|
|
use gpui::{
|
|
AtlasTextureId, Background, Bounds, DevicePixels, GpuSpecs, MonochromeSprite, Path, Point,
|
|
PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, SubpixelSprite,
|
|
Underline, get_gamma_correction_ratios,
|
|
};
|
|
use log::warn;
|
|
#[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, 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,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Pod, Zeroable)]
|
|
struct GammaParams {
|
|
gamma_ratios: [f32; 4],
|
|
grayscale_enhanced_contrast: f32,
|
|
subpixel_enhanced_contrast: f32,
|
|
_pad: [f32; 2],
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
struct WgpuBindGroupLayouts {
|
|
globals: wgpu::BindGroupLayout,
|
|
instances: wgpu::BindGroupLayout,
|
|
instances_with_texture: wgpu::BindGroupLayout,
|
|
surfaces: 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,
|
|
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>,
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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>,
|
|
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,
|
|
rendering_params: RenderingParameters,
|
|
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>,
|
|
) -> 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)?),
|
|
};
|
|
|
|
let atlas = Arc::new(WgpuAtlas::from_context(context));
|
|
|
|
Self::new_internal(
|
|
Some(Rc::clone(&gpu_context)),
|
|
context,
|
|
surface,
|
|
config,
|
|
compositor_gpu,
|
|
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, atlas)
|
|
}
|
|
|
|
fn new_internal(
|
|
gpu_context: Option<GpuContext>,
|
|
context: &WgpuContext,
|
|
surface: wgpu::Surface<'static>,
|
|
config: WgpuSurfaceConfig,
|
|
compositor_gpu: Option<CompositorGpuHint>,
|
|
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 uniform_alignment = device.limits().min_uniform_buffer_offset_alignment as u64;
|
|
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,
|
|
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,
|
|
};
|
|
|
|
Ok(Self {
|
|
context: gpu_context,
|
|
compositor_gpu,
|
|
resources: Some(resources),
|
|
surface_config,
|
|
atlas,
|
|
path_globals_offset,
|
|
gamma_offset,
|
|
instance_buffer_capacity: initial_instance_buffer_capacity,
|
|
max_buffer_size,
|
|
storage_buffer_alignment,
|
|
rendering_params,
|
|
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::Texture {
|
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
|
view_dimension: wgpu::TextureViewDimension::D2,
|
|
multisampled: false,
|
|
},
|
|
count: None,
|
|
},
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 3,
|
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
|
count: None,
|
|
},
|
|
],
|
|
});
|
|
|
|
WgpuBindGroupLayouts {
|
|
globals,
|
|
instances,
|
|
instances_with_texture,
|
|
surfaces,
|
|
}
|
|
}
|
|
|
|
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,
|
|
);
|
|
|
|
WgpuPipelines {
|
|
quads,
|
|
shadows,
|
|
path_rasterization,
|
|
paths,
|
|
underlines,
|
|
mono_sprites,
|
|
subpixel_sprites,
|
|
poly_sprites,
|
|
surfaces,
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
let resources = self.resources_mut();
|
|
|
|
// 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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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_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) {
|
|
// 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;
|
|
}
|
|
|
|
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 > 5 {
|
|
if let Some(res) = self.resources.as_mut() {
|
|
res.invalidate_intermediate_textures();
|
|
}
|
|
self.atlas.clear();
|
|
self.needs_redraw = true;
|
|
return;
|
|
} else if self.failed_frame_count > 10 {
|
|
panic!("Too many consecutive GPU errors. Last error: {error}");
|
|
}
|
|
} 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;
|
|
}
|
|
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;
|
|
}
|
|
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
|
|
return;
|
|
}
|
|
wgpu::CurrentSurfaceTexture::Validation => {
|
|
*self.last_error.lock().unwrap() =
|
|
Some("Surface texture validation error".to_string());
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Now that we know the surface is healthy, ensure intermediate textures exist
|
|
self.ensure_intermediate_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,
|
|
_pad: [0.0; 2],
|
|
};
|
|
|
|
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;
|
|
let mut overflow = false;
|
|
|
|
let mut encoder =
|
|
self.resources()
|
|
.device
|
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: Some("main_encoder"),
|
|
});
|
|
|
|
{
|
|
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
label: Some("main_pass"),
|
|
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()
|
|
});
|
|
|
|
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: &frame_view,
|
|
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(_surfaces) => {
|
|
// Surfaces are macOS-only for video playback
|
|
// Not implemented for Linux/wgpu
|
|
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;
|
|
}
|
|
self.grow_instance_buffer();
|
|
continue;
|
|
}
|
|
|
|
self.resources()
|
|
.queue
|
|
.submit(std::iter::once(encoder.finish()));
|
|
frame.present();
|
|
return;
|
|
}
|
|
}
|
|
|
|
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,
|
|
)
|
|
}
|
|
|
|
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 for GPU driver to stabilize (350ms copied from windows :shrug:)
|
|
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(instance, &surface, self.compositor_gpu)?;
|
|
*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);
|
|
|
|
*self = Self::new_internal(
|
|
Some(gpu_context.clone()),
|
|
context,
|
|
surface,
|
|
config,
|
|
self.compositor_gpu,
|
|
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,
|
|
}
|
|
}
|
|
}
|