Skip to main content

gpui_wgpu/
wgpu_renderer.rs

1use crate::{CompositorGpuHint, WgpuAtlas, WgpuContext};
2use anyhow::{Context as _, Result};
3use bytemuck::{Pod, Zeroable};
4use gpui_engine::{
5    AtlasTextureId, Path, PlatformAtlas, PrimitiveBatch, Scene, SceneRenderer,
6    get_gamma_correction_ratios,
7};
8use gpui_platform::{Background, Bounds, DevicePixels, GpuSpecs, Point, ScaledPixels, Size};
9use log::warn;
10#[cfg(not(target_family = "wasm"))]
11use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
12use std::cell::RefCell;
13use std::num::NonZeroU64;
14use std::ops::Range;
15use std::rc::Rc;
16use std::sync::{Arc, Mutex};
17
18const MAX_INSTANCE_BUFFER_SIZE: u64 = 256 * 1024 * 1024;
19
20const INSTANCE_TEXTURE_TEXEL_SIZE: u64 = 16;
21
22/// Shader variant for backends with storage buffer support: the shared shader
23/// logic plus the storage-buffer instance transport.
24const STORAGE_BUFFER_SHADERS: &str = concat!(
25    include_str!("shaders.wgsl"),
26    include_str!("shaders_storage.wgsl"),
27);
28
29/// Shader variant for WebGL2, which has no storage buffers: the shared shader
30/// logic plus the texture-based instance transport.
31const WEBGL_SHADERS: &str = concat!(
32    include_str!("shaders.wgsl"),
33    include_str!("shaders_webgl.wgsl"),
34);
35
36/// Subpixel text rendering requires dual-source blending, which WebGL2 lacks, so
37/// this variant only ever runs with the storage-buffer transport. The `enable`
38/// directive must precede all declarations.
39const SUBPIXEL_SHADERS: &str = concat!(
40    "enable dual_source_blending;\n",
41    include_str!("shaders.wgsl"),
42    include_str!("shaders_storage.wgsl"),
43    include_str!("shaders_subpixel.wgsl"),
44);
45
46fn least_common_multiple(left: u64, right: u64) -> u64 {
47    let mut first = left;
48    let mut second = right;
49    while second != 0 {
50        let remainder = first % second;
51        first = second;
52        second = remainder;
53    }
54    left / first * right
55}
56
57#[repr(C)]
58#[derive(Clone, Copy, Pod, Zeroable)]
59struct GlobalParams {
60    viewport_size: [f32; 2],
61    premultiplied_alpha: u32,
62    pad: u32,
63}
64
65#[repr(C)]
66#[derive(Clone, Copy, Pod, Zeroable)]
67struct PodBounds {
68    origin: [f32; 2],
69    size: [f32; 2],
70}
71
72impl From<Bounds<ScaledPixels>> for PodBounds {
73    fn from(bounds: Bounds<ScaledPixels>) -> Self {
74        Self {
75            origin: [bounds.origin.x.0, bounds.origin.y.0],
76            size: [bounds.size.width.0, bounds.size.height.0],
77        }
78    }
79}
80
81#[repr(C)]
82#[derive(Clone, Copy, Pod, Zeroable)]
83struct SurfaceParams {
84    bounds: PodBounds,
85    content_mask: PodBounds,
86}
87
88#[repr(C)]
89#[derive(Clone, Copy, Pod, Zeroable)]
90struct GammaParams {
91    gamma_ratios: [f32; 4],
92    grayscale_enhanced_contrast: f32,
93    subpixel_enhanced_contrast: f32,
94    is_bgr: u32,
95    _pad: u32,
96}
97
98#[derive(Clone, Debug)]
99#[repr(C)]
100struct PathSprite {
101    bounds: Bounds<ScaledPixels>,
102}
103
104#[derive(Clone, Debug)]
105#[repr(C)]
106struct PathRasterizationVertex {
107    xy_position: Point<ScaledPixels>,
108    st_position: Point<f32>,
109    color: Background,
110    bounds: Bounds<ScaledPixels>,
111}
112
113pub struct WgpuSurfaceConfig {
114    pub size: Size<DevicePixels>,
115    pub transparent: bool,
116    /// Preferred presentation mode. When `Some`, the renderer will use this
117    /// mode if supported by the surface, falling back to `Fifo`.
118    /// When `None`, defaults to `Fifo` (VSync).
119    ///
120    /// Mobile platforms may prefer `Mailbox` (triple-buffering) to avoid
121    /// blocking in `get_current_texture()` during lifecycle transitions.
122    pub preferred_present_mode: Option<wgpu::PresentMode>,
123}
124
125struct WgpuPipelines {
126    quads: wgpu::RenderPipeline,
127    shadows: wgpu::RenderPipeline,
128    path_rasterization: wgpu::RenderPipeline,
129    paths: wgpu::RenderPipeline,
130    underlines: wgpu::RenderPipeline,
131    mono_sprites: wgpu::RenderPipeline,
132    subpixel_sprites: Option<wgpu::RenderPipeline>,
133    poly_sprites: wgpu::RenderPipeline,
134    #[allow(dead_code)]
135    surfaces: wgpu::RenderPipeline,
136}
137
138/// One frame allocation of instance data, ready to bind.
139struct InstanceBinding {
140    bind_group: wgpu::BindGroup,
141    /// Index of the allocation's first instance within the bound data. Always
142    /// zero on the storage-buffer path, where the binding offset already
143    /// positions the array; on the WebGL texture path the shader indexes the
144    /// shared instance texture absolutely, so draws must offset their
145    /// instance (or vertex) ranges by this value.
146    first_instance: u32,
147}
148
149struct InstanceBindings {
150    quads: InstanceBinding,
151    shadows: InstanceBinding,
152    underlines: InstanceBinding,
153    monochrome_sprites: InstanceBinding,
154    subpixel_sprites: InstanceBinding,
155    polychrome_sprites: InstanceBinding,
156}
157
158struct WgpuBindGroupLayouts {
159    globals: wgpu::BindGroupLayout,
160    instances: wgpu::BindGroupLayout,
161    texture: wgpu::BindGroupLayout,
162    surfaces: wgpu::BindGroupLayout,
163}
164
165/// Shared GPU context reference, used to coordinate device recovery across multiple windows.
166pub type GpuContext = Rc<RefCell<Option<WgpuContext>>>;
167
168enum InstanceData {
169    Storage(wgpu::Buffer),
170    // WebGL2 has no storage buffers. A uint texture keeps the records available to both shader
171    // stages while preserving integer and floating-point bit patterns exactly.
172    Texture {
173        texture: wgpu::Texture,
174        view: wgpu::TextureView,
175        width: u32,
176        height: u32,
177    },
178}
179
180/// GPU resources that must be dropped together during device recovery.
181struct WgpuResources {
182    device: Arc<wgpu::Device>,
183    queue: Arc<wgpu::Queue>,
184    surface: wgpu::Surface<'static>,
185    pipelines: WgpuPipelines,
186    bind_group_layouts: WgpuBindGroupLayouts,
187    atlas_sampler: wgpu::Sampler,
188    globals_buffer: wgpu::Buffer,
189    globals_bind_group: wgpu::BindGroup,
190    path_globals_bind_group: wgpu::BindGroup,
191    instance_data: InstanceData,
192    path_intermediate_texture: Option<wgpu::Texture>,
193    path_intermediate_view: Option<wgpu::TextureView>,
194    path_msaa_texture: Option<wgpu::Texture>,
195    path_msaa_view: Option<wgpu::TextureView>,
196}
197
198impl WgpuResources {
199    fn invalidate_intermediate_textures(&mut self) {
200        self.path_intermediate_texture = None;
201        self.path_intermediate_view = None;
202        self.path_msaa_texture = None;
203        self.path_msaa_view = None;
204    }
205}
206
207pub struct WgpuRenderer {
208    /// Shared GPU context for device recovery coordination (unused on WASM).
209    #[allow(dead_code)]
210    context: Option<GpuContext>,
211    /// Compositor GPU hint for adapter selection (unused on WASM).
212    #[allow(dead_code)]
213    compositor_gpu: Option<CompositorGpuHint>,
214    resources: Option<WgpuResources>,
215    surface_config: wgpu::SurfaceConfiguration,
216    atlas: Arc<WgpuAtlas>,
217    path_globals_offset: u64,
218    gamma_offset: u64,
219    instance_data_capacity: u64,
220    max_instance_data_size: u64,
221    instance_data_alignment: u64,
222    uses_webgl_instance_data: bool,
223    rendering_params: RenderingParameters,
224    is_bgr: bool,
225    dual_source_blending: bool,
226    adapter_info: wgpu::AdapterInfo,
227    transparent_alpha_mode: wgpu::CompositeAlphaMode,
228    opaque_alpha_mode: wgpu::CompositeAlphaMode,
229    max_texture_size: u32,
230    last_error: Arc<Mutex<Option<String>>>,
231    failed_frame_count: u32,
232    device_lost: std::sync::Arc<std::sync::atomic::AtomicBool>,
233    surface_configured: bool,
234    needs_redraw: bool,
235}
236
237impl WgpuRenderer {
238    fn resources(&self) -> &WgpuResources {
239        self.resources
240            .as_ref()
241            .expect("GPU resources not available")
242    }
243
244    fn resources_mut(&mut self) -> &mut WgpuResources {
245        self.resources
246            .as_mut()
247            .expect("GPU resources not available")
248    }
249
250    /// Creates a new WgpuRenderer from raw window handles.
251    ///
252    /// The `gpu_context` is a shared reference that coordinates GPU context across
253    /// multiple windows. The first window to create a renderer will initialize the
254    /// context; subsequent windows will share it.
255    ///
256    /// # Safety
257    /// The caller must ensure that the window handle remains valid for the lifetime
258    /// of the returned renderer.
259    #[cfg(not(target_family = "wasm"))]
260    pub fn new<W>(
261        gpu_context: GpuContext,
262        window: &W,
263        config: WgpuSurfaceConfig,
264        compositor_gpu: Option<CompositorGpuHint>,
265    ) -> anyhow::Result<Self>
266    where
267        W: HasWindowHandle + HasDisplayHandle + std::fmt::Debug + Send + Sync + Clone + 'static,
268    {
269        let window_handle = window
270            .window_handle()
271            .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?;
272
273        let target = wgpu::SurfaceTargetUnsafe::RawHandle {
274            // Fall back to the display handle already provided via InstanceDescriptor::display.
275            raw_display_handle: None,
276            raw_window_handle: window_handle.as_raw(),
277        };
278
279        // Use the existing context's instance if available, otherwise create a new one.
280        // The surface must be created with the same instance that will be used for
281        // adapter selection, otherwise wgpu will panic.
282        let instance = gpu_context
283            .borrow()
284            .as_ref()
285            .map(|ctx| ctx.instance.clone())
286            .unwrap_or_else(|| WgpuContext::instance(Box::new(window.clone())));
287
288        // Safety: The caller guarantees that the window handle is valid for the
289        // lifetime of this renderer. In practice, the RawWindow struct is created
290        // from the native window handles and the surface is dropped before the window.
291        let surface = unsafe {
292            instance
293                .create_surface_unsafe(target)
294                .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?
295        };
296
297        let mut ctx_ref = gpu_context.borrow_mut();
298        let context = match ctx_ref.as_mut() {
299            Some(context) => {
300                context.check_compatible_with_surface(&surface)?;
301                context
302            }
303            None => ctx_ref.insert(WgpuContext::new(instance, &surface, compositor_gpu)?),
304        };
305
306        let atlas = Arc::new(WgpuAtlas::from_context(context));
307
308        Self::new_internal(
309            Some(Rc::clone(&gpu_context)),
310            context,
311            surface,
312            config,
313            compositor_gpu,
314            atlas,
315        )
316    }
317
318    #[cfg(target_family = "wasm")]
319    pub fn new_from_canvas(
320        context: &WgpuContext,
321        canvas: &web_sys::HtmlCanvasElement,
322        config: WgpuSurfaceConfig,
323    ) -> anyhow::Result<Self> {
324        let surface = context
325            .instance
326            .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
327            .map_err(|e| anyhow::anyhow!("Failed to create surface: {e}"))?;
328        Self::new_from_surface(context, surface, config)
329    }
330
331    #[cfg(target_family = "wasm")]
332    #[allow(clippy::arc_with_non_send_sync)]
333    pub fn new_from_surface(
334        context: &WgpuContext,
335        surface: wgpu::Surface<'static>,
336        config: WgpuSurfaceConfig,
337    ) -> anyhow::Result<Self> {
338        let atlas = Arc::new(WgpuAtlas::from_context(context));
339        Self::new_internal(None, context, surface, config, None, atlas)
340    }
341
342    fn new_internal(
343        gpu_context: Option<GpuContext>,
344        context: &WgpuContext,
345        surface: wgpu::Surface<'static>,
346        config: WgpuSurfaceConfig,
347        compositor_gpu: Option<CompositorGpuHint>,
348        atlas: Arc<WgpuAtlas>,
349    ) -> anyhow::Result<Self> {
350        let surface_caps = surface.get_capabilities(&context.adapter);
351        let preferred_formats = [
352            wgpu::TextureFormat::Bgra8Unorm,
353            wgpu::TextureFormat::Rgba8Unorm,
354        ];
355        let surface_format = preferred_formats
356            .iter()
357            .find(|f| surface_caps.formats.contains(f))
358            .copied()
359            .or_else(|| surface_caps.formats.iter().find(|f| !f.is_srgb()).copied())
360            .or_else(|| surface_caps.formats.first().copied())
361            .ok_or_else(|| {
362                anyhow::anyhow!(
363                    "Surface reports no supported texture formats for adapter {:?}",
364                    context.adapter.get_info().name
365                )
366            })?;
367
368        let pick_alpha_mode =
369            |preferences: &[wgpu::CompositeAlphaMode]| -> anyhow::Result<wgpu::CompositeAlphaMode> {
370                preferences
371                    .iter()
372                    .find(|p| surface_caps.alpha_modes.contains(p))
373                    .copied()
374                    .or_else(|| surface_caps.alpha_modes.first().copied())
375                    .ok_or_else(|| {
376                        anyhow::anyhow!(
377                            "Surface reports no supported alpha modes for adapter {:?}",
378                            context.adapter.get_info().name
379                        )
380                    })
381            };
382
383        let transparent_alpha_mode = pick_alpha_mode(&[
384            wgpu::CompositeAlphaMode::PreMultiplied,
385            wgpu::CompositeAlphaMode::Inherit,
386        ])?;
387
388        let opaque_alpha_mode = pick_alpha_mode(&[
389            wgpu::CompositeAlphaMode::Opaque,
390            wgpu::CompositeAlphaMode::Inherit,
391        ])?;
392
393        let alpha_mode = if config.transparent {
394            transparent_alpha_mode
395        } else {
396            opaque_alpha_mode
397        };
398
399        let device = Arc::clone(&context.device);
400        let max_texture_size = device.limits().max_texture_dimension_2d;
401
402        let requested_width = config.size.width.0 as u32;
403        let requested_height = config.size.height.0 as u32;
404        let clamped_width = requested_width.min(max_texture_size);
405        let clamped_height = requested_height.min(max_texture_size);
406
407        if clamped_width != requested_width || clamped_height != requested_height {
408            warn!(
409                "Requested surface size ({}, {}) exceeds maximum texture dimension {}. \
410                 Clamping to ({}, {}). Window content may not fill the entire window.",
411                requested_width, requested_height, max_texture_size, clamped_width, clamped_height
412            );
413        }
414
415        let surface_config = wgpu::SurfaceConfiguration {
416            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
417            format: surface_format,
418            width: clamped_width.max(1),
419            height: clamped_height.max(1),
420            present_mode: config
421                .preferred_present_mode
422                .filter(|mode| surface_caps.present_modes.contains(mode))
423                .unwrap_or(wgpu::PresentMode::Fifo),
424            desired_maximum_frame_latency: 2,
425            alpha_mode,
426            view_formats: vec![],
427        };
428        // Configure the surface immediately. The adapter selection process already validated
429        // that this adapter can successfully configure this surface.
430        surface.configure(&context.device, &surface_config);
431
432        let queue = Arc::clone(&context.queue);
433        let rendering_params = RenderingParameters::new(&context.adapter, surface_format);
434        let uses_webgl_instance_data = context.uses_webgl_instance_data();
435        let dual_source_blending =
436            context.supports_dual_source_blending() && !uses_webgl_instance_data;
437        let bind_group_layouts = Self::create_bind_group_layouts(&device, uses_webgl_instance_data);
438        let pipelines = Self::create_pipelines(
439            &device,
440            &bind_group_layouts,
441            surface_format,
442            alpha_mode,
443            rendering_params.path_sample_count,
444            dual_source_blending,
445            uses_webgl_instance_data,
446        );
447
448        let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
449            label: Some("atlas_sampler"),
450            mag_filter: wgpu::FilterMode::Linear,
451            min_filter: wgpu::FilterMode::Linear,
452            ..Default::default()
453        });
454
455        let uniform_alignment = device.limits().min_uniform_buffer_offset_alignment as u64;
456        let globals_size = std::mem::size_of::<GlobalParams>() as u64;
457        let gamma_size = std::mem::size_of::<GammaParams>() as u64;
458        let path_globals_offset = globals_size.next_multiple_of(uniform_alignment);
459        let gamma_offset = (path_globals_offset + globals_size).next_multiple_of(uniform_alignment);
460
461        let globals_buffer = device.create_buffer(&wgpu::BufferDescriptor {
462            label: Some("globals_buffer"),
463            size: gamma_offset + gamma_size,
464            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
465            mapped_at_creation: false,
466        });
467
468        let (
469            instance_data,
470            instance_data_capacity,
471            max_instance_data_size,
472            instance_data_alignment,
473        ) = if uses_webgl_instance_data {
474            let max_texture_dimension = device.limits().max_texture_dimension_2d;
475            let max_instance_data_size = (u64::from(max_texture_dimension).pow(2)
476                * INSTANCE_TEXTURE_TEXEL_SIZE)
477                .min(MAX_INSTANCE_BUFFER_SIZE);
478            let initial_capacity = (2 * 1024 * 1024).min(max_instance_data_size);
479            let (instance_data, capacity) =
480                Self::create_instance_texture(&device, initial_capacity, max_texture_dimension);
481            (
482                instance_data,
483                capacity,
484                max_instance_data_size,
485                INSTANCE_TEXTURE_TEXEL_SIZE,
486            )
487        } else {
488            // Every frame allocation is exposed as one storage-buffer binding, so
489            // its backing buffer must satisfy both the allocation and binding limits.
490            let max_buffer_size = device
491                .limits()
492                .max_buffer_size
493                .min(device.limits().max_storage_buffer_binding_size)
494                .min(MAX_INSTANCE_BUFFER_SIZE);
495            let initial_capacity = (2 * 1024 * 1024).min(max_buffer_size);
496            let buffer = device.create_buffer(&wgpu::BufferDescriptor {
497                label: Some("instance_buffer"),
498                size: initial_capacity,
499                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
500                mapped_at_creation: false,
501            });
502            (
503                InstanceData::Storage(buffer),
504                initial_capacity,
505                max_buffer_size,
506                device.limits().min_storage_buffer_offset_alignment as u64,
507            )
508        };
509
510        let globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
511            label: Some("globals_bind_group"),
512            layout: &bind_group_layouts.globals,
513            entries: &[
514                wgpu::BindGroupEntry {
515                    binding: 0,
516                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
517                        buffer: &globals_buffer,
518                        offset: 0,
519                        size: Some(NonZeroU64::new(globals_size).unwrap()),
520                    }),
521                },
522                wgpu::BindGroupEntry {
523                    binding: 1,
524                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
525                        buffer: &globals_buffer,
526                        offset: gamma_offset,
527                        size: Some(NonZeroU64::new(gamma_size).unwrap()),
528                    }),
529                },
530            ],
531        });
532
533        let path_globals_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
534            label: Some("path_globals_bind_group"),
535            layout: &bind_group_layouts.globals,
536            entries: &[
537                wgpu::BindGroupEntry {
538                    binding: 0,
539                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
540                        buffer: &globals_buffer,
541                        offset: path_globals_offset,
542                        size: Some(NonZeroU64::new(globals_size).unwrap()),
543                    }),
544                },
545                wgpu::BindGroupEntry {
546                    binding: 1,
547                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
548                        buffer: &globals_buffer,
549                        offset: gamma_offset,
550                        size: Some(NonZeroU64::new(gamma_size).unwrap()),
551                    }),
552                },
553            ],
554        });
555
556        let adapter_info = context.adapter.get_info();
557
558        let last_error: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
559        let last_error_clone = Arc::clone(&last_error);
560        device.on_uncaptured_error(Arc::new(move |error| {
561            let mut guard = last_error_clone.lock().unwrap();
562            *guard = Some(error.to_string());
563        }));
564
565        let resources = WgpuResources {
566            device,
567            queue,
568            surface,
569            pipelines,
570            bind_group_layouts,
571            atlas_sampler,
572            globals_buffer,
573            globals_bind_group,
574            path_globals_bind_group,
575            instance_data,
576            // Defer intermediate texture creation to first draw call via ensure_intermediate_textures().
577            // This avoids panics when the device/surface is in an invalid state during initialization.
578            path_intermediate_texture: None,
579            path_intermediate_view: None,
580            path_msaa_texture: None,
581            path_msaa_view: None,
582        };
583
584        Ok(Self {
585            context: gpu_context,
586            compositor_gpu,
587            resources: Some(resources),
588            surface_config,
589            atlas,
590            path_globals_offset,
591            gamma_offset,
592            instance_data_capacity,
593            max_instance_data_size,
594            instance_data_alignment,
595            uses_webgl_instance_data,
596            rendering_params,
597            is_bgr: false,
598            dual_source_blending,
599            adapter_info,
600            transparent_alpha_mode,
601            opaque_alpha_mode,
602            max_texture_size,
603            last_error,
604            failed_frame_count: 0,
605            device_lost: context.device_lost_flag(),
606            surface_configured: true,
607            needs_redraw: false,
608        })
609    }
610
611    fn create_bind_group_layouts(
612        device: &wgpu::Device,
613        uses_webgl_instance_data: bool,
614    ) -> WgpuBindGroupLayouts {
615        let globals =
616            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
617                label: Some("globals_layout"),
618                entries: &[
619                    wgpu::BindGroupLayoutEntry {
620                        binding: 0,
621                        visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
622                        ty: wgpu::BindingType::Buffer {
623                            ty: wgpu::BufferBindingType::Uniform,
624                            has_dynamic_offset: false,
625                            min_binding_size: NonZeroU64::new(
626                                std::mem::size_of::<GlobalParams>() as u64
627                            ),
628                        },
629                        count: None,
630                    },
631                    wgpu::BindGroupLayoutEntry {
632                        binding: 1,
633                        visibility: wgpu::ShaderStages::FRAGMENT,
634                        ty: wgpu::BindingType::Buffer {
635                            ty: wgpu::BufferBindingType::Uniform,
636                            has_dynamic_offset: false,
637                            min_binding_size: NonZeroU64::new(
638                                std::mem::size_of::<GammaParams>() as u64
639                            ),
640                        },
641                        count: None,
642                    },
643                ],
644            });
645
646        let instance_data_entry = wgpu::BindGroupLayoutEntry {
647            binding: 0,
648            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
649            ty: if uses_webgl_instance_data {
650                wgpu::BindingType::Texture {
651                    sample_type: wgpu::TextureSampleType::Uint,
652                    view_dimension: wgpu::TextureViewDimension::D2,
653                    multisampled: false,
654                }
655            } else {
656                wgpu::BindingType::Buffer {
657                    ty: wgpu::BufferBindingType::Storage { read_only: true },
658                    has_dynamic_offset: false,
659                    min_binding_size: None,
660                }
661            },
662            count: None,
663        };
664
665        let instances = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
666            label: Some("instances_layout"),
667            entries: &[instance_data_entry],
668        });
669
670        let texture = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
671            label: Some("texture_layout"),
672            entries: &[
673                wgpu::BindGroupLayoutEntry {
674                    binding: 0,
675                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
676                    ty: wgpu::BindingType::Texture {
677                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
678                        view_dimension: wgpu::TextureViewDimension::D2,
679                        multisampled: false,
680                    },
681                    count: None,
682                },
683                wgpu::BindGroupLayoutEntry {
684                    binding: 1,
685                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
686                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
687                    count: None,
688                },
689            ],
690        });
691
692        let surfaces = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
693            label: Some("surfaces_layout"),
694            entries: &[
695                wgpu::BindGroupLayoutEntry {
696                    binding: 0,
697                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
698                    ty: wgpu::BindingType::Buffer {
699                        ty: wgpu::BufferBindingType::Uniform,
700                        has_dynamic_offset: false,
701                        min_binding_size: NonZeroU64::new(
702                            std::mem::size_of::<SurfaceParams>() as u64
703                        ),
704                    },
705                    count: None,
706                },
707                wgpu::BindGroupLayoutEntry {
708                    binding: 1,
709                    visibility: wgpu::ShaderStages::FRAGMENT,
710                    ty: wgpu::BindingType::Texture {
711                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
712                        view_dimension: wgpu::TextureViewDimension::D2,
713                        multisampled: false,
714                    },
715                    count: None,
716                },
717                wgpu::BindGroupLayoutEntry {
718                    binding: 2,
719                    visibility: wgpu::ShaderStages::FRAGMENT,
720                    ty: wgpu::BindingType::Texture {
721                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
722                        view_dimension: wgpu::TextureViewDimension::D2,
723                        multisampled: false,
724                    },
725                    count: None,
726                },
727                wgpu::BindGroupLayoutEntry {
728                    binding: 3,
729                    visibility: wgpu::ShaderStages::FRAGMENT,
730                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
731                    count: None,
732                },
733            ],
734        });
735
736        WgpuBindGroupLayouts {
737            globals,
738            instances,
739            texture,
740            surfaces,
741        }
742    }
743
744    fn create_instance_texture(
745        device: &wgpu::Device,
746        requested_capacity: u64,
747        max_texture_dimension: u32,
748    ) -> (InstanceData, u64) {
749        let texel_count = requested_capacity.div_ceil(INSTANCE_TEXTURE_TEXEL_SIZE);
750        let width = texel_count.min(u64::from(max_texture_dimension)).max(1) as u32;
751        let height = texel_count
752            .div_ceil(u64::from(width))
753            .min(u64::from(max_texture_dimension))
754            .max(1) as u32;
755        let texture = device.create_texture(&wgpu::TextureDescriptor {
756            label: Some("instance_texture"),
757            size: wgpu::Extent3d {
758                width,
759                height,
760                depth_or_array_layers: 1,
761            },
762            mip_level_count: 1,
763            sample_count: 1,
764            dimension: wgpu::TextureDimension::D2,
765            format: wgpu::TextureFormat::Rgba32Uint,
766            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
767            view_formats: &[],
768        });
769        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
770        let capacity = u64::from(width) * u64::from(height) * INSTANCE_TEXTURE_TEXEL_SIZE;
771        (
772            InstanceData::Texture {
773                texture,
774                view,
775                width,
776                height,
777            },
778            capacity,
779        )
780    }
781
782    fn create_pipelines(
783        device: &wgpu::Device,
784        layouts: &WgpuBindGroupLayouts,
785        surface_format: wgpu::TextureFormat,
786        alpha_mode: wgpu::CompositeAlphaMode,
787        path_sample_count: u32,
788        dual_source_blending: bool,
789        uses_webgl_instance_data: bool,
790    ) -> WgpuPipelines {
791        // Diagnostic guard: verify the device actually has
792        // DUAL_SOURCE_BLENDING. We have a crash report (ZED-5G1) where a
793        // feature mismatch caused a wgpu-hal abort, but we haven't
794        // identified the code path that produces the mismatch. This
795        // guard prevents the crash and logs more evidence.
796        // Remove this check once:
797        // a) We find and fix the root cause, or
798        // b) There are no reports of this warning appearing for some time.
799        let device_has_feature = device
800            .features()
801            .contains(wgpu::Features::DUAL_SOURCE_BLENDING);
802        if dual_source_blending && !device_has_feature {
803            log::error!(
804                "BUG: dual_source_blending flag is true but device does not \
805                 have DUAL_SOURCE_BLENDING enabled (device features: {:?}). \
806                 Falling back to mono text rendering. Please report this at \
807                 https://github.com/zed-industries/zed/issues",
808                device.features(),
809            );
810        }
811        let dual_source_blending =
812            dual_source_blending && device_has_feature && !uses_webgl_instance_data;
813
814        let shader_source = if uses_webgl_instance_data {
815            WEBGL_SHADERS
816        } else {
817            STORAGE_BUFFER_SHADERS
818        };
819        let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
820            label: Some("gpui_shaders"),
821            source: wgpu::ShaderSource::Wgsl(shader_source.into()),
822        });
823
824        let subpixel_shader_module = if dual_source_blending {
825            Some(device.create_shader_module(wgpu::ShaderModuleDescriptor {
826                label: Some("gpui_subpixel_shaders"),
827                source: wgpu::ShaderSource::Wgsl(SUBPIXEL_SHADERS.into()),
828            }))
829        } else {
830            None
831        };
832
833        let blend_mode = match alpha_mode {
834            wgpu::CompositeAlphaMode::PreMultiplied => {
835                wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING
836            }
837            _ => wgpu::BlendState::ALPHA_BLENDING,
838        };
839
840        let color_target = wgpu::ColorTargetState {
841            format: surface_format,
842            blend: Some(blend_mode),
843            write_mask: wgpu::ColorWrites::ALL,
844        };
845
846        let create_pipeline = |name: &str,
847                               vs_entry: &str,
848                               fs_entry: &str,
849                               globals_layout: &wgpu::BindGroupLayout,
850                               data_layout: &wgpu::BindGroupLayout,
851                               texture_layout: Option<&wgpu::BindGroupLayout>,
852                               topology: wgpu::PrimitiveTopology,
853                               color_targets: &[Option<wgpu::ColorTargetState>],
854                               sample_count: u32,
855                               module: &wgpu::ShaderModule| {
856            let mut bind_group_layouts = vec![Some(globals_layout), Some(data_layout)];
857            bind_group_layouts.extend(texture_layout.map(Some));
858            let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
859                label: Some(&format!("{name}_layout")),
860                bind_group_layouts: &bind_group_layouts,
861                immediate_size: 0,
862            });
863
864            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
865                label: Some(name),
866                layout: Some(&pipeline_layout),
867                vertex: wgpu::VertexState {
868                    module,
869                    entry_point: Some(vs_entry),
870                    buffers: &[],
871                    compilation_options: wgpu::PipelineCompilationOptions::default(),
872                },
873                fragment: Some(wgpu::FragmentState {
874                    module,
875                    entry_point: Some(fs_entry),
876                    targets: color_targets,
877                    compilation_options: wgpu::PipelineCompilationOptions::default(),
878                }),
879                primitive: wgpu::PrimitiveState {
880                    topology,
881                    strip_index_format: None,
882                    front_face: wgpu::FrontFace::Ccw,
883                    cull_mode: None,
884                    polygon_mode: wgpu::PolygonMode::Fill,
885                    unclipped_depth: false,
886                    conservative: false,
887                },
888                depth_stencil: None,
889                multisample: wgpu::MultisampleState {
890                    count: sample_count,
891                    mask: !0,
892                    alpha_to_coverage_enabled: false,
893                },
894                multiview_mask: None,
895                cache: None,
896            })
897        };
898
899        let quads = create_pipeline(
900            "quads",
901            "vs_quad",
902            "fs_quad",
903            &layouts.globals,
904            &layouts.instances,
905            None,
906            wgpu::PrimitiveTopology::TriangleStrip,
907            &[Some(color_target.clone())],
908            1,
909            &shader_module,
910        );
911
912        let shadows = create_pipeline(
913            "shadows",
914            "vs_shadow",
915            "fs_shadow",
916            &layouts.globals,
917            &layouts.instances,
918            None,
919            wgpu::PrimitiveTopology::TriangleStrip,
920            &[Some(color_target.clone())],
921            1,
922            &shader_module,
923        );
924
925        let path_rasterization = create_pipeline(
926            "path_rasterization",
927            "vs_path_rasterization",
928            "fs_path_rasterization",
929            &layouts.globals,
930            &layouts.instances,
931            None,
932            wgpu::PrimitiveTopology::TriangleList,
933            &[Some(wgpu::ColorTargetState {
934                format: surface_format,
935                blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
936                write_mask: wgpu::ColorWrites::ALL,
937            })],
938            path_sample_count,
939            &shader_module,
940        );
941
942        let paths_blend = wgpu::BlendState {
943            color: wgpu::BlendComponent {
944                src_factor: wgpu::BlendFactor::One,
945                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
946                operation: wgpu::BlendOperation::Add,
947            },
948            alpha: wgpu::BlendComponent {
949                src_factor: wgpu::BlendFactor::One,
950                dst_factor: wgpu::BlendFactor::One,
951                operation: wgpu::BlendOperation::Add,
952            },
953        };
954
955        let paths = create_pipeline(
956            "paths",
957            "vs_path",
958            "fs_path",
959            &layouts.globals,
960            &layouts.instances,
961            Some(&layouts.texture),
962            wgpu::PrimitiveTopology::TriangleStrip,
963            &[Some(wgpu::ColorTargetState {
964                format: surface_format,
965                blend: Some(paths_blend),
966                write_mask: wgpu::ColorWrites::ALL,
967            })],
968            1,
969            &shader_module,
970        );
971
972        let underlines = create_pipeline(
973            "underlines",
974            "vs_underline",
975            "fs_underline",
976            &layouts.globals,
977            &layouts.instances,
978            None,
979            wgpu::PrimitiveTopology::TriangleStrip,
980            &[Some(color_target.clone())],
981            1,
982            &shader_module,
983        );
984
985        let mono_sprites = create_pipeline(
986            "mono_sprites",
987            "vs_mono_sprite",
988            "fs_mono_sprite",
989            &layouts.globals,
990            &layouts.instances,
991            Some(&layouts.texture),
992            wgpu::PrimitiveTopology::TriangleStrip,
993            &[Some(color_target.clone())],
994            1,
995            &shader_module,
996        );
997
998        let subpixel_sprites = if let Some(subpixel_module) = &subpixel_shader_module {
999            let subpixel_blend = wgpu::BlendState {
1000                color: wgpu::BlendComponent {
1001                    src_factor: wgpu::BlendFactor::Src1,
1002                    dst_factor: wgpu::BlendFactor::OneMinusSrc1,
1003                    operation: wgpu::BlendOperation::Add,
1004                },
1005                alpha: wgpu::BlendComponent {
1006                    src_factor: wgpu::BlendFactor::One,
1007                    dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
1008                    operation: wgpu::BlendOperation::Add,
1009                },
1010            };
1011
1012            Some(create_pipeline(
1013                "subpixel_sprites",
1014                "vs_subpixel_sprite",
1015                "fs_subpixel_sprite",
1016                &layouts.globals,
1017                &layouts.instances,
1018                Some(&layouts.texture),
1019                wgpu::PrimitiveTopology::TriangleStrip,
1020                &[Some(wgpu::ColorTargetState {
1021                    format: surface_format,
1022                    blend: Some(subpixel_blend),
1023                    write_mask: wgpu::ColorWrites::COLOR,
1024                })],
1025                1,
1026                subpixel_module,
1027            ))
1028        } else {
1029            None
1030        };
1031
1032        let poly_sprites = create_pipeline(
1033            "poly_sprites",
1034            "vs_poly_sprite",
1035            "fs_poly_sprite",
1036            &layouts.globals,
1037            &layouts.instances,
1038            Some(&layouts.texture),
1039            wgpu::PrimitiveTopology::TriangleStrip,
1040            &[Some(color_target.clone())],
1041            1,
1042            &shader_module,
1043        );
1044
1045        let surfaces = create_pipeline(
1046            "surfaces",
1047            "vs_surface",
1048            "fs_surface",
1049            &layouts.globals,
1050            &layouts.surfaces,
1051            None,
1052            wgpu::PrimitiveTopology::TriangleStrip,
1053            &[Some(color_target)],
1054            1,
1055            &shader_module,
1056        );
1057
1058        WgpuPipelines {
1059            quads,
1060            shadows,
1061            path_rasterization,
1062            paths,
1063            underlines,
1064            mono_sprites,
1065            subpixel_sprites,
1066            poly_sprites,
1067            surfaces,
1068        }
1069    }
1070
1071    fn create_path_intermediate(
1072        device: &wgpu::Device,
1073        format: wgpu::TextureFormat,
1074        width: u32,
1075        height: u32,
1076    ) -> (wgpu::Texture, wgpu::TextureView) {
1077        let texture = device.create_texture(&wgpu::TextureDescriptor {
1078            label: Some("path_intermediate"),
1079            size: wgpu::Extent3d {
1080                width: width.max(1),
1081                height: height.max(1),
1082                depth_or_array_layers: 1,
1083            },
1084            mip_level_count: 1,
1085            sample_count: 1,
1086            dimension: wgpu::TextureDimension::D2,
1087            format,
1088            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
1089            view_formats: &[],
1090        });
1091        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1092        (texture, view)
1093    }
1094
1095    fn create_msaa_if_needed(
1096        device: &wgpu::Device,
1097        format: wgpu::TextureFormat,
1098        width: u32,
1099        height: u32,
1100        sample_count: u32,
1101    ) -> Option<(wgpu::Texture, wgpu::TextureView)> {
1102        if sample_count <= 1 {
1103            return None;
1104        }
1105        let texture = device.create_texture(&wgpu::TextureDescriptor {
1106            label: Some("path_msaa"),
1107            size: wgpu::Extent3d {
1108                width: width.max(1),
1109                height: height.max(1),
1110                depth_or_array_layers: 1,
1111            },
1112            mip_level_count: 1,
1113            sample_count,
1114            dimension: wgpu::TextureDimension::D2,
1115            format,
1116            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1117            view_formats: &[],
1118        });
1119        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1120        Some((texture, view))
1121    }
1122
1123    pub fn update_drawable_size(&mut self, size: Size<DevicePixels>) {
1124        let width = size.width.0 as u32;
1125        let height = size.height.0 as u32;
1126
1127        if width != self.surface_config.width || height != self.surface_config.height {
1128            let clamped_width = width.min(self.max_texture_size);
1129            let clamped_height = height.min(self.max_texture_size);
1130
1131            if clamped_width != width || clamped_height != height {
1132                warn!(
1133                    "Requested surface size ({}, {}) exceeds maximum texture dimension {}. \
1134                     Clamping to ({}, {}). Window content may not fill the entire window.",
1135                    width, height, self.max_texture_size, clamped_width, clamped_height
1136                );
1137            }
1138
1139            self.surface_config.width = clamped_width.max(1);
1140            self.surface_config.height = clamped_height.max(1);
1141            let surface_config = self.surface_config.clone();
1142
1143            let Some(resources) = self.resources.as_mut() else {
1144                return;
1145            };
1146
1147            // Wait for any in-flight GPU work to complete before destroying textures
1148            if let Err(e) = resources.device.poll(wgpu::PollType::Wait {
1149                submission_index: None,
1150                timeout: None,
1151            }) {
1152                warn!("Failed to poll device during resize: {e:?}");
1153            }
1154
1155            // Destroy old textures before allocating new ones to avoid GPU memory spikes
1156            if let Some(ref texture) = resources.path_intermediate_texture {
1157                texture.destroy();
1158            }
1159            if let Some(ref texture) = resources.path_msaa_texture {
1160                texture.destroy();
1161            }
1162
1163            resources
1164                .surface
1165                .configure(&resources.device, &surface_config);
1166
1167            // Invalidate intermediate textures - they will be lazily recreated
1168            // in draw() after we confirm the surface is healthy. This avoids
1169            // panics when the device/surface is in an invalid state during resize.
1170            resources.invalidate_intermediate_textures();
1171        }
1172    }
1173
1174    fn ensure_intermediate_textures(&mut self) {
1175        if self.resources().path_intermediate_texture.is_some() {
1176            return;
1177        }
1178
1179        let format = self.surface_config.format;
1180        let width = self.surface_config.width;
1181        let height = self.surface_config.height;
1182        let path_sample_count = self.rendering_params.path_sample_count;
1183        let resources = self.resources_mut();
1184
1185        let (t, v) = Self::create_path_intermediate(&resources.device, format, width, height);
1186        resources.path_intermediate_texture = Some(t);
1187        resources.path_intermediate_view = Some(v);
1188
1189        let (path_msaa_texture, path_msaa_view) = Self::create_msaa_if_needed(
1190            &resources.device,
1191            format,
1192            width,
1193            height,
1194            path_sample_count,
1195        )
1196        .map(|(t, v)| (Some(t), Some(v)))
1197        .unwrap_or((None, None));
1198        resources.path_msaa_texture = path_msaa_texture;
1199        resources.path_msaa_view = path_msaa_view;
1200    }
1201
1202    pub fn set_subpixel_layout(&mut self, is_bgr: bool) {
1203        self.is_bgr = is_bgr;
1204    }
1205
1206    pub fn update_transparency(&mut self, transparent: bool) {
1207        let new_alpha_mode = if transparent {
1208            self.transparent_alpha_mode
1209        } else {
1210            self.opaque_alpha_mode
1211        };
1212
1213        if new_alpha_mode != self.surface_config.alpha_mode {
1214            self.surface_config.alpha_mode = new_alpha_mode;
1215            let surface_config = self.surface_config.clone();
1216            let path_sample_count = self.rendering_params.path_sample_count;
1217            let dual_source_blending = self.dual_source_blending;
1218            let uses_webgl_instance_data = self.uses_webgl_instance_data;
1219            let Some(resources) = self.resources.as_mut() else {
1220                return;
1221            };
1222            resources
1223                .surface
1224                .configure(&resources.device, &surface_config);
1225            resources.pipelines = Self::create_pipelines(
1226                &resources.device,
1227                &resources.bind_group_layouts,
1228                surface_config.format,
1229                surface_config.alpha_mode,
1230                path_sample_count,
1231                dual_source_blending,
1232                uses_webgl_instance_data,
1233            );
1234        }
1235    }
1236
1237    #[allow(dead_code)]
1238    pub fn viewport_size(&self) -> Size<DevicePixels> {
1239        Size {
1240            width: DevicePixels(self.surface_config.width as i32),
1241            height: DevicePixels(self.surface_config.height as i32),
1242        }
1243    }
1244
1245    pub fn sprite_atlas(&self) -> &Arc<WgpuAtlas> {
1246        &self.atlas
1247    }
1248
1249    pub fn supports_dual_source_blending(&self) -> bool {
1250        self.dual_source_blending
1251    }
1252
1253    pub fn gpu_specs(&self) -> GpuSpecs {
1254        GpuSpecs {
1255            is_software_emulated: self.adapter_info.device_type == wgpu::DeviceType::Cpu,
1256            device_name: self.adapter_info.name.clone(),
1257            driver_name: self.adapter_info.driver.clone(),
1258            driver_info: self.adapter_info.driver_info.clone(),
1259        }
1260    }
1261
1262    pub fn max_texture_size(&self) -> u32 {
1263        self.max_texture_size
1264    }
1265
1266    pub fn draw(&mut self, scene: &Scene) -> bool {
1267        #[cfg(target_family = "wasm")]
1268        if self.device_lost() {
1269            if self.surface_configured {
1270                log::error!(
1271                    "Browser graphics context was lost; rendering has stopped. Reload the page to recover."
1272                );
1273                self.surface_configured = false;
1274            }
1275            return false;
1276        }
1277
1278        // Bail out early if the surface has been unconfigured (e.g. during
1279        // Android background/rotation transitions).  Attempting to acquire
1280        // a texture from an unconfigured surface can block indefinitely on
1281        // some drivers (Adreno).
1282        if !self.surface_configured {
1283            return false;
1284        }
1285
1286        let last_error = self.last_error.lock().unwrap().take();
1287        if let Some(error) = last_error {
1288            self.failed_frame_count += 1;
1289            log::error!(
1290                "GPU error during frame (failure {} of 10): {error}",
1291                self.failed_frame_count
1292            );
1293
1294            // TBD. Does retrying more actually help?
1295            if self.failed_frame_count > 10 {
1296                panic!("Too many consecutive GPU errors. Last error: {error}");
1297            } else if self.failed_frame_count > 5 {
1298                if let Some(res) = self.resources.as_mut() {
1299                    res.invalidate_intermediate_textures();
1300                }
1301                self.atlas.clear();
1302                self.needs_redraw = true;
1303                self.failed_frame_count = 0;
1304                return false;
1305            }
1306        } else {
1307            self.failed_frame_count = 0;
1308        }
1309
1310        self.atlas.before_frame();
1311
1312        let frame = match self.resources().surface.get_current_texture() {
1313            wgpu::CurrentSurfaceTexture::Success(frame) => frame,
1314            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
1315                // Textures must be destroyed before the surface can be reconfigured.
1316                drop(frame);
1317                let surface_config = self.surface_config.clone();
1318                let resources = self.resources_mut();
1319                resources
1320                    .surface
1321                    .configure(&resources.device, &surface_config);
1322                return false;
1323            }
1324            wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
1325                let surface_config = self.surface_config.clone();
1326                let resources = self.resources_mut();
1327                resources
1328                    .surface
1329                    .configure(&resources.device, &surface_config);
1330                return false;
1331            }
1332            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
1333                return false;
1334            }
1335            wgpu::CurrentSurfaceTexture::Validation => {
1336                *self.last_error.lock().unwrap() =
1337                    Some("Surface texture validation error".to_string());
1338                return false;
1339            }
1340        };
1341
1342        // Now that we know the surface is healthy, ensure intermediate textures exist
1343        self.ensure_intermediate_textures();
1344
1345        let frame_view = frame
1346            .texture
1347            .create_view(&wgpu::TextureViewDescriptor::default());
1348
1349        let gamma_params = GammaParams {
1350            gamma_ratios: self.rendering_params.gamma_ratios,
1351            grayscale_enhanced_contrast: self.rendering_params.grayscale_enhanced_contrast,
1352            subpixel_enhanced_contrast: self.rendering_params.subpixel_enhanced_contrast,
1353            is_bgr: self.is_bgr as u32,
1354            _pad: 0,
1355        };
1356
1357        let globals = GlobalParams {
1358            viewport_size: [
1359                self.surface_config.width as f32,
1360                self.surface_config.height as f32,
1361            ],
1362            premultiplied_alpha: if self.surface_config.alpha_mode
1363                == wgpu::CompositeAlphaMode::PreMultiplied
1364            {
1365                1
1366            } else {
1367                0
1368            },
1369            pad: 0,
1370        };
1371
1372        let path_globals = GlobalParams {
1373            premultiplied_alpha: 0,
1374            ..globals
1375        };
1376
1377        {
1378            let resources = self.resources();
1379            resources.queue.write_buffer(
1380                &resources.globals_buffer,
1381                0,
1382                bytemuck::bytes_of(&globals),
1383            );
1384            resources.queue.write_buffer(
1385                &resources.globals_buffer,
1386                self.path_globals_offset,
1387                bytemuck::bytes_of(&path_globals),
1388            );
1389            resources.queue.write_buffer(
1390                &resources.globals_buffer,
1391                self.gamma_offset,
1392                bytemuck::bytes_of(&gamma_params),
1393            );
1394        }
1395
1396        if let Err(error) = self.record_frame(scene, &frame_view) {
1397            log::error!("{error:#}");
1398            self.resources().queue.submit(std::iter::empty());
1399            return false;
1400        }
1401
1402        frame.present();
1403        true
1404    }
1405
1406    fn record_frame(&mut self, scene: &Scene, frame_view: &wgpu::TextureView) -> Result<()> {
1407        let mut instance_offset = 0;
1408        let instance_bindings = self
1409            .write_instances(scene, &mut instance_offset)
1410            .with_context(|| {
1411                format!(
1412                    "scene too large: {} paths, {} shadows, {} quads, {} underlines, {} monochrome sprites, {} subpixel sprites, {} polychrome sprites",
1413                    scene.paths.len(),
1414                    scene.shadows.len(),
1415                    scene.quads.len(),
1416                    scene.underlines.len(),
1417                    scene.monochrome_sprites.len(),
1418                    scene.subpixel_sprites.len(),
1419                    scene.polychrome_sprites.len(),
1420                )
1421            })?;
1422
1423        let mut encoder =
1424            self.resources()
1425                .device
1426                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1427                    label: Some("main_encoder"),
1428                });
1429
1430        {
1431            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1432                label: Some("main_pass"),
1433                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1434                    view: frame_view,
1435                    resolve_target: None,
1436                    ops: wgpu::Operations {
1437                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1438                        store: wgpu::StoreOp::Store,
1439                    },
1440                    depth_slice: None,
1441                })],
1442                depth_stencil_attachment: None,
1443                ..Default::default()
1444            });
1445
1446            for batch in scene.batches() {
1447                match batch {
1448                    PrimitiveBatch::Quads(range) => self.draw_instances(
1449                        &instance_bindings.quads,
1450                        &self.resources().pipelines.quads,
1451                        instance_range(range),
1452                        &mut pass,
1453                    ),
1454                    PrimitiveBatch::Shadows(range) => self.draw_instances(
1455                        &instance_bindings.shadows,
1456                        &self.resources().pipelines.shadows,
1457                        instance_range(range),
1458                        &mut pass,
1459                    ),
1460                    PrimitiveBatch::Paths(range) => {
1461                        let paths = &scene.paths[range];
1462                        if paths.is_empty() {
1463                            continue;
1464                        }
1465
1466                        drop(pass);
1467                        let rasterized = self.draw_paths_to_intermediate(
1468                            &mut encoder,
1469                            paths,
1470                            &mut instance_offset,
1471                        )?;
1472
1473                        pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1474                            label: Some("main_pass_continued"),
1475                            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1476                                view: frame_view,
1477                                resolve_target: None,
1478                                ops: wgpu::Operations {
1479                                    load: wgpu::LoadOp::Load,
1480                                    store: wgpu::StoreOp::Store,
1481                                },
1482                                depth_slice: None,
1483                            })],
1484                            depth_stencil_attachment: None,
1485                            ..Default::default()
1486                        });
1487
1488                        if rasterized {
1489                            self.draw_paths_from_intermediate(
1490                                paths,
1491                                &mut instance_offset,
1492                                &mut pass,
1493                            )?;
1494                        }
1495                    }
1496                    PrimitiveBatch::Underlines(range) => self.draw_instances(
1497                        &instance_bindings.underlines,
1498                        &self.resources().pipelines.underlines,
1499                        instance_range(range),
1500                        &mut pass,
1501                    ),
1502                    PrimitiveBatch::MonochromeSprites { texture_id, range } => self.draw_sprites(
1503                        &instance_bindings.monochrome_sprites,
1504                        texture_id,
1505                        &self.resources().pipelines.mono_sprites,
1506                        instance_range(range),
1507                        &mut pass,
1508                    ),
1509                    PrimitiveBatch::SubpixelSprites { texture_id, range } => {
1510                        let resources = self.resources();
1511                        self.draw_sprites(
1512                            &instance_bindings.subpixel_sprites,
1513                            texture_id,
1514                            resources
1515                                .pipelines
1516                                .subpixel_sprites
1517                                .as_ref()
1518                                .unwrap_or(&resources.pipelines.mono_sprites),
1519                            instance_range(range),
1520                            &mut pass,
1521                        );
1522                    }
1523                    PrimitiveBatch::PolychromeSprites { texture_id, range } => self.draw_sprites(
1524                        &instance_bindings.polychrome_sprites,
1525                        texture_id,
1526                        &self.resources().pipelines.poly_sprites,
1527                        instance_range(range),
1528                        &mut pass,
1529                    ),
1530                    // Surfaces are macOS-only for video playback and are not
1531                    // implemented by the WGPU renderer.
1532                    PrimitiveBatch::Surfaces(_surfaces) => {}
1533                }
1534            }
1535        }
1536
1537        self.resources()
1538            .queue
1539            .submit(std::iter::once(encoder.finish()));
1540        Ok(())
1541    }
1542
1543    fn write_instances(
1544        &mut self,
1545        scene: &Scene,
1546        instance_offset: &mut u64,
1547    ) -> Result<InstanceBindings> {
1548        Ok(InstanceBindings {
1549            quads: self.write_instance_binding(
1550                "quads_bind_group",
1551                instance_offset,
1552                &scene.quads,
1553            )?,
1554            shadows: self.write_instance_binding(
1555                "shadows_bind_group",
1556                instance_offset,
1557                &scene.shadows,
1558            )?,
1559            underlines: self.write_instance_binding(
1560                "underlines_bind_group",
1561                instance_offset,
1562                &scene.underlines,
1563            )?,
1564            monochrome_sprites: self.write_instance_binding(
1565                "monochrome_sprites_bind_group",
1566                instance_offset,
1567                &scene.monochrome_sprites,
1568            )?,
1569            subpixel_sprites: self.write_instance_binding(
1570                "subpixel_sprites_bind_group",
1571                instance_offset,
1572                &scene.subpixel_sprites,
1573            )?,
1574            polychrome_sprites: self.write_instance_binding(
1575                "polychrome_sprites_bind_group",
1576                instance_offset,
1577                &scene.polychrome_sprites,
1578            )?,
1579        })
1580    }
1581
1582    fn create_texture_bind_group(
1583        &self,
1584        label: &str,
1585        texture_view: &wgpu::TextureView,
1586    ) -> wgpu::BindGroup {
1587        let resources = self.resources();
1588        resources
1589            .device
1590            .create_bind_group(&wgpu::BindGroupDescriptor {
1591                label: Some(label),
1592                layout: &resources.bind_group_layouts.texture,
1593                entries: &[
1594                    wgpu::BindGroupEntry {
1595                        binding: 0,
1596                        resource: wgpu::BindingResource::TextureView(texture_view),
1597                    },
1598                    wgpu::BindGroupEntry {
1599                        binding: 1,
1600                        resource: wgpu::BindingResource::Sampler(&resources.atlas_sampler),
1601                    },
1602                ],
1603            })
1604    }
1605
1606    fn draw_instances(
1607        &self,
1608        instances: &InstanceBinding,
1609        pipeline: &wgpu::RenderPipeline,
1610        range: Range<u32>,
1611        pass: &mut wgpu::RenderPass<'_>,
1612    ) {
1613        if range.is_empty() {
1614            return;
1615        }
1616        pass.set_pipeline(pipeline);
1617        pass.set_bind_group(0, &self.resources().globals_bind_group, &[]);
1618        pass.set_bind_group(1, &instances.bind_group, &[]);
1619        pass.draw(
1620            0..4,
1621            instances.first_instance + range.start..instances.first_instance + range.end,
1622        );
1623    }
1624
1625    fn draw_sprites(
1626        &self,
1627        sprite_instances: &InstanceBinding,
1628        texture_id: AtlasTextureId,
1629        pipeline: &wgpu::RenderPipeline,
1630        range: Range<u32>,
1631        pass: &mut wgpu::RenderPass<'_>,
1632    ) {
1633        if range.is_empty() {
1634            return;
1635        }
1636        let texture_info = self.atlas.get_texture_info(texture_id);
1637        let texture =
1638            self.create_texture_bind_group("atlas_texture_bind_group", &texture_info.view);
1639        pass.set_pipeline(pipeline);
1640        pass.set_bind_group(0, &self.resources().globals_bind_group, &[]);
1641        pass.set_bind_group(1, &sprite_instances.bind_group, &[]);
1642        pass.set_bind_group(2, &texture, &[]);
1643        pass.draw(
1644            0..4,
1645            sprite_instances.first_instance + range.start
1646                ..sprite_instances.first_instance + range.end,
1647        );
1648    }
1649
1650    unsafe fn instance_bytes<T>(instances: &[T]) -> &[u8] {
1651        unsafe {
1652            std::slice::from_raw_parts(
1653                instances.as_ptr() as *const u8,
1654                std::mem::size_of_val(instances),
1655            )
1656        }
1657    }
1658
1659    fn draw_paths_from_intermediate(
1660        &mut self,
1661        paths: &[Path<ScaledPixels>],
1662        instance_offset: &mut u64,
1663        pass: &mut wgpu::RenderPass<'_>,
1664    ) -> Result<()> {
1665        let first_path = &paths[0];
1666        let sprites: Vec<PathSprite> = if paths.last().map(|p| &p.order) == Some(&first_path.order)
1667        {
1668            paths
1669                .iter()
1670                .map(|p| PathSprite {
1671                    bounds: p.clipped_bounds(),
1672                })
1673                .collect()
1674        } else {
1675            let mut bounds = first_path.clipped_bounds();
1676            for path in paths.iter().skip(1) {
1677                bounds = bounds.union(&path.clipped_bounds());
1678            }
1679            vec![PathSprite { bounds }]
1680        };
1681
1682        let Some(path_intermediate_view) = self.resources().path_intermediate_view.clone() else {
1683            return Ok(());
1684        };
1685        let instances =
1686            self.write_instance_binding("path_sprites_bind_group", instance_offset, &sprites)?;
1687        let texture = self.create_texture_bind_group(
1688            "path_intermediate_texture_bind_group",
1689            &path_intermediate_view,
1690        );
1691        let resources = self.resources();
1692        pass.set_pipeline(&resources.pipelines.paths);
1693        pass.set_bind_group(0, &resources.globals_bind_group, &[]);
1694        pass.set_bind_group(1, &instances.bind_group, &[]);
1695        pass.set_bind_group(2, &texture, &[]);
1696        pass.draw(
1697            0..4,
1698            instances.first_instance..instances.first_instance + sprites.len() as u32,
1699        );
1700        Ok(())
1701    }
1702
1703    fn draw_paths_to_intermediate(
1704        &mut self,
1705        encoder: &mut wgpu::CommandEncoder,
1706        paths: &[Path<ScaledPixels>],
1707        instance_offset: &mut u64,
1708    ) -> Result<bool> {
1709        let mut vertices = Vec::new();
1710        for path in paths {
1711            let bounds = path.clipped_bounds();
1712            vertices.extend(path.vertices.iter().map(|v| PathRasterizationVertex {
1713                xy_position: v.xy_position,
1714                st_position: v.st_position,
1715                color: path.color,
1716                bounds,
1717            }));
1718        }
1719
1720        if vertices.is_empty() {
1721            return Ok(false);
1722        }
1723
1724        let vertex_binding = self.write_instance_binding(
1725            "path_rasterization_bind_group",
1726            instance_offset,
1727            &vertices,
1728        )?;
1729
1730        let resources = self.resources();
1731        let Some(path_intermediate_view) = resources.path_intermediate_view.as_ref() else {
1732            return Ok(false);
1733        };
1734
1735        let (target_view, resolve_target) = if let Some(ref msaa_view) = resources.path_msaa_view {
1736            (msaa_view, Some(path_intermediate_view))
1737        } else {
1738            (path_intermediate_view, None)
1739        };
1740
1741        {
1742            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1743                label: Some("path_rasterization_pass"),
1744                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1745                    view: target_view,
1746                    resolve_target,
1747                    ops: wgpu::Operations {
1748                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1749                        store: wgpu::StoreOp::Store,
1750                    },
1751                    depth_slice: None,
1752                })],
1753                depth_stencil_attachment: None,
1754                ..Default::default()
1755            });
1756
1757            pass.set_pipeline(&resources.pipelines.path_rasterization);
1758            pass.set_bind_group(0, &resources.path_globals_bind_group, &[]);
1759            pass.set_bind_group(1, &vertex_binding.bind_group, &[]);
1760            // The path rasterization shader loads records by vertex index
1761            // rather than instance index, so the allocation's base shifts the
1762            // vertex range here.
1763            pass.draw(
1764                vertex_binding.first_instance
1765                    ..vertex_binding.first_instance + vertices.len() as u32,
1766                0..1,
1767            );
1768        }
1769
1770        Ok(true)
1771    }
1772
1773    fn write_instance_binding<T>(
1774        &mut self,
1775        label: &str,
1776        instance_offset: &mut u64,
1777        instances: &[T],
1778    ) -> Result<InstanceBinding> {
1779        let data = unsafe { Self::instance_bytes(instances) };
1780        // wgpu rejects zero-sized bindings, so empty primitive arrays still
1781        // reserve the 16-byte minimum.
1782        let size = (data.len() as u64).max(16);
1783        let stride = (std::mem::size_of::<T>() as u64).max(1);
1784        let (alignment, allocation_size) = if self.uses_webgl_instance_data {
1785            // The texture transport has no binding offset: the shader indexes
1786            // the instance texture absolutely, so each allocation must start on
1787            // a whole instance (a stride multiple) and a whole texel, and must
1788            // end on a texel boundary so the zero padding of its final partial
1789            // texel cannot overlap the next allocation.
1790            (
1791                least_common_multiple(self.instance_data_alignment, stride),
1792                size.next_multiple_of(INSTANCE_TEXTURE_TEXEL_SIZE),
1793            )
1794        } else {
1795            (self.instance_data_alignment.max(1), size)
1796        };
1797        let mut offset = (*instance_offset).next_multiple_of(alignment);
1798        if offset + allocation_size > self.instance_data_capacity {
1799            self.grow_instance_data(allocation_size)?;
1800            offset = 0;
1801        }
1802        *instance_offset = offset + allocation_size;
1803
1804        let first_instance = if self.uses_webgl_instance_data {
1805            u32::try_from(offset / stride).context("instance index exceeds u32 range")?
1806        } else {
1807            0
1808        };
1809
1810        let resources = self.resources();
1811        if !data.is_empty() {
1812            match &resources.instance_data {
1813                InstanceData::Storage(buffer) => resources.queue.write_buffer(buffer, offset, data),
1814                InstanceData::Texture { .. } => {
1815                    Self::write_instance_texture(resources, offset, data)
1816                }
1817            }
1818        }
1819        let bind_group = resources
1820            .device
1821            .create_bind_group(&wgpu::BindGroupDescriptor {
1822                label: Some(label),
1823                layout: &resources.bind_group_layouts.instances,
1824                entries: &[wgpu::BindGroupEntry {
1825                    binding: 0,
1826                    resource: match &resources.instance_data {
1827                        InstanceData::Storage(buffer) => {
1828                            wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1829                                buffer,
1830                                offset,
1831                                size: NonZeroU64::new(size),
1832                            })
1833                        }
1834                        InstanceData::Texture { view, .. } => {
1835                            wgpu::BindingResource::TextureView(view)
1836                        }
1837                    },
1838                }],
1839            });
1840        Ok(InstanceBinding {
1841            bind_group,
1842            first_instance,
1843        })
1844    }
1845
1846    fn write_instance_texture(resources: &WgpuResources, offset: u64, data: &[u8]) {
1847        let InstanceData::Texture {
1848            texture,
1849            width,
1850            height,
1851            ..
1852        } = &resources.instance_data
1853        else {
1854            return;
1855        };
1856        let mut byte_offset = 0usize;
1857        let mut texel_offset = offset / INSTANCE_TEXTURE_TEXEL_SIZE;
1858        while byte_offset < data.len() {
1859            let x = (texel_offset % u64::from(*width)) as u32;
1860            let y = (texel_offset / u64::from(*width)) as u32;
1861            if y >= *height {
1862                // The capacity check in write_instance_binding should make this
1863                // unreachable. Truncating silently would leave stale bytes in the
1864                // texture and draw garbage for the remaining instances.
1865                debug_assert!(
1866                    false,
1867                    "instance texture write out of bounds: row {y} >= height {}",
1868                    *height
1869                );
1870                log::error!(
1871                    "instance texture write out of bounds; dropping {} bytes of instance data",
1872                    data.len() - byte_offset
1873                );
1874                return;
1875            }
1876            let available_texels = u64::from(*width - x);
1877            let remaining_bytes = data.len() - byte_offset;
1878            let complete_texels = remaining_bytes as u64 / INSTANCE_TEXTURE_TEXEL_SIZE;
1879            let texels = complete_texels.min(available_texels);
1880            if texels > 0 {
1881                let byte_count = (texels * INSTANCE_TEXTURE_TEXEL_SIZE) as usize;
1882                resources.queue.write_texture(
1883                    wgpu::TexelCopyTextureInfo {
1884                        texture,
1885                        mip_level: 0,
1886                        origin: wgpu::Origin3d { x, y, z: 0 },
1887                        aspect: wgpu::TextureAspect::All,
1888                    },
1889                    &data[byte_offset..byte_offset + byte_count],
1890                    wgpu::TexelCopyBufferLayout {
1891                        offset: 0,
1892                        bytes_per_row: Some(byte_count as u32),
1893                        rows_per_image: None,
1894                    },
1895                    wgpu::Extent3d {
1896                        width: texels as u32,
1897                        height: 1,
1898                        depth_or_array_layers: 1,
1899                    },
1900                );
1901                byte_offset += byte_count;
1902                texel_offset += texels;
1903                continue;
1904            }
1905
1906            let mut final_texel = [0; INSTANCE_TEXTURE_TEXEL_SIZE as usize];
1907            final_texel[..remaining_bytes].copy_from_slice(&data[byte_offset..]);
1908            resources.queue.write_texture(
1909                wgpu::TexelCopyTextureInfo {
1910                    texture,
1911                    mip_level: 0,
1912                    origin: wgpu::Origin3d { x, y, z: 0 },
1913                    aspect: wgpu::TextureAspect::All,
1914                },
1915                &final_texel,
1916                wgpu::TexelCopyBufferLayout {
1917                    offset: 0,
1918                    bytes_per_row: Some(INSTANCE_TEXTURE_TEXEL_SIZE as u32),
1919                    rows_per_image: None,
1920                },
1921                wgpu::Extent3d {
1922                    width: 1,
1923                    height: 1,
1924                    depth_or_array_layers: 1,
1925                },
1926            );
1927            break;
1928        }
1929    }
1930
1931    fn grow_instance_data(&mut self, required: u64) -> Result<()> {
1932        let capacity = (self.instance_data_capacity * 2)
1933            .max(required.next_power_of_two())
1934            .min(self.max_instance_data_size);
1935        anyhow::ensure!(
1936            capacity >= required,
1937            "instance data needs {required} bytes, above the maximum of {}",
1938            self.max_instance_data_size
1939        );
1940        anyhow::ensure!(
1941            capacity > self.instance_data_capacity,
1942            "frame instance data exceeds the {}-byte maximum",
1943            self.max_instance_data_size
1944        );
1945        log::debug!(
1946            "instance data grown from {} to {capacity}",
1947            self.instance_data_capacity
1948        );
1949        // Bind groups created earlier in the frame keep the previous buffer or
1950        // texture alive, so allocations written before the grow remain valid;
1951        // only subsequent writes land in the new allocation.
1952        let uses_webgl_instance_data = self.uses_webgl_instance_data;
1953        let resources = self.resources_mut();
1954        if uses_webgl_instance_data {
1955            let max_texture_dimension = resources.device.limits().max_texture_dimension_2d;
1956            let (instance_data, actual_capacity) =
1957                Self::create_instance_texture(&resources.device, capacity, max_texture_dimension);
1958            resources.instance_data = instance_data;
1959            self.instance_data_capacity = actual_capacity;
1960        } else {
1961            resources.instance_data =
1962                InstanceData::Storage(resources.device.create_buffer(&wgpu::BufferDescriptor {
1963                    label: Some("instance_buffer"),
1964                    size: capacity,
1965                    usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1966                    mapped_at_creation: false,
1967                }));
1968            self.instance_data_capacity = capacity;
1969        }
1970        Ok(())
1971    }
1972
1973    /// Mark the surface as unconfigured so rendering is skipped until a new
1974    /// surface is provided via [`replace_surface`](Self::replace_surface).
1975    ///
1976    /// This does **not** drop the renderer — the device, queue, atlas, and
1977    /// pipelines stay alive.  Use this when the native window is destroyed
1978    /// (e.g. Android `TerminateWindow`) but you intend to re-create the
1979    /// surface later without losing cached atlas textures.
1980    pub fn unconfigure_surface(&mut self) {
1981        self.surface_configured = false;
1982        // Drop intermediate textures since they reference the old surface size.
1983        if let Some(res) = self.resources.as_mut() {
1984            res.invalidate_intermediate_textures();
1985        }
1986    }
1987
1988    /// Replace the wgpu surface with a new one (e.g. after Android destroys
1989    /// and recreates the native window).  Keeps the device, queue, atlas, and
1990    /// all pipelines intact so cached `AtlasTextureId`s remain valid.
1991    ///
1992    /// The `instance` **must** be the same [`wgpu::Instance`] that was used to
1993    /// create the adapter and device (i.e. from the [`WgpuContext`]).  Using a
1994    /// different instance will cause a "Device does not exist" panic because
1995    /// the wgpu device is bound to its originating instance.
1996    #[cfg(not(target_family = "wasm"))]
1997    pub fn replace_surface<W: HasWindowHandle>(
1998        &mut self,
1999        window: &W,
2000        config: WgpuSurfaceConfig,
2001        instance: &wgpu::Instance,
2002    ) -> anyhow::Result<()> {
2003        let window_handle = window
2004            .window_handle()
2005            .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?;
2006
2007        let surface = create_surface(instance, window_handle.as_raw())?;
2008
2009        let width = (config.size.width.0 as u32).max(1);
2010        let height = (config.size.height.0 as u32).max(1);
2011
2012        let alpha_mode = if config.transparent {
2013            self.transparent_alpha_mode
2014        } else {
2015            self.opaque_alpha_mode
2016        };
2017
2018        self.surface_config.width = width;
2019        self.surface_config.height = height;
2020        self.surface_config.alpha_mode = alpha_mode;
2021        if let Some(mode) = config.preferred_present_mode {
2022            self.surface_config.present_mode = mode;
2023        }
2024
2025        {
2026            let res = self
2027                .resources
2028                .as_mut()
2029                .expect("GPU resources not available");
2030            surface.configure(&res.device, &self.surface_config);
2031            res.surface = surface;
2032
2033            // Invalidate intermediate textures — they'll be recreated lazily.
2034            res.invalidate_intermediate_textures();
2035        }
2036
2037        self.surface_configured = true;
2038
2039        Ok(())
2040    }
2041
2042    pub fn destroy(&mut self) {
2043        // Release surface-bound GPU resources eagerly so the underlying native
2044        // window can be destroyed before the renderer itself is dropped.
2045        self.resources.take();
2046    }
2047
2048    /// Returns true if the GPU device was lost and recovery is needed.
2049    pub fn device_lost(&self) -> bool {
2050        self.device_lost.load(std::sync::atomic::Ordering::SeqCst)
2051    }
2052
2053    /// Returns true if a redraw is needed because GPU state was cleared.
2054    /// Calling this method clears the flag.
2055    pub fn needs_redraw(&mut self) -> bool {
2056        std::mem::take(&mut self.needs_redraw)
2057    }
2058
2059    /// Recovers from a lost GPU device by recreating the renderer with a new context.
2060    ///
2061    /// Call this after detecting `device_lost()` returns true.
2062    ///
2063    /// This method coordinates recovery across multiple windows:
2064    /// - The first window to call this will recreate the shared context
2065    /// - Subsequent windows will adopt the already-recovered context
2066    #[cfg(not(target_family = "wasm"))]
2067    pub fn recover<W>(&mut self, window: &W) -> anyhow::Result<()>
2068    where
2069        W: HasWindowHandle + HasDisplayHandle + std::fmt::Debug + Send + Sync + Clone + 'static,
2070    {
2071        let gpu_context = self.context.as_ref().expect("recover requires gpu_context");
2072
2073        // Check if another window already recovered the context
2074        let needs_new_context = gpu_context
2075            .borrow()
2076            .as_ref()
2077            .is_none_or(|ctx| ctx.device_lost());
2078
2079        let window_handle = window
2080            .window_handle()
2081            .map_err(|e| anyhow::anyhow!("Failed to get window handle: {e}"))?;
2082
2083        let surface = if needs_new_context {
2084            log::warn!("GPU device lost, recreating context...");
2085
2086            // Drop old resources to release Arc<Device>/Arc<Queue> and GPU resources
2087            self.resources = None;
2088            *gpu_context.borrow_mut() = None;
2089
2090            // Wait briefly for the GPU driver to stabilize, then try to
2091            // recreate the context without software renderers. If this fails
2092            // the caller should request another frame and retry — the real GPU
2093            // may need more time to come back (e.g. after suspend/resume).
2094            std::thread::sleep(std::time::Duration::from_millis(350));
2095
2096            let instance = WgpuContext::instance(Box::new(window.clone()));
2097            let surface = create_surface(&instance, window_handle.as_raw())?;
2098            let new_context =
2099                WgpuContext::new_rejecting_software(instance, &surface, self.compositor_gpu)?;
2100            *gpu_context.borrow_mut() = Some(new_context);
2101            surface
2102        } else {
2103            let ctx_ref = gpu_context.borrow();
2104            let instance = &ctx_ref.as_ref().unwrap().instance;
2105            create_surface(instance, window_handle.as_raw())?
2106        };
2107
2108        let config = WgpuSurfaceConfig {
2109            size: gpui_platform::Size {
2110                width: gpui_platform::DevicePixels(self.surface_config.width as i32),
2111                height: gpui_platform::DevicePixels(self.surface_config.height as i32),
2112            },
2113            transparent: self.surface_config.alpha_mode != wgpu::CompositeAlphaMode::Opaque,
2114            preferred_present_mode: Some(self.surface_config.present_mode),
2115        };
2116        let gpu_context = Rc::clone(gpu_context);
2117        let ctx_ref = gpu_context.borrow();
2118        let context = ctx_ref.as_ref().expect("context should exist");
2119
2120        self.resources = None;
2121        self.atlas.handle_device_lost(context);
2122
2123        *self = Self::new_internal(
2124            Some(gpu_context.clone()),
2125            context,
2126            surface,
2127            config,
2128            self.compositor_gpu,
2129            self.atlas.clone(),
2130        )?;
2131
2132        log::info!("GPU recovery complete");
2133        Ok(())
2134    }
2135}
2136
2137fn instance_range(range: Range<usize>) -> Range<u32> {
2138    range.start as u32..range.end as u32
2139}
2140
2141#[cfg(not(target_family = "wasm"))]
2142fn create_surface(
2143    instance: &wgpu::Instance,
2144    raw_window_handle: raw_window_handle::RawWindowHandle,
2145) -> anyhow::Result<wgpu::Surface<'static>> {
2146    unsafe {
2147        instance
2148            .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
2149                // Fall back to the display handle already provided via InstanceDescriptor::display.
2150                raw_display_handle: None,
2151                raw_window_handle,
2152            })
2153            .map_err(|e| anyhow::anyhow!("{e}"))
2154    }
2155}
2156
2157struct RenderingParameters {
2158    path_sample_count: u32,
2159    gamma_ratios: [f32; 4],
2160    grayscale_enhanced_contrast: f32,
2161    subpixel_enhanced_contrast: f32,
2162}
2163
2164impl RenderingParameters {
2165    fn new(adapter: &wgpu::Adapter, surface_format: wgpu::TextureFormat) -> Self {
2166        use std::env;
2167
2168        let format_features = adapter.get_texture_format_features(surface_format);
2169        let path_sample_count = [4, 2, 1]
2170            .into_iter()
2171            .find(|&n| format_features.flags.sample_count_supported(n))
2172            .unwrap_or(1);
2173
2174        let gamma = env::var("ZED_FONTS_GAMMA")
2175            .ok()
2176            .and_then(|v| v.parse().ok())
2177            .unwrap_or(1.8_f32)
2178            .clamp(1.0, 2.2);
2179        let gamma_ratios = get_gamma_correction_ratios(gamma);
2180
2181        let grayscale_enhanced_contrast = env::var("ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST")
2182            .ok()
2183            .and_then(|v| v.parse().ok())
2184            .unwrap_or(1.0_f32)
2185            .max(0.0);
2186
2187        let subpixel_enhanced_contrast = env::var("ZED_FONTS_SUBPIXEL_ENHANCED_CONTRAST")
2188            .ok()
2189            .and_then(|v| v.parse().ok())
2190            .unwrap_or(0.5_f32)
2191            .max(0.0);
2192
2193        Self {
2194            path_sample_count,
2195            gamma_ratios,
2196            grayscale_enhanced_contrast,
2197            subpixel_enhanced_contrast,
2198        }
2199    }
2200}
2201
2202#[cfg(test)]
2203mod tests {
2204    use super::*;
2205    use gpui_engine::{MonochromeSprite, PolychromeSprite, Quad, SubpixelSprite, Underline};
2206    use gpui_platform::Shadow;
2207
2208    #[test]
2209    fn webgl_shader_is_valid_wgsl_without_storage_buffers() {
2210        assert!(!WEBGL_SHADERS.contains("var<storage"));
2211        validate_wgsl(WEBGL_SHADERS, naga::valid::Capabilities::empty());
2212    }
2213
2214    #[test]
2215    fn storage_buffer_shader_is_valid_wgsl() {
2216        validate_wgsl(STORAGE_BUFFER_SHADERS, naga::valid::Capabilities::empty());
2217    }
2218
2219    #[test]
2220    fn subpixel_shader_is_valid_wgsl() {
2221        validate_wgsl(
2222            SUBPIXEL_SHADERS,
2223            naga::valid::Capabilities::DUAL_SOURCE_BLENDING,
2224        );
2225    }
2226
2227    fn validate_wgsl(source: &str, capabilities: naga::valid::Capabilities) {
2228        let module = naga::front::wgsl::parse_str(source).expect("shader should parse");
2229        naga::valid::Validator::new(naga::valid::ValidationFlags::all(), capabilities)
2230            .validate(&module)
2231            .expect("shader should validate");
2232    }
2233
2234    #[test]
2235    fn webgl_record_sizes_match_shader_word_strides() {
2236        assert_eq!(std::mem::size_of::<Quad>(), 40 * 4);
2237        assert_eq!(std::mem::size_of::<Shadow>(), 28 * 4);
2238        assert_eq!(std::mem::size_of::<PathRasterizationVertex>(), 26 * 4);
2239        assert_eq!(std::mem::size_of::<PathSprite>(), 4 * 4);
2240        assert_eq!(std::mem::size_of::<Underline>(), 16 * 4);
2241        assert_eq!(std::mem::size_of::<MonochromeSprite>(), 28 * 4);
2242        assert_eq!(std::mem::size_of::<SubpixelSprite>(), 28 * 4);
2243        assert_eq!(std::mem::size_of::<PolychromeSprite>(), 24 * 4);
2244    }
2245}
2246
2247impl SceneRenderer for WgpuRenderer {
2248    fn draw(&mut self, scene: &Scene) -> bool {
2249        WgpuRenderer::draw(self, scene)
2250    }
2251
2252    fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
2253        self.sprite_atlas().clone()
2254    }
2255}