Skip to main content

gpui_wgpu/
wgpu_renderer.rs

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