Skip to main content

gpui_wgpu/
wgpu_renderer.rs

1// Modified for gpui-pre (snapshot of zed@d2a4fb6): adapt device queues and sRGB detection to wgpu 30.0.1.
2use crate::{CompositorGpuHint, DeviceErrorState, WgpuAtlas, WgpuContext};
3use anyhow::{Context as _, Result};
4use bytemuck::{Pod, Zeroable};
5use collections::FxHashMap;
6use gpui::{
7    AtlasTextureId, Background, Bounds, DevicePixels, GpuSpecs, Path, Point, PrimitiveBatch,
8    ScaledPixels, Scene, Size, get_gamma_correction_ratios,
9};
10use log::warn;
11#[cfg(not(target_family = "wasm"))]
12use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
13use smallvec::SmallVec;
14use std::cell::RefCell;
15use std::num::NonZeroU64;
16use std::ops::Range;
17use std::rc::Rc;
18use std::sync::Arc;
19
20const MAX_INSTANCE_BUFFER_SIZE: u64 = 256 * 1024 * 1024;
21
22const INSTANCE_TEXTURE_TEXEL_SIZE: u64 = 16;
23
24/// Shader variant for backends with storage buffer support: the shared shader
25/// logic plus the storage-buffer instance transport.
26const STORAGE_BUFFER_SHADERS: &str = concat!(
27    include_str!("shaders.wgsl"),
28    include_str!("shaders_storage.wgsl"),
29);
30
31/// Shader variant for WebGL2, which has no storage buffers: the shared shader
32/// logic plus the texture-based instance transport.
33const WEBGL_SHADERS: &str = concat!(
34    include_str!("shaders.wgsl"),
35    include_str!("shaders_webgl.wgsl"),
36);
37
38/// Subpixel text rendering requires dual-source blending, which WebGL2 lacks, so
39/// this variant only ever runs with the storage-buffer transport. The `enable`
40/// directive must precede all declarations.
41const SUBPIXEL_SHADERS: &str = concat!(
42    "enable dual_source_blending;\n",
43    include_str!("shaders.wgsl"),
44    include_str!("shaders_storage.wgsl"),
45    include_str!("shaders_subpixel.wgsl"),
46);
47
48fn least_common_multiple(left: u64, right: u64) -> u64 {
49    let mut first = left;
50    let mut second = right;
51    while second != 0 {
52        let remainder = first % second;
53        first = second;
54        second = remainder;
55    }
56    left / first * right
57}
58
59#[repr(C)]
60#[derive(Clone, Copy, Pod, Zeroable)]
61struct GlobalParams {
62    viewport_size: [f32; 2],
63    premultiplied_alpha: u32,
64    pad: u32,
65}
66
67#[repr(C)]
68#[derive(Clone, Copy, Pod, Zeroable)]
69struct PodBounds {
70    origin: [f32; 2],
71    size: [f32; 2],
72}
73
74impl From<Bounds<ScaledPixels>> for PodBounds {
75    fn from(bounds: Bounds<ScaledPixels>) -> Self {
76        Self {
77            origin: [bounds.origin.x.0, bounds.origin.y.0],
78            size: [bounds.size.width.0, bounds.size.height.0],
79        }
80    }
81}
82
83#[repr(C)]
84#[derive(Clone, Copy, Pod, Zeroable)]
85struct SurfaceParams {
86    bounds: PodBounds,
87    content_mask: PodBounds,
88}
89
90#[repr(C)]
91#[derive(Clone, Copy, Pod, Zeroable)]
92struct GammaParams {
93    gamma_ratios: [f32; 4],
94    grayscale_enhanced_contrast: f32,
95    subpixel_enhanced_contrast: f32,
96    is_bgr: u32,
97    _pad: u32,
98}
99
100#[derive(Clone, Debug)]
101#[repr(C)]
102struct PathSprite {
103    bounds: Bounds<ScaledPixels>,
104}
105
106#[derive(Clone, Debug)]
107#[repr(C)]
108struct PathRasterizationVertex {
109    xy_position: Point<ScaledPixels>,
110    st_position: Point<f32>,
111    color: Background,
112    bounds: Bounds<ScaledPixels>,
113}
114
115pub struct WgpuSurfaceConfig {
116    pub size: Size<DevicePixels>,
117    pub transparent: bool,
118    /// Preferred presentation mode. When `Some`, the renderer will use this
119    /// mode if supported by the surface, falling back to `Fifo`.
120    /// When `None`, defaults to `Fifo` (VSync).
121    ///
122    /// Mobile platforms may prefer `Mailbox` (triple-buffering) to avoid
123    /// blocking in `get_current_texture()` during lifecycle transitions.
124    pub preferred_present_mode: Option<wgpu::PresentMode>,
125}
126
127struct WgpuPipelines {
128    quads: wgpu::RenderPipeline,
129    shadows: wgpu::RenderPipeline,
130    path_rasterization: wgpu::RenderPipeline,
131    paths: wgpu::RenderPipeline,
132    underlines: wgpu::RenderPipeline,
133    mono_sprites: wgpu::RenderPipeline,
134    subpixel_sprites: Option<wgpu::RenderPipeline>,
135    poly_sprites: wgpu::RenderPipeline,
136    #[allow(dead_code)]
137    surfaces: wgpu::RenderPipeline,
138}
139
140/// One frame allocation of instance data, ready to bind.
141struct InstanceBinding {
142    bind_group: wgpu::BindGroup,
143    /// Index of the allocation's first instance within the bound data. Always
144    /// zero on the storage-buffer path, where the binding offset already
145    /// positions the array; on the WebGL texture path the shader indexes the
146    /// shared instance texture absolutely, so draws must offset their
147    /// instance (or vertex) ranges by this value.
148    first_instance: u32,
149}
150
151struct InstanceBindings {
152    quads: InstanceBinding,
153    shadows: InstanceBinding,
154    underlines: InstanceBinding,
155    monochrome_sprites: InstanceBinding,
156    subpixel_sprites: InstanceBinding,
157    polychrome_sprites: InstanceBinding,
158}
159
160struct WgpuBindGroupLayouts {
161    globals: wgpu::BindGroupLayout,
162    instances: wgpu::BindGroupLayout,
163    texture: wgpu::BindGroupLayout,
164    surfaces: wgpu::BindGroupLayout,
165}
166
167/// Shared GPU context reference, used to coordinate device recovery across multiple windows.
168pub type GpuContext = Rc<RefCell<Option<WgpuContext>>>;
169
170enum InstanceData {
171    Storage(wgpu::Buffer),
172    // WebGL2 has no storage buffers. A uint texture keeps the records available to both shader
173    // stages while preserving integer and floating-point bit patterns exactly.
174    Texture {
175        texture: wgpu::Texture,
176        view: wgpu::TextureView,
177        width: u32,
178        height: u32,
179    },
180}
181
182/// GPU resources that must be dropped together during device recovery.
183struct WgpuResources {
184    device: Arc<wgpu::Device>,
185    queue: Arc<wgpu::Queue>,
186    pipelines: WgpuPipelines,
187    bind_group_layouts: WgpuBindGroupLayouts,
188    atlas_sampler: wgpu::Sampler,
189    atlas_texture_bind_groups: FxHashMap<AtlasTextureId, CachedTextureBindGroup>,
190    globals_buffer: wgpu::Buffer,
191    globals_bind_group: wgpu::BindGroup,
192    path_globals_bind_group: wgpu::BindGroup,
193    instance_data: InstanceData,
194    path_intermediate_texture: Option<wgpu::Texture>,
195    path_intermediate_view: Option<wgpu::TextureView>,
196    path_msaa_texture: Option<wgpu::Texture>,
197    path_msaa_view: Option<wgpu::TextureView>,
198}
199
200struct CachedTextureBindGroup {
201    texture_generation: u64,
202    bind_group: wgpu::BindGroup,
203}
204
205impl WgpuResources {
206    fn invalidate_intermediate_textures(&mut self) {
207        self.path_intermediate_texture = None;
208        self.path_intermediate_view = None;
209        self.path_msaa_texture = None;
210        self.path_msaa_view = None;
211    }
212}
213
214struct WgpuRendererCore {
215    resources: WgpuResources,
216    atlas: Arc<WgpuAtlas>,
217    path_globals_offset: u64,
218    gamma_offset: u64,
219    instance_data_capacity: u64,
220    max_instance_data_size: u64,
221    instance_data_alignment: u64,
222    uses_webgl_instance_data: bool,
223    rendering_params: RenderingParameters,
224    is_bgr: bool,
225    dual_source_blending: bool,
226    adapter_info: wgpu::AdapterInfo,
227    target_format: wgpu::TextureFormat,
228    max_texture_size: u32,
229}
230
231/// GPU resources of a windowed renderer. A surface is only ever configured against the
232/// device that owns `core`, so it cannot outlive it: there is no surface-only state.
233enum RendererState {
234    /// Frames can be drawn.
235    Ready {
236        surface: wgpu::Surface<'static>,
237        core: WgpuRendererCore,
238    },
239    /// The native surface is gone (Android `TerminateWindow`, browser context loss) but
240    /// the device, pipelines, and atlas remain so `replace_surface` can resume without
241    /// re-uploading cached textures.
242    Unconfigured { core: WgpuRendererCore },
243    /// Released by `destroy`, or dropped for a device recovery that has not succeeded yet.
244    Released,
245}
246
247pub struct WgpuRenderer {
248    /// Shared GPU context for device recovery coordination (unused on WASM).
249    #[allow(dead_code)]
250    context: Option<GpuContext>,
251    /// Compositor GPU hint for adapter selection (unused on WASM).
252    #[allow(dead_code)]
253    compositor_gpu: Option<CompositorGpuHint>,
254    state: RendererState,
255    surface_config: wgpu::SurfaceConfiguration,
256    atlas: Arc<WgpuAtlas>,
257    transparent_alpha_mode: wgpu::CompositeAlphaMode,
258    opaque_alpha_mode: wgpu::CompositeAlphaMode,
259    max_texture_size: u32,
260    is_bgr: bool,
261    failed_frame_count: u32,
262    device_errors: Arc<DeviceErrorState>,
263    observed_error_generation: u64,
264    last_surface_error: Option<String>,
265    needs_redraw: bool,
266}
267
268impl WgpuRenderer {
269    fn core(&self) -> Option<&WgpuRendererCore> {
270        match &self.state {
271            RendererState::Ready { core, .. } | RendererState::Unconfigured { core } => Some(core),
272            RendererState::Released => None,
273        }
274    }
275
276    fn core_mut(&mut self) -> Option<&mut WgpuRendererCore> {
277        match &mut self.state {
278            RendererState::Ready { core, .. } | RendererState::Unconfigured { core } => Some(core),
279            RendererState::Released => None,
280        }
281    }
282
283    /// Creates a new WgpuRenderer from raw window handles.
284    ///
285    /// The `gpu_context` is a shared reference that coordinates GPU context across
286    /// multiple windows. The first window to create a renderer will initialize the
287    /// context; subsequent windows will share it.
288    ///
289    /// # Safety
290    /// The caller must ensure that the window handle remains valid for the lifetime
291    /// of the returned renderer.
292    #[cfg(not(target_family = "wasm"))]
293    pub fn new<W>(
294        gpu_context: GpuContext,
295        window: &W,
296        config: WgpuSurfaceConfig,
297        compositor_gpu: Option<CompositorGpuHint>,
298    ) -> anyhow::Result<Self>
299    where
300        W: HasWindowHandle + HasDisplayHandle + std::fmt::Debug + Send + Sync + Clone + 'static,
301    {
302        let window_handle = window
303            .window_handle()
304            .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?;
305
306        let target = wgpu::SurfaceTargetUnsafe::RawHandle {
307            // Fall back to the display handle already provided via InstanceDescriptor::display.
308            raw_display_handle: None,
309            raw_window_handle: window_handle.as_raw(),
310        };
311
312        // Use the existing context's instance if available, otherwise create a new one.
313        // The surface must be created with the same instance that will be used for
314        // adapter selection, otherwise wgpu will panic.
315        let instance = gpu_context
316            .borrow()
317            .as_ref()
318            .map(|ctx| ctx.instance.clone())
319            .unwrap_or_else(|| WgpuContext::instance(Some(Box::new(window.clone()))));
320
321        // Safety: The caller guarantees that the window handle is valid for the
322        // lifetime of this renderer. In practice, the RawWindow struct is created
323        // from the native window handles and the surface is dropped before the window.
324        let surface = unsafe {
325            instance
326                .create_surface_unsafe(target)
327                .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?
328        };
329
330        let mut ctx_ref = gpu_context.borrow_mut();
331        let surface_configured = ctx_ref.is_none();
332        let context = match ctx_ref.as_mut() {
333            Some(context) => {
334                context.check_compatible_with_surface(&surface)?;
335                context
336            }
337            None => ctx_ref.insert(WgpuContext::new(
338                instance,
339                &surface,
340                &config,
341                compositor_gpu,
342            )?),
343        };
344
345        let atlas = Arc::new(WgpuAtlas::from_context(context));
346
347        Self::new_internal(
348            Some(Rc::clone(&gpu_context)),
349            context,
350            surface,
351            config,
352            surface_configured,
353            compositor_gpu,
354            atlas,
355        )
356    }
357
358    #[cfg(target_family = "wasm")]
359    pub fn new_from_canvas(
360        context: &WgpuContext,
361        canvas: &web_sys::HtmlCanvasElement,
362        config: WgpuSurfaceConfig,
363    ) -> anyhow::Result<Self> {
364        let surface = context
365            .instance
366            .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
367            .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?;
368        Self::new_from_surface(context, surface, config)
369    }
370
371    #[cfg(target_family = "wasm")]
372    #[allow(clippy::arc_with_non_send_sync)]
373    pub fn new_from_surface(
374        context: &WgpuContext,
375        surface: wgpu::Surface<'static>,
376        config: WgpuSurfaceConfig,
377    ) -> anyhow::Result<Self> {
378        let atlas = Arc::new(WgpuAtlas::from_context(context));
379        Self::new_internal(None, context, surface, config, false, None, atlas)
380    }
381
382    /// Returns the surface configuration for `config`, along with the transparent and
383    /// opaque alpha modes that the surface supports.
384    pub(crate) fn surface_configuration(
385        surface: &wgpu::Surface<'_>,
386        adapter: &wgpu::Adapter,
387        device: &wgpu::Device,
388        config: &WgpuSurfaceConfig,
389    ) -> anyhow::Result<(
390        wgpu::SurfaceConfiguration,
391        wgpu::CompositeAlphaMode,
392        wgpu::CompositeAlphaMode,
393    )> {
394        let surface_caps = surface.get_capabilities(adapter);
395        let preferred_formats = [
396            wgpu::TextureFormat::Bgra8Unorm,
397            wgpu::TextureFormat::Rgba8Unorm,
398        ];
399        let surface_format = preferred_formats
400            .iter()
401            .find(|f| surface_caps.formats.contains(f))
402            .copied()
403            .or_else(|| {
404                surface_caps
405                    .formats
406                    .iter()
407                    .find(|format| !format.is_srgb())
408                    .copied()
409            })
410            .or_else(|| surface_caps.formats.first().copied())
411            .ok_or_else(|| {
412                anyhow::anyhow!(
413                    "Surface reports no supported texture formats for adapter {:?}",
414                    adapter.get_info().name
415                )
416            })?;
417
418        let pick_alpha_mode =
419            |preferences: &[wgpu::CompositeAlphaMode]| -> anyhow::Result<wgpu::CompositeAlphaMode> {
420                preferences
421                    .iter()
422                    .find(|p| surface_caps.alpha_modes.contains(p))
423                    .copied()
424                    .or_else(|| surface_caps.alpha_modes.first().copied())
425                    .ok_or_else(|| {
426                        anyhow::anyhow!(
427                            "Surface reports no supported alpha modes for adapter {:?}",
428                            adapter.get_info().name
429                        )
430                    })
431            };
432
433        let transparent_alpha_mode = pick_alpha_mode(&[
434            wgpu::CompositeAlphaMode::PreMultiplied,
435            wgpu::CompositeAlphaMode::Inherit,
436        ])?;
437
438        let opaque_alpha_mode = pick_alpha_mode(&[
439            wgpu::CompositeAlphaMode::Opaque,
440            wgpu::CompositeAlphaMode::Inherit,
441        ])?;
442
443        let alpha_mode = if config.transparent {
444            transparent_alpha_mode
445        } else {
446            opaque_alpha_mode
447        };
448
449        let max_texture_size = device.limits().max_texture_dimension_2d;
450
451        let requested_width = config.size.width.0 as u32;
452        let requested_height = config.size.height.0 as u32;
453        let clamped_width = requested_width.min(max_texture_size);
454        let clamped_height = requested_height.min(max_texture_size);
455
456        if clamped_width != requested_width || clamped_height != requested_height {
457            warn!(
458                "Requested surface size ({}, {}) exceeds maximum texture dimension {}. \
459                 Clamping to ({}, {}). Window content may not fill the entire window.",
460                requested_width, requested_height, max_texture_size, clamped_width, clamped_height
461            );
462        }
463
464        let surface_config = wgpu::SurfaceConfiguration {
465            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
466            format: surface_format,
467            color_space: wgpu::SurfaceColorSpace::Auto,
468            width: clamped_width.max(1),
469            height: clamped_height.max(1),
470            present_mode: config
471                .preferred_present_mode
472                .filter(|mode| surface_caps.present_modes.contains(mode))
473                .unwrap_or(wgpu::PresentMode::Fifo),
474            desired_maximum_frame_latency: 2,
475            alpha_mode,
476            view_formats: vec![],
477        };
478
479        Ok((surface_config, transparent_alpha_mode, opaque_alpha_mode))
480    }
481
482    fn new_internal(
483        gpu_context: Option<GpuContext>,
484        context: &WgpuContext,
485        surface: wgpu::Surface<'static>,
486        config: WgpuSurfaceConfig,
487        surface_configured: bool,
488        compositor_gpu: Option<CompositorGpuHint>,
489        atlas: Arc<WgpuAtlas>,
490    ) -> anyhow::Result<Self> {
491        let (surface_config, transparent_alpha_mode, opaque_alpha_mode) =
492            Self::surface_configuration(&surface, &context.adapter, &context.device, &config)?;
493        // Adapter selection configures the surface with this same configuration when it
494        // creates the context, so only configure it here when that did not happen.
495        if !surface_configured {
496            surface.configure(&context.device, &surface_config);
497        }
498
499        let surface_format = surface_config.format;
500        let alpha_mode = surface_config.alpha_mode;
501        let max_texture_size = context.device.limits().max_texture_dimension_2d;
502        let core = WgpuRendererCore::new(context, atlas.clone(), surface_format, alpha_mode);
503
504        Ok(Self {
505            context: gpu_context,
506            compositor_gpu,
507            state: RendererState::Ready { surface, core },
508            surface_config,
509            atlas,
510            transparent_alpha_mode,
511            opaque_alpha_mode,
512            max_texture_size,
513            is_bgr: false,
514            failed_frame_count: 0,
515            device_errors: Arc::clone(context.errors()),
516            observed_error_generation: 0,
517            last_surface_error: None,
518            needs_redraw: false,
519        })
520    }
521}
522
523impl WgpuRendererCore {
524    fn create_bind_group_layouts(
525        device: &wgpu::Device,
526        uses_webgl_instance_data: bool,
527    ) -> WgpuBindGroupLayouts {
528        let globals =
529            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
530                label: Some("globals_layout"),
531                entries: &[
532                    wgpu::BindGroupLayoutEntry {
533                        binding: 0,
534                        visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
535                        ty: wgpu::BindingType::Buffer {
536                            ty: wgpu::BufferBindingType::Uniform,
537                            has_dynamic_offset: false,
538                            min_binding_size: NonZeroU64::new(
539                                std::mem::size_of::<GlobalParams>() as u64
540                            ),
541                        },
542                        count: None,
543                    },
544                    wgpu::BindGroupLayoutEntry {
545                        binding: 1,
546                        visibility: wgpu::ShaderStages::FRAGMENT,
547                        ty: wgpu::BindingType::Buffer {
548                            ty: wgpu::BufferBindingType::Uniform,
549                            has_dynamic_offset: false,
550                            min_binding_size: NonZeroU64::new(
551                                std::mem::size_of::<GammaParams>() as u64
552                            ),
553                        },
554                        count: None,
555                    },
556                ],
557            });
558
559        let instance_data_entry = wgpu::BindGroupLayoutEntry {
560            binding: 0,
561            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
562            ty: if uses_webgl_instance_data {
563                wgpu::BindingType::Texture {
564                    sample_type: wgpu::TextureSampleType::Uint,
565                    view_dimension: wgpu::TextureViewDimension::D2,
566                    multisampled: false,
567                }
568            } else {
569                wgpu::BindingType::Buffer {
570                    ty: wgpu::BufferBindingType::Storage { read_only: true },
571                    has_dynamic_offset: false,
572                    min_binding_size: None,
573                }
574            },
575            count: None,
576        };
577
578        let instances = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
579            label: Some("instances_layout"),
580            entries: &[instance_data_entry],
581        });
582
583        let texture = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
584            label: Some("texture_layout"),
585            entries: &[
586                wgpu::BindGroupLayoutEntry {
587                    binding: 0,
588                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
589                    ty: wgpu::BindingType::Texture {
590                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
591                        view_dimension: wgpu::TextureViewDimension::D2,
592                        multisampled: false,
593                    },
594                    count: None,
595                },
596                wgpu::BindGroupLayoutEntry {
597                    binding: 1,
598                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
599                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
600                    count: None,
601                },
602            ],
603        });
604
605        let surfaces = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
606            label: Some("surfaces_layout"),
607            entries: &[
608                wgpu::BindGroupLayoutEntry {
609                    binding: 0,
610                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
611                    ty: wgpu::BindingType::Buffer {
612                        ty: wgpu::BufferBindingType::Uniform,
613                        has_dynamic_offset: false,
614                        min_binding_size: NonZeroU64::new(
615                            std::mem::size_of::<SurfaceParams>() as u64
616                        ),
617                    },
618                    count: None,
619                },
620                wgpu::BindGroupLayoutEntry {
621                    binding: 1,
622                    visibility: wgpu::ShaderStages::FRAGMENT,
623                    ty: wgpu::BindingType::Texture {
624                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
625                        view_dimension: wgpu::TextureViewDimension::D2,
626                        multisampled: false,
627                    },
628                    count: None,
629                },
630                wgpu::BindGroupLayoutEntry {
631                    binding: 2,
632                    visibility: wgpu::ShaderStages::FRAGMENT,
633                    ty: wgpu::BindingType::Texture {
634                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
635                        view_dimension: wgpu::TextureViewDimension::D2,
636                        multisampled: false,
637                    },
638                    count: None,
639                },
640                wgpu::BindGroupLayoutEntry {
641                    binding: 3,
642                    visibility: wgpu::ShaderStages::FRAGMENT,
643                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
644                    count: None,
645                },
646            ],
647        });
648
649        WgpuBindGroupLayouts {
650            globals,
651            instances,
652            texture,
653            surfaces,
654        }
655    }
656
657    fn create_instance_texture(
658        device: &wgpu::Device,
659        requested_capacity: u64,
660        max_texture_dimension: u32,
661    ) -> (InstanceData, u64) {
662        let texel_count = requested_capacity.div_ceil(INSTANCE_TEXTURE_TEXEL_SIZE);
663        let width = texel_count.min(u64::from(max_texture_dimension)).max(1) as u32;
664        let height = texel_count
665            .div_ceil(u64::from(width))
666            .min(u64::from(max_texture_dimension))
667            .max(1) as u32;
668        let texture = device.create_texture(&wgpu::TextureDescriptor {
669            label: Some("instance_texture"),
670            size: wgpu::Extent3d {
671                width,
672                height,
673                depth_or_array_layers: 1,
674            },
675            mip_level_count: 1,
676            sample_count: 1,
677            dimension: wgpu::TextureDimension::D2,
678            format: wgpu::TextureFormat::Rgba32Uint,
679            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
680            view_formats: &[],
681        });
682        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
683        let capacity = u64::from(width) * u64::from(height) * INSTANCE_TEXTURE_TEXEL_SIZE;
684        (
685            InstanceData::Texture {
686                texture,
687                view,
688                width,
689                height,
690            },
691            capacity,
692        )
693    }
694
695    fn create_pipelines(
696        device: &wgpu::Device,
697        layouts: &WgpuBindGroupLayouts,
698        surface_format: wgpu::TextureFormat,
699        alpha_mode: wgpu::CompositeAlphaMode,
700        path_sample_count: u32,
701        dual_source_blending: bool,
702        uses_webgl_instance_data: bool,
703    ) -> WgpuPipelines {
704        // Diagnostic guard: verify the device actually has
705        // DUAL_SOURCE_BLENDING. We have a crash report (ZED-5G1) where a
706        // feature mismatch caused a wgpu-hal abort, but we haven't
707        // identified the code path that produces the mismatch. This
708        // guard prevents the crash and logs more evidence.
709        // Remove this check once:
710        // a) We find and fix the root cause, or
711        // b) There are no reports of this warning appearing for some time.
712        let device_has_feature = device
713            .features()
714            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
715        if dual_source_blending && !device_has_feature {
716            log::error!(
717                "BUG: dual_source_blending flag is true but device does not \
718                 have DUAL_SOURCE_BLENDING enabled (device features: {:?}). \
719                 Falling back to mono text rendering. Please report this at \
720                 https://github.com/zed-industries/zed/issues",
721                device.features(),
722            );
723        }
724        let dual_source_blending =
725            dual_source_blending && device_has_feature && !uses_webgl_instance_data;
726
727        let shader_source = if uses_webgl_instance_data {
728            WEBGL_SHADERS
729        } else {
730            STORAGE_BUFFER_SHADERS
731        };
732        let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
733            label: Some("gpui_shaders"),
734            source: wgpu::ShaderSource::Wgsl(shader_source.into()),
735        });
736
737        let subpixel_shader_module = if dual_source_blending {
738            Some(device.create_shader_module(wgpu::ShaderModuleDescriptor {
739                label: Some("gpui_subpixel_shaders"),
740                source: wgpu::ShaderSource::Wgsl(SUBPIXEL_SHADERS.into()),
741            }))
742        } else {
743            None
744        };
745
746        let blend_mode = match alpha_mode {
747            wgpu::CompositeAlphaMode::PreMultiplied => {
748                wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING
749            }
750            _ => wgpu::BlendState::ALPHA_BLENDING,
751        };
752
753        let color_target = wgpu::ColorTargetState {
754            format: surface_format,
755            blend: Some(blend_mode),
756            write_mask: wgpu::ColorWrites::ALL,
757        };
758
759        let create_pipeline = |name: &str,
760                               vs_entry: &str,
761                               fs_entry: &str,
762                               globals_layout: &wgpu::BindGroupLayout,
763                               data_layout: &wgpu::BindGroupLayout,
764                               texture_layout: Option<&wgpu::BindGroupLayout>,
765                               topology: wgpu::PrimitiveTopology,
766                               color_targets: &[Option<wgpu::ColorTargetState>],
767                               sample_count: u32,
768                               module: &wgpu::ShaderModule| {
769            let mut bind_group_layouts = vec![Some(globals_layout), Some(data_layout)];
770            bind_group_layouts.extend(texture_layout.map(Some));
771            let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
772                label: Some(&format!("{name}_layout")),
773                bind_group_layouts: &bind_group_layouts,
774                immediate_size: 0,
775            });
776
777            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
778                label: Some(name),
779                layout: Some(&pipeline_layout),
780                vertex: wgpu::VertexState {
781                    module,
782                    entry_point: Some(vs_entry),
783                    buffers: &[],
784                    compilation_options: wgpu::PipelineCompilationOptions::default(),
785                },
786                fragment: Some(wgpu::FragmentState {
787                    module,
788                    entry_point: Some(fs_entry),
789                    targets: color_targets,
790                    compilation_options: wgpu::PipelineCompilationOptions::default(),
791                }),
792                primitive: wgpu::PrimitiveState {
793                    topology,
794                    strip_index_format: None,
795                    front_face: wgpu::FrontFace::Ccw,
796                    cull_mode: None,
797                    polygon_mode: wgpu::PolygonMode::Fill,
798                    unclipped_depth: false,
799                    conservative: false,
800                },
801                depth_stencil: None,
802                multisample: wgpu::MultisampleState {
803                    count: sample_count,
804                    mask: !0,
805                    alpha_to_coverage_enabled: false,
806                },
807                multiview_mask: None,
808                cache: None,
809            })
810        };
811
812        let quads = create_pipeline(
813            "quads",
814            "vs_quad",
815            "fs_quad",
816            &layouts.globals,
817            &layouts.instances,
818            None,
819            wgpu::PrimitiveTopology::TriangleStrip,
820            &[Some(color_target.clone())],
821            1,
822            &shader_module,
823        );
824
825        let shadows = create_pipeline(
826            "shadows",
827            "vs_shadow",
828            "fs_shadow",
829            &layouts.globals,
830            &layouts.instances,
831            None,
832            wgpu::PrimitiveTopology::TriangleStrip,
833            &[Some(color_target.clone())],
834            1,
835            &shader_module,
836        );
837
838        let path_rasterization = create_pipeline(
839            "path_rasterization",
840            "vs_path_rasterization",
841            "fs_path_rasterization",
842            &layouts.globals,
843            &layouts.instances,
844            None,
845            wgpu::PrimitiveTopology::TriangleList,
846            &[Some(wgpu::ColorTargetState {
847                format: surface_format,
848                blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
849                write_mask: wgpu::ColorWrites::ALL,
850            })],
851            path_sample_count,
852            &shader_module,
853        );
854
855        let paths = create_pipeline(
856            "paths",
857            "vs_path",
858            "fs_path",
859            &layouts.globals,
860            &layouts.instances,
861            Some(&layouts.texture),
862            wgpu::PrimitiveTopology::TriangleStrip,
863            &[Some(wgpu::ColorTargetState {
864                format: surface_format,
865                blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
866                write_mask: wgpu::ColorWrites::ALL,
867            })],
868            1,
869            &shader_module,
870        );
871
872        let underlines = create_pipeline(
873            "underlines",
874            "vs_underline",
875            "fs_underline",
876            &layouts.globals,
877            &layouts.instances,
878            None,
879            wgpu::PrimitiveTopology::TriangleStrip,
880            &[Some(color_target.clone())],
881            1,
882            &shader_module,
883        );
884
885        let mono_sprites = create_pipeline(
886            "mono_sprites",
887            "vs_mono_sprite",
888            "fs_mono_sprite",
889            &layouts.globals,
890            &layouts.instances,
891            Some(&layouts.texture),
892            wgpu::PrimitiveTopology::TriangleStrip,
893            &[Some(color_target.clone())],
894            1,
895            &shader_module,
896        );
897
898        let subpixel_sprites = if let Some(subpixel_module) = &subpixel_shader_module {
899            let subpixel_blend = wgpu::BlendState {
900                color: wgpu::BlendComponent {
901                    src_factor: wgpu::BlendFactor::Src1,
902                    dst_factor: wgpu::BlendFactor::OneMinusSrc1,
903                    operation: wgpu::BlendOperation::Add,
904                },
905                alpha: wgpu::BlendComponent {
906                    src_factor: wgpu::BlendFactor::One,
907                    dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
908                    operation: wgpu::BlendOperation::Add,
909                },
910            };
911
912            Some(create_pipeline(
913                "subpixel_sprites",
914                "vs_subpixel_sprite",
915                "fs_subpixel_sprite",
916                &layouts.globals,
917                &layouts.instances,
918                Some(&layouts.texture),
919                wgpu::PrimitiveTopology::TriangleStrip,
920                &[Some(wgpu::ColorTargetState {
921                    format: surface_format,
922                    blend: Some(subpixel_blend),
923                    write_mask: wgpu::ColorWrites::COLOR,
924                })],
925                1,
926                subpixel_module,
927            ))
928        } else {
929            None
930        };
931
932        let poly_sprites = create_pipeline(
933            "poly_sprites",
934            "vs_poly_sprite",
935            "fs_poly_sprite",
936            &layouts.globals,
937            &layouts.instances,
938            Some(&layouts.texture),
939            wgpu::PrimitiveTopology::TriangleStrip,
940            &[Some(color_target.clone())],
941            1,
942            &shader_module,
943        );
944
945        let surfaces = create_pipeline(
946            "surfaces",
947            "vs_surface",
948            "fs_surface",
949            &layouts.globals,
950            &layouts.surfaces,
951            None,
952            wgpu::PrimitiveTopology::TriangleStrip,
953            &[Some(color_target)],
954            1,
955            &shader_module,
956        );
957
958        WgpuPipelines {
959            quads,
960            shadows,
961            path_rasterization,
962            paths,
963            underlines,
964            mono_sprites,
965            subpixel_sprites,
966            poly_sprites,
967            surfaces,
968        }
969    }
970
971    fn create_path_intermediate(
972        device: &wgpu::Device,
973        format: wgpu::TextureFormat,
974        width: u32,
975        height: u32,
976    ) -> (wgpu::Texture, wgpu::TextureView) {
977        let texture = device.create_texture(&wgpu::TextureDescriptor {
978            label: Some("path_intermediate"),
979            size: wgpu::Extent3d {
980                width: width.max(1),
981                height: height.max(1),
982                depth_or_array_layers: 1,
983            },
984            mip_level_count: 1,
985            sample_count: 1,
986            dimension: wgpu::TextureDimension::D2,
987            format,
988            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
989            view_formats: &[],
990        });
991        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
992        (texture, view)
993    }
994
995    fn create_msaa_if_needed(
996        device: &wgpu::Device,
997        format: wgpu::TextureFormat,
998        width: u32,
999        height: u32,
1000        sample_count: u32,
1001    ) -> Option<(wgpu::Texture, wgpu::TextureView)> {
1002        if sample_count <= 1 {
1003            return None;
1004        }
1005        let texture = device.create_texture(&wgpu::TextureDescriptor {
1006            label: Some("path_msaa"),
1007            size: wgpu::Extent3d {
1008                width: width.max(1),
1009                height: height.max(1),
1010                depth_or_array_layers: 1,
1011            },
1012            mip_level_count: 1,
1013            sample_count,
1014            dimension: wgpu::TextureDimension::D2,
1015            format,
1016            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1017            view_formats: &[],
1018        });
1019        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1020        Some((texture, view))
1021    }
1022}
1023
1024impl WgpuRenderer {
1025    /// Records the new drawable size and reconfigures the surface when one is present.
1026    /// The size is kept even without GPU resources so that recovery restores it.
1027    pub fn update_drawable_size(&mut self, size: Size<DevicePixels>) {
1028        let width = size.width.0 as u32;
1029        let height = size.height.0 as u32;
1030
1031        if width == self.surface_config.width && height == self.surface_config.height {
1032            return;
1033        }
1034
1035        let clamped_width = width.min(self.max_texture_size);
1036        let clamped_height = height.min(self.max_texture_size);
1037        if clamped_width != width || clamped_height != height {
1038            warn!(
1039                "Requested surface size ({}, {}) exceeds maximum texture dimension {}. \
1040                 Clamping to ({}, {}). Window content may not fill the entire window.",
1041                width, height, self.max_texture_size, clamped_width, clamped_height
1042            );
1043        }
1044        self.surface_config.width = clamped_width.max(1);
1045        self.surface_config.height = clamped_height.max(1);
1046
1047        let Some(core) = self.core_mut() else {
1048            return;
1049        };
1050        let resources = &mut core.resources;
1051
1052        // Wait for any in-flight GPU work to complete before destroying textures
1053        if let Err(e) = resources.device.poll(wgpu::PollType::Wait {
1054            submission_index: None,
1055            timeout: None,
1056        }) {
1057            warn!("Failed to poll device during resize: {e:?}");
1058        }
1059
1060        // Destroy old textures before allocating new ones to avoid GPU memory spikes
1061        if let Some(ref texture) = resources.path_intermediate_texture {
1062            texture.destroy();
1063        }
1064        if let Some(ref texture) = resources.path_msaa_texture {
1065            texture.destroy();
1066        }
1067
1068        // Invalidate intermediate textures - they will be lazily recreated
1069        // in draw() after we confirm the surface is healthy. This avoids
1070        // panics when the device/surface is in an invalid state during resize.
1071        resources.invalidate_intermediate_textures();
1072
1073        if let RendererState::Ready { surface, core } = &self.state {
1074            surface.configure(&core.resources.device, &self.surface_config);
1075        }
1076    }
1077
1078    pub fn set_subpixel_layout(&mut self, is_bgr: bool) {
1079        self.is_bgr = is_bgr;
1080        if let Some(core) = self.core_mut() {
1081            core.is_bgr = is_bgr;
1082        }
1083    }
1084
1085    pub fn update_transparency(&mut self, transparent: bool) {
1086        let new_alpha_mode = if transparent {
1087            self.transparent_alpha_mode
1088        } else {
1089            self.opaque_alpha_mode
1090        };
1091        if new_alpha_mode == self.surface_config.alpha_mode {
1092            return;
1093        }
1094        self.surface_config.alpha_mode = new_alpha_mode;
1095        let format = self.surface_config.format;
1096
1097        let Some(core) = self.core_mut() else {
1098            return;
1099        };
1100        let resources = &mut core.resources;
1101        resources.pipelines = WgpuRendererCore::create_pipelines(
1102            &resources.device,
1103            &resources.bind_group_layouts,
1104            format,
1105            new_alpha_mode,
1106            core.rendering_params.path_sample_count,
1107            core.dual_source_blending,
1108            core.uses_webgl_instance_data,
1109        );
1110
1111        if let RendererState::Ready { surface, core } = &self.state {
1112            surface.configure(&core.resources.device, &self.surface_config);
1113        }
1114    }
1115
1116    #[allow(dead_code)]
1117    pub fn viewport_size(&self) -> Size<DevicePixels> {
1118        Size {
1119            width: DevicePixels(self.surface_config.width as i32),
1120            height: DevicePixels(self.surface_config.height as i32),
1121        }
1122    }
1123
1124    pub fn sprite_atlas(&self) -> &Arc<WgpuAtlas> {
1125        &self.atlas
1126    }
1127
1128    pub fn supports_dual_source_blending(&self) -> bool {
1129        self.core().is_some_and(|core| core.dual_source_blending)
1130    }
1131
1132    /// Returns `None` once GPU resources have been released by `destroy` or a pending
1133    /// device recovery.
1134    pub fn gpu_specs(&self) -> Option<GpuSpecs> {
1135        let adapter_info = &self.core()?.adapter_info;
1136        Some(GpuSpecs {
1137            is_software_emulated: adapter_info.device_type == wgpu::DeviceType::Cpu,
1138            device_name: adapter_info.name.clone(),
1139            driver_name: adapter_info.driver.clone(),
1140            driver_info: adapter_info.driver_info.clone(),
1141        })
1142    }
1143
1144    pub fn max_texture_size(&self) -> u32 {
1145        self.max_texture_size
1146    }
1147
1148    pub fn draw(&mut self, scene: &Scene) -> bool {
1149        #[cfg(target_family = "wasm")]
1150        if self.device_lost() {
1151            if matches!(self.state, RendererState::Ready { .. }) {
1152                log::error!(
1153                    "Browser graphics context was lost; rendering has stopped. Reload the page to recover."
1154                );
1155                self.unconfigure_surface();
1156            }
1157            return false;
1158        }
1159
1160        // Bail out early if the surface has been unconfigured (e.g. during
1161        // Android background/rotation transitions).  Attempting to acquire
1162        // a texture from an unconfigured surface can block indefinitely on
1163        // some drivers (Adreno).
1164        let RendererState::Ready { surface, core } = &mut self.state else {
1165            return false;
1166        };
1167
1168        if let Some(error) = self.last_surface_error.take().or_else(|| {
1169            self.device_errors
1170                .observe_error(&mut self.observed_error_generation)
1171        }) {
1172            self.failed_frame_count += 1;
1173            log::error!(
1174                "GPU error during frame (failure {} of 10): {error}",
1175                self.failed_frame_count
1176            );
1177
1178            // TBD. Does retrying more actually help?
1179            if self.failed_frame_count > 10 {
1180                panic!("Too many consecutive GPU errors. Last error: {error}");
1181            } else if self.failed_frame_count > 5 {
1182                core.resources.invalidate_intermediate_textures();
1183                self.atlas.clear();
1184                self.needs_redraw = true;
1185                self.failed_frame_count = 0;
1186                return false;
1187            }
1188        } else {
1189            self.failed_frame_count = 0;
1190        }
1191
1192        let frame = match surface.get_current_texture() {
1193            wgpu::CurrentSurfaceTexture::Success(frame) => frame,
1194            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
1195                // Textures must be destroyed before the surface can be reconfigured.
1196                drop(frame);
1197                surface.configure(&core.resources.device, &self.surface_config);
1198                return false;
1199            }
1200            wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
1201                surface.configure(&core.resources.device, &self.surface_config);
1202                return false;
1203            }
1204            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
1205                return false;
1206            }
1207            wgpu::CurrentSurfaceTexture::Validation => {
1208                self.last_surface_error = Some("Surface texture validation error".to_string());
1209                return false;
1210            }
1211        };
1212
1213        // The acquired texture is the authority on frame dimensions; the surface
1214        // configuration is only a request.
1215        let size = Size {
1216            width: DevicePixels(frame.texture.width() as i32),
1217            height: DevicePixels(frame.texture.height() as i32),
1218        };
1219        let frame_view = frame
1220            .texture
1221            .create_view(&wgpu::TextureViewDescriptor::default());
1222        let premultiplied_alpha =
1223            self.surface_config.alpha_mode == wgpu::CompositeAlphaMode::PreMultiplied;
1224        if let Err(error) = core.render_frame(
1225            scene,
1226            &frame_view,
1227            size,
1228            premultiplied_alpha,
1229            wgpu::Color::TRANSPARENT,
1230        ) {
1231            log::error!("{error:#}");
1232            return false;
1233        }
1234
1235        core.resources.queue.present(frame);
1236        true
1237    }
1238}
1239
1240impl WgpuRendererCore {
1241    fn new(
1242        context: &WgpuContext,
1243        atlas: Arc<WgpuAtlas>,
1244        target_format: wgpu::TextureFormat,
1245        alpha_mode: wgpu::CompositeAlphaMode,
1246    ) -> Self {
1247        let device = Arc::clone(&context.device);
1248        let queue = Arc::clone(&context.queue);
1249        let rendering_params = RenderingParameters::new(&context.adapter, target_format);
1250        let uses_webgl_instance_data = context.uses_webgl_instance_data();
1251        let dual_source_blending =
1252            context.supports_dual_source_blending() && !uses_webgl_instance_data;
1253        let bind_group_layouts = Self::create_bind_group_layouts(&device, uses_webgl_instance_data);
1254        let pipelines = Self::create_pipelines(
1255            &device,
1256            &bind_group_layouts,
1257            target_format,
1258            alpha_mode,
1259            rendering_params.path_sample_count,
1260            dual_source_blending,
1261            uses_webgl_instance_data,
1262        );
1263        let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1264            label: Some("atlas_sampler"),
1265            mag_filter: wgpu::FilterMode::Linear,
1266            min_filter: wgpu::FilterMode::Linear,
1267            ..Default::default()
1268        });
1269        let uniform_alignment = device.limits().min_uniform_buffer_offset_alignment as u64;
1270        let globals_size = std::mem::size_of::<GlobalParams>() as u64;
1271        let gamma_size = std::mem::size_of::<GammaParams>() as u64;
1272        let path_globals_offset = globals_size.next_multiple_of(uniform_alignment);
1273        let gamma_offset = (path_globals_offset + globals_size).next_multiple_of(uniform_alignment);
1274        let globals_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1275            label: Some("globals_buffer"),
1276            size: gamma_offset + gamma_size,
1277            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1278            mapped_at_creation: false,
1279        });
1280        let (
1281            instance_data,
1282            instance_data_capacity,
1283            max_instance_data_size,
1284            instance_data_alignment,
1285        ) = if uses_webgl_instance_data {
1286            let max_texture_dimension = device.limits().max_texture_dimension_2d;
1287            let max_instance_data_size = (u64::from(max_texture_dimension).pow(2)
1288                * INSTANCE_TEXTURE_TEXEL_SIZE)
1289                .min(MAX_INSTANCE_BUFFER_SIZE);
1290            let initial_capacity = (2 * 1024 * 1024).min(max_instance_data_size);
1291            let (instance_data, capacity) =
1292                Self::create_instance_texture(&device, initial_capacity, max_texture_dimension);
1293            (
1294                instance_data,
1295                capacity,
1296                max_instance_data_size,
1297                INSTANCE_TEXTURE_TEXEL_SIZE,
1298            )
1299        } else {
1300            let max_buffer_size = device
1301                .limits()
1302                .max_buffer_size
1303                .min(device.limits().max_storage_buffer_binding_size)
1304                .min(MAX_INSTANCE_BUFFER_SIZE);
1305            let initial_capacity = (2 * 1024 * 1024).min(max_buffer_size);
1306            let buffer = device.create_buffer(&wgpu::BufferDescriptor {
1307                label: Some("instance_buffer"),
1308                size: initial_capacity,
1309                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1310                mapped_at_creation: false,
1311            });
1312            (
1313                InstanceData::Storage(buffer),
1314                initial_capacity,
1315                max_buffer_size,
1316                device.limits().min_storage_buffer_offset_alignment as u64,
1317            )
1318        };
1319        let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1320            label: Some("globals_bind_group"),
1321            layout: &bind_group_layouts.globals,
1322            entries: &[
1323                wgpu::BindGroupEntry {
1324                    binding: 0,
1325                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1326                        buffer: &globals_buffer,
1327                        offset: 0,
1328                        size: NonZeroU64::new(globals_size),
1329                    }),
1330                },
1331                wgpu::BindGroupEntry {
1332                    binding: 1,
1333                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1334                        buffer: &globals_buffer,
1335                        offset: gamma_offset,
1336                        size: NonZeroU64::new(gamma_size),
1337                    }),
1338                },
1339            ],
1340        });
1341        let path_globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1342            label: Some("path_globals_bind_group"),
1343            layout: &bind_group_layouts.globals,
1344            entries: &[
1345                wgpu::BindGroupEntry {
1346                    binding: 0,
1347                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1348                        buffer: &globals_buffer,
1349                        offset: path_globals_offset,
1350                        size: NonZeroU64::new(globals_size),
1351                    }),
1352                },
1353                wgpu::BindGroupEntry {
1354                    binding: 1,
1355                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1356                        buffer: &globals_buffer,
1357                        offset: gamma_offset,
1358                        size: NonZeroU64::new(gamma_size),
1359                    }),
1360                },
1361            ],
1362        });
1363        let max_texture_size = device.limits().max_texture_dimension_2d;
1364
1365        Self {
1366            resources: WgpuResources {
1367                device,
1368                queue,
1369                pipelines,
1370                bind_group_layouts,
1371                atlas_sampler,
1372                atlas_texture_bind_groups: FxHashMap::default(),
1373                globals_buffer,
1374                globals_bind_group,
1375                path_globals_bind_group,
1376                instance_data,
1377                path_intermediate_texture: None,
1378                path_intermediate_view: None,
1379                path_msaa_texture: None,
1380                path_msaa_view: None,
1381            },
1382            atlas,
1383            path_globals_offset,
1384            gamma_offset,
1385            instance_data_capacity,
1386            max_instance_data_size,
1387            instance_data_alignment,
1388            uses_webgl_instance_data,
1389            rendering_params,
1390            is_bgr: false,
1391            dual_source_blending,
1392            adapter_info: context.adapter.get_info(),
1393            target_format,
1394            max_texture_size,
1395        }
1396    }
1397
1398    fn resources(&self) -> &WgpuResources {
1399        &self.resources
1400    }
1401
1402    fn resources_mut(&mut self) -> &mut WgpuResources {
1403        &mut self.resources
1404    }
1405
1406    /// Path rendering goes through an intermediate texture that must match the frame
1407    /// target's dimensions, so the textures are keyed by size and rebuilt on mismatch.
1408    fn ensure_intermediate_textures(&mut self, size: Size<DevicePixels>) {
1409        let width = size.width.0 as u32;
1410        let height = size.height.0 as u32;
1411        if self
1412            .resources
1413            .path_intermediate_texture
1414            .as_ref()
1415            .is_some_and(|texture| texture.width() == width && texture.height() == height)
1416        {
1417            return;
1418        }
1419
1420        let format = self.target_format;
1421        let path_sample_count = self.rendering_params.path_sample_count;
1422        let resources = &mut self.resources;
1423
1424        let (texture, view) =
1425            Self::create_path_intermediate(&resources.device, format, width, height);
1426        resources.path_intermediate_texture = Some(texture);
1427        resources.path_intermediate_view = Some(view);
1428
1429        let (path_msaa_texture, path_msaa_view) = Self::create_msaa_if_needed(
1430            &resources.device,
1431            format,
1432            width,
1433            height,
1434            path_sample_count,
1435        )
1436        .map(|(texture, view)| (Some(texture), Some(view)))
1437        .unwrap_or((None, None));
1438        resources.path_msaa_texture = path_msaa_texture;
1439        resources.path_msaa_view = path_msaa_view;
1440    }
1441
1442    fn render_frame(
1443        &mut self,
1444        scene: &Scene,
1445        target_view: &wgpu::TextureView,
1446        size: Size<DevicePixels>,
1447        premultiplied_alpha: bool,
1448        clear_color: wgpu::Color,
1449    ) -> Result<wgpu::SubmissionIndex> {
1450        anyhow::ensure!(
1451            size.width.0 > 0 && size.height.0 > 0,
1452            "invalid render target size: {size:?}"
1453        );
1454        anyhow::ensure!(
1455            size.width.0 as u32 <= self.max_texture_size
1456                && size.height.0 as u32 <= self.max_texture_size,
1457            "render target size {size:?} exceeds maximum texture dimension {}",
1458            self.max_texture_size
1459        );
1460
1461        self.atlas.before_frame();
1462        self.ensure_intermediate_textures(size);
1463
1464        let gamma_params = GammaParams {
1465            gamma_ratios: self.rendering_params.gamma_ratios,
1466            grayscale_enhanced_contrast: self.rendering_params.grayscale_enhanced_contrast,
1467            subpixel_enhanced_contrast: self.rendering_params.subpixel_enhanced_contrast,
1468            is_bgr: self.is_bgr as u32,
1469            _pad: 0,
1470        };
1471        let globals = GlobalParams {
1472            viewport_size: [size.width.0 as f32, size.height.0 as f32],
1473            premultiplied_alpha: premultiplied_alpha as u32,
1474            pad: 0,
1475        };
1476        let path_globals = GlobalParams {
1477            premultiplied_alpha: 0,
1478            ..globals
1479        };
1480        self.resources.queue.write_buffer(
1481            &self.resources.globals_buffer,
1482            0,
1483            bytemuck::bytes_of(&globals),
1484        );
1485        self.resources.queue.write_buffer(
1486            &self.resources.globals_buffer,
1487            self.path_globals_offset,
1488            bytemuck::bytes_of(&path_globals),
1489        );
1490        self.resources.queue.write_buffer(
1491            &self.resources.globals_buffer,
1492            self.gamma_offset,
1493            bytemuck::bytes_of(&gamma_params),
1494        );
1495
1496        self.record_frame(scene, target_view, clear_color)
1497            .inspect_err(|_| {
1498                // Queue writes are staged before encoding; flush them even if the frame fails.
1499                self.resources.queue.submit(std::iter::empty());
1500            })
1501    }
1502
1503    fn record_frame(
1504        &mut self,
1505        scene: &Scene,
1506        frame_view: &wgpu::TextureView,
1507        clear_color: wgpu::Color,
1508    ) -> Result<wgpu::SubmissionIndex> {
1509        let mut instance_offset = 0;
1510        let instance_bindings = self
1511            .write_instances(scene, &mut instance_offset)
1512            .with_context(|| {
1513                format!(
1514                    "scene too large: {} paths, {} shadows, {} quads, {} underlines, {} monochrome sprites, {} subpixel sprites, {} polychrome sprites",
1515                    scene.paths.len(),
1516                    scene.shadows.len(),
1517                    scene.quads.len(),
1518                    scene.underlines.len(),
1519                    scene.monochrome_sprites.len(),
1520                    scene.subpixel_sprites.len(),
1521                    scene.polychrome_sprites.len(),
1522                )
1523            })?;
1524        self.prepare_texture_bind_groups(scene);
1525
1526        let mut encoder =
1527            self.resources()
1528                .device
1529                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1530                    label: Some("main_encoder"),
1531                });
1532
1533        {
1534            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1535                label: Some("main_pass"),
1536                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1537                    view: frame_view,
1538                    resolve_target: None,
1539                    ops: wgpu::Operations {
1540                        load: wgpu::LoadOp::Clear(clear_color),
1541                        store: wgpu::StoreOp::Store,
1542                    },
1543                    depth_slice: None,
1544                })],
1545                depth_stencil_attachment: None,
1546                ..Default::default()
1547            });
1548
1549            for batch in scene.batches() {
1550                match batch {
1551                    PrimitiveBatch::Quads(range) => self.draw_instances(
1552                        &instance_bindings.quads,
1553                        &self.resources().pipelines.quads,
1554                        instance_range(range),
1555                        &mut pass,
1556                    ),
1557                    PrimitiveBatch::Shadows(range) => self.draw_instances(
1558                        &instance_bindings.shadows,
1559                        &self.resources().pipelines.shadows,
1560                        instance_range(range),
1561                        &mut pass,
1562                    ),
1563                    PrimitiveBatch::Paths(range) => {
1564                        let paths = &scene.paths[range];
1565                        if paths.is_empty() {
1566                            continue;
1567                        }
1568
1569                        drop(pass);
1570                        let rasterized = self.draw_paths_to_intermediate(
1571                            &mut encoder,
1572                            paths,
1573                            &mut instance_offset,
1574                        )?;
1575
1576                        pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1577                            label: Some("main_pass_continued"),
1578                            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1579                                view: frame_view,
1580                                resolve_target: None,
1581                                ops: wgpu::Operations {
1582                                    load: wgpu::LoadOp::Load,
1583                                    store: wgpu::StoreOp::Store,
1584                                },
1585                                depth_slice: None,
1586                            })],
1587                            depth_stencil_attachment: None,
1588                            ..Default::default()
1589                        });
1590
1591                        if rasterized {
1592                            self.draw_paths_from_intermediate(
1593                                paths,
1594                                &mut instance_offset,
1595                                &mut pass,
1596                            )?;
1597                        }
1598                    }
1599                    PrimitiveBatch::Underlines(range) => self.draw_instances(
1600                        &instance_bindings.underlines,
1601                        &self.resources().pipelines.underlines,
1602                        instance_range(range),
1603                        &mut pass,
1604                    ),
1605                    PrimitiveBatch::MonochromeSprites { texture_id, range } => {
1606                        self.draw_sprites(
1607                            &instance_bindings.monochrome_sprites,
1608                            texture_id,
1609                            &self.resources().pipelines.mono_sprites,
1610                            instance_range(range),
1611                            &mut pass,
1612                        )?;
1613                    }
1614                    PrimitiveBatch::SubpixelSprites { texture_id, range } => {
1615                        let resources = self.resources();
1616                        self.draw_sprites(
1617                            &instance_bindings.subpixel_sprites,
1618                            texture_id,
1619                            resources
1620                                .pipelines
1621                                .subpixel_sprites
1622                                .as_ref()
1623                                .unwrap_or(&resources.pipelines.mono_sprites),
1624                            instance_range(range),
1625                            &mut pass,
1626                        )?;
1627                    }
1628                    PrimitiveBatch::PolychromeSprites { texture_id, range } => {
1629                        self.draw_sprites(
1630                            &instance_bindings.polychrome_sprites,
1631                            texture_id,
1632                            &self.resources().pipelines.poly_sprites,
1633                            instance_range(range),
1634                            &mut pass,
1635                        )?;
1636                    }
1637                    // Surfaces are macOS-only for video playback and are not
1638                    // implemented by the WGPU renderer.
1639                    PrimitiveBatch::Surfaces(_surfaces) => {}
1640                }
1641            }
1642        }
1643
1644        let submission = self
1645            .resources()
1646            .queue
1647            .submit(std::iter::once(encoder.finish()));
1648        Ok(submission)
1649    }
1650
1651    fn write_instances(
1652        &mut self,
1653        scene: &Scene,
1654        instance_offset: &mut u64,
1655    ) -> Result<InstanceBindings> {
1656        Ok(InstanceBindings {
1657            quads: self.write_instance_binding(
1658                "quads_bind_group",
1659                instance_offset,
1660                &scene.quads,
1661            )?,
1662            shadows: self.write_instance_binding(
1663                "shadows_bind_group",
1664                instance_offset,
1665                &scene.shadows,
1666            )?,
1667            underlines: self.write_instance_binding(
1668                "underlines_bind_group",
1669                instance_offset,
1670                &scene.underlines,
1671            )?,
1672            monochrome_sprites: self.write_instance_binding(
1673                "monochrome_sprites_bind_group",
1674                instance_offset,
1675                &scene.monochrome_sprites,
1676            )?,
1677            subpixel_sprites: self.write_instance_binding(
1678                "subpixel_sprites_bind_group",
1679                instance_offset,
1680                &scene.subpixel_sprites,
1681            )?,
1682            polychrome_sprites: self.write_instance_binding(
1683                "polychrome_sprites_bind_group",
1684                instance_offset,
1685                &scene.polychrome_sprites,
1686            )?,
1687        })
1688    }
1689
1690    fn create_texture_bind_group(
1691        &self,
1692        label: &str,
1693        texture_view: &wgpu::TextureView,
1694    ) -> wgpu::BindGroup {
1695        let resources = self.resources();
1696        resources
1697            .device
1698            .create_bind_group(&wgpu::BindGroupDescriptor {
1699                label: Some(label),
1700                layout: &resources.bind_group_layouts.texture,
1701                entries: &[
1702                    wgpu::BindGroupEntry {
1703                        binding: 0,
1704                        resource: wgpu::BindingResource::TextureView(texture_view),
1705                    },
1706                    wgpu::BindGroupEntry {
1707                        binding: 1,
1708                        resource: wgpu::BindingResource::Sampler(&resources.atlas_sampler),
1709                    },
1710                ],
1711            })
1712    }
1713
1714    fn prepare_texture_bind_groups(&mut self, scene: &Scene) {
1715        let mut texture_ids = SmallVec::<[AtlasTextureId; 8]>::new();
1716        for batch in scene.batches() {
1717            let texture_id = match batch {
1718                PrimitiveBatch::MonochromeSprites { texture_id, .. }
1719                | PrimitiveBatch::SubpixelSprites { texture_id, .. }
1720                | PrimitiveBatch::PolychromeSprites { texture_id, .. } => texture_id,
1721                _ => continue,
1722            };
1723            if !texture_ids.contains(&texture_id) {
1724                texture_ids.push(texture_id);
1725            }
1726        }
1727
1728        self.resources_mut()
1729            .atlas_texture_bind_groups
1730            .retain(|texture_id, _| texture_ids.contains(texture_id));
1731
1732        for texture_id in texture_ids {
1733            let Some(texture_info) = self.atlas.get_texture_info(texture_id) else {
1734                self.resources_mut()
1735                    .atlas_texture_bind_groups
1736                    .remove(&texture_id);
1737                continue;
1738            };
1739            let is_current = self
1740                .resources()
1741                .atlas_texture_bind_groups
1742                .get(&texture_id)
1743                .is_some_and(|cached| cached.texture_generation == texture_info.generation);
1744            if is_current {
1745                continue;
1746            }
1747
1748            let bind_group =
1749                self.create_texture_bind_group("atlas_texture_bind_group", &texture_info.view);
1750            self.resources_mut().atlas_texture_bind_groups.insert(
1751                texture_id,
1752                CachedTextureBindGroup {
1753                    texture_generation: texture_info.generation,
1754                    bind_group,
1755                },
1756            );
1757        }
1758    }
1759
1760    fn draw_instances(
1761        &self,
1762        instances: &InstanceBinding,
1763        pipeline: &wgpu::RenderPipeline,
1764        range: Range<u32>,
1765        pass: &mut wgpu::RenderPass<'_>,
1766    ) {
1767        if range.is_empty() {
1768            return;
1769        }
1770        pass.set_pipeline(pipeline);
1771        pass.set_bind_group(0, &self.resources().globals_bind_group, &[]);
1772        pass.set_bind_group(1, &instances.bind_group, &[]);
1773        pass.draw(
1774            0..4,
1775            instances.first_instance + range.start..instances.first_instance + range.end,
1776        );
1777    }
1778
1779    fn draw_sprites(
1780        &self,
1781        sprite_instances: &InstanceBinding,
1782        texture_id: AtlasTextureId,
1783        pipeline: &wgpu::RenderPipeline,
1784        range: Range<u32>,
1785        pass: &mut wgpu::RenderPass<'_>,
1786    ) -> Result<()> {
1787        if range.is_empty() {
1788            return Ok(());
1789        }
1790        let resources = self.resources();
1791        // The atlas has released this texture; the batch belongs to a stale
1792        // paint that will be replaced once its view re-renders.
1793        let Some(texture) = resources.atlas_texture_bind_groups.get(&texture_id) else {
1794            return Ok(());
1795        };
1796        pass.set_pipeline(pipeline);
1797        pass.set_bind_group(0, &resources.globals_bind_group, &[]);
1798        pass.set_bind_group(1, &sprite_instances.bind_group, &[]);
1799        pass.set_bind_group(2, &texture.bind_group, &[]);
1800        pass.draw(
1801            0..4,
1802            sprite_instances.first_instance + range.start
1803                ..sprite_instances.first_instance + range.end,
1804        );
1805        Ok(())
1806    }
1807
1808    unsafe fn instance_bytes<T>(instances: &[T]) -> &[u8] {
1809        unsafe {
1810            std::slice::from_raw_parts(
1811                instances.as_ptr() as *const u8,
1812                std::mem::size_of_val(instances),
1813            )
1814        }
1815    }
1816
1817    fn draw_paths_from_intermediate(
1818        &mut self,
1819        paths: &[Path<ScaledPixels>],
1820        instance_offset: &mut u64,
1821        pass: &mut wgpu::RenderPass<'_>,
1822    ) -> Result<()> {
1823        let first_path = &paths[0];
1824        let sprites: Vec<PathSprite> = if paths.last().map(|p| &p.order) == Some(&first_path.order)
1825        {
1826            paths
1827                .iter()
1828                .map(|p| PathSprite {
1829                    bounds: p.clipped_bounds(),
1830                })
1831                .collect()
1832        } else {
1833            let mut bounds = first_path.clipped_bounds();
1834            for path in paths.iter().skip(1) {
1835                bounds = bounds.union(&path.clipped_bounds());
1836            }
1837            vec![PathSprite { bounds }]
1838        };
1839
1840        let Some(path_intermediate_view) = self.resources().path_intermediate_view.clone() else {
1841            return Ok(());
1842        };
1843        let instances =
1844            self.write_instance_binding("path_sprites_bind_group", instance_offset, &sprites)?;
1845        let texture = self.create_texture_bind_group(
1846            "path_intermediate_texture_bind_group",
1847            &path_intermediate_view,
1848        );
1849        let resources = self.resources();
1850        pass.set_pipeline(&resources.pipelines.paths);
1851        pass.set_bind_group(0, &resources.globals_bind_group, &[]);
1852        pass.set_bind_group(1, &instances.bind_group, &[]);
1853        pass.set_bind_group(2, &texture, &[]);
1854        pass.draw(
1855            0..4,
1856            instances.first_instance..instances.first_instance + sprites.len() as u32,
1857        );
1858        Ok(())
1859    }
1860
1861    fn draw_paths_to_intermediate(
1862        &mut self,
1863        encoder: &mut wgpu::CommandEncoder,
1864        paths: &[Path<ScaledPixels>],
1865        instance_offset: &mut u64,
1866    ) -> Result<bool> {
1867        let mut vertices = Vec::new();
1868        for path in paths {
1869            let bounds = path.clipped_bounds();
1870            vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex {
1871                xy_position: v.xy_position,
1872                st_position: v.st_position,
1873                color: path.color,
1874                bounds,
1875            }));
1876        }
1877
1878        if vertices.is_empty() {
1879            return Ok(false);
1880        }
1881
1882        let vertex_binding = self.write_instance_binding(
1883            "path_rasterization_bind_group",
1884            instance_offset,
1885            &vertices,
1886        )?;
1887
1888        let resources = self.resources();
1889        let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else {
1890            return Ok(false);
1891        };
1892
1893        let (target_view, resolve_target) = if let Some(ref msaa_view) = resources.path_msaa_view {
1894            (msaa_view, Some(path_intermediate_view))
1895        } else {
1896            (path_intermediate_view, None)
1897        };
1898
1899        {
1900            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1901                label: Some("path_rasterization_pass"),
1902                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1903                    view: target_view,
1904                    resolve_target,
1905                    ops: wgpu::Operations {
1906                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1907                        store: wgpu::StoreOp::Store,
1908                    },
1909                    depth_slice: None,
1910                })],
1911                depth_stencil_attachment: None,
1912                ..Default::default()
1913            });
1914
1915            pass.set_pipeline(&resources.pipelines.path_rasterization);
1916            pass.set_bind_group(0, &resources.path_globals_bind_group, &[]);
1917            pass.set_bind_group(1, &vertex_binding.bind_group, &[]);
1918            // The path rasterization shader loads records by vertex index
1919            // rather than instance index, so the allocation's base shifts the
1920            // vertex range here.
1921            pass.draw(
1922                vertex_binding.first_instance
1923                    ..vertex_binding.first_instance + vertices.len() as u32,
1924                0..1,
1925            );
1926        }
1927
1928        Ok(true)
1929    }
1930
1931    fn write_instance_binding<T>(
1932        &mut self,
1933        label: &str,
1934        instance_offset: &mut u64,
1935        instances: &[T],
1936    ) -> Result<InstanceBinding> {
1937        let data = unsafe { Self::instance_bytes(instances) };
1938        // wgpu rejects zero-sized bindings, so empty primitive arrays still
1939        // reserve the 16-byte minimum.
1940        let size = (data.len() as u64).max(16);
1941        let stride = (std::mem::size_of::<T>() as u64).max(1);
1942        let (alignment, allocation_size) = if self.uses_webgl_instance_data {
1943            // The texture transport has no binding offset: the shader indexes
1944            // the instance texture absolutely, so each allocation must start on
1945            // a whole instance (a stride multiple) and a whole texel, and must
1946            // end on a texel boundary so the zero padding of its final partial
1947            // texel cannot overlap the next allocation.
1948            (
1949                least_common_multiple(self.instance_data_alignment, stride),
1950                size.next_multiple_of(INSTANCE_TEXTURE_TEXEL_SIZE),
1951            )
1952        } else {
1953            (self.instance_data_alignment.max(1), size)
1954        };
1955        let mut offset = (*instance_offset).next_multiple_of(alignment);
1956        if offset + allocation_size > self.instance_data_capacity {
1957            self.grow_instance_data(allocation_size)?;
1958            offset = 0;
1959        }
1960        *instance_offset = offset + allocation_size;
1961
1962        let first_instance = if self.uses_webgl_instance_data {
1963            u32::try_from(offset / stride).context("instance index exceeds u32 range")?
1964        } else {
1965            0
1966        };
1967
1968        let resources = self.resources();
1969        if !data.is_empty() {
1970            match &resources.instance_data {
1971                InstanceData::Storage(buffer) => resources.queue.write_buffer(buffer, offset, data),
1972                InstanceData::Texture { .. } => {
1973                    Self::write_instance_texture(resources, offset, data)
1974                }
1975            }
1976        }
1977        let bind_group = resources
1978            .device
1979            .create_bind_group(&wgpu::BindGroupDescriptor {
1980                label: Some(label),
1981                layout: &resources.bind_group_layouts.instances,
1982                entries: &[wgpu::BindGroupEntry {
1983                    binding: 0,
1984                    resource: match &resources.instance_data {
1985                        InstanceData::Storage(buffer) => {
1986                            wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1987                                buffer,
1988                                offset,
1989                                size: NonZeroU64::new(size),
1990                            })
1991                        }
1992                        InstanceData::Texture { view, .. } => {
1993                            wgpu::BindingResource::TextureView(view)
1994                        }
1995                    },
1996                }],
1997            });
1998        Ok(InstanceBinding {
1999            bind_group,
2000            first_instance,
2001        })
2002    }
2003
2004    fn write_instance_texture(resources: &WgpuResources, offset: u64, data: &[u8]) {
2005        let InstanceData::Texture {
2006            texture,
2007            width,
2008            height,
2009            ..
2010        } = &resources.instance_data
2011        else {
2012            return;
2013        };
2014        let mut byte_offset = 0usize;
2015        let mut texel_offset = offset / INSTANCE_TEXTURE_TEXEL_SIZE;
2016        while byte_offset < data.len() {
2017            let x = (texel_offset % u64::from(*width)) as u32;
2018            let y = (texel_offset / u64::from(*width)) as u32;
2019            if y >= *height {
2020                // The capacity check in write_instance_binding should make this
2021                // unreachable. Truncating silently would leave stale bytes in the
2022                // texture and draw garbage for the remaining instances.
2023                debug_assert!(
2024                    false,
2025                    "instance texture write out of bounds: row {y} >= height {}",
2026                    *height
2027                );
2028                log::error!(
2029                    "instance texture write out of bounds; dropping {} bytes of instance data",
2030                    data.len() - byte_offset
2031                );
2032                return;
2033            }
2034            let available_texels = u64::from(*width - x);
2035            let remaining_bytes = data.len() - byte_offset;
2036            let complete_texels = remaining_bytes as u64 / INSTANCE_TEXTURE_TEXEL_SIZE;
2037            let texels = complete_texels.min(available_texels);
2038            if texels > 0 {
2039                let byte_count = (texels * INSTANCE_TEXTURE_TEXEL_SIZE) as usize;
2040                resources.queue.write_texture(
2041                    wgpu::TexelCopyTextureInfo {
2042                        texture,
2043                        mip_level: 0,
2044                        origin: wgpu::Origin3d { x, y, z: 0 },
2045                        aspect: wgpu::TextureAspect::All,
2046                    },
2047                    &data[byte_offset..byte_offset + byte_count],
2048                    wgpu::TexelCopyBufferLayout {
2049                        offset: 0,
2050                        bytes_per_row: Some(byte_count as u32),
2051                        rows_per_image: None,
2052                    },
2053                    wgpu::Extent3d {
2054                        width: texels as u32,
2055                        height: 1,
2056                        depth_or_array_layers: 1,
2057                    },
2058                );
2059                byte_offset += byte_count;
2060                texel_offset += texels;
2061                continue;
2062            }
2063
2064            let mut final_texel = [0; INSTANCE_TEXTURE_TEXEL_SIZE as usize];
2065            final_texel[..remaining_bytes].copy_from_slice(&data[byte_offset..]);
2066            resources.queue.write_texture(
2067                wgpu::TexelCopyTextureInfo {
2068                    texture,
2069                    mip_level: 0,
2070                    origin: wgpu::Origin3d { x, y, z: 0 },
2071                    aspect: wgpu::TextureAspect::All,
2072                },
2073                &final_texel,
2074                wgpu::TexelCopyBufferLayout {
2075                    offset: 0,
2076                    bytes_per_row: Some(INSTANCE_TEXTURE_TEXEL_SIZE as u32),
2077                    rows_per_image: None,
2078                },
2079                wgpu::Extent3d {
2080                    width: 1,
2081                    height: 1,
2082                    depth_or_array_layers: 1,
2083                },
2084            );
2085            break;
2086        }
2087    }
2088
2089    fn grow_instance_data(&mut self, required: u64) -> Result<()> {
2090        let capacity = (self.instance_data_capacity * 2)
2091            .max(required.next_power_of_two())
2092            .min(self.max_instance_data_size);
2093        anyhow::ensure!(
2094            capacity >= required,
2095            "instance data needs {required} bytes, above the maximum of {}",
2096            self.max_instance_data_size
2097        );
2098        anyhow::ensure!(
2099            capacity > self.instance_data_capacity,
2100            "frame instance data exceeds the {}-byte maximum",
2101            self.max_instance_data_size
2102        );
2103        log::debug!(
2104            "instance data grown from {} to {capacity}",
2105            self.instance_data_capacity
2106        );
2107        // Bind groups created earlier in the frame keep the previous buffer or
2108        // texture alive, so allocations written before the grow remain valid;
2109        // only subsequent writes land in the new allocation.
2110        let uses_webgl_instance_data = self.uses_webgl_instance_data;
2111        let resources = self.resources_mut();
2112        if uses_webgl_instance_data {
2113            let max_texture_dimension = resources.device.limits().max_texture_dimension_2d;
2114            let (instance_data, actual_capacity) =
2115                Self::create_instance_texture(&resources.device, capacity, max_texture_dimension);
2116            resources.instance_data = instance_data;
2117            self.instance_data_capacity = actual_capacity;
2118        } else {
2119            resources.instance_data =
2120                InstanceData::Storage(resources.device.create_buffer(&wgpu::BufferDescriptor {
2121                    label: Some("instance_buffer"),
2122                    size: capacity,
2123                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
2124                    mapped_at_creation: false,
2125                }));
2126            self.instance_data_capacity = capacity;
2127        }
2128        Ok(())
2129    }
2130}
2131
2132impl WgpuRenderer {
2133    /// Mark the surface as unconfigured so rendering is skipped until a new
2134    /// surface is provided via [`replace_surface`](Self::replace_surface).
2135    ///
2136    /// This does **not** drop the renderer — the device, queue, atlas, and
2137    /// pipelines stay alive.  Use this when the native window is destroyed
2138    /// (e.g. Android `TerminateWindow`) but you intend to re-create the
2139    /// surface later without losing cached atlas textures.
2140    pub fn unconfigure_surface(&mut self) {
2141        self.state = match std::mem::replace(&mut self.state, RendererState::Released) {
2142            RendererState::Ready { core, surface } => {
2143                drop(surface);
2144                RendererState::Unconfigured { core }
2145            }
2146            state @ (RendererState::Unconfigured { .. } | RendererState::Released) => state,
2147        };
2148        // Drop intermediate textures since they reference the old surface size.
2149        if let Some(core) = self.core_mut() {
2150            core.resources.invalidate_intermediate_textures();
2151        }
2152    }
2153
2154    /// Replace the wgpu surface with a new one (e.g. after Android destroys
2155    /// and recreates the native window).  Keeps the device, queue, atlas, and
2156    /// all pipelines intact so cached `AtlasTextureId`s remain valid.
2157    ///
2158    /// The `instance` **must** be the same [`wgpu::Instance`] that was used to
2159    /// create the adapter and device (i.e. from the [`WgpuContext`]).  Using a
2160    /// different instance will cause a "Device does not exist" panic because
2161    /// the wgpu device is bound to its originating instance.
2162    #[cfg(not(target_family = "wasm"))]
2163    pub fn replace_surface<W: HasWindowHandle>(
2164        &mut self,
2165        window: &W,
2166        config: WgpuSurfaceConfig,
2167        instance: &wgpu::Instance,
2168    ) -> anyhow::Result<()> {
2169        let window_handle = window
2170            .window_handle()
2171            .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?;
2172
2173        let surface = create_surface(instance, window_handle.as_raw())?;
2174
2175        let width = (config.size.width.0 as u32).max(1);
2176        let height = (config.size.height.0 as u32).max(1);
2177
2178        let alpha_mode = if config.transparent {
2179            self.transparent_alpha_mode
2180        } else {
2181            self.opaque_alpha_mode
2182        };
2183
2184        self.surface_config.width = width;
2185        self.surface_config.height = height;
2186        self.surface_config.alpha_mode = alpha_mode;
2187        if let Some(mode) = config.preferred_present_mode {
2188            self.surface_config.present_mode = mode;
2189        }
2190
2191        let mut core = match std::mem::replace(&mut self.state, RendererState::Released) {
2192            RendererState::Ready {
2193                core,
2194                surface: old_surface,
2195            } => {
2196                drop(old_surface);
2197                core
2198            }
2199            RendererState::Unconfigured { core } => core,
2200            RendererState::Released => {
2201                anyhow::bail!("Cannot replace the surface: GPU resources have been released")
2202            }
2203        };
2204        surface.configure(&core.resources.device, &self.surface_config);
2205        core.resources.invalidate_intermediate_textures();
2206        self.state = RendererState::Ready { surface, core };
2207
2208        Ok(())
2209    }
2210
2211    pub fn destroy(&mut self) {
2212        // Release surface-bound GPU resources eagerly so the underlying native
2213        // window can be destroyed before the renderer itself is dropped.
2214        self.state = RendererState::Released;
2215    }
2216
2217    /// Returns true if the GPU device was lost and recovery is needed.
2218    pub fn device_lost(&self) -> bool {
2219        self.device_errors.device_lost()
2220    }
2221
2222    /// Returns true if a redraw is needed because GPU state was cleared.
2223    /// Calling this method clears the flag.
2224    pub fn needs_redraw(&mut self) -> bool {
2225        std::mem::take(&mut self.needs_redraw)
2226    }
2227
2228    /// Recovers from a lost GPU device by recreating the renderer with a new context.
2229    ///
2230    /// Call this after detecting `device_lost()` returns true.
2231    ///
2232    /// This method coordinates recovery across multiple windows:
2233    /// - The first window to call this will recreate the shared context
2234    /// - Subsequent windows will adopt the already-recovered context
2235    #[cfg(not(target_family = "wasm"))]
2236    pub fn recover<W>(&mut self, window: &W) -> anyhow::Result<()>
2237    where
2238        W: HasWindowHandle + HasDisplayHandle + std::fmt::Debug + Send + Sync + Clone + 'static,
2239    {
2240        let gpu_context = self.context.as_ref().expect("recover requires gpu_context");
2241
2242        // Check if another window already recovered the context
2243        let needs_new_context = gpu_context
2244            .borrow()
2245            .as_ref()
2246            .is_none_or(|ctx| ctx.device_lost());
2247
2248        let window_handle = window
2249            .window_handle()
2250            .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?;
2251
2252        let config = WgpuSurfaceConfig {
2253            size: gpui::Size {
2254                width: gpui::DevicePixels(self.surface_config.width as i32),
2255                height: gpui::DevicePixels(self.surface_config.height as i32),
2256            },
2257            transparent: self.surface_config.alpha_mode != wgpu::CompositeAlphaMode::Opaque,
2258            preferred_present_mode: Some(self.surface_config.present_mode),
2259        };
2260
2261        let surface = if needs_new_context {
2262            log::warn!("GPU device lost, recreating context...");
2263
2264            // Drop old resources to release Arc<Device>/Arc<Queue> and GPU resources
2265            self.state = RendererState::Released;
2266            *gpu_context.borrow_mut() = None;
2267
2268            // Wait briefly for the GPU driver to stabilize, then try to
2269            // recreate the context without software renderers. If this fails
2270            // the caller should request another frame and retry — the real GPU
2271            // may need more time to come back (e.g. after suspend/resume).
2272            std::thread::sleep(std::time::Duration::from_millis(350));
2273
2274            let instance = WgpuContext::instance(Some(Box::new(window.clone())));
2275            let surface = create_surface(&instance, window_handle.as_raw())?;
2276            let new_context = WgpuContext::new_rejecting_software(
2277                instance,
2278                &surface,
2279                &config,
2280                self.compositor_gpu,
2281            )?;
2282            *gpu_context.borrow_mut() = Some(new_context);
2283            surface
2284        } else {
2285            let ctx_ref = gpu_context.borrow();
2286            let instance = &ctx_ref.as_ref().unwrap().instance;
2287            create_surface(instance, window_handle.as_raw())?
2288        };
2289
2290        let gpu_context = Rc::clone(gpu_context);
2291        let ctx_ref = gpu_context.borrow();
2292        let context = ctx_ref.as_ref().expect("context should exist");
2293
2294        self.state = RendererState::Released;
2295        self.atlas.handle_device_lost(context);
2296
2297        let is_bgr = self.is_bgr;
2298        *self = Self::new_internal(
2299            Some(gpu_context.clone()),
2300            context,
2301            surface,
2302            config,
2303            needs_new_context,
2304            self.compositor_gpu,
2305            self.atlas.clone(),
2306        )?;
2307        self.set_subpixel_layout(is_bgr);
2308
2309        log::info!("GPU recovery complete");
2310        Ok(())
2311    }
2312}
2313
2314#[cfg(all(
2315    not(target_family = "wasm"),
2316    any(test, feature = "bench-support", feature = "test-support")
2317))]
2318struct HeadlessRenderTarget {
2319    texture: wgpu::Texture,
2320    view: wgpu::TextureView,
2321}
2322
2323#[cfg(all(
2324    not(target_family = "wasm"),
2325    any(test, feature = "bench-support", feature = "test-support")
2326))]
2327impl HeadlessRenderTarget {
2328    fn size(&self) -> Size<DevicePixels> {
2329        Size {
2330            width: DevicePixels(self.texture.width() as i32),
2331            height: DevicePixels(self.texture.height() as i32),
2332        }
2333    }
2334}
2335
2336#[cfg(all(
2337    not(target_family = "wasm"),
2338    any(test, feature = "bench-support", feature = "test-support")
2339))]
2340pub struct WgpuHeadlessRenderer {
2341    context: WgpuContext,
2342    core: WgpuRendererCore,
2343    render_target: Option<HeadlessRenderTarget>,
2344    observed_error_generation: u64,
2345}
2346
2347#[cfg(all(
2348    not(target_family = "wasm"),
2349    any(test, feature = "bench-support", feature = "test-support")
2350))]
2351impl WgpuHeadlessRenderer {
2352    pub fn new() -> anyhow::Result<Self> {
2353        let (context, target_format) = WgpuContext::new_headless()?;
2354        // The device may be shared with earlier renderers; observe only errors raised from
2355        // here on, including any while this renderer builds its pipelines.
2356        let observed_error_generation = context.errors().current_generation();
2357        let atlas = Arc::new(WgpuAtlas::from_context(&context));
2358        let core = WgpuRendererCore::new(
2359            &context,
2360            atlas,
2361            target_format,
2362            wgpu::CompositeAlphaMode::Opaque,
2363        );
2364
2365        Ok(Self {
2366            context,
2367            core,
2368            render_target: None,
2369            observed_error_generation,
2370        })
2371    }
2372
2373    fn ensure_render_target(&mut self, size: Size<DevicePixels>) -> anyhow::Result<()> {
2374        anyhow::ensure!(
2375            size.width.0 > 0 && size.height.0 > 0,
2376            "invalid headless render target size: {size:?}"
2377        );
2378        anyhow::ensure!(
2379            size.width.0 as u32 <= self.core.max_texture_size
2380                && size.height.0 as u32 <= self.core.max_texture_size,
2381            "headless render target size {size:?} exceeds maximum texture dimension {}",
2382            self.core.max_texture_size
2383        );
2384        if self
2385            .render_target
2386            .as_ref()
2387            .is_some_and(|target| target.size() == size)
2388        {
2389            return Ok(());
2390        }
2391
2392        let texture = self
2393            .core
2394            .resources
2395            .device
2396            .create_texture(&wgpu::TextureDescriptor {
2397                label: Some("headless_render_target"),
2398                size: wgpu::Extent3d {
2399                    width: size.width.0 as u32,
2400                    height: size.height.0 as u32,
2401                    depth_or_array_layers: 1,
2402                },
2403                mip_level_count: 1,
2404                sample_count: 1,
2405                dimension: wgpu::TextureDimension::D2,
2406                format: self.core.target_format,
2407                usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2408                view_formats: &[],
2409            });
2410        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
2411        self.render_target = Some(HeadlessRenderTarget { texture, view });
2412        Ok(())
2413    }
2414
2415    fn check_gpu_errors(&mut self) -> anyhow::Result<()> {
2416        anyhow::ensure!(
2417            !self.context.device_lost(),
2418            "GPU device was lost during headless rendering"
2419        );
2420        if let Some(error) = self
2421            .context
2422            .errors()
2423            .observe_error(&mut self.observed_error_generation)
2424        {
2425            anyhow::bail!("GPU error during headless rendering: {error}");
2426        }
2427        Ok(())
2428    }
2429
2430    fn render(&mut self, scene: &Scene, size: Size<DevicePixels>) -> anyhow::Result<()> {
2431        self.check_gpu_errors()?;
2432        self.ensure_render_target(size)?;
2433        let view = self
2434            .render_target
2435            .as_ref()
2436            .ok_or_else(|| anyhow::anyhow!("Headless render target was not created"))?
2437            .view
2438            .clone();
2439        self.core
2440            .render_frame(scene, &view, size, false, wgpu::Color::BLACK)?;
2441        Ok(())
2442    }
2443
2444    /// Copies the current render target back to the CPU. Dimensions come from the
2445    /// target texture itself, so the copy can never disagree with what was rendered.
2446    fn read_image(&mut self) -> anyhow::Result<image::RgbaImage> {
2447        let target = self
2448            .render_target
2449            .as_ref()
2450            .ok_or_else(|| anyhow::anyhow!("Headless render target was not created"))?;
2451        let width = target.texture.width();
2452        let height = target.texture.height();
2453        let bytes_per_row = width
2454            .checked_mul(4)
2455            .ok_or_else(|| anyhow::anyhow!("Headless render target row size overflowed"))?;
2456        let padded_bytes_per_row = bytes_per_row
2457            .checked_next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
2458            .ok_or_else(|| anyhow::anyhow!("Headless padded row size overflowed"))?;
2459        let buffer_size = u64::from(padded_bytes_per_row)
2460            .checked_mul(u64::from(height))
2461            .ok_or_else(|| anyhow::anyhow!("Headless readback buffer size overflowed"))?;
2462        anyhow::ensure!(
2463            buffer_size <= self.core.resources.device.limits().max_buffer_size,
2464            "Headless readback buffer size {buffer_size} exceeds maximum buffer size {}",
2465            self.core.resources.device.limits().max_buffer_size
2466        );
2467        let readback_buffer = self
2468            .core
2469            .resources
2470            .device
2471            .create_buffer(&wgpu::BufferDescriptor {
2472                label: Some("headless_readback_buffer"),
2473                size: buffer_size,
2474                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2475                mapped_at_creation: false,
2476            });
2477        let mut encoder =
2478            self.core
2479                .resources
2480                .device
2481                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2482                    label: Some("headless_readback_encoder"),
2483                });
2484        encoder.copy_texture_to_buffer(
2485            wgpu::TexelCopyTextureInfo {
2486                texture: &target.texture,
2487                mip_level: 0,
2488                origin: wgpu::Origin3d::ZERO,
2489                aspect: wgpu::TextureAspect::All,
2490            },
2491            wgpu::TexelCopyBufferInfo {
2492                buffer: &readback_buffer,
2493                layout: wgpu::TexelCopyBufferLayout {
2494                    offset: 0,
2495                    bytes_per_row: Some(padded_bytes_per_row),
2496                    rows_per_image: Some(height),
2497                },
2498            },
2499            wgpu::Extent3d {
2500                width,
2501                height,
2502                depth_or_array_layers: 1,
2503            },
2504        );
2505        let submission = self
2506            .core
2507            .resources
2508            .queue
2509            .submit(std::iter::once(encoder.finish()));
2510        let (sender, receiver) = std::sync::mpsc::channel();
2511        readback_buffer
2512            .slice(..)
2513            .map_async(wgpu::MapMode::Read, move |result| {
2514                if sender.send(result).is_err() {
2515                    log::error!("Headless readback receiver was dropped before mapping completed");
2516                }
2517            });
2518        self.core
2519            .resources
2520            .device
2521            .poll(wgpu::PollType::Wait {
2522                submission_index: Some(submission),
2523                timeout: Some(std::time::Duration::from_secs(30)),
2524            })
2525            .map_err(|error| anyhow::anyhow!("Failed to wait for headless rendering: {error}"))?;
2526        receiver
2527            .recv_timeout(std::time::Duration::from_secs(30))
2528            .map_err(|error| anyhow::anyhow!("Failed to receive headless mapping result: {error}"))?
2529            .map_err(|error| anyhow::anyhow!("Failed to map headless readback buffer: {error}"))?;
2530        self.check_gpu_errors()?;
2531
2532        let mapped_data = readback_buffer.slice(..).get_mapped_range()?;
2533        let pixel_capacity = usize::try_from(u64::from(bytes_per_row) * u64::from(height))
2534            .map_err(|_| anyhow::anyhow!("Headless image size exceeds addressable memory"))?;
2535        let mut pixels = Vec::with_capacity(pixel_capacity);
2536        for row in mapped_data
2537            .chunks_exact(padded_bytes_per_row as usize)
2538            .take(height as usize)
2539        {
2540            pixels.extend_from_slice(&row[..bytes_per_row as usize]);
2541        }
2542        drop(mapped_data);
2543        readback_buffer.unmap();
2544
2545        if self.core.target_format == wgpu::TextureFormat::Bgra8Unorm {
2546            for pixel in pixels.chunks_exact_mut(4) {
2547                pixel.swap(0, 2);
2548            }
2549        }
2550
2551        image::RgbaImage::from_raw(width, height, pixels)
2552            .ok_or_else(|| anyhow::anyhow!("Failed to create image from headless pixel data"))
2553    }
2554}
2555
2556#[cfg(all(
2557    not(target_family = "wasm"),
2558    any(test, feature = "bench-support", feature = "test-support")
2559))]
2560impl gpui::PlatformHeadlessRenderer for WgpuHeadlessRenderer {
2561    fn render_scene_to_image(
2562        &mut self,
2563        scene: &Scene,
2564        size: Size<DevicePixels>,
2565    ) -> anyhow::Result<image::RgbaImage> {
2566        self.render(scene, size)?;
2567        self.read_image()
2568    }
2569
2570    fn render_scene(&mut self, scene: &Scene, size: Size<DevicePixels>) -> anyhow::Result<()> {
2571        self.render(scene, size)
2572    }
2573
2574    fn sprite_atlas(&self) -> Arc<dyn gpui::PlatformAtlas> {
2575        self.core.atlas.clone()
2576    }
2577}
2578
2579fn instance_range(range: Range<usize>) -> Range<u32> {
2580    range.start as u32..range.end as u32
2581}
2582
2583#[cfg(not(target_family = "wasm"))]
2584fn create_surface(
2585    instance: &wgpu::Instance,
2586    raw_window_handle: raw_window_handle::RawWindowHandle,
2587) -> anyhow::Result<wgpu::Surface<'static>> {
2588    unsafe {
2589        instance
2590            .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
2591                // Fall back to the display handle already provided via InstanceDescriptor::display.
2592                raw_display_handle: None,
2593                raw_window_handle,
2594            })
2595            .map_err(|e| anyhow::anyhow!("{e}"))
2596    }
2597}
2598
2599struct RenderingParameters {
2600    path_sample_count: u32,
2601    gamma_ratios: [f32; 4],
2602    grayscale_enhanced_contrast: f32,
2603    subpixel_enhanced_contrast: f32,
2604}
2605
2606impl RenderingParameters {
2607    fn new(adapter: &wgpu::Adapter, surface_format: wgpu::TextureFormat) -> Self {
2608        use std::env;
2609
2610        let format_features = adapter.get_texture_format_features(surface_format);
2611        let path_sample_count = [4, 2, 1]
2612            .into_iter()
2613            .find(|&n| format_features.flags.sample_count_supported(n))
2614            .unwrap_or(1);
2615
2616        let gamma = env::var("ZED_FONTS_GAMMA")
2617            .ok()
2618            .and_then(|v| v.parse().ok())
2619            .unwrap_or(1.8_f32)
2620            .clamp(1.0, 2.2);
2621        let gamma_ratios = get_gamma_correction_ratios(gamma);
2622
2623        let grayscale_enhanced_contrast = env::var("ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST")
2624            .ok()
2625            .and_then(|v| v.parse().ok())
2626            .unwrap_or(1.0_f32)
2627            .max(0.0);
2628
2629        let subpixel_enhanced_contrast = env::var("ZED_FONTS_SUBPIXEL_ENHANCED_CONTRAST")
2630            .ok()
2631            .and_then(|v| v.parse().ok())
2632            .unwrap_or(0.5_f32)
2633            .max(0.0);
2634
2635        Self {
2636            path_sample_count,
2637            gamma_ratios,
2638            grayscale_enhanced_contrast,
2639            subpixel_enhanced_contrast,
2640        }
2641    }
2642}
2643
2644#[cfg(test)]
2645mod tests {
2646    use super::*;
2647    use gpui::{
2648        BorderStyle, ColorSpace, ContentMask, Corners, Edges, Hsla, MonochromeSprite,
2649        PolychromeSprite, Quad, Shadow, Size, SubpixelSprite, Underline, linear_color_stop,
2650        linear_gradient,
2651    };
2652    #[cfg(target_os = "linux")]
2653    use gpui::{DevicePixels, PlatformHeadlessRenderer, Scene};
2654
2655    #[cfg(target_os = "linux")]
2656    fn device_size(width: i32, height: i32) -> Size<DevicePixels> {
2657        Size {
2658            width: DevicePixels(width),
2659            height: DevicePixels(height),
2660        }
2661    }
2662
2663    #[cfg(target_os = "linux")]
2664    fn solid_quad(x: f32, y: f32, width: f32, height: f32, color: Hsla) -> Quad {
2665        let bounds = Bounds {
2666            origin: Point {
2667                x: x.into(),
2668                y: y.into(),
2669            },
2670            size: Size {
2671                width: width.into(),
2672                height: height.into(),
2673            },
2674        };
2675        Quad {
2676            order: 0,
2677            border_style: BorderStyle::Solid,
2678            bounds,
2679            content_mask: ContentMask { bounds },
2680            background: color.into(),
2681            border_color: color,
2682            corner_radii: Corners::default(),
2683            border_widths: Edges::default(),
2684        }
2685    }
2686
2687    /// Channels are compared with a small tolerance so the assertions hold across
2688    /// drivers without pinning exact rasterizer output.
2689    #[cfg(target_os = "linux")]
2690    fn assert_pixel(image: &image::RgbaImage, x: u32, y: u32, expected: [u8; 4]) {
2691        let actual = image.get_pixel(x, y).0;
2692        assert!(
2693            actual
2694                .iter()
2695                .zip(expected)
2696                .all(|(actual, expected)| actual.abs_diff(expected) <= 3),
2697            "pixel ({x}, {y}) was {actual:?}, expected {expected:?}"
2698        );
2699    }
2700
2701    #[cfg(target_os = "linux")]
2702    const RED: [u8; 4] = [255, 0, 0, 255];
2703    #[cfg(target_os = "linux")]
2704    const BLUE: [u8; 4] = [0, 0, 255, 255];
2705    #[cfg(target_os = "linux")]
2706    const BLACK: [u8; 4] = [0, 0, 0, 255];
2707
2708    #[cfg(target_os = "linux")]
2709    #[test]
2710    fn headless_renderer_draws_quads_with_distinct_colors() -> anyhow::Result<()> {
2711        let mut renderer = WgpuHeadlessRenderer::new()?;
2712        let mut scene = Scene::default();
2713        scene.insert_primitive(solid_quad(0.0, 0.0, 32.0, 32.0, gpui::red()));
2714        scene.insert_primitive(solid_quad(32.0, 0.0, 32.0, 32.0, gpui::blue()));
2715        scene.finish();
2716
2717        let image = renderer.render_scene_to_image(&scene, device_size(64, 32))?;
2718        assert_eq!(image.dimensions(), (64, 32));
2719        assert_pixel(&image, 8, 16, RED);
2720        assert_pixel(&image, 24, 16, RED);
2721        assert_pixel(&image, 40, 16, BLUE);
2722        assert_pixel(&image, 56, 16, BLUE);
2723        Ok(())
2724    }
2725
2726    #[cfg(target_os = "linux")]
2727    #[test]
2728    fn headless_renderer_captures_each_requested_size() -> anyhow::Result<()> {
2729        let mut renderer = WgpuHeadlessRenderer::new()?;
2730        let mut scene = Scene::default();
2731        scene.insert_primitive(solid_quad(2.0, 2.0, 4.0, 3.0, gpui::red()));
2732        scene.finish();
2733
2734        // 13 px rows are 52 bytes, forcing readback to strip copy-row padding.
2735        let image = renderer.render_scene_to_image(&scene, device_size(13, 7))?;
2736        assert_eq!(image.dimensions(), (13, 7));
2737        assert_pixel(&image, 0, 0, BLACK);
2738        assert_pixel(&image, 3, 3, RED);
2739        assert_pixel(&image, 12, 6, BLACK);
2740
2741        let image = renderer.render_scene_to_image(&scene, device_size(17, 9))?;
2742        assert_eq!(image.dimensions(), (17, 9));
2743        assert_pixel(&image, 3, 3, RED);
2744        assert_pixel(&image, 16, 8, BLACK);
2745
2746        let image = renderer.render_scene_to_image(&Scene::default(), device_size(13, 7))?;
2747        assert_eq!(image.dimensions(), (13, 7));
2748        assert!(image.pixels().all(|pixel| pixel.0 == BLACK));
2749        Ok(())
2750    }
2751
2752    #[cfg(target_os = "linux")]
2753    #[test]
2754    fn headless_renderer_reuses_target_for_same_size() -> anyhow::Result<()> {
2755        let mut renderer = WgpuHeadlessRenderer::new()?;
2756        let target_texture = |renderer: &WgpuHeadlessRenderer| {
2757            renderer
2758                .render_target
2759                .as_ref()
2760                .map(|target| target.texture.clone())
2761        };
2762
2763        renderer.render_scene(&Scene::default(), device_size(16, 16))?;
2764        let first = target_texture(&renderer);
2765        assert!(first.is_some());
2766
2767        renderer.render_scene(&Scene::default(), device_size(16, 16))?;
2768        assert_eq!(target_texture(&renderer), first);
2769
2770        renderer.render_scene(&Scene::default(), device_size(16, 17))?;
2771        assert_ne!(target_texture(&renderer), first);
2772        Ok(())
2773    }
2774
2775    #[cfg(target_os = "linux")]
2776    #[test]
2777    fn headless_renderer_rejects_invalid_sizes() -> anyhow::Result<()> {
2778        let mut renderer = WgpuHeadlessRenderer::new()?;
2779        let too_large = renderer.core.max_texture_size as i32 + 1;
2780
2781        for size in [
2782            device_size(0, 8),
2783            device_size(8, 0),
2784            device_size(-1, 8),
2785            device_size(too_large, 8),
2786        ] {
2787            assert!(
2788                renderer.render_scene(&Scene::default(), size).is_err(),
2789                "{size:?} should be rejected"
2790            );
2791            assert!(
2792                renderer
2793                    .render_scene_to_image(&Scene::default(), size)
2794                    .is_err(),
2795                "{size:?} should be rejected"
2796            );
2797        }
2798
2799        // Rejection must leave the renderer usable.
2800        let image = renderer.render_scene_to_image(&Scene::default(), device_size(4, 4))?;
2801        assert_eq!(image.dimensions(), (4, 4));
2802        Ok(())
2803    }
2804
2805    #[test]
2806    fn webgl_shader_is_valid_wgsl_without_storage_buffers() {
2807        assert!(!WEBGL_SHADERS.contains("var<storage"));
2808        validate_wgsl(WEBGL_SHADERS, naga::valid::Capabilities::empty());
2809    }
2810
2811    #[test]
2812    fn storage_buffer_shader_is_valid_wgsl() {
2813        validate_wgsl(STORAGE_BUFFER_SHADERS, naga::valid::Capabilities::empty());
2814    }
2815
2816    #[test]
2817    fn subpixel_shader_is_valid_wgsl() {
2818        validate_wgsl(
2819            SUBPIXEL_SHADERS,
2820            naga::valid::Capabilities::DUAL_SOURCE_BLENDING,
2821        );
2822    }
2823
2824    fn validate_wgsl(source: &str, capabilities: naga::valid::Capabilities) {
2825        let module = naga::front::wgsl::parse_str(source).expect("shader should parse");
2826        naga::valid::Validator::new(naga::valid::ValidationFlags::all(), capabilities)
2827            .validate(&module)
2828            .expect("shader should validate");
2829    }
2830
2831    #[test]
2832    fn webgl_record_sizes_match_shader_word_strides() {
2833        assert_eq!(std::mem::size_of::<Quad>(), 40 * 4);
2834        assert_eq!(std::mem::size_of::<Shadow>(), 28 * 4);
2835        assert_eq!(std::mem::size_of::<PathRasterizationVertex>(), 26 * 4);
2836        assert_eq!(std::mem::size_of::<PathSprite>(), 4 * 4);
2837        assert_eq!(std::mem::size_of::<Underline>(), 16 * 4);
2838        assert_eq!(std::mem::size_of::<MonochromeSprite>(), 28 * 4);
2839        assert_eq!(std::mem::size_of::<SubpixelSprite>(), 28 * 4);
2840        assert_eq!(std::mem::size_of::<PolychromeSprite>(), 24 * 4);
2841    }
2842
2843    #[test]
2844    fn webgl_quad_layout_matches_fixed_decoder() {
2845        let quad = Quad {
2846            order: 41,
2847            border_style: BorderStyle::Dashed,
2848            bounds: Bounds {
2849                origin: Point {
2850                    x: 1.0.into(),
2851                    y: 2.0.into(),
2852                },
2853                size: Size {
2854                    width: 3.0.into(),
2855                    height: 4.0.into(),
2856                },
2857            },
2858            content_mask: ContentMask {
2859                bounds: Bounds {
2860                    origin: Point {
2861                        x: 5.0.into(),
2862                        y: 6.0.into(),
2863                    },
2864                    size: Size {
2865                        width: 7.0.into(),
2866                        height: 8.0.into(),
2867                    },
2868                },
2869            },
2870            background: linear_gradient(
2871                11.0,
2872                linear_color_stop(
2873                    Hsla {
2874                        h: 12.0,
2875                        s: 13.0,
2876                        l: 14.0,
2877                        a: 15.0,
2878                    },
2879                    16.0,
2880                ),
2881                linear_color_stop(
2882                    Hsla {
2883                        h: 17.0,
2884                        s: 18.0,
2885                        l: 19.0,
2886                        a: 20.0,
2887                    },
2888                    21.0,
2889                ),
2890            )
2891            .color_space(ColorSpace::Oklab),
2892            border_color: Hsla {
2893                h: 22.0,
2894                s: 23.0,
2895                l: 24.0,
2896                a: 25.0,
2897            },
2898            corner_radii: Corners {
2899                top_left: 26.0.into(),
2900                top_right: 27.0.into(),
2901                bottom_right: 28.0.into(),
2902                bottom_left: 29.0.into(),
2903            },
2904            border_widths: Edges {
2905                top: 30.0.into(),
2906                right: 31.0.into(),
2907                bottom: 32.0.into(),
2908                left: 33.0.into(),
2909            },
2910        };
2911
2912        let bytes = unsafe { WgpuRendererCore::instance_bytes(std::slice::from_ref(&quad)) };
2913        let words: &[u32] = bytemuck::cast_slice(bytes);
2914        assert_eq!(
2915            words,
2916            &[
2917                41,
2918                1,
2919                1.0_f32.to_bits(),
2920                2.0_f32.to_bits(),
2921                3.0_f32.to_bits(),
2922                4.0_f32.to_bits(),
2923                5.0_f32.to_bits(),
2924                6.0_f32.to_bits(),
2925                7.0_f32.to_bits(),
2926                8.0_f32.to_bits(),
2927                1,
2928                1,
2929                0,
2930                0,
2931                0,
2932                0,
2933                11.0_f32.to_bits(),
2934                12.0_f32.to_bits(),
2935                13.0_f32.to_bits(),
2936                14.0_f32.to_bits(),
2937                15.0_f32.to_bits(),
2938                16.0_f32.to_bits(),
2939                17.0_f32.to_bits(),
2940                18.0_f32.to_bits(),
2941                19.0_f32.to_bits(),
2942                20.0_f32.to_bits(),
2943                21.0_f32.to_bits(),
2944                0,
2945                22.0_f32.to_bits(),
2946                23.0_f32.to_bits(),
2947                24.0_f32.to_bits(),
2948                25.0_f32.to_bits(),
2949                26.0_f32.to_bits(),
2950                27.0_f32.to_bits(),
2951                28.0_f32.to_bits(),
2952                29.0_f32.to_bits(),
2953                30.0_f32.to_bits(),
2954                31.0_f32.to_bits(),
2955                32.0_f32.to_bits(),
2956                33.0_f32.to_bits(),
2957            ]
2958        );
2959    }
2960}