Skip to main content

gpui_wgpu/
wgpu_renderer.rs

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