Skip to main content

gpui_wgpu/
wgpu_renderer.rs

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