use crate::{CompositorGpuHint, WgpuAtlas, WgpuContext}; use anyhow::{Context as _, Result}; use bytemuck::{Pod, Zeroable}; use gpui::{ AtlasTextureId, Background, Bounds, DevicePixels, GpuSpecs, Path, Point, PrimitiveBatch, ScaledPixels, Scene, Size, get_gamma_correction_ratios, quad_depth, }; use log::warn; #[cfg(not(target_family = "wasm"))] use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use std::cell::RefCell; use std::num::NonZeroU64; use std::ops::Range; 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> for PodBounds { fn from(bounds: Bounds) -> 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, is_bgr: u32, _pad: u32, } #[derive(Clone, Debug)] #[repr(C)] struct PathSprite { bounds: Bounds, } #[derive(Clone, Debug)] #[repr(C)] struct PathRasterizationVertex { xy_position: Point, st_position: Point, color: Background, bounds: Bounds, } pub struct WgpuSurfaceConfig { pub size: Size, 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, } const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; struct WgpuPipelines { quads: wgpu::RenderPipeline, opaque_quads: wgpu::RenderPipeline, shadows: wgpu::RenderPipeline, path_rasterization: wgpu::RenderPipeline, paths: wgpu::RenderPipeline, underlines: wgpu::RenderPipeline, mono_sprites: wgpu::RenderPipeline, subpixel_sprites: Option, poly_sprites: wgpu::RenderPipeline, #[allow(dead_code)] surfaces: wgpu::RenderPipeline, } #[derive(Clone)] struct BufferRegion { buffer: wgpu::Buffer, offset: u64, size: NonZeroU64, } impl BufferRegion { fn binding(&self) -> wgpu::BindingResource<'_> { wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &self.buffer, offset: self.offset, size: Some(self.size), }) } } struct FrameInstances { quads: wgpu::BindGroup, shadows: wgpu::BindGroup, underlines: wgpu::BindGroup, monochrome_sprites: wgpu::BindGroup, subpixel_sprites: wgpu::BindGroup, polychrome_sprites: wgpu::BindGroup, } struct WgpuBindGroupLayouts { globals: wgpu::BindGroupLayout, instances: wgpu::BindGroupLayout, quads: wgpu::BindGroupLayout, texture: wgpu::BindGroupLayout, surfaces: wgpu::BindGroupLayout, } /// Shared GPU context reference, used to coordinate device recovery across multiple windows. pub type GpuContext = Rc>>; /// GPU resources that must be dropped together during device recovery. struct WgpuResources { device: Arc, queue: Arc, 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, depth_texture: Option, depth_view: Option, path_intermediate_texture: Option, path_intermediate_view: Option, path_msaa_texture: Option, path_msaa_view: Option, } impl WgpuResources { fn invalidate_intermediate_textures(&mut self) { self.depth_texture = None; self.depth_view = None; 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, /// Compositor GPU hint for adapter selection (unused on WASM). #[allow(dead_code)] compositor_gpu: Option, resources: Option, surface_config: wgpu::SurfaceConfiguration, atlas: Arc, path_globals_offset: u64, gamma_offset: u64, instance_buffer_capacity: u64, max_buffer_size: u64, storage_buffer_alignment: 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>>, failed_frame_count: u32, device_lost: std::sync::Arc, 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( gpu_context: GpuContext, window: &W, config: WgpuSurfaceConfig, compositor_gpu: Option, ) -> anyhow::Result 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 { 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, context: &WgpuContext, surface: wgpu::Surface<'static>, config: WgpuSurfaceConfig, compositor_gpu: Option, atlas: Arc, ) -> anyhow::Result { 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 { 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::() as u64; let gamma_size = std::mem::size_of::() 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>> = 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. depth_texture: None, depth_view: None, 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, 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::() 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::() 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 quads = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("quads_layout"), entries: &[storage_buffer_entry(0), storage_buffer_entry(1)], }); let texture = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("texture_layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, 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: 1, 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::() 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, quads, 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 depth_stencil = |depth_write_enabled: bool| { Some(wgpu::DepthStencilState { format: DEPTH_FORMAT, depth_write_enabled: Some(depth_write_enabled), depth_compare: Some(wgpu::CompareFunction::Greater), stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }) }; let create_pipeline = |name: &str, vs_entry: &str, fs_entry: &str, globals_layout: &wgpu::BindGroupLayout, data_layout: &wgpu::BindGroupLayout, texture_layout: Option<&wgpu::BindGroupLayout>, topology: wgpu::PrimitiveTopology, color_targets: &[Option], depth_stencil: Option, sample_count: u32, module: &wgpu::ShaderModule| { let mut group_layouts = vec![Some(globals_layout), Some(data_layout)]; group_layouts.extend(texture_layout.map(Some)); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some(&format!("{name}_layout")), bind_group_layouts: &group_layouts, 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, 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.quads, None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], depth_stencil(false), 1, &shader_module, ); let opaque_quads = create_pipeline( "opaque_quads", "vs_opaque_quad", "fs_opaque_quad", &layouts.globals, &layouts.quads, None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(wgpu::ColorTargetState { format: surface_format, blend: None, write_mask: wgpu::ColorWrites::ALL, })], depth_stencil(true), 1, &shader_module, ); let shadows = create_pipeline( "shadows", "vs_shadow", "fs_shadow", &layouts.globals, &layouts.instances, None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], depth_stencil(false), 1, &shader_module, ); let path_rasterization = create_pipeline( "path_rasterization", "vs_path_rasterization", "fs_path_rasterization", &layouts.globals, &layouts.instances, None, wgpu::PrimitiveTopology::TriangleList, &[Some(wgpu::ColorTargetState { format: surface_format, blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], None, 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, Some(&layouts.texture), wgpu::PrimitiveTopology::TriangleStrip, &[Some(wgpu::ColorTargetState { format: surface_format, blend: Some(paths_blend), write_mask: wgpu::ColorWrites::ALL, })], depth_stencil(false), 1, &shader_module, ); let underlines = create_pipeline( "underlines", "vs_underline", "fs_underline", &layouts.globals, &layouts.instances, None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], depth_stencil(false), 1, &shader_module, ); let mono_sprites = create_pipeline( "mono_sprites", "vs_mono_sprite", "fs_mono_sprite", &layouts.globals, &layouts.instances, Some(&layouts.texture), wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], depth_stencil(false), 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, Some(&layouts.texture), wgpu::PrimitiveTopology::TriangleStrip, &[Some(wgpu::ColorTargetState { format: surface_format, blend: Some(subpixel_blend), write_mask: wgpu::ColorWrites::COLOR, })], depth_stencil(false), 1, subpixel_module, )) } else { None }; let poly_sprites = create_pipeline( "poly_sprites", "vs_poly_sprite", "fs_poly_sprite", &layouts.globals, &layouts.instances, Some(&layouts.texture), wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target.clone())], depth_stencil(false), 1, &shader_module, ); let surfaces = create_pipeline( "surfaces", "vs_surface", "fs_surface", &layouts.globals, &layouts.surfaces, None, wgpu::PrimitiveTopology::TriangleStrip, &[Some(color_target)], depth_stencil(false), 1, &shader_module, ); WgpuPipelines { quads, opaque_quads, shadows, path_rasterization, paths, underlines, mono_sprites, subpixel_sprites, poly_sprites, surfaces, } } fn create_depth_texture( device: &wgpu::Device, width: u32, height: u32, ) -> (wgpu::Texture, wgpu::TextureView) { let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("depth"), 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: DEPTH_FORMAT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); (texture, view) } 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) { 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 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.depth_texture { texture.destroy(); } 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 (depth_texture, depth_view) = Self::create_depth_texture(&resources.device, width, height); resources.depth_texture = Some(depth_texture); resources.depth_view = Some(depth_view); 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 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 Some(resources) = self.resources.as_mut() else { return; }; 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 { Size { width: DevicePixels(self.surface_config.width as i32), height: DevicePixels(self.surface_config.height as i32), } } pub fn sprite_atlas(&self) -> &Arc { &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) -> 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(); let frame_view = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); let Some(depth_view) = self.resources().depth_view.clone() else { log::error!("depth buffer missing after ensuring intermediate textures"); return false; }; 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), ); } if let Err(error) = self.record_frame(scene, &frame_view, &depth_view) { log::error!("{error:#}"); return false; } frame.present(); true } /// Records and submits the frame's commands. fn record_frame( &mut self, scene: &Scene, frame_view: &wgpu::TextureView, depth_view: &wgpu::TextureView, ) -> Result<()> { let mut instance_offset: u64 = 0; let frame_instances = self .upload_frame_instances(scene, &mut instance_offset) .with_context(|| { format!( "scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} subpixel, {} poly", scene.paths.len(), scene.shadows.len(), scene.quads.len(), scene.underlines.len(), scene.monochrome_sprites.len(), scene.subpixel_sprites.len(), scene.polychrome_sprites.len(), ) })?; 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: Some(wgpu::RenderPassDepthStencilAttachment { view: depth_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0.0), store: wgpu::StoreOp::Store, }), stencil_ops: None, }), ..Default::default() }); self.draw_instances( &frame_instances.quads, &self.resources().pipelines.opaque_quads, scene.blended_quad_indices.len() as u32 ..(scene.blended_quad_indices.len() + scene.opaque_quad_indices.len()) as u32, &mut pass, ); let mut quad_cursor: u32 = 0; for batch in scene.batches() { // Quad shaders assign each quad its own depth. Every other batch // is flattened onto the depth of the quad cursor by collapsing // the viewport's depth range onto a single value. let batch_depth = quad_depth(quad_cursor); if matches!(batch, PrimitiveBatch::Quads { .. }) { self.set_pass_depth_range(&mut pass, 0.0, 1.0); } else { self.set_pass_depth_range(&mut pass, batch_depth, batch_depth); } match batch { PrimitiveBatch::Quads { range, blended_range, } => { quad_cursor += range.len() as u32; self.draw_instances( &frame_instances.quads, &self.resources().pipelines.quads, instance_range(blended_range), &mut pass, ); } PrimitiveBatch::Shadows(range) => self.draw_instances( &frame_instances.shadows, &self.resources().pipelines.shadows, instance_range(range), &mut pass, ), PrimitiveBatch::Paths(range) => { let paths = &scene.paths[range]; if paths.is_empty() { continue; } drop(pass); let rasterized = 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: Some( wgpu::RenderPassDepthStencilAttachment { view: depth_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }), stencil_ops: None, }, ), ..Default::default() }); self.set_pass_depth_range(&mut pass, batch_depth, batch_depth); if rasterized { self.draw_paths_from_intermediate( paths, &mut instance_offset, &mut pass, )?; } } PrimitiveBatch::Underlines(range) => self.draw_instances( &frame_instances.underlines, &self.resources().pipelines.underlines, instance_range(range), &mut pass, ), PrimitiveBatch::MonochromeSprites { texture_id, range } => self.draw_sprites( &frame_instances.monochrome_sprites, texture_id, &self.resources().pipelines.mono_sprites, instance_range(range), &mut pass, ), PrimitiveBatch::SubpixelSprites { texture_id, range } => { let resources = self.resources(); self.draw_sprites( &frame_instances.subpixel_sprites, texture_id, resources .pipelines .subpixel_sprites .as_ref() .unwrap_or(&resources.pipelines.mono_sprites), instance_range(range), &mut pass, ); } PrimitiveBatch::PolychromeSprites { texture_id, range } => self.draw_sprites( &frame_instances.polychrome_sprites, texture_id, &self.resources().pipelines.poly_sprites, instance_range(range), &mut pass, ), // Surfaces are macOS-only for video playback, and are // not implemented for Linux/wgpu. PrimitiveBatch::Surfaces(_surfaces) => {} } } } self.resources() .queue .submit(std::iter::once(encoder.finish())); Ok(()) } fn upload_frame_instances( &mut self, scene: &Scene, instance_offset: &mut u64, ) -> Result { let quads = self.write_to_instance_buffer(instance_offset, unsafe { Self::instance_bytes(&scene.quads) })?; let quad_indices = self.write_parts_to_instance_buffer( instance_offset, &[ bytemuck::cast_slice(&scene.blended_quad_indices), bytemuck::cast_slice(&scene.opaque_quad_indices), ], )?; let shadows = self.write_to_instance_buffer(instance_offset, unsafe { Self::instance_bytes(&scene.shadows) })?; let underlines = self.write_to_instance_buffer(instance_offset, unsafe { Self::instance_bytes(&scene.underlines) })?; let monochrome_sprites = self.write_to_instance_buffer(instance_offset, unsafe { Self::instance_bytes(&scene.monochrome_sprites) })?; let subpixel_sprites = self.write_to_instance_buffer(instance_offset, unsafe { Self::instance_bytes(&scene.subpixel_sprites) })?; let polychrome_sprites = self.write_to_instance_buffer(instance_offset, unsafe { Self::instance_bytes(&scene.polychrome_sprites) })?; let resources = self.resources(); Ok(FrameInstances { quads: resources .device .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("quads_bind_group"), layout: &resources.bind_group_layouts.quads, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: quads.binding(), }, wgpu::BindGroupEntry { binding: 1, resource: quad_indices.binding(), }, ], }), shadows: self.create_instances_bind_group("shadows_bind_group", &shadows), underlines: self.create_instances_bind_group("underlines_bind_group", &underlines), monochrome_sprites: self .create_instances_bind_group("monochrome_sprites_bind_group", &monochrome_sprites), subpixel_sprites: self .create_instances_bind_group("subpixel_sprites_bind_group", &subpixel_sprites), polychrome_sprites: self .create_instances_bind_group("polychrome_sprites_bind_group", &polychrome_sprites), }) } fn create_instances_bind_group( &self, label: &str, instances: &BufferRegion, ) -> wgpu::BindGroup { let resources = self.resources(); resources .device .create_bind_group(&wgpu::BindGroupDescriptor { label: Some(label), layout: &resources.bind_group_layouts.instances, entries: &[wgpu::BindGroupEntry { binding: 0, resource: instances.binding(), }], }) } fn create_texture_bind_group(&self, texture_view: &wgpu::TextureView) -> wgpu::BindGroup { let resources = self.resources(); resources .device .create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &resources.bind_group_layouts.texture, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(texture_view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&resources.atlas_sampler), }, ], }) } fn set_pass_depth_range( &self, pass: &mut wgpu::RenderPass<'_>, min_depth: f32, max_depth: f32, ) { pass.set_viewport( 0.0, 0.0, self.surface_config.width as f32, self.surface_config.height as f32, min_depth, max_depth, ); } fn draw_instances( &self, bind_group: &wgpu::BindGroup, pipeline: &wgpu::RenderPipeline, instances: Range, pass: &mut wgpu::RenderPass<'_>, ) { if instances.is_empty() { return; } pass.set_pipeline(pipeline); pass.set_bind_group(0, &self.resources().globals_bind_group, &[]); pass.set_bind_group(1, bind_group, &[]); pass.draw(0..4, instances); } fn draw_sprites( &self, sprite_instances: &wgpu::BindGroup, texture_id: AtlasTextureId, pipeline: &wgpu::RenderPipeline, instances: Range, pass: &mut wgpu::RenderPass<'_>, ) { if instances.is_empty() { return; } let texture_info = self.atlas.get_texture_info(texture_id); let texture = self.create_texture_bind_group(&texture_info.view); pass.set_pipeline(pipeline); pass.set_bind_group(0, &self.resources().globals_bind_group, &[]); pass.set_bind_group(1, sprite_instances, &[]); pass.set_bind_group(2, &texture, &[]); pass.draw(0..4, instances); } fn draw_paths_to_intermediate( &mut self, encoder: &mut wgpu::CommandEncoder, paths: &[Path], instance_offset: &mut u64, ) -> Result { 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 Ok(false); } let vertex_data = unsafe { Self::instance_bytes(&vertices) }; let vertex_instances = self.write_to_instance_buffer(instance_offset, vertex_data)?; let data_bind_group = self.create_instances_bind_group("path_rasterization_bind_group", &vertex_instances); let resources = self.resources(); let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else { return Ok(false); }; 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); } Ok(true) } fn draw_paths_from_intermediate( &mut self, paths: &[Path], instance_offset: &mut u64, pass: &mut wgpu::RenderPass<'_>, ) -> Result<()> { let first_path = &paths[0]; let sprites: Vec = 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 Some(path_intermediate_view) = self.resources().path_intermediate_view.clone() else { return Ok(()); }; let sprite_data = unsafe { Self::instance_bytes(&sprites) }; let sprite_instances = self.write_to_instance_buffer(instance_offset, sprite_data)?; let instances = self.create_instances_bind_group("path_sprites_bind_group", &sprite_instances); let texture = self.create_texture_bind_group(&path_intermediate_view); let resources = self.resources(); pass.set_pipeline(&resources.pipelines.paths); pass.set_bind_group(0, &resources.globals_bind_group, &[]); pass.set_bind_group(1, &instances, &[]); pass.set_bind_group(2, &texture, &[]); pass.draw(0..4, 0..sprites.len() as u32); Ok(()) } unsafe fn instance_bytes(instances: &[T]) -> &[u8] { unsafe { std::slice::from_raw_parts( instances.as_ptr() as *const u8, std::mem::size_of_val(instances), ) } } fn write_to_instance_buffer( &mut self, instance_offset: &mut u64, data: &[u8], ) -> Result { self.write_parts_to_instance_buffer(instance_offset, &[data]) } fn write_parts_to_instance_buffer( &mut self, instance_offset: &mut u64, parts: &[&[u8]], ) -> Result { let size = parts .iter() .map(|part| part.len() as u64) .sum::() .max(16); let mut offset = (*instance_offset).next_multiple_of(self.storage_buffer_alignment); if offset + size > self.instance_buffer_capacity { self.grow_instance_buffer(size)?; offset = 0; } let resources = self.resources(); let mut part_offset = offset; for part in parts { if part.is_empty() { continue; } resources .queue .write_buffer(&resources.instance_buffer, part_offset, part); part_offset += part.len() as u64; } *instance_offset = offset + size; Ok(BufferRegion { buffer: resources.instance_buffer.clone(), offset, size: NonZeroU64::new(size).expect("size is at least 16"), }) } fn grow_instance_buffer(&mut self, required: u64) -> Result<()> { let capacity = (self.instance_buffer_capacity * 2) .max(required.next_power_of_two()) .min(self.max_buffer_size); anyhow::ensure!( capacity >= required, "instance buffer needs {required} bytes, above the device maximum of {}", self.max_buffer_size ); log::debug!( "instance buffer grown from {} to {capacity}", self.instance_buffer_capacity ); let resources = self.resources_mut(); resources.instance_buffer = resources.device.create_buffer(&wgpu::BufferDescriptor { label: Some("instance_buffer"), size: capacity, usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); self.instance_buffer_capacity = capacity; Ok(()) } /// 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( &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(&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/Arc 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)?; *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(()) } } fn instance_range(range: Range) -> Range { range.start as u32..range.end as u32 } #[cfg(not(target_family = "wasm"))] fn create_surface( instance: &wgpu::Instance, raw_window_handle: raw_window_handle::RawWindowHandle, ) -> anyhow::Result> { 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, } } }