Skip to main content

jag_draw/
pass_manager.rs

1use std::sync::Arc;
2
3// use anyhow::Result;
4
5use crate::allocator::{RenderAllocator, TexKey};
6// use crate::display_list::{Command, DisplayList, Viewport};
7use crate::pipeline::{
8    BackgroundRenderer, BasicSolidRenderer, Blitter, BlurRenderer, Compositor,
9    OverlaySolidRenderer, ScrimSolidRenderer, ScrimStencilMaskRenderer, ScrimStencilRenderer,
10    ShadowCompositeRenderer, SmaaRenderer, TextRenderer,
11};
12use crate::scene::{BoxShadowSpec, RoundedRadii, RoundedRect};
13use crate::upload::GpuScene;
14
15/// Apply a 2D affine transform to a point
16fn apply_transform_to_point(point: [f32; 2], transform: crate::Transform2D) -> [f32; 2] {
17    let [a, b, c, d, e, f] = transform.m;
18    let x = point[0];
19    let y = point[1];
20    [a * x + c * y + e, b * x + d * y + f]
21}
22
23fn u16_unorm_to_u8(v: u16) -> u8 {
24    ((u32::from(v) * 255 + 32767) / 65535) as u8
25}
26
27fn glyph_mask_for_atlas(
28    mask: &crate::text::GlyphMask,
29    force_grayscale: bool,
30) -> (u32, u32, std::borrow::Cow<'_, [u8]>) {
31    match mask {
32        crate::text::GlyphMask::Color(m) => {
33            (m.width, m.height, std::borrow::Cow::Borrowed(&m.data))
34        }
35        crate::text::GlyphMask::Subpixel(m) => match m.format {
36            crate::text::MaskFormat::Rgba8 => {
37                if !force_grayscale {
38                    return (m.width, m.height, std::borrow::Cow::Borrowed(&m.data));
39                }
40
41                let mut out = Vec::with_capacity((m.width as usize) * (m.height as usize) * 4);
42                for px in m.data.chunks_exact(4) {
43                    let gray = ((u16::from(px[0]) + u16::from(px[1]) + u16::from(px[2])) / 3) as u8;
44                    out.extend_from_slice(&[gray, gray, gray, 0]);
45                }
46                (m.width, m.height, std::borrow::Cow::Owned(out))
47            }
48            crate::text::MaskFormat::Rgba16 => {
49                // Text atlas is RGBA8, so normalize 16-bit glyph masks to RGBA8 on upload.
50                let mut out = Vec::with_capacity((m.width as usize) * (m.height as usize) * 4);
51                for px in m.data.chunks_exact(8) {
52                    let r = u16::from_le_bytes([px[0], px[1]]);
53                    let g = u16::from_le_bytes([px[2], px[3]]);
54                    let b = u16::from_le_bytes([px[4], px[5]]);
55                    let (r8, g8, b8) = if force_grayscale {
56                        let gray16 = ((u32::from(r) + u32::from(g) + u32::from(b)) / 3) as u16;
57                        let gray8 = u16_unorm_to_u8(gray16);
58                        (gray8, gray8, gray8)
59                    } else {
60                        (u16_unorm_to_u8(r), u16_unorm_to_u8(g), u16_unorm_to_u8(b))
61                    };
62                    out.extend_from_slice(&[r8, g8, b8, 0]);
63                }
64                (m.width, m.height, std::borrow::Cow::Owned(out))
65            }
66        },
67    }
68}
69
70pub struct PassTargets {
71    pub color: crate::OwnedTexture,
72}
73
74pub enum Background {
75    Solid(crate::scene::ColorLinPremul),
76    LinearGradient {
77        start_uv: [f32; 2],
78        end_uv: [f32; 2],
79        stop0: (f32, crate::scene::ColorLinPremul),
80        stop1: (f32, crate::scene::ColorLinPremul),
81    },
82}
83
84pub struct PassManager {
85    device: Arc<wgpu::Device>,
86    pub solid_offscreen: BasicSolidRenderer,
87    pub solid_direct: BasicSolidRenderer,
88    pub transparent_solid_offscreen: BasicSolidRenderer,
89    pub transparent_solid_direct: BasicSolidRenderer,
90    pub solid_direct_no_msaa: BasicSolidRenderer,
91    overlay_solid: OverlaySolidRenderer,
92    scrim_solid: ScrimSolidRenderer,
93    pub compositor: Compositor,
94    pub blitter: Blitter,
95    pub smaa: SmaaRenderer,
96    scrim_mask: ScrimStencilMaskRenderer,
97    scrim_stencil: ScrimStencilRenderer,
98    // Shadow/blur pipelines and helpers
99    pub mask_renderer: BasicSolidRenderer,
100    pub blur_r8: BlurRenderer,
101    pub shadow_comp: ShadowCompositeRenderer,
102    pub text: TextRenderer,
103    pub text_offscreen: TextRenderer,
104    pub image: crate::pipeline::ImageRenderer,
105    pub image_offscreen: crate::pipeline::ImageRenderer,
106    pub svg_cache: crate::svg::SvgRasterCache,
107    pub image_cache: crate::image_cache::ImageCache,
108    offscreen_format: wgpu::TextureFormat,
109    surface_format: wgpu::TextureFormat,
110    vp_buffer: wgpu::Buffer,
111    /// Scroll offset applied via the viewport uniform (GPU-side scroll).
112    /// Set by the caller before `render_unified` to shift content without
113    /// rebuilding geometry. Values are in logical pixels (negative = scrolled down/right).
114    scroll_offset: [f32; 2],
115    // Z-index uniform buffer for dynamic depth control (Phase 2)
116    z_index_buffer: wgpu::Buffer,
117    bg: BackgroundRenderer,
118    bg_param_buffer: wgpu::Buffer,
119    bg_stops_buffer: wgpu::Buffer,
120    // Platform DPI scale factor (used for mac-specific radial centering fix)
121    scale_factor: f32,
122    // Additional UI scale multiplier for logical pixel mode
123    ui_scale: f32,
124    // When true, treat positions as logical pixels and scale by `scale_factor` centrally
125    logical_pixels: bool,
126    // Intermediate texture for Vello-style smooth resizing
127    pub intermediate_texture: Option<crate::OwnedTexture>,
128    smaa_edges: Option<crate::OwnedTexture>,
129    smaa_weights: Option<crate::OwnedTexture>,
130    // Depth texture for z-ordering across all element types
131    depth_texture: Option<crate::OwnedTexture>,
132    // Stencil texture for scrim cutouts
133    scrim_stencil_tex: Option<crate::OwnedTexture>,
134    // Reusable GPU resources for text rendering to avoid per-glyph allocations.
135    text_mask_atlas: wgpu::Texture,
136    // Note: This view is not directly read but must be kept alive for the bind group reference
137    #[allow(dead_code)]
138    text_mask_atlas_view: wgpu::TextureView,
139    text_bind_group: wgpu::BindGroup,
140    // Track atlas region used in previous frame for efficient clearing
141    prev_atlas_max_x: u32,
142    prev_atlas_max_y: u32,
143    smaa_param_buffer: wgpu::Buffer,
144    // Registry for externally-rendered textures (e.g., 3D viewports)
145    external_textures:
146        std::collections::HashMap<crate::display_list::ExternalTextureId, wgpu::TextureView>,
147}
148
149// Vertex structures for unified rendering
150#[repr(C)]
151#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
152struct TextQuadVtx {
153    pos: [f32; 2],
154    uv: [f32; 2],
155    color: [f32; 4],
156}
157
158#[repr(C)]
159#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
160struct ImageQuadVtx {
161    pos: [f32; 2],
162    uv: [f32; 2],
163}
164
165impl PassManager {
166    /// Choose the best offscreen format based on scene color space.
167    ///
168    /// - If the whole render target is sRGB, prefer Rgba8UnormSrgb so wgpu handles the
169    ///   linear→sRGB conversion on write.
170    /// - If the scene is linear-light, keep the offscreen target linear via Rgba8Unorm.
171    fn choose_offscreen_format(
172        device: &wgpu::Device,
173        target_format: wgpu::TextureFormat,
174    ) -> wgpu::TextureFormat {
175        // WORKAROUND: Stay on 8-bit formats due to Metal blending issues with Rgba16Float.
176        let prefer_srgb = target_format.is_srgb();
177        let preferred = if prefer_srgb {
178            wgpu::TextureFormat::Rgba8UnormSrgb
179        } else {
180            wgpu::TextureFormat::Rgba8Unorm
181        };
182
183        // Query capabilities directly to avoid invoking driver code that might throw
184        // foreign exceptions on unsupported formats.
185        let device_features = device.features();
186        let supports = |format: wgpu::TextureFormat| {
187            format
188                .guaranteed_format_features(device_features)
189                .allowed_usages
190                .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
191        };
192
193        if supports(preferred) {
194            preferred
195        } else {
196            let fallback = if prefer_srgb {
197                wgpu::TextureFormat::Rgba8Unorm
198            } else {
199                wgpu::TextureFormat::Rgba8UnormSrgb
200            };
201
202            if supports(fallback) {
203                fallback
204            } else {
205                // Last resort: keep the original target format.
206                target_format
207            }
208        }
209    }
210
211    pub fn new(device: Arc<wgpu::Device>, target_format: wgpu::TextureFormat) -> Self {
212        // Try Rgba16Float for better gradient quality, fallback to Rgba8Unorm if not supported
213        let offscreen_format = Self::choose_offscreen_format(&device, target_format);
214        // MSAA>1 interacts poorly with our current depth setup (depth textures
215        // are 1×). Keep sample count at 1 and rely on SMAA / pixel snapping
216        // for edge smoothing to avoid crashes and validation errors.
217        let msaa_count = 1;
218        let solid_offscreen = BasicSolidRenderer::new(device.clone(), offscreen_format, msaa_count);
219        let solid_direct = BasicSolidRenderer::new(device.clone(), target_format, msaa_count);
220        let transparent_solid_offscreen = BasicSolidRenderer::new_with_depth_state(
221            device.clone(),
222            offscreen_format,
223            msaa_count,
224            false,
225            wgpu::CompareFunction::LessEqual,
226        );
227        let transparent_solid_direct = BasicSolidRenderer::new_with_depth_state(
228            device.clone(),
229            target_format,
230            msaa_count,
231            false,
232            wgpu::CompareFunction::LessEqual,
233        );
234        let solid_direct_no_msaa = BasicSolidRenderer::new(device.clone(), target_format, 1);
235        let overlay_solid = OverlaySolidRenderer::new(device.clone(), target_format);
236        let scrim_solid = ScrimSolidRenderer::new(device.clone(), target_format);
237        let compositor = Compositor::new(device.clone(), target_format);
238        let blitter = Blitter::new(device.clone(), target_format);
239        let smaa = SmaaRenderer::new(device.clone(), target_format);
240        let scrim_mask = ScrimStencilMaskRenderer::new(device.clone(), target_format);
241        let scrim_stencil = ScrimStencilRenderer::new(device.clone(), target_format);
242        // Shadow/blur pipelines
243        let mask_renderer =
244            BasicSolidRenderer::new(device.clone(), wgpu::TextureFormat::R8Unorm, 1);
245        let blur_r8 = BlurRenderer::new(device.clone(), wgpu::TextureFormat::R8Unorm);
246        let shadow_comp = ShadowCompositeRenderer::new(device.clone(), target_format);
247        let text = TextRenderer::new(device.clone(), target_format);
248        let text_offscreen = TextRenderer::new(device.clone(), offscreen_format);
249        let image = crate::pipeline::ImageRenderer::new(device.clone(), target_format);
250        let image_offscreen = crate::pipeline::ImageRenderer::new(device.clone(), offscreen_format);
251        let svg_cache = crate::svg::SvgRasterCache::new(device.clone());
252        let image_cache = crate::image_cache::ImageCache::new(device.clone());
253        let bg = BackgroundRenderer::new(device.clone(), target_format);
254        let vp_buffer = device.create_buffer(&wgpu::BufferDescriptor {
255            label: Some("viewport-uniform"),
256            size: 32, // [scale.x, scale.y, translate.x, translate.y, scroll.x, scroll.y, pad, pad]
257            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
258            mapped_at_creation: false,
259        });
260        // Z-index uniform buffer for dynamic depth control (Phase 2)
261        let z_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
262            label: Some("z-index-uniform"),
263            size: 4, // Single f32 value
264            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
265            mapped_at_creation: false,
266        });
267        let bg_param_buffer = device.create_buffer(&wgpu::BufferDescriptor {
268            label: Some("background-params"),
269            size: 64,
270            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
271            mapped_at_creation: false,
272        });
273        let bg_stops_buffer = device.create_buffer(&wgpu::BufferDescriptor {
274            label: Some("background-stops"),
275            size: 256, // 8 stops x 32 bytes
276            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
277            mapped_at_creation: false,
278        });
279        let smaa_param_buffer = device.create_buffer(&wgpu::BufferDescriptor {
280            label: Some("smaa-params"),
281            size: 16,
282            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
283            mapped_at_creation: false,
284        });
285        // Text pipeline GPU resources
286        let text_mask_atlas = device.create_texture(&wgpu::TextureDescriptor {
287            label: Some("text-mask-atlas"),
288            size: wgpu::Extent3d {
289                width: 4096,
290                height: 4096,
291                depth_or_array_layers: 1,
292            },
293            mip_level_count: 1,
294            sample_count: 1,
295            dimension: wgpu::TextureDimension::D2,
296            // Use RGBA8 so we can store RGB subpixel coverage masks directly.
297            format: wgpu::TextureFormat::Rgba8Unorm,
298            usage: wgpu::TextureUsages::TEXTURE_BINDING
299                | wgpu::TextureUsages::COPY_DST
300                | wgpu::TextureUsages::RENDER_ATTACHMENT,
301            view_formats: &[],
302        });
303        let text_mask_atlas_view =
304            text_mask_atlas.create_view(&wgpu::TextureViewDescriptor::default());
305        let text_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
306            label: Some("text-mask-bgl"),
307            layout: &text.tex_bgl,
308            entries: &[
309                wgpu::BindGroupEntry {
310                    binding: 0,
311                    resource: wgpu::BindingResource::TextureView(&text_mask_atlas_view),
312                },
313                wgpu::BindGroupEntry {
314                    binding: 1,
315                    resource: wgpu::BindingResource::Sampler(&text.sampler),
316                },
317            ],
318        });
319        // Defaults: always interpret author coords as logical pixels and scale by DPI.
320        let logical_default = true;
321        let ui_scale = 1.0;
322        Self {
323            device,
324            solid_offscreen,
325            solid_direct,
326            transparent_solid_offscreen,
327            transparent_solid_direct,
328            solid_direct_no_msaa,
329            overlay_solid,
330            scrim_solid,
331            compositor,
332            blitter,
333            smaa,
334            scrim_mask,
335            scrim_stencil,
336            mask_renderer,
337            blur_r8,
338            shadow_comp,
339            text,
340            text_offscreen,
341            image,
342            image_offscreen,
343            svg_cache,
344            image_cache,
345            offscreen_format,
346            surface_format: target_format,
347            vp_buffer,
348            scroll_offset: [0.0, 0.0],
349            z_index_buffer,
350            bg,
351            bg_param_buffer,
352            bg_stops_buffer,
353            scale_factor: 1.0,
354            ui_scale,
355            logical_pixels: logical_default,
356            intermediate_texture: None,
357            smaa_edges: None,
358            smaa_weights: None,
359            depth_texture: None,
360            text_mask_atlas,
361            text_mask_atlas_view,
362            text_bind_group,
363            prev_atlas_max_x: 0,
364            prev_atlas_max_y: 0,
365            smaa_param_buffer,
366            scrim_stencil_tex: None,
367            external_textures: std::collections::HashMap::new(),
368        }
369    }
370
371    /// Expose the device for scenes that need to create textures.
372    pub fn device(&self) -> Arc<wgpu::Device> {
373        self.device.clone()
374    }
375
376    /// Register an externally-rendered texture for compositing in the current frame.
377    pub fn register_external_texture(
378        &mut self,
379        id: crate::display_list::ExternalTextureId,
380        view: wgpu::TextureView,
381    ) {
382        self.external_textures.insert(id, view);
383    }
384
385    /// Clear all registered external textures (call after frame).
386    pub fn clear_external_textures(&mut self) {
387        self.external_textures.clear();
388    }
389
390    /// Create a z-index bind group for the given z-index value.
391    /// This is used for dynamic depth control in Phase 2.
392    pub fn create_z_bind_group(&self, z_index: f32, queue: &wgpu::Queue) -> wgpu::BindGroup {
393        queue.write_buffer(&self.z_index_buffer, 0, bytemuck::bytes_of(&z_index));
394        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
395            label: Some("z-index-bg"),
396            layout: self.solid_direct.z_index_bgl(),
397            entries: &[wgpu::BindGroupEntry {
398                binding: 0,
399                resource: self.z_index_buffer.as_entire_binding(),
400            }],
401        })
402    }
403
404    /// Create a z-index bind group backed by a dedicated uniform buffer for this draw group.
405    /// This avoids sharing a single z-index uniform across multiple groups, which would cause
406    /// all draws to use the last-written z value (breaking per-group z-ordering).
407    fn create_group_z_bind_group(
408        &self,
409        z_index: f32,
410        queue: &wgpu::Queue,
411    ) -> (wgpu::BindGroup, wgpu::Buffer) {
412        let z_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
413            label: Some("z-index-group-buffer"),
414            size: std::mem::size_of::<f32>() as u64,
415            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
416            mapped_at_creation: false,
417        });
418        queue.write_buffer(&z_buf, 0, bytemuck::bytes_of(&z_index));
419        let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
420            label: Some("z-index-bg-group"),
421            layout: self.solid_direct.z_index_bgl(),
422            entries: &[wgpu::BindGroupEntry {
423                binding: 0,
424                resource: z_buf.as_entire_binding(),
425            }],
426        });
427        (bg, z_buf)
428    }
429
430    /// Render an image texture to the target at origin with size (in pixels, y-down).
431    /// Expects `tex_view` to be created from an `Rgba8UnormSrgb` texture for proper sRGB sampling.
432    pub fn draw_image_quad(
433        &self,
434        encoder: &mut wgpu::CommandEncoder,
435        target_view: &wgpu::TextureView,
436        origin: [f32; 2],
437        size: [f32; 2],
438        tex_view: &wgpu::TextureView,
439        queue: &wgpu::Queue,
440        width: u32,
441        height: u32,
442    ) {
443        // Update viewport uniform based on render target dimensions (+ logical pixel scale)
444        let logical =
445            crate::dpi::logical_multiplier(self.logical_pixels, self.scale_factor, self.ui_scale);
446        let scale = [
447            (2.0f32 / (width.max(1) as f32)) * logical,
448            (-2.0f32 / (height.max(1) as f32)) * logical,
449        ];
450        let translate = [-1.0f32, 1.0f32];
451        let vp_data: [f32; 8] = [
452            scale[0],
453            scale[1],
454            translate[0],
455            translate[1],
456            0.0,
457            0.0,
458            0.0,
459            0.0,
460        ];
461        // debug log removed
462        queue.write_buffer(&self.vp_buffer, 0, bytemuck::bytes_of(&vp_data));
463
464        #[repr(C)]
465        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
466        struct QuadVtx {
467            pos: [f32; 2],
468            uv: [f32; 2],
469        }
470        let x = origin[0];
471        let y = origin[1];
472        let w = size[0].max(0.0);
473        let h = size[1].max(0.0);
474        let verts = [
475            QuadVtx {
476                pos: [x, y],
477                uv: [0.0, 0.0],
478            },
479            QuadVtx {
480                pos: [x + w, y],
481                uv: [1.0, 0.0],
482            },
483            QuadVtx {
484                pos: [x + w, y + h],
485                uv: [1.0, 1.0],
486            },
487            QuadVtx {
488                pos: [x, y + h],
489                uv: [0.0, 1.0],
490            },
491        ];
492        let idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
493        let vsize = (verts.len() * std::mem::size_of::<QuadVtx>()) as u64;
494        let isize = (idx.len() * std::mem::size_of::<u16>()) as u64;
495        let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
496            label: Some("image-vbuf"),
497            size: vsize.max(4),
498            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
499            mapped_at_creation: false,
500        });
501        let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
502            label: Some("image-ibuf"),
503            size: isize.max(4),
504            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
505            mapped_at_creation: false,
506        });
507        if vsize > 0 {
508            queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
509        }
510        if isize > 0 {
511            queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&idx));
512        }
513
514        let vp_bg = self.image.vp_bind_group(&self.device, &self.vp_buffer);
515        let z_bg = self.create_z_bind_group(0.0, queue);
516        let tex_bg = self.image.tex_bind_group(&self.device, tex_view);
517        let (params_bg, _params_buf) = self.image.params_bind_group(&self.device, 1.0, false);
518
519        // Create depth texture for image rendering (1x)
520        let depth_tex = self.device.create_texture(&wgpu::TextureDescriptor {
521            label: Some("image-depth"),
522            size: wgpu::Extent3d {
523                width,
524                height,
525                depth_or_array_layers: 1,
526            },
527            mip_level_count: 1,
528            sample_count: 1,
529            dimension: wgpu::TextureDimension::D2,
530            format: wgpu::TextureFormat::Depth32Float,
531            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
532            view_formats: &[],
533        });
534        let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
535
536        let depth_attachment = Some(wgpu::RenderPassDepthStencilAttachment {
537            view: &depth_view,
538            depth_ops: Some(wgpu::Operations {
539                load: wgpu::LoadOp::Load, // Preserve existing depth values
540                store: wgpu::StoreOp::Store,
541            }),
542            stencil_ops: None,
543        });
544
545        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
546            label: Some("image-pass"),
547            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
548                view: target_view,
549                resolve_target: None,
550                ops: wgpu::Operations {
551                    load: wgpu::LoadOp::Load,
552                    store: wgpu::StoreOp::Store,
553                },
554            })],
555            depth_stencil_attachment: depth_attachment,
556            occlusion_query_set: None,
557            timestamp_writes: None,
558        });
559        self.image.record(
560            &mut pass,
561            &vp_bg,
562            &z_bg,
563            &tex_bg,
564            &params_bg,
565            &vbuf,
566            &ibuf,
567            idx.len() as u32,
568        );
569    }
570
571    /// Rasterize an SVG file to a cached texture for the given scale.
572    /// Returns a texture view and its pixel dimensions on success.
573    /// Optional style parameter allows overriding fill, stroke, and stroke-width.
574    pub fn rasterize_svg_to_view(
575        &mut self,
576        path: &std::path::Path,
577        scale: f32,
578        style: Option<crate::svg::SvgStyle>,
579        queue: &wgpu::Queue,
580    ) -> Option<(wgpu::TextureView, u32, u32)> {
581        let svg_style = style.unwrap_or_default();
582        let (tex, w, h) = self
583            .svg_cache
584            .get_or_rasterize(path, scale, svg_style, queue)?;
585        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
586        Some((view, w, h))
587    }
588
589    /// Load a raster image (PNG/JPEG/GIF/WebP) from disk to a cached GPU texture.
590    /// Returns a texture view and its pixel dimensions on success.
591    pub fn load_image_to_view(
592        &mut self,
593        path: &std::path::Path,
594        queue: &wgpu::Queue,
595    ) -> Option<(wgpu::TextureView, u32, u32)> {
596        let (tex, w, h) = self.image_cache.get_or_load(path, queue)?;
597        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
598        Some((view, w, h))
599    }
600
601    /// Try to get an image from cache without blocking. Returns None if not ready.
602    pub fn try_get_image_view(
603        &mut self,
604        path: &std::path::Path,
605    ) -> Option<(wgpu::TextureView, u32, u32)> {
606        let (tex, w, h) = self.image_cache.get(path)?;
607        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
608        Some((view, w, h))
609    }
610
611    /// Request an image to be loaded. Marks it as loading if not already in cache.
612    pub fn request_image_load(&mut self, path: &std::path::Path) {
613        self.image_cache.start_load(path);
614    }
615
616    /// Check if an image is ready in the cache.
617    pub fn is_image_ready(&self, path: &std::path::Path) -> bool {
618        self.image_cache.is_ready(path)
619    }
620
621    /// Store a pre-loaded image texture in the cache.
622    pub fn store_loaded_image(
623        &mut self,
624        path: &std::path::Path,
625        tex: Arc<wgpu::Texture>,
626        width: u32,
627        height: u32,
628    ) {
629        self.image_cache.store_ready(path, tex, width, height);
630    }
631
632    /// Get a cached texture directly (for updating pixel data in-place).
633    /// Returns the Arc<Texture> and dimensions if found.
634    pub fn get_cached_texture(
635        &mut self,
636        path: &std::path::Path,
637    ) -> Option<(Arc<wgpu::Texture>, u32, u32)> {
638        self.image_cache.get(path)
639    }
640
641    /// Set the platform DPI scale factor. On macOS this is used to correct
642    /// radial gradient centering when using normalized UVs for fullscreen fills.
643    pub fn set_scale_factor(&mut self, sf: f32) {
644        if sf.is_finite() && sf > 0.0 {
645            self.scale_factor = sf;
646        } else {
647            self.scale_factor = 1.0;
648        }
649    }
650
651    /// Set author-controlled UI scale multiplier (applies in logical mode).
652    pub fn set_ui_scale(&mut self, s: f32) {
653        let s = if s.is_finite() { s } else { 1.0 };
654        self.ui_scale = s.clamp(0.25, 4.0);
655    }
656
657    /// Set the GPU-side scroll offset (in logical pixels, typically negative).
658    /// This value is written into the viewport uniform so the GPU applies the
659    /// scroll transform without rebuilding geometry.
660    pub fn set_scroll_offset(&mut self, offset: [f32; 2]) {
661        self.scroll_offset = offset;
662    }
663
664    /// Get the current GPU-side scroll offset.
665    pub fn scroll_offset(&self) -> [f32; 2] {
666        self.scroll_offset
667    }
668
669    /// Toggle logical pixel mode.
670    pub fn set_logical_pixels(&mut self, on: bool) {
671        self.logical_pixels = on;
672    }
673
674    pub fn alloc_targets(
675        &self,
676        allocator: &mut RenderAllocator,
677        width: u32,
678        height: u32,
679    ) -> PassTargets {
680        let color = allocator.allocate_texture(TexKey {
681            width,
682            height,
683            format: self.offscreen_format,
684            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
685        });
686        PassTargets { color }
687    }
688
689    /// Allocate or reuse intermediate texture matching the surface size.
690    /// This texture is used for Vello-style smooth resizing.
691    ///
692    /// Strategy: Always ensure texture matches exact size for MSAA resolve compatibility.
693    /// We preserve content by using LoadOp::Load when rendering, not by keeping oversized textures.
694    pub fn ensure_intermediate_texture(
695        &mut self,
696        allocator: &mut RenderAllocator,
697        width: u32,
698        height: u32,
699    ) {
700        let needs_realloc = match &self.intermediate_texture {
701            Some(tex) => {
702                // Reallocate if size doesn't match exactly
703                // MSAA resolve requires exact size match between MSAA texture and resolve target
704                tex.key.width != width || tex.key.height != height
705            }
706            None => true,
707        };
708
709        if needs_realloc {
710            // Release old texture if it exists
711            if let Some(old_tex) = self.intermediate_texture.take() {
712                allocator.release_texture(old_tex);
713            }
714
715            // Allocate new intermediate texture with surface format at exact size
716            let tex = allocator.allocate_texture(TexKey {
717                width,
718                height,
719                format: self.surface_format,
720                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
721                    | wgpu::TextureUsages::TEXTURE_BINDING
722                    | wgpu::TextureUsages::COPY_SRC
723                    | wgpu::TextureUsages::COPY_DST,
724            });
725            self.intermediate_texture = Some(tex);
726        }
727    }
728
729    /// Clear the intermediate texture with the specified color.
730    /// This should be called before rendering to the intermediate texture.
731    pub fn clear_intermediate_texture(
732        &self,
733        encoder: &mut wgpu::CommandEncoder,
734        clear_color: wgpu::Color,
735    ) {
736        let intermediate = self
737            .intermediate_texture
738            .as_ref()
739            .expect("intermediate texture must be allocated before clearing");
740
741        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
742            label: Some("clear-intermediate"),
743            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
744                view: &intermediate.view,
745                resolve_target: None,
746                ops: wgpu::Operations {
747                    load: wgpu::LoadOp::Clear(clear_color),
748                    store: wgpu::StoreOp::Store,
749                },
750            })],
751            depth_stencil_attachment: None,
752            occlusion_query_set: None,
753            timestamp_writes: None,
754        });
755    }
756
757    /// Blit the intermediate texture to the surface. This is a very fast operation
758    /// that enables smooth window resizing (Vello-style).
759    pub fn blit_to_surface(
760        &self,
761        encoder: &mut wgpu::CommandEncoder,
762        surface_view: &wgpu::TextureView,
763    ) {
764        let intermediate = self
765            .intermediate_texture
766            .as_ref()
767            .expect("intermediate texture must be allocated before blitting");
768
769        let bg = self.blitter.bind_group(&self.device, &intermediate.view);
770        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
771            label: Some("blit-pass"),
772            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
773                view: surface_view,
774                resolve_target: None,
775                ops: wgpu::Operations {
776                    load: wgpu::LoadOp::Load,
777                    store: wgpu::StoreOp::Store,
778                },
779            })],
780            depth_stencil_attachment: None,
781            occlusion_query_set: None,
782            timestamp_writes: None,
783        });
784        self.blitter.record(&mut pass, &bg);
785    }
786
787    /// Ensure depth texture is allocated and matches the given dimensions.
788    /// Depth texture is used for z-ordering across all element types (solids, text, images, SVGs).
789    pub fn ensure_depth_texture(
790        &mut self,
791        allocator: &mut RenderAllocator,
792        width: u32,
793        height: u32,
794    ) {
795        let needs_realloc = match &self.depth_texture {
796            Some(tex) => tex.key.width != width || tex.key.height != height,
797            None => true,
798        };
799
800        if needs_realloc {
801            // Release old texture if it exists
802            if let Some(old_tex) = self.depth_texture.take() {
803                allocator.release_texture(old_tex);
804            }
805
806            // Allocate new depth texture at exact size
807            let tex = allocator.allocate_texture(TexKey {
808                width,
809                height,
810                format: wgpu::TextureFormat::Depth32Float,
811                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
812            });
813            self.depth_texture = Some(tex);
814        }
815    }
816
817    /// Get the depth texture view for use in render passes.
818    /// Panics if depth texture hasn't been allocated via ensure_depth_texture.
819    pub fn depth_view(&self) -> &wgpu::TextureView {
820        &self
821            .depth_texture
822            .as_ref()
823            .expect("depth texture must be allocated before use")
824            .view
825    }
826
827    fn ensure_scrim_stencil_texture(
828        &mut self,
829        allocator: &mut RenderAllocator,
830        width: u32,
831        height: u32,
832    ) {
833        let needs_realloc = match &self.scrim_stencil_tex {
834            Some(tex) => tex.key.width != width || tex.key.height != height,
835            None => true,
836        };
837
838        if needs_realloc {
839            if let Some(old) = self.scrim_stencil_tex.take() {
840                allocator.release_texture(old);
841            }
842            let tex = allocator.allocate_texture(TexKey {
843                width,
844                height,
845                format: wgpu::TextureFormat::Depth24PlusStencil8,
846                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
847            });
848            self.scrim_stencil_tex = Some(tex);
849        }
850    }
851
852    fn ensure_smaa_textures(&mut self, allocator: &mut RenderAllocator, width: u32, height: u32) {
853        let key = TexKey {
854            width,
855            height,
856            format: wgpu::TextureFormat::Rgba8Unorm,
857            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
858        };
859
860        if self.smaa_edges.as_ref().map_or(true, |tex| tex.key != key) {
861            if let Some(old) = self.smaa_edges.take() {
862                allocator.release_texture(old);
863            }
864            self.smaa_edges = Some(allocator.allocate_texture(key));
865        }
866
867        if self
868            .smaa_weights
869            .as_ref()
870            .map_or(true, |tex| tex.key != key)
871        {
872            if let Some(old) = self.smaa_weights.take() {
873                allocator.release_texture(old);
874            }
875            self.smaa_weights = Some(allocator.allocate_texture(key));
876        }
877    }
878
879    pub fn apply_smaa(
880        &mut self,
881        encoder: &mut wgpu::CommandEncoder,
882        allocator: &mut RenderAllocator,
883        src_view: &wgpu::TextureView,
884        dst_view: &wgpu::TextureView,
885        width: u32,
886        height: u32,
887        queue: &wgpu::Queue,
888    ) {
889        if width == 0 || height == 0 {
890            return;
891        }
892
893        self.ensure_smaa_textures(allocator, width, height);
894        let texel_size = [
895            1.0f32 / width.max(1) as f32,
896            1.0f32 / height.max(1) as f32,
897            0.0,
898            0.0,
899        ];
900        queue.write_buffer(&self.smaa_param_buffer, 0, bytemuck::bytes_of(&texel_size));
901
902        let edges = self
903            .smaa_edges
904            .as_ref()
905            .expect("SMAA edges texture must exist");
906        let weights = self
907            .smaa_weights
908            .as_ref()
909            .expect("SMAA weights texture must exist");
910
911        let edge_bg = self
912            .smaa
913            .edge_bind_group(&self.device, src_view, &self.smaa_param_buffer);
914        let blend_bg =
915            self.smaa
916                .blend_bind_group(&self.device, &edges.view, &self.smaa_param_buffer);
917        let resolve_bg = self.smaa.resolve_bind_group(
918            &self.device,
919            src_view,
920            &weights.view,
921            &self.smaa_param_buffer,
922        );
923
924        // Pass 1: edge detect
925        {
926            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
927                label: Some("smaa-edge-pass"),
928                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
929                    view: &edges.view,
930                    resolve_target: None,
931                    ops: wgpu::Operations {
932                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
933                        store: wgpu::StoreOp::Store,
934                    },
935                })],
936                depth_stencil_attachment: None,
937                occlusion_query_set: None,
938                timestamp_writes: None,
939            });
940            self.smaa.record_edges(&mut pass, &edge_bg);
941        }
942
943        // Pass 2: blend weights
944        {
945            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
946                label: Some("smaa-blend-pass"),
947                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
948                    view: &weights.view,
949                    resolve_target: None,
950                    ops: wgpu::Operations {
951                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
952                        store: wgpu::StoreOp::Store,
953                    },
954                })],
955                depth_stencil_attachment: None,
956                occlusion_query_set: None,
957                timestamp_writes: None,
958            });
959            self.smaa.record_blend(&mut pass, &blend_bg);
960        }
961
962        // Pass 3: resolve onto the swapchain/offscreen destination
963        {
964            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
965                label: Some("smaa-resolve-pass"),
966                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
967                    view: dst_view,
968                    resolve_target: None,
969                    ops: wgpu::Operations {
970                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
971                        store: wgpu::StoreOp::Store,
972                    },
973                })],
974                depth_stencil_attachment: None,
975                occlusion_query_set: None,
976                timestamp_writes: None,
977            });
978            self.smaa.record_resolve(&mut pass, &resolve_bg);
979        }
980    }
981
982    /// Draw a box shadow for a rounded rect using an R8 mask + separable Gaussian blur pipeline.
983    /// This composes the tinted shadow beneath current content on the target view.
984    pub fn draw_box_shadow(
985        &self,
986        encoder: &mut wgpu::CommandEncoder,
987        target_view: &wgpu::TextureView,
988        width: u32,
989        height: u32,
990        rrect: RoundedRect,
991        spec: BoxShadowSpec,
992        queue: &wgpu::Queue,
993    ) {
994        // --- 1) Calibrate parameters ---
995        // Soften falloff: browsers feel closer to sigma ≈ blur_radius
996        // Larger sigma reduces the "band" look and increases penumbra.
997        let blur = spec.blur_radius.max(0.0);
998        let sigma = if blur > 0.0 { blur } else { 0.5 };
999        let spread = spec.spread.max(0.0);
1000        let create_tex = |label: &str| -> wgpu::Texture {
1001            self.device.create_texture(&wgpu::TextureDescriptor {
1002                label: Some(label),
1003                size: wgpu::Extent3d {
1004                    width: width.max(1),
1005                    height: height.max(1),
1006                    depth_or_array_layers: 1,
1007                },
1008                mip_level_count: 1,
1009                sample_count: 1,
1010                dimension: wgpu::TextureDimension::D2,
1011                format: wgpu::TextureFormat::R8Unorm,
1012                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1013                    | wgpu::TextureUsages::TEXTURE_BINDING,
1014                view_formats: &[],
1015            })
1016        };
1017        let mask_tex = create_tex("shadow-mask");
1018        let ping_tex = create_tex("shadow-ping");
1019        let mask_view = mask_tex.create_view(&wgpu::TextureViewDescriptor::default());
1020        let ping_view = ping_tex.create_view(&wgpu::TextureViewDescriptor::default());
1021
1022        // Viewport for full target size (y-down)
1023        let logical =
1024            crate::dpi::logical_multiplier(self.logical_pixels, self.scale_factor, self.ui_scale);
1025        let scale = [
1026            (2.0f32 / (width.max(1) as f32)) * logical,
1027            (-2.0f32 / (height.max(1) as f32)) * logical,
1028        ];
1029        let translate = [-1.0f32, 1.0f32];
1030        let vp_data: [f32; 8] = [
1031            scale[0],
1032            scale[1],
1033            translate[0],
1034            translate[1],
1035            0.0,
1036            0.0,
1037            0.0,
1038            0.0,
1039        ];
1040        // debug log removed
1041        queue.write_buffer(&self.vp_buffer, 0, bytemuck::bytes_of(&vp_data));
1042
1043        let shadow_radii = RoundedRadii {
1044            tl: (rrect.radii.tl + spread).max(0.0),
1045            tr: (rrect.radii.tr + spread).max(0.0),
1046            br: (rrect.radii.br + spread).max(0.0),
1047            bl: (rrect.radii.bl + spread).max(0.0),
1048        };
1049        // Expand source to give blur room so the outer halo is broad enough.
1050        // Slightly higher multiplier works better with the wider blur support above.
1051        let expand = spread + 1.8 * sigma + 1.0;
1052        let mut rect = rrect.rect;
1053        rect.x = rect.x + spec.offset[0] - expand;
1054        rect.y = rect.y + spec.offset[1] - expand;
1055        rect.w = (rect.w + 2.0 * expand).max(0.0);
1056        rect.h = (rect.h + 2.0 * expand).max(0.0);
1057        let expanded = RoundedRect {
1058            rect,
1059            radii: shadow_radii,
1060        };
1061        // Render with white for the shadow shape
1062        // Build vertices/indices for expanded rounded rect (fill)
1063        #[repr(C)]
1064        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1065        struct Vtx {
1066            pos: [f32; 2],
1067            color: [f32; 4],
1068        }
1069        let mut vertices: Vec<Vtx> = Vec::new();
1070        let mut indices: Vec<u16> = Vec::new();
1071        let rect = expanded.rect;
1072        let tl = expanded.radii.tl.min(rect.w * 0.5).min(rect.h * 0.5);
1073        let tr = expanded.radii.tr.min(rect.w * 0.5).min(rect.h * 0.5);
1074        let br = expanded.radii.br.min(rect.w * 0.5).min(rect.h * 0.5);
1075        let bl = expanded.radii.bl.min(rect.w * 0.5).min(rect.h * 0.5);
1076        // Higher tessellation for smoother rounded corners (reduces polygonal artifacts before blur)
1077        let segs = 64u32;
1078        let mut ring: Vec<[f32; 2]> = Vec::new();
1079        fn arc_append(
1080            ring: &mut Vec<[f32; 2]>,
1081            c: [f32; 2],
1082            r: f32,
1083            start: f32,
1084            end: f32,
1085            segs: u32,
1086            include_start: bool,
1087        ) {
1088            if r <= 0.0 {
1089                return;
1090            }
1091            for i in 0..=segs {
1092                if i == 0 && !include_start {
1093                    continue;
1094                }
1095                let t = (i as f32) / (segs as f32);
1096                let ang = start + t * (end - start);
1097                let p = [c[0] + r * ang.cos(), c[1] - r * ang.sin()];
1098                ring.push(p);
1099            }
1100        }
1101        if tl > 0.0 {
1102            arc_append(
1103                &mut ring,
1104                [rect.x + tl, rect.y + tl],
1105                tl,
1106                std::f32::consts::FRAC_PI_2,
1107                std::f32::consts::PI,
1108                segs,
1109                true,
1110            );
1111        } else {
1112            ring.push([rect.x + 0.0, rect.y + 0.0]);
1113        }
1114        if bl > 0.0 {
1115            arc_append(
1116                &mut ring,
1117                [rect.x + bl, rect.y + rect.h - bl],
1118                bl,
1119                std::f32::consts::PI,
1120                std::f32::consts::FRAC_PI_2 * 3.0,
1121                segs,
1122                true,
1123            );
1124        } else {
1125            ring.push([rect.x + 0.0, rect.y + rect.h]);
1126        }
1127        if br > 0.0 {
1128            arc_append(
1129                &mut ring,
1130                [rect.x + rect.w - br, rect.y + rect.h - br],
1131                br,
1132                std::f32::consts::FRAC_PI_2 * 3.0,
1133                std::f32::consts::TAU,
1134                segs,
1135                true,
1136            );
1137        } else {
1138            ring.push([rect.x + rect.w, rect.y + rect.h]);
1139        }
1140        if tr > 0.0 {
1141            arc_append(
1142                &mut ring,
1143                [rect.x + rect.w - tr, rect.y + tr],
1144                tr,
1145                0.0,
1146                std::f32::consts::FRAC_PI_2,
1147                segs,
1148                true,
1149            );
1150        } else {
1151            ring.push([rect.x + rect.w, rect.y + 0.0]);
1152        }
1153        let center = [rect.x + rect.w * 0.5, rect.y + rect.h * 0.5];
1154        let white = [1.0, 1.0, 1.0, 1.0];
1155        let base = vertices.len() as u16;
1156        vertices.push(Vtx {
1157            pos: center,
1158            color: white,
1159        });
1160        for p in ring.iter() {
1161            vertices.push(Vtx {
1162                pos: *p,
1163                color: white,
1164            });
1165        }
1166        let ring_len = (vertices.len() as u16) - base - 1;
1167        for i in 0..ring_len {
1168            let i0 = base;
1169            let i1 = base + 1 + i;
1170            let i2 = base + 1 + ((i + 1) % ring_len);
1171            indices.extend_from_slice(&[i0, i1, i2]);
1172        }
1173        // Create GPU buffers directly
1174        let vsize = (vertices.len() * std::mem::size_of::<Vtx>()) as u64;
1175        let isize = (indices.len() * std::mem::size_of::<u16>()) as u64;
1176        let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1177            label: Some("shadow-mask-vbuf"),
1178            size: vsize.max(4),
1179            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1180            mapped_at_creation: false,
1181        });
1182        let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1183            label: Some("shadow-mask-ibuf"),
1184            size: isize.max(4),
1185            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1186            mapped_at_creation: false,
1187        });
1188        if vsize > 0 {
1189            queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&vertices));
1190        }
1191        if isize > 0 {
1192            queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&indices));
1193        }
1194        let gpu = crate::upload::GpuScene {
1195            vertex: crate::allocator::OwnedBuffer {
1196                buffer: vbuf,
1197                key: crate::allocator::BufKey {
1198                    size: vsize.max(4),
1199                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1200                },
1201            },
1202            index: crate::allocator::OwnedBuffer {
1203                buffer: ibuf,
1204                key: crate::allocator::BufKey {
1205                    size: isize.max(4),
1206                    usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1207                },
1208            },
1209            vertices: vertices.len() as u32,
1210            indices: indices.len() as u32,
1211        };
1212
1213        // Bind groups for viewport
1214        let vp_bg_mask = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1215            label: Some("vp-bg-mask"),
1216            layout: self.mask_renderer.viewport_bgl(),
1217            entries: &[wgpu::BindGroupEntry {
1218                binding: 0,
1219                resource: self.vp_buffer.as_entire_binding(),
1220            }],
1221        });
1222        // Render mask shape to R8 texture
1223        // Clear to BLACK, render WHITE for shadow shape
1224        // After blur: soft white blob. After cutout: white ring (shadow area)
1225        let _z_bg = self.create_z_bind_group(0.0, queue);
1226        {
1227            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1228                label: Some("shadow-mask-pass"),
1229                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1230                    view: &mask_view,
1231                    resolve_target: None,
1232                    ops: wgpu::Operations {
1233                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1234                        store: wgpu::StoreOp::Store,
1235                    },
1236                })],
1237                depth_stencil_attachment: None,
1238                occlusion_query_set: None,
1239                timestamp_writes: None,
1240            });
1241            self.mask_renderer.record(&mut pass, &vp_bg_mask, &gpu);
1242        }
1243
1244        // Horizontal blur (mask -> ping)
1245        #[repr(C)]
1246        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1247        struct BlurParams {
1248            dir: [f32; 2],
1249            texel: [f32; 2],
1250            sigma: f32,
1251            _pad: f32,
1252        }
1253        let texel = [
1254            1.0f32 / (width.max(1) as f32),
1255            1.0f32 / (height.max(1) as f32),
1256        ];
1257        let bp_h = BlurParams {
1258            dir: [1.0, 0.0],
1259            texel,
1260            sigma,
1261            _pad: 0.0,
1262        };
1263        queue.write_buffer(&self.blur_r8.param_buffer, 0, bytemuck::bytes_of(&bp_h));
1264        let bg_h = self.blur_r8.bind_group(&self.device, &mask_view);
1265        {
1266            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1267                label: Some("shadow-blur-h"),
1268                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1269                    view: &ping_view,
1270                    resolve_target: None,
1271                    ops: wgpu::Operations {
1272                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1273                        store: wgpu::StoreOp::Store,
1274                    },
1275                })],
1276                depth_stencil_attachment: None,
1277                occlusion_query_set: None,
1278                timestamp_writes: None,
1279            });
1280            self.blur_r8.record(&mut pass, &bg_h);
1281        }
1282
1283        // Vertical blur (ping -> mask)
1284        let bp_v = BlurParams {
1285            dir: [0.0, 1.0],
1286            texel,
1287            sigma,
1288            _pad: 0.0,
1289        };
1290        queue.write_buffer(&self.blur_r8.param_buffer, 0, bytemuck::bytes_of(&bp_v));
1291        let bg_v = self.blur_r8.bind_group(&self.device, &ping_view);
1292        {
1293            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1294                label: Some("shadow-blur-v"),
1295                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1296                    view: &mask_view,
1297                    resolve_target: None,
1298                    ops: wgpu::Operations {
1299                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1300                        store: wgpu::StoreOp::Store,
1301                    },
1302                })],
1303                depth_stencil_attachment: None,
1304                occlusion_query_set: None,
1305                timestamp_writes: None,
1306            });
1307            self.blur_r8.record(&mut pass, &bg_v);
1308        }
1309
1310        // Step 5: Cut out the ORIGINAL shape (at original position, no offset)
1311        // This prevents the shadow from showing through semi-transparent elements
1312        {
1313            let mut cutout_vertices: Vec<Vtx> = Vec::new();
1314            let mut cutout_indices: Vec<u16> = Vec::new();
1315            // Use ORIGINAL rect (no spread/offset) in full target space
1316            let rect = rrect.rect;
1317            let tl = rrect.radii.tl.min(rect.w * 0.5).min(rect.h * 0.5);
1318            let tr = rrect.radii.tr.min(rect.w * 0.5).min(rect.h * 0.5);
1319            let br = rrect.radii.br.min(rect.w * 0.5).min(rect.h * 0.5);
1320            let bl = rrect.radii.bl.min(rect.w * 0.5).min(rect.h * 0.5);
1321            let mut ring: Vec<[f32; 2]> = Vec::new();
1322            if tl > 0.0 {
1323                arc_append(
1324                    &mut ring,
1325                    [rect.x + tl, rect.y + tl],
1326                    tl,
1327                    std::f32::consts::FRAC_PI_2,
1328                    std::f32::consts::PI,
1329                    segs,
1330                    true,
1331                );
1332            } else {
1333                ring.push([rect.x, rect.y]);
1334            }
1335            if bl > 0.0 {
1336                arc_append(
1337                    &mut ring,
1338                    [rect.x + bl, rect.y + rect.h - bl],
1339                    bl,
1340                    std::f32::consts::PI,
1341                    std::f32::consts::FRAC_PI_2 * 3.0,
1342                    segs,
1343                    true,
1344                );
1345            } else {
1346                ring.push([rect.x, rect.y + rect.h]);
1347            }
1348            if br > 0.0 {
1349                arc_append(
1350                    &mut ring,
1351                    [rect.x + rect.w - br, rect.y + rect.h - br],
1352                    br,
1353                    std::f32::consts::FRAC_PI_2 * 3.0,
1354                    std::f32::consts::TAU,
1355                    segs,
1356                    true,
1357                );
1358            } else {
1359                ring.push([rect.x + rect.w, rect.y + rect.h]);
1360            }
1361            if tr > 0.0 {
1362                arc_append(
1363                    &mut ring,
1364                    [rect.x + rect.w - tr, rect.y + tr],
1365                    tr,
1366                    0.0,
1367                    std::f32::consts::FRAC_PI_2,
1368                    segs,
1369                    true,
1370                );
1371            } else {
1372                ring.push([rect.x + rect.w, rect.y]);
1373            }
1374            let center = [rect.x + rect.w * 0.5, rect.y + rect.h * 0.5];
1375            // Use transparent (alpha=0) to clear the mask area
1376            // With premultiplied alpha: result = src * src.a + dst * (1 - src.a) = 0 * 0 + dst * 1 = dst
1377            // That won't work! We need alpha=1 to replace: result = src * 1 + dst * 0 = src
1378            // For R8, we want to write 0.0, so use black with alpha=1
1379            let clear_color = [0.0, 0.0, 0.0, 1.0];
1380            let base = cutout_vertices.len() as u16;
1381            cutout_vertices.push(Vtx {
1382                pos: center,
1383                color: clear_color,
1384            });
1385            for p in ring.iter() {
1386                cutout_vertices.push(Vtx {
1387                    pos: *p,
1388                    color: clear_color,
1389                });
1390            }
1391            let ring_len = (cutout_vertices.len() as u16) - base - 1;
1392            for i in 0..ring_len {
1393                let i0 = base;
1394                let i1 = base + 1 + i;
1395                let i2 = base + 1 + ((i + 1) % ring_len);
1396                cutout_indices.extend_from_slice(&[i0, i1, i2]);
1397            }
1398
1399            let vsize = (cutout_vertices.len() * std::mem::size_of::<Vtx>()) as u64;
1400            let isize = (cutout_indices.len() * std::mem::size_of::<u16>()) as u64;
1401            let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1402                label: Some("shadow-cutout-vbuf"),
1403                size: vsize.max(4),
1404                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1405                mapped_at_creation: false,
1406            });
1407            let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1408                label: Some("shadow-cutout-ibuf"),
1409                size: isize.max(4),
1410                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1411                mapped_at_creation: false,
1412            });
1413            if vsize > 0 {
1414                queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&cutout_vertices));
1415            }
1416            if isize > 0 {
1417                queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&cutout_indices));
1418            }
1419            let cutout_gpu = crate::upload::GpuScene {
1420                vertex: crate::allocator::OwnedBuffer {
1421                    buffer: vbuf,
1422                    key: crate::allocator::BufKey {
1423                        size: vsize.max(4),
1424                        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1425                    },
1426                },
1427                index: crate::allocator::OwnedBuffer {
1428                    buffer: ibuf,
1429                    key: crate::allocator::BufKey {
1430                        size: isize.max(4),
1431                        usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1432                    },
1433                },
1434                vertices: cutout_vertices.len() as u32,
1435                indices: cutout_indices.len() as u32,
1436            };
1437
1438            let _z_bg_cutout = self.create_z_bind_group(0.0, queue);
1439            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1440                label: Some("shadow-cutout"),
1441                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1442                    view: &mask_view,
1443                    resolve_target: None,
1444                    ops: wgpu::Operations {
1445                        load: wgpu::LoadOp::Load,
1446                        store: wgpu::StoreOp::Store,
1447                    },
1448                })],
1449                depth_stencil_attachment: None,
1450                occlusion_query_set: None,
1451                timestamp_writes: None,
1452            });
1453            self.mask_renderer
1454                .record(&mut pass, &vp_bg_mask, &cutout_gpu);
1455        }
1456
1457        // Composite tinted shadow to target using premultiplied color
1458        #[repr(C)]
1459        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1460        struct ShadowColor {
1461            color: [f32; 4],
1462        }
1463        let c = spec.color;
1464        let scol = ShadowColor {
1465            color: [c.r, c.g, c.b, c.a],
1466        };
1467        queue.write_buffer(&self.shadow_comp.color_buffer, 0, bytemuck::bytes_of(&scol));
1468        let bg = self.shadow_comp.bind_group(&self.device, &mask_view);
1469        {
1470            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1471                label: Some("shadow-composite"),
1472                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1473                    view: target_view,
1474                    resolve_target: None,
1475                    ops: wgpu::Operations {
1476                        load: wgpu::LoadOp::Load,
1477                        store: wgpu::StoreOp::Store,
1478                    },
1479                })],
1480                depth_stencil_attachment: None,
1481                occlusion_query_set: None,
1482                timestamp_writes: None,
1483            });
1484            self.shadow_comp.record(&mut pass, &bg);
1485        }
1486
1487        // Temp textures are dropped at end of scope
1488    }
1489
1490    /// Draw a simple overlay rectangle that darkens existing content without affecting depth.
1491    /// This is intended for UI overlays like modal scrims that should blend over the scene
1492    /// but not participate in depth testing.
1493    pub fn draw_overlay_rect(
1494        &self,
1495        encoder: &mut wgpu::CommandEncoder,
1496        target_view: &wgpu::TextureView,
1497        width: u32,
1498        height: u32,
1499        rect: crate::scene::Rect,
1500        color: crate::scene::ColorLinPremul,
1501        queue: &wgpu::Queue,
1502    ) {
1503        // Update viewport uniform based on render target dimensions (+ logical pixel scale)
1504        let logical =
1505            crate::dpi::logical_multiplier(self.logical_pixels, self.scale_factor, self.ui_scale);
1506        let scale = [
1507            (2.0f32 / (width.max(1) as f32)) * logical,
1508            (-2.0f32 / (height.max(1) as f32)) * logical,
1509        ];
1510        let translate = [-1.0f32, 1.0f32];
1511        let vp_data: [f32; 8] = [
1512            scale[0],
1513            scale[1],
1514            translate[0],
1515            translate[1],
1516            0.0,
1517            0.0,
1518            0.0,
1519            0.0,
1520        ];
1521        queue.write_buffer(&self.vp_buffer, 0, bytemuck::bytes_of(&vp_data));
1522
1523        #[repr(C)]
1524        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1525        struct OverlayVtx {
1526            pos: [f32; 2],
1527            color: [f32; 4],
1528            z_index: f32,
1529        }
1530
1531        let overlay_color = [color.r, color.g, color.b, color.a];
1532        let z_index = 0.0f32;
1533        let x = rect.x;
1534        let y = rect.y;
1535        let w = rect.w.max(0.0);
1536        let h = rect.h.max(0.0);
1537
1538        // Skip degenerate rectangles
1539        if w <= 0.0 || h <= 0.0 {
1540            return;
1541        }
1542
1543        let verts = [
1544            OverlayVtx {
1545                pos: [x, y],
1546                color: overlay_color,
1547                z_index,
1548            },
1549            OverlayVtx {
1550                pos: [x + w, y],
1551                color: overlay_color,
1552                z_index,
1553            },
1554            OverlayVtx {
1555                pos: [x + w, y + h],
1556                color: overlay_color,
1557                z_index,
1558            },
1559            OverlayVtx {
1560                pos: [x, y + h],
1561                color: overlay_color,
1562                z_index,
1563            },
1564        ];
1565        let idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
1566
1567        let vsize = (verts.len() * std::mem::size_of::<OverlayVtx>()) as u64;
1568        let isize = (idx.len() * std::mem::size_of::<u16>()) as u64;
1569        let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1570            label: Some("overlay-rect-vbuf"),
1571            size: vsize.max(4),
1572            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1573            mapped_at_creation: false,
1574        });
1575        let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1576            label: Some("overlay-rect-ibuf"),
1577            size: isize.max(4),
1578            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1579            mapped_at_creation: false,
1580        });
1581        if vsize > 0 {
1582            queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
1583        }
1584        if isize > 0 {
1585            queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&idx));
1586        }
1587
1588        let vp_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1589            label: Some("overlay-vp-bg"),
1590            layout: self.overlay_solid.viewport_bgl(),
1591            entries: &[wgpu::BindGroupEntry {
1592                binding: 0,
1593                resource: self.vp_buffer.as_entire_binding(),
1594            }],
1595        });
1596
1597        // Overlay pass: no depth attachment so the quad simply blends over existing content.
1598        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1599            label: Some("overlay-rect-pass"),
1600            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1601                view: target_view,
1602                resolve_target: None,
1603                ops: wgpu::Operations {
1604                    load: wgpu::LoadOp::Load,
1605                    store: wgpu::StoreOp::Store,
1606                },
1607            })],
1608            depth_stencil_attachment: None,
1609            occlusion_query_set: None,
1610            timestamp_writes: None,
1611        });
1612        self.overlay_solid
1613            .record(&mut pass, &vp_bg, &vbuf, &ibuf, idx.len() as u32);
1614    }
1615
1616    /// Draw a full-viewport scrim rectangle that blends over existing content.
1617    /// Unlike draw_overlay_rect, this uses a depth buffer attachment but with:
1618    /// - depth_write_enabled = false (doesn't affect depth buffer)
1619    /// - depth_compare = Always (always passes depth test)
1620    /// This allows the scrim to render over all existing content while letting
1621    /// subsequent draws at higher z-index render on top of the scrim.
1622    ///
1623    /// NOTE: Scrim renders directly to target without MSAA or depth attachment.
1624    /// The scrim pipeline uses depth_compare=Always and depth_write_enabled=false.
1625    pub fn draw_scrim_rect(
1626        &self,
1627        encoder: &mut wgpu::CommandEncoder,
1628        target_view: &wgpu::TextureView,
1629        width: u32,
1630        height: u32,
1631        rect: crate::scene::Rect,
1632        color: crate::scene::ColorLinPremul,
1633        queue: &wgpu::Queue,
1634    ) {
1635        // Update viewport uniform based on render target dimensions (+ logical pixel scale)
1636        let logical =
1637            crate::dpi::logical_multiplier(self.logical_pixels, self.scale_factor, self.ui_scale);
1638        let scale = [
1639            (2.0f32 / (width.max(1) as f32)) * logical,
1640            (-2.0f32 / (height.max(1) as f32)) * logical,
1641        ];
1642        let translate = [-1.0f32, 1.0f32];
1643        let vp_data: [f32; 8] = [
1644            scale[0],
1645            scale[1],
1646            translate[0],
1647            translate[1],
1648            0.0,
1649            0.0,
1650            0.0,
1651            0.0,
1652        ];
1653        queue.write_buffer(&self.vp_buffer, 0, bytemuck::bytes_of(&vp_data));
1654
1655        #[repr(C)]
1656        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1657        struct ScrimVtx {
1658            pos: [f32; 2],
1659            color: [f32; 4],
1660            z_index: f32,
1661        }
1662
1663        let scrim_color = [color.r, color.g, color.b, color.a];
1664        // Use a middle z-index - the scrim pipeline ignores depth testing anyway
1665        let z_index = 0.5f32;
1666        let x = rect.x;
1667        let y = rect.y;
1668        let w = rect.w.max(0.0);
1669        let h = rect.h.max(0.0);
1670
1671        // Skip degenerate rectangles
1672        if w <= 0.0 || h <= 0.0 {
1673            return;
1674        }
1675
1676        let verts = [
1677            ScrimVtx {
1678                pos: [x, y],
1679                color: scrim_color,
1680                z_index,
1681            },
1682            ScrimVtx {
1683                pos: [x + w, y],
1684                color: scrim_color,
1685                z_index,
1686            },
1687            ScrimVtx {
1688                pos: [x + w, y + h],
1689                color: scrim_color,
1690                z_index,
1691            },
1692            ScrimVtx {
1693                pos: [x, y + h],
1694                color: scrim_color,
1695                z_index,
1696            },
1697        ];
1698        let idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
1699
1700        let vsize = (verts.len() * std::mem::size_of::<ScrimVtx>()) as u64;
1701        let isize = (idx.len() * std::mem::size_of::<u16>()) as u64;
1702        let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1703            label: Some("scrim-rect-vbuf"),
1704            size: vsize.max(4),
1705            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1706            mapped_at_creation: false,
1707        });
1708        let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1709            label: Some("scrim-rect-ibuf"),
1710            size: isize.max(4),
1711            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1712            mapped_at_creation: false,
1713        });
1714        if vsize > 0 {
1715            queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
1716        }
1717        if isize > 0 {
1718            queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&idx));
1719        }
1720
1721        let vp_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1722            label: Some("scrim-vp-bg"),
1723            layout: self.scrim_solid.viewport_bgl(),
1724            entries: &[wgpu::BindGroupEntry {
1725                binding: 0,
1726                resource: self.vp_buffer.as_entire_binding(),
1727            }],
1728        });
1729
1730        // Scrim pass: no depth attachment. The scrim pipeline is configured with
1731        // depth_compare=Always and depth_write_enabled=false, so depth isn't needed.
1732        // It simply blends over existing content without affecting any depth state.
1733        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1734            label: Some("scrim-rect-pass"),
1735            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1736                view: target_view,
1737                resolve_target: None,
1738                ops: wgpu::Operations {
1739                    load: wgpu::LoadOp::Load,
1740                    store: wgpu::StoreOp::Store,
1741                },
1742            })],
1743            depth_stencil_attachment: None,
1744            occlusion_query_set: None,
1745            timestamp_writes: None,
1746        });
1747        self.scrim_solid
1748            .record(&mut pass, &vp_bg, &vbuf, &ibuf, idx.len() as u32);
1749    }
1750
1751    /// Draw a full scrim but cut out a rounded-rect hole via stencil.
1752    pub fn draw_scrim_with_cutout(
1753        &mut self,
1754        encoder: &mut wgpu::CommandEncoder,
1755        allocator: &mut RenderAllocator,
1756        target_view: &wgpu::TextureView,
1757        width: u32,
1758        height: u32,
1759        hole: RoundedRect,
1760        color: crate::scene::ColorLinPremul,
1761        queue: &wgpu::Queue,
1762    ) {
1763        self.ensure_scrim_stencil_texture(allocator, width, height);
1764        let stencil_tex = self
1765            .scrim_stencil_tex
1766            .as_ref()
1767            .expect("stencil texture must exist");
1768
1769        // Update viewport uniform
1770        let logical =
1771            crate::dpi::logical_multiplier(self.logical_pixels, self.scale_factor, self.ui_scale);
1772        let scale = [
1773            (2.0f32 / (width.max(1) as f32)) * logical,
1774            (-2.0f32 / (height.max(1) as f32)) * logical,
1775        ];
1776        let translate = [-1.0f32, 1.0f32];
1777        let vp_data: [f32; 8] = [
1778            scale[0],
1779            scale[1],
1780            translate[0],
1781            translate[1],
1782            0.0,
1783            0.0,
1784            0.0,
1785            0.0,
1786        ];
1787        queue.write_buffer(&self.vp_buffer, 0, bytemuck::bytes_of(&vp_data));
1788
1789        #[repr(C)]
1790        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1791        struct Vtx {
1792            pos: [f32; 2],
1793            color: [f32; 4],
1794            z: f32,
1795        }
1796
1797        // Tessellate filled rounded rect (copied from draw_filled_rounded_rect)
1798        let mut vertices: Vec<Vtx> = Vec::new();
1799        let mut indices: Vec<u16> = Vec::new();
1800        let rect = hole.rect;
1801        let tl = hole.radii.tl.min(rect.w * 0.5).min(rect.h * 0.5);
1802        let tr = hole.radii.tr.min(rect.w * 0.5).min(rect.h * 0.5);
1803        let br = hole.radii.br.min(rect.w * 0.5).min(rect.h * 0.5);
1804        let bl = hole.radii.bl.min(rect.w * 0.5).min(rect.h * 0.5);
1805        let segs = 32u32;
1806        let mut ring: Vec<[f32; 2]> = Vec::new();
1807        fn arc_append(
1808            ring: &mut Vec<[f32; 2]>,
1809            c: [f32; 2],
1810            r: f32,
1811            start: f32,
1812            end: f32,
1813            segs: u32,
1814            include_start: bool,
1815        ) {
1816            if r <= 0.0 {
1817                return;
1818            }
1819            for i in 0..=segs {
1820                if i == 0 && !include_start {
1821                    continue;
1822                }
1823                let t = (i as f32) / (segs as f32);
1824                let ang = start + t * (end - start);
1825                let p = [c[0] + r * ang.cos(), c[1] - r * ang.sin()];
1826                ring.push(p);
1827            }
1828        }
1829        if tl > 0.0 {
1830            arc_append(
1831                &mut ring,
1832                [rect.x + tl, rect.y + tl],
1833                tl,
1834                std::f32::consts::FRAC_PI_2,
1835                std::f32::consts::PI,
1836                segs,
1837                true,
1838            );
1839        } else {
1840            ring.push([rect.x + 0.0, rect.y + 0.0]);
1841        }
1842        if bl > 0.0 {
1843            arc_append(
1844                &mut ring,
1845                [rect.x + bl, rect.y + rect.h - bl],
1846                bl,
1847                std::f32::consts::PI,
1848                std::f32::consts::FRAC_PI_2 * 3.0,
1849                segs,
1850                true,
1851            );
1852        } else {
1853            ring.push([rect.x + 0.0, rect.y + rect.h]);
1854        }
1855        if br > 0.0 {
1856            arc_append(
1857                &mut ring,
1858                [rect.x + rect.w - br, rect.y + rect.h - br],
1859                br,
1860                std::f32::consts::FRAC_PI_2 * 3.0,
1861                std::f32::consts::TAU,
1862                segs,
1863                true,
1864            );
1865        } else {
1866            ring.push([rect.x + rect.w, rect.y + rect.h]);
1867        }
1868        if tr > 0.0 {
1869            arc_append(
1870                &mut ring,
1871                [rect.x + rect.w - tr, rect.y + tr],
1872                tr,
1873                0.0,
1874                std::f32::consts::FRAC_PI_2,
1875                segs,
1876                true,
1877            );
1878        } else {
1879            ring.push([rect.x + rect.w, rect.y + 0.0]);
1880        }
1881
1882        // Triangulate fan
1883        let center = [rect.x + rect.w * 0.5, rect.y + rect.h * 0.5];
1884        vertices.push(Vtx {
1885            pos: center,
1886            color: [color.r, color.g, color.b, color.a],
1887            z: 0.5,
1888        });
1889        for p in ring.iter() {
1890            vertices.push(Vtx {
1891                pos: *p,
1892                color: [color.r, color.g, color.b, color.a],
1893                z: 0.5,
1894            });
1895        }
1896        // Triangle fan around center
1897        for i in 1..(vertices.len() - 1) {
1898            indices.extend_from_slice(&[0, i as u16, (i as u16) + 1]);
1899        }
1900        if vertices.len() > 2 {
1901            indices.extend_from_slice(&[0, (vertices.len() - 1) as u16, 1]);
1902        }
1903
1904        // Ensure index byte length is 4-byte aligned for write_buffer
1905        if indices.len() % 2 != 0 {
1906            indices.push(*indices.last().unwrap_or(&0));
1907        }
1908
1909        let vsize = (vertices.len() * std::mem::size_of::<Vtx>()) as u64;
1910        let isize = (indices.len() * std::mem::size_of::<u16>()) as u64;
1911        let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1912            label: Some("scrim-hole-vbuf"),
1913            size: vsize.max(4),
1914            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1915            mapped_at_creation: false,
1916        });
1917        let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1918            label: Some("scrim-hole-ibuf"),
1919            size: isize.max(4),
1920            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1921            mapped_at_creation: false,
1922        });
1923        if vsize > 0 {
1924            queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&vertices));
1925        }
1926        if isize > 0 {
1927            queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&indices));
1928        }
1929
1930        let vp_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1931            label: Some("scrim-stencil-vp-bg"),
1932            layout: self.scrim_mask.viewport_bgl(),
1933            entries: &[wgpu::BindGroupEntry {
1934                binding: 0,
1935                resource: self.vp_buffer.as_entire_binding(),
1936            }],
1937        });
1938
1939        // Pass 1: write stencil = 1 inside hole (color writes disabled)
1940        {
1941            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1942                label: Some("scrim-stencil-mask-pass"),
1943                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1944                    view: target_view,
1945                    resolve_target: None,
1946                    ops: wgpu::Operations {
1947                        load: wgpu::LoadOp::Load,
1948                        store: wgpu::StoreOp::Store,
1949                    },
1950                })],
1951                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1952                    view: &stencil_tex.view,
1953                    depth_ops: None,
1954                    stencil_ops: Some(wgpu::Operations {
1955                        load: wgpu::LoadOp::Clear(0),
1956                        store: wgpu::StoreOp::Store,
1957                    }),
1958                }),
1959                occlusion_query_set: None,
1960                timestamp_writes: None,
1961            });
1962            pass.set_stencil_reference(1);
1963            self.scrim_mask
1964                .record(&mut pass, &vp_bg, &vbuf, &ibuf, indices.len() as u32);
1965        }
1966
1967        // Fullscreen quad for scrim (cover entire viewport)
1968        let quad = [
1969            Vtx {
1970                pos: [0.0, 0.0],
1971                color: [color.r, color.g, color.b, color.a],
1972                z: 0.5,
1973            },
1974            Vtx {
1975                pos: [width as f32, 0.0],
1976                color: [color.r, color.g, color.b, color.a],
1977                z: 0.5,
1978            },
1979            Vtx {
1980                pos: [width as f32, height as f32],
1981                color: [color.r, color.g, color.b, color.a],
1982                z: 0.5,
1983            },
1984            Vtx {
1985                pos: [0.0, height as f32],
1986                color: [color.r, color.g, color.b, color.a],
1987                z: 0.5,
1988            },
1989        ];
1990        let quad_idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
1991        let qvbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1992            label: Some("scrim-fullscreen-vbuf"),
1993            size: (quad.len() * std::mem::size_of::<Vtx>()) as u64,
1994            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1995            mapped_at_creation: false,
1996        });
1997        let qibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
1998            label: Some("scrim-fullscreen-ibuf"),
1999            size: (quad_idx.len() * std::mem::size_of::<u16>()) as u64,
2000            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2001            mapped_at_creation: false,
2002        });
2003        queue.write_buffer(&qvbuf, 0, bytemuck::cast_slice(&quad));
2004        queue.write_buffer(&qibuf, 0, bytemuck::cast_slice(&quad_idx));
2005
2006        let vp_bg_scrim = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2007            label: Some("scrim-stencil-vp-bg-scrim"),
2008            layout: self.scrim_stencil.viewport_bgl(),
2009            entries: &[wgpu::BindGroupEntry {
2010                binding: 0,
2011                resource: self.vp_buffer.as_entire_binding(),
2012            }],
2013        });
2014
2015        // Pass 2: draw scrim where stencil == 0 (outside hole)
2016        {
2017            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2018                label: Some("scrim-stencil-pass"),
2019                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2020                    view: target_view,
2021                    resolve_target: None,
2022                    ops: wgpu::Operations {
2023                        load: wgpu::LoadOp::Load,
2024                        store: wgpu::StoreOp::Store,
2025                    },
2026                })],
2027                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2028                    view: &stencil_tex.view,
2029                    depth_ops: None,
2030                    stencil_ops: Some(wgpu::Operations {
2031                        load: wgpu::LoadOp::Load,
2032                        store: wgpu::StoreOp::Store,
2033                    }),
2034                }),
2035                occlusion_query_set: None,
2036                timestamp_writes: None,
2037            });
2038            pass.set_stencil_reference(0);
2039            self.scrim_stencil.record(
2040                &mut pass,
2041                &vp_bg_scrim,
2042                &qvbuf,
2043                &qibuf,
2044                quad_idx.len() as u32,
2045            );
2046        }
2047    }
2048
2049    /// Draw a filled rounded rectangle directly onto the target using the solid_direct pipeline.
2050    /// Uses premultiplied linear color.
2051    pub fn draw_filled_rounded_rect(
2052        &self,
2053        encoder: &mut wgpu::CommandEncoder,
2054        target_view: &wgpu::TextureView,
2055        width: u32,
2056        height: u32,
2057        rrect: RoundedRect,
2058        color: crate::scene::ColorLinPremul,
2059        queue: &wgpu::Queue,
2060    ) {
2061        // Update viewport uniform
2062        let logical =
2063            crate::dpi::logical_multiplier(self.logical_pixels, self.scale_factor, self.ui_scale);
2064        let scale = [
2065            (2.0f32 / (width.max(1) as f32)) * logical,
2066            (-2.0f32 / (height.max(1) as f32)) * logical,
2067        ];
2068        let translate = [-1.0f32, 1.0f32];
2069        let vp_data: [f32; 8] = [
2070            scale[0],
2071            scale[1],
2072            translate[0],
2073            translate[1],
2074            0.0,
2075            0.0,
2076            0.0,
2077            0.0,
2078        ];
2079        // debug log removed
2080        queue.write_buffer(&self.vp_buffer, 0, bytemuck::bytes_of(&vp_data));
2081
2082        // Tessellate rounded rect fill
2083        #[repr(C)]
2084        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2085        struct Vtx {
2086            pos: [f32; 2],
2087            color: [f32; 4],
2088        }
2089        let mut vertices: Vec<Vtx> = Vec::new();
2090        let mut indices: Vec<u16> = Vec::new();
2091        let rect = rrect.rect;
2092        let tl = rrect.radii.tl.min(rect.w * 0.5).min(rect.h * 0.5);
2093        let tr = rrect.radii.tr.min(rect.w * 0.5).min(rect.h * 0.5);
2094        let br = rrect.radii.br.min(rect.w * 0.5).min(rect.h * 0.5);
2095        let bl = rrect.radii.bl.min(rect.w * 0.5).min(rect.h * 0.5);
2096        let segs = 64u32;
2097        let mut ring: Vec<[f32; 2]> = Vec::new();
2098        fn arc_append(
2099            ring: &mut Vec<[f32; 2]>,
2100            c: [f32; 2],
2101            r: f32,
2102            start: f32,
2103            end: f32,
2104            segs: u32,
2105            include_start: bool,
2106        ) {
2107            if r <= 0.0 {
2108                return;
2109            }
2110            for i in 0..=segs {
2111                if i == 0 && !include_start {
2112                    continue;
2113                }
2114                let t = (i as f32) / (segs as f32);
2115                let ang = start + t * (end - start);
2116                let p = [c[0] + r * ang.cos(), c[1] - r * ang.sin()];
2117                ring.push(p);
2118            }
2119        }
2120        if tl > 0.0 {
2121            arc_append(
2122                &mut ring,
2123                [rect.x + tl, rect.y + tl],
2124                tl,
2125                std::f32::consts::FRAC_PI_2,
2126                std::f32::consts::PI,
2127                segs,
2128                true,
2129            );
2130        } else {
2131            ring.push([rect.x + 0.0, rect.y + 0.0]);
2132        }
2133        if bl > 0.0 {
2134            arc_append(
2135                &mut ring,
2136                [rect.x + bl, rect.y + rect.h - bl],
2137                bl,
2138                std::f32::consts::PI,
2139                std::f32::consts::FRAC_PI_2 * 3.0,
2140                segs,
2141                true,
2142            );
2143        } else {
2144            ring.push([rect.x + 0.0, rect.y + rect.h]);
2145        }
2146        if br > 0.0 {
2147            arc_append(
2148                &mut ring,
2149                [rect.x + rect.w - br, rect.y + rect.h - br],
2150                br,
2151                std::f32::consts::FRAC_PI_2 * 3.0,
2152                std::f32::consts::TAU,
2153                segs,
2154                true,
2155            );
2156        } else {
2157            ring.push([rect.x + rect.w, rect.y + rect.h]);
2158        }
2159        if tr > 0.0 {
2160            arc_append(
2161                &mut ring,
2162                [rect.x + rect.w - tr, rect.y + tr],
2163                tr,
2164                0.0,
2165                std::f32::consts::FRAC_PI_2,
2166                segs,
2167                true,
2168            );
2169        } else {
2170            ring.push([rect.x + rect.w, rect.y + 0.0]);
2171        }
2172        let center = [rect.x + rect.w * 0.5, rect.y + rect.h * 0.5];
2173        let col = [color.r, color.g, color.b, color.a];
2174        let base = vertices.len() as u16;
2175        vertices.push(Vtx {
2176            pos: center,
2177            color: col,
2178        });
2179        for p in ring.iter() {
2180            vertices.push(Vtx {
2181                pos: *p,
2182                color: col,
2183            });
2184        }
2185        let ring_len = (vertices.len() as u16) - base - 1;
2186        for i in 0..ring_len {
2187            let i0 = base;
2188            let i1 = base + 1 + i;
2189            let i2 = base + 1 + ((i + 1) % ring_len);
2190            indices.extend_from_slice(&[i0, i1, i2]);
2191        }
2192
2193        // Create GPU buffers
2194        let vsize = (vertices.len() * std::mem::size_of::<Vtx>()) as u64;
2195        let isize = (indices.len() * std::mem::size_of::<u16>()) as u64;
2196        let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
2197            label: Some("rounded-rect-fill-vbuf"),
2198            size: vsize.max(4),
2199            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2200            mapped_at_creation: false,
2201        });
2202        let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
2203            label: Some("rounded-rect-fill-ibuf"),
2204            size: isize.max(4),
2205            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2206            mapped_at_creation: false,
2207        });
2208        if vsize > 0 {
2209            queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&vertices));
2210        }
2211        if isize > 0 {
2212            queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&indices));
2213        }
2214        let gpu = crate::upload::GpuScene {
2215            vertex: crate::allocator::OwnedBuffer {
2216                buffer: vbuf,
2217                key: crate::allocator::BufKey {
2218                    size: vsize.max(4),
2219                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2220                },
2221            },
2222            index: crate::allocator::OwnedBuffer {
2223                buffer: ibuf,
2224                key: crate::allocator::BufKey {
2225                    size: isize.max(4),
2226                    usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2227                },
2228            },
2229            vertices: vertices.len() as u32,
2230            indices: indices.len() as u32,
2231        };
2232
2233        // Bind viewport
2234        let vp_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2235            label: Some("vp-bg-direct-no-msaa"),
2236            layout: self.solid_direct_no_msaa.viewport_bgl(),
2237            entries: &[wgpu::BindGroupEntry {
2238                binding: 0,
2239                resource: self.vp_buffer.as_entire_binding(),
2240            }],
2241        });
2242
2243        // Render directly to target without MSAA to preserve existing content through blending
2244        // MSAA+resolve doesn't apply blend state correctly for layered rendering
2245        let _z_bg = self.create_z_bind_group(0.0, queue);
2246
2247        // Add depth attachment (using 1x since this is non-MSAA rendering)
2248        let depth_attachment = self.depth_texture.as_ref().map(|tex| {
2249            wgpu::RenderPassDepthStencilAttachment {
2250                view: &tex.view,
2251                depth_ops: Some(wgpu::Operations {
2252                    load: wgpu::LoadOp::Load, // Preserve existing depth
2253                    store: wgpu::StoreOp::Store,
2254                }),
2255                stencil_ops: None,
2256            }
2257        });
2258
2259        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2260            label: Some("rounded-rect-fill-pass"),
2261            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2262                view: target_view,
2263                resolve_target: None,
2264                ops: wgpu::Operations {
2265                    load: wgpu::LoadOp::Load,
2266                    store: wgpu::StoreOp::Store,
2267                },
2268            })],
2269            depth_stencil_attachment: depth_attachment,
2270            occlusion_query_set: None,
2271            timestamp_writes: None,
2272        });
2273        self.solid_direct_no_msaa.record(&mut pass, &vp_bg, &gpu);
2274    }
2275
2276    pub fn render_solids_to_offscreen(
2277        &self,
2278        encoder: &mut wgpu::CommandEncoder,
2279        vp_bg: &wgpu::BindGroup,
2280        targets: &PassTargets,
2281        scene: &GpuScene,
2282        clear_color: wgpu::Color,
2283        queue: &wgpu::Queue,
2284    ) {
2285        // Depth attachment for offscreen rendering (1x)
2286        let depth_tex = self.device.create_texture(&wgpu::TextureDescriptor {
2287            label: Some("solid-depth-offscreen"),
2288            size: wgpu::Extent3d {
2289                width: targets.color.key.width,
2290                height: targets.color.key.height,
2291                depth_or_array_layers: 1,
2292            },
2293            mip_level_count: 1,
2294            sample_count: 1,
2295            dimension: wgpu::TextureDimension::D2,
2296            format: wgpu::TextureFormat::Depth32Float,
2297            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2298            view_formats: &[],
2299        });
2300        let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
2301
2302        let _z_bg = self.create_z_bind_group(0.0, queue);
2303        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2304            label: Some("solid-offscreen-pass"),
2305            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2306                view: &targets.color.view,
2307                resolve_target: None,
2308                ops: wgpu::Operations {
2309                    load: wgpu::LoadOp::Clear(clear_color),
2310                    store: wgpu::StoreOp::Store,
2311                },
2312            })],
2313            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2314                view: &depth_view,
2315                depth_ops: Some(wgpu::Operations {
2316                    load: wgpu::LoadOp::Clear(1.0),
2317                    store: wgpu::StoreOp::Store,
2318                }),
2319                stencil_ops: None,
2320            }),
2321            occlusion_query_set: None,
2322            timestamp_writes: None,
2323        });
2324        self.solid_offscreen.record(&mut pass, vp_bg, scene);
2325    }
2326
2327    pub fn composite_to_surface(
2328        &self,
2329        encoder: &mut wgpu::CommandEncoder,
2330        surface_view: &wgpu::TextureView,
2331        offscreen: &PassTargets,
2332        clear: Option<wgpu::Color>,
2333    ) {
2334        let bg = self
2335            .compositor
2336            .bind_group(&self.device, &offscreen.color.view);
2337        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2338            label: Some("composite-pass"),
2339            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2340                view: surface_view,
2341                resolve_target: None,
2342                ops: wgpu::Operations {
2343                    load: match clear {
2344                        Some(c) => wgpu::LoadOp::Clear(c),
2345                        None => wgpu::LoadOp::Load,
2346                    },
2347                    store: wgpu::StoreOp::Store,
2348                },
2349            })],
2350            depth_stencil_attachment: None,
2351            occlusion_query_set: None,
2352            timestamp_writes: None,
2353        });
2354        self.compositor.record(&mut pass, &bg);
2355    }
2356
2357    /// Paint background to intermediate texture instead of directly to surface.
2358    /// This enables smooth resizing when combined with blit_to_surface.
2359    pub fn paint_root_to_intermediate(
2360        &self,
2361        encoder: &mut wgpu::CommandEncoder,
2362        bg: &Background,
2363        queue: &wgpu::Queue,
2364    ) {
2365        let intermediate = self
2366            .intermediate_texture
2367            .as_ref()
2368            .expect("intermediate texture must be allocated before painting");
2369        self.paint_root(encoder, &intermediate.view, bg, queue);
2370    }
2371
2372    pub fn paint_root(
2373        &self,
2374        encoder: &mut wgpu::CommandEncoder,
2375        surface_view: &wgpu::TextureView,
2376        bg: &Background,
2377        queue: &wgpu::Queue,
2378    ) {
2379        // If solid, do a minimal clear pass
2380        if let Background::Solid(c) = bg {
2381            let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2382                label: Some("bg-solid-pass"),
2383                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2384                    view: surface_view,
2385                    resolve_target: None,
2386                    ops: wgpu::Operations {
2387                        load: wgpu::LoadOp::Clear(wgpu::Color {
2388                            r: c.r as f64,
2389                            g: c.g as f64,
2390                            b: c.b as f64,
2391                            a: c.a as f64,
2392                        }),
2393                        store: wgpu::StoreOp::Store,
2394                    },
2395                })],
2396                depth_stencil_attachment: None,
2397                occlusion_query_set: None,
2398                timestamp_writes: None,
2399            });
2400            return;
2401        }
2402
2403        // For gradient, draw fullscreen triangle
2404        let (start_uv, end_uv, stop0, stop1) = match bg {
2405            Background::LinearGradient {
2406                start_uv,
2407                end_uv,
2408                stop0,
2409                stop1,
2410            } => (*start_uv, *end_uv, *stop0, *stop1),
2411            _ => unreachable!(),
2412        };
2413        #[repr(C)]
2414        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2415        struct BgParams {
2416            start: [f32; 2],
2417            end: [f32; 2],
2418            center: [f32; 2],
2419            radius: f32,
2420            stop_count: u32,
2421            mode: u32,
2422            _pad: u32,
2423        }
2424        #[repr(C)]
2425        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2426        struct Stop {
2427            pos: f32,
2428            _pad0: [f32; 3],
2429            color: [f32; 4],
2430        }
2431
2432        let params = BgParams {
2433            start: start_uv,
2434            end: end_uv,
2435            center: [0.5, 0.5],
2436            radius: 1.0,
2437            stop_count: 2,
2438            mode: 1,
2439            _pad: 0,
2440        };
2441        let c0 = stop0.1;
2442        let c1 = stop1.1;
2443        let stops = [
2444            Stop {
2445                pos: stop0.0,
2446                _pad0: [0.0; 3],
2447                color: [c0.r, c0.g, c0.b, c0.a],
2448            },
2449            Stop {
2450                pos: stop1.0,
2451                _pad0: [0.0; 3],
2452                color: [c1.r, c1.g, c1.b, c1.a],
2453            },
2454            Stop {
2455                pos: 0.0,
2456                _pad0: [0.0; 3],
2457                color: [0.0; 4],
2458            },
2459            Stop {
2460                pos: 0.0,
2461                _pad0: [0.0; 3],
2462                color: [0.0; 4],
2463            },
2464            Stop {
2465                pos: 0.0,
2466                _pad0: [0.0; 3],
2467                color: [0.0; 4],
2468            },
2469            Stop {
2470                pos: 0.0,
2471                _pad0: [0.0; 3],
2472                color: [0.0; 4],
2473            },
2474            Stop {
2475                pos: 0.0,
2476                _pad0: [0.0; 3],
2477                color: [0.0; 4],
2478            },
2479            Stop {
2480                pos: 0.0,
2481                _pad0: [0.0; 3],
2482                color: [0.0; 4],
2483            },
2484        ];
2485
2486        queue.write_buffer(&self.bg_param_buffer, 0, bytemuck::bytes_of(&params));
2487        queue.write_buffer(&self.bg_stops_buffer, 0, bytemuck::cast_slice(&stops));
2488        let bg_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2489            label: Some("bg-bind"),
2490            layout: self.bg.bgl(),
2491            entries: &[
2492                wgpu::BindGroupEntry {
2493                    binding: 0,
2494                    resource: self.bg_param_buffer.as_entire_binding(),
2495                },
2496                wgpu::BindGroupEntry {
2497                    binding: 1,
2498                    resource: self.bg_stops_buffer.as_entire_binding(),
2499                },
2500            ],
2501        });
2502        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2503            label: Some("bg-grad-pass"),
2504            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2505                view: surface_view,
2506                resolve_target: None,
2507                ops: wgpu::Operations {
2508                    load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2509                    store: wgpu::StoreOp::Store,
2510                },
2511            })],
2512            depth_stencil_attachment: None,
2513            occlusion_query_set: None,
2514            timestamp_writes: None,
2515        });
2516        self.bg.record(&mut pass, &bg_bind);
2517    }
2518
2519    /// Paint linear gradient to intermediate texture.
2520    pub fn paint_root_linear_gradient_multi_to_intermediate(
2521        &self,
2522        encoder: &mut wgpu::CommandEncoder,
2523        start_uv: [f32; 2],
2524        end_uv: [f32; 2],
2525        stops_in: &[(f32, crate::scene::ColorLinPremul)],
2526        queue: &wgpu::Queue,
2527    ) {
2528        let intermediate = self
2529            .intermediate_texture
2530            .as_ref()
2531            .expect("intermediate texture must be allocated before painting");
2532        self.paint_root_linear_gradient_multi(
2533            encoder,
2534            &intermediate.view,
2535            start_uv,
2536            end_uv,
2537            stops_in,
2538            queue,
2539        );
2540    }
2541
2542    /// Multi-stop linear gradient background
2543    pub fn paint_root_linear_gradient_multi(
2544        &self,
2545        encoder: &mut wgpu::CommandEncoder,
2546        surface_view: &wgpu::TextureView,
2547        start_uv: [f32; 2],
2548        end_uv: [f32; 2],
2549        stops_in: &[(f32, crate::scene::ColorLinPremul)],
2550        queue: &wgpu::Queue,
2551    ) {
2552        // Normalize and sort stops for deterministic evaluation
2553        let mut sorted: Vec<(f32, crate::scene::ColorLinPremul)> = stops_in
2554            .iter()
2555            .map(|(p, c)| (p.clamp(0.0, 1.0), *c))
2556            .collect();
2557        sorted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
2558        let count = sorted.len().min(8).max(2) as u32;
2559        #[repr(C)]
2560        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2561        struct BgParams {
2562            start_end: [f32; 4],
2563            center_radius_stop: [f32; 4],
2564            flags: [f32; 4],
2565        }
2566        #[repr(C)]
2567        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2568        struct Stop {
2569            pos: f32,
2570            _pad0: [f32; 3],
2571            color: [f32; 4],
2572        }
2573        let mut stops: [Stop; 8] = [Stop {
2574            pos: 0.0,
2575            _pad0: [0.0; 3],
2576            color: [0.0; 4],
2577        }; 8];
2578        for (i, (p, c)) in sorted.iter().take(8).enumerate() {
2579            stops[i] = Stop {
2580                pos: *p,
2581                _pad0: [0.0; 3],
2582                color: [c.r, c.g, c.b, c.a],
2583            };
2584        }
2585        let debug_flag = std::env::var("DEBUG_RADIAL")
2586            .ok()
2587            .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
2588            .unwrap_or(false);
2589        let params = BgParams {
2590            start_end: [start_uv[0], start_uv[1], end_uv[0], end_uv[1]],
2591            center_radius_stop: [0.5, 0.5, 1.0, count as f32],
2592            flags: [1.0, if debug_flag { 1.0 } else { 0.0 }, 0.0, 0.0],
2593        };
2594        queue.write_buffer(&self.bg_param_buffer, 0, bytemuck::bytes_of(&params));
2595        queue.write_buffer(&self.bg_stops_buffer, 0, bytemuck::cast_slice(&stops));
2596        let bg_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2597            label: Some("bg-bind-linear"),
2598            layout: self.bg.bgl(),
2599            entries: &[
2600                wgpu::BindGroupEntry {
2601                    binding: 0,
2602                    resource: self.bg_param_buffer.as_entire_binding(),
2603                },
2604                wgpu::BindGroupEntry {
2605                    binding: 1,
2606                    resource: self.bg_stops_buffer.as_entire_binding(),
2607                },
2608            ],
2609        });
2610        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2611            label: Some("bg-linear-pass"),
2612            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2613                view: surface_view,
2614                resolve_target: None,
2615                ops: wgpu::Operations {
2616                    load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2617                    store: wgpu::StoreOp::Store,
2618                },
2619            })],
2620            depth_stencil_attachment: None,
2621            occlusion_query_set: None,
2622            timestamp_writes: None,
2623        });
2624        self.bg.record(&mut pass, &bg_bind);
2625    }
2626
2627    /// Paint radial gradient to intermediate texture.
2628    pub fn paint_root_radial_gradient_multi_to_intermediate(
2629        &self,
2630        encoder: &mut wgpu::CommandEncoder,
2631        center_uv: [f32; 2],
2632        radius: f32,
2633        stops_in: &[(f32, crate::scene::ColorLinPremul)],
2634        queue: &wgpu::Queue,
2635        width: u32,
2636        height: u32,
2637    ) {
2638        let intermediate = self
2639            .intermediate_texture
2640            .as_ref()
2641            .expect("intermediate texture must be allocated before painting");
2642        self.paint_root_radial_gradient_multi(
2643            encoder,
2644            &intermediate.view,
2645            center_uv,
2646            radius,
2647            stops_in,
2648            queue,
2649            width,
2650            height,
2651        );
2652    }
2653
2654    /// Multi-stop radial gradient background
2655    pub fn paint_root_radial_gradient_multi(
2656        &self,
2657        encoder: &mut wgpu::CommandEncoder,
2658        surface_view: &wgpu::TextureView,
2659        center_uv: [f32; 2],
2660        radius: f32,
2661        stops_in: &[(f32, crate::scene::ColorLinPremul)],
2662        queue: &wgpu::Queue,
2663        width: u32,
2664        height: u32,
2665    ) {
2666        // Normalize and sort stops for deterministic evaluation
2667        let mut sorted: Vec<(f32, crate::scene::ColorLinPremul)> = stops_in
2668            .iter()
2669            .map(|(p, c)| (p.clamp(0.0, 1.0), *c))
2670            .collect();
2671        sorted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
2672        let count = sorted.len().min(8).max(2) as u32;
2673        #[repr(C)]
2674        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2675        struct BgParams {
2676            start_end: [f32; 4],
2677            center_radius_stop: [f32; 4],
2678            flags: [f32; 4],
2679        }
2680        #[repr(C)]
2681        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2682        struct Stop {
2683            pos: f32,
2684            _pad0: [f32; 3],
2685            color: [f32; 4],
2686        }
2687        let mut stops: [Stop; 8] = [Stop {
2688            pos: 0.0,
2689            _pad0: [0.0; 3],
2690            color: [0.0; 4],
2691        }; 8];
2692        for (i, (p, c)) in sorted.iter().take(8).enumerate() {
2693            stops[i] = Stop {
2694                pos: *p,
2695                _pad0: [0.0; 3],
2696                color: [c.r, c.g, c.b, c.a],
2697            };
2698        }
2699        let debug_flag = std::env::var("DEBUG_RADIAL")
2700            .ok()
2701            .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
2702            .unwrap_or(false);
2703        let aspect_ratio = (width.max(1) as f32) / (height.max(1) as f32);
2704        if debug_flag {
2705            // debug logging removed
2706        }
2707        // macOS-specific DPI correction: Only adjust for centered fullscreen radials.
2708        // When center ~ [0.5,0.5], divide center and radius by scale factor to correct
2709        // for retina scaling differences in UV sampling. No-op elsewhere.
2710        let mut adj_center = center_uv;
2711        let mut adj_radius = radius;
2712        #[cfg(target_os = "macos")]
2713        {
2714            let sf = self.scale_factor.max(1.0);
2715            // Within ~1e-3 of exact center counts as centered
2716            if (adj_center[0] - 0.5).abs() < 1e-3 && (adj_center[1] - 0.5).abs() < 1e-3 {
2717                adj_center = [adj_center[0] / sf, adj_center[1] / sf];
2718                adj_radius = adj_radius / sf;
2719                if debug_flag {
2720                    // debug logging removed
2721                }
2722            }
2723        }
2724        let params = BgParams {
2725            start_end: [0.0, 0.0, 1.0, 1.0],
2726            center_radius_stop: [adj_center[0], adj_center[1], adj_radius, count as f32],
2727            flags: [2.0, if debug_flag { 1.0 } else { 0.0 }, aspect_ratio, 0.0],
2728        };
2729        queue.write_buffer(&self.bg_param_buffer, 0, bytemuck::bytes_of(&params));
2730        queue.write_buffer(&self.bg_stops_buffer, 0, bytemuck::cast_slice(&stops));
2731        let bg_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2732            label: Some("bg-bind-radial"),
2733            layout: self.bg.bgl(),
2734            entries: &[
2735                wgpu::BindGroupEntry {
2736                    binding: 0,
2737                    resource: self.bg_param_buffer.as_entire_binding(),
2738                },
2739                wgpu::BindGroupEntry {
2740                    binding: 1,
2741                    resource: self.bg_stops_buffer.as_entire_binding(),
2742                },
2743            ],
2744        });
2745        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2746            label: Some("bg-radial-pass"),
2747            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2748                view: surface_view,
2749                resolve_target: None,
2750                ops: wgpu::Operations {
2751                    load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2752                    store: wgpu::StoreOp::Store,
2753                },
2754            })],
2755            depth_stencil_attachment: None,
2756            occlusion_query_set: None,
2757            timestamp_writes: None,
2758        });
2759        self.bg.record(&mut pass, &bg_bind);
2760    }
2761
2762    /// Convenience: paint a solid background color directly to the surface.
2763    pub fn paint_root_color(
2764        &self,
2765        encoder: &mut wgpu::CommandEncoder,
2766        surface_view: &wgpu::TextureView,
2767        color: crate::scene::ColorLinPremul,
2768        queue: &wgpu::Queue,
2769    ) {
2770        // Draw solid via the background fullscreen shader to avoid sRGB clear vs blit inconsistencies.
2771        #[repr(C)]
2772        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2773        struct BgParams {
2774            start_end: [f32; 4],
2775            center_radius_stop: [f32; 4],
2776            flags: [f32; 4],
2777        }
2778        #[repr(C)]
2779        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2780        struct Stop {
2781            pos: f32,
2782            _pad0: [f32; 3],
2783            color: [f32; 4],
2784        }
2785        let params = BgParams {
2786            start_end: [0.0, 0.0, 1.0, 1.0],
2787            center_radius_stop: [0.5, 0.5, 1.0, 1.0],
2788            flags: [0.0, 0.0, 0.0, 0.0], // mode = 0 => solid
2789        };
2790        let stops: [Stop; 1] = [Stop {
2791            pos: 0.0,
2792            _pad0: [0.0; 3],
2793            color: [color.r, color.g, color.b, color.a],
2794        }];
2795        // Write uniforms (only first stop used for solid mode)
2796        queue.write_buffer(&self.bg_param_buffer, 0, bytemuck::bytes_of(&params));
2797        queue.write_buffer(&self.bg_stops_buffer, 0, bytemuck::cast_slice(&stops));
2798        let bg_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2799            label: Some("bg-bind-solid"),
2800            layout: self.bg.bgl(),
2801            entries: &[
2802                wgpu::BindGroupEntry {
2803                    binding: 0,
2804                    resource: self.bg_param_buffer.as_entire_binding(),
2805                },
2806                wgpu::BindGroupEntry {
2807                    binding: 1,
2808                    resource: self.bg_stops_buffer.as_entire_binding(),
2809                },
2810            ],
2811        });
2812        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2813            label: Some("bg-solid-pass"),
2814            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2815                view: surface_view,
2816                resolve_target: None,
2817                ops: wgpu::Operations {
2818                    load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2819                    store: wgpu::StoreOp::Store,
2820                },
2821            })],
2822            depth_stencil_attachment: None,
2823            occlusion_query_set: None,
2824            timestamp_writes: None,
2825        });
2826        self.bg.record(&mut pass, &bg_bind);
2827    }
2828
2829    /// Convenience: paint a simple 2-stop linear gradient to the surface.
2830    pub fn paint_root_gradient(
2831        &self,
2832        encoder: &mut wgpu::CommandEncoder,
2833        surface_view: &wgpu::TextureView,
2834        start_uv: [f32; 2],
2835        end_uv: [f32; 2],
2836        stop0: (f32, crate::scene::ColorLinPremul),
2837        stop1: (f32, crate::scene::ColorLinPremul),
2838        queue: &wgpu::Queue,
2839    ) {
2840        #[repr(C)]
2841        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2842        struct BgData {
2843            start_end: [f32; 4],
2844            center_radius_stop: [f32; 4],
2845            flags: [f32; 4],
2846        }
2847        let c0 = stop0.1;
2848        let c1 = stop1.1;
2849        // Reuse the multi-stop layout by writing two stops into the stops buffer
2850        let debug_flag = std::env::var("DEBUG_RADIAL")
2851            .ok()
2852            .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
2853            .unwrap_or(false);
2854        let params = BgData {
2855            start_end: [start_uv[0], start_uv[1], end_uv[0], end_uv[1]],
2856            center_radius_stop: [0.5, 0.5, 1.0, 2.0],
2857            flags: [1.0, if debug_flag { 1.0 } else { 0.0 }, 0.0, 0.0],
2858        };
2859        // Populate first two stops in the stop buffer for the simple gradient helper
2860        #[repr(C)]
2861        #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
2862        struct Stop {
2863            pos: f32,
2864            _pad0: [f32; 3],
2865            color: [f32; 4],
2866        }
2867        let stops: [Stop; 2] = [
2868            Stop {
2869                pos: stop0.0,
2870                _pad0: [0.0; 3],
2871                color: [c0.r, c0.g, c0.b, c0.a],
2872            },
2873            Stop {
2874                pos: stop1.0,
2875                _pad0: [0.0; 3],
2876                color: [c1.r, c1.g, c1.b, c1.a],
2877            },
2878        ];
2879        queue.write_buffer(&self.bg_param_buffer, 0, bytemuck::bytes_of(&params));
2880        queue.write_buffer(&self.bg_stops_buffer, 0, bytemuck::cast_slice(&stops));
2881        let bg_bind = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2882            label: Some("bg-bind"),
2883            layout: self.bg.bgl(),
2884            entries: &[
2885                wgpu::BindGroupEntry {
2886                    binding: 0,
2887                    resource: self.bg_param_buffer.as_entire_binding(),
2888                },
2889                wgpu::BindGroupEntry {
2890                    binding: 1,
2891                    resource: self.bg_stops_buffer.as_entire_binding(),
2892                },
2893            ],
2894        });
2895        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2896            label: Some("bg-grad-pass"),
2897            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2898                view: surface_view,
2899                resolve_target: None,
2900                ops: wgpu::Operations {
2901                    load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2902                    store: wgpu::StoreOp::Store,
2903                },
2904            })],
2905            depth_stencil_attachment: None,
2906            occlusion_query_set: None,
2907            timestamp_writes: None,
2908        });
2909        self.bg.record(&mut pass, &bg_bind);
2910    }
2911
2912    /// Unified rendering: Render all draw types (solids, text, images, SVGs) in a single pass
2913    /// with proper z-ordering. This is Phase 3 of the depth buffer implementation.
2914    ///
2915    /// This method interleaves all draw calls based on z-index for optimal z-ordering performance.
2916    /// Draw calls are batched by material type when possible for efficiency.
2917    pub fn render_unified(
2918        &mut self,
2919        encoder: &mut wgpu::CommandEncoder,
2920        allocator: &mut RenderAllocator,
2921        surface_view: &wgpu::TextureView,
2922        width: u32,
2923        height: u32,
2924        scene: &GpuScene,
2925        transparent_scene: &GpuScene,
2926        transparent_batches: &[crate::upload::TransparentBatch],
2927        glyph_draws: &[(
2928            [f32; 2],
2929            crate::text::RasterizedGlyph,
2930            crate::ColorLinPremul,
2931            i32,
2932        )], // (origin, glyph, color, z)
2933        svg_draws: &[(
2934            std::path::PathBuf,
2935            [f32; 2],
2936            [f32; 2],
2937            Option<crate::SvgStyle>,
2938            i32,
2939            crate::Transform2D,
2940            Option<crate::Rect>,
2941        )],
2942        image_draws: &[(
2943            std::path::PathBuf,
2944            [f32; 2],
2945            [f32; 2],
2946            i32,
2947            Option<crate::Rect>,
2948        )],
2949        external_texture_draws: &[crate::upload::ExtractedExternalTextureDraw],
2950        clear: wgpu::Color,
2951        direct: bool,
2952        queue: &wgpu::Queue,
2953        preserve_surface: bool,
2954    ) {
2955        // Update viewport uniform. `logical` is the combined DPI/UI scale factor
2956        // applied to all scene coordinates when mapping to device pixels.
2957        let logical =
2958            crate::dpi::logical_multiplier(self.logical_pixels, self.scale_factor, self.ui_scale);
2959        let inv_logical = if logical.is_finite() && logical > 0.0 {
2960            1.0 / logical
2961        } else {
2962            1.0
2963        };
2964        let scale = [
2965            (2.0f32 / (width.max(1) as f32)) * logical,
2966            (-2.0f32 / (height.max(1) as f32)) * logical,
2967        ];
2968        let translate = [-1.0f32, 1.0f32];
2969        let vp_data: [f32; 8] = [
2970            scale[0],
2971            scale[1],
2972            translate[0],
2973            translate[1],
2974            self.scroll_offset[0],
2975            self.scroll_offset[1],
2976            0.0, // padding
2977            0.0, // padding
2978        ];
2979        let data = bytemuck::bytes_of(&vp_data);
2980        queue.write_buffer(&self.vp_buffer, 0, data);
2981        let transparent_text_z: std::collections::HashSet<i32> =
2982            transparent_batches.iter().map(|b| b.z).collect();
2983
2984        // Ensure depth buffer matches current render size (1x sample)
2985        self.ensure_depth_texture(allocator, width.max(1), height.max(1));
2986
2987        // Create viewport bind groups
2988        let vp_bg_off = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2989            label: Some("vp-bg-offscreen"),
2990            layout: self.solid_offscreen.viewport_bgl(),
2991            entries: &[wgpu::BindGroupEntry {
2992                binding: 0,
2993                resource: self.vp_buffer.as_entire_binding(),
2994            }],
2995        });
2996        if direct {
2997            let vp_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2998                label: Some("vp-bg-direct-local"),
2999                layout: self.solid_direct.viewport_bgl(),
3000                entries: &[wgpu::BindGroupEntry {
3001                    binding: 0,
3002                    resource: self.vp_buffer.as_entire_binding(),
3003                }],
3004            });
3005
3006            // Create z-index bind group before render pass (must outlive the pass)
3007            let _z_bg = self.create_z_bind_group(0.0, queue);
3008
3009            // Pre-fetch (and lazily load) all image views before render pass (to avoid mutable borrow conflicts)
3010            let mut image_views: Vec<(
3011                wgpu::TextureView,
3012                [f32; 2],
3013                [f32; 2],
3014                f32,
3015                Option<crate::Rect>,
3016            )> = Vec::new();
3017            for (path, origin, size, z, clip) in image_draws.iter() {
3018                let tex_opt =
3019                    if let Some(view) = self.try_get_image_view(std::path::Path::new(path)) {
3020                        Some(view)
3021                    } else {
3022                        self.load_image_to_view(std::path::Path::new(path), queue)
3023                    };
3024                if let Some((tex_view, _w, _h)) = tex_opt {
3025                    image_views.push((tex_view, *origin, *size, *z as f32, *clip));
3026                }
3027            }
3028
3029            // Pre-rasterize all SVGs before render pass (to avoid mutable borrow conflicts)
3030            let mut svg_views: Vec<(
3031                wgpu::TextureView,
3032                [f32; 2],
3033                [f32; 2],
3034                f32,
3035                Option<crate::Rect>,
3036            )> = Vec::new();
3037            for (path, origin, max_size, style, _z, transform, clip) in svg_draws.iter() {
3038                if let Some((_view, w, h)) =
3039                    self.rasterize_svg_to_view(std::path::Path::new(path), 1.0, *style, queue)
3040                {
3041                    let base_w = w.max(1) as f32;
3042                    let base_h = h.max(1) as f32;
3043                    let scale = (max_size[0] / base_w).min(max_size[1] / base_h).max(0.0);
3044
3045                    if let Some((view_scaled, _sw, _sh)) =
3046                        self.rasterize_svg_to_view(std::path::Path::new(path), scale, *style, queue)
3047                    {
3048                        let draw_w = base_w * scale;
3049                        let draw_h = base_h * scale;
3050                        let transformed_origin = apply_transform_to_point(*origin, *transform);
3051                        svg_views.push((
3052                            view_scaled,
3053                            transformed_origin,
3054                            [draw_w, draw_h],
3055                            *_z as f32,
3056                            *clip,
3057                        ));
3058                    }
3059                }
3060            }
3061
3062            // Group text by z-index for proper depth rendering
3063            // eprintln!("🎨 render_unified received {} glyph_draws", glyph_draws.len());
3064            let mut text_by_z: std::collections::HashMap<
3065                i32,
3066                Vec<(
3067                    usize,
3068                    [f32; 2],
3069                    &crate::text::RasterizedGlyph,
3070                    &crate::ColorLinPremul,
3071                )>,
3072            > = std::collections::HashMap::new();
3073            for (idx, (origin, glyph, color, z)) in glyph_draws.iter().enumerate() {
3074                text_by_z
3075                    .entry(*z)
3076                    .or_insert_with(Vec::new)
3077                    .push((idx, *origin, glyph, color));
3078            }
3079            // eprintln!("🎨 Grouped text into {} z-index groups", text_by_z.len());
3080
3081            // Prepare text rendering data before render pass
3082            let mut text_groups = if !glyph_draws.is_empty() {
3083                // Clear the atlas region used in the previous frame (efficient partial clear)
3084                if self.prev_atlas_max_x > 0 && self.prev_atlas_max_y > 0 {
3085                    let clear_width = self.prev_atlas_max_x.min(4096);
3086                    let clear_height = self.prev_atlas_max_y.min(4096);
3087                    let clear_size = (clear_width * clear_height * 4) as usize;
3088                    let clear_data = vec![0u8; clear_size];
3089                    queue.write_texture(
3090                        wgpu::ImageCopyTexture {
3091                            texture: &self.text_mask_atlas,
3092                            mip_level: 0,
3093                            origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },
3094                            aspect: wgpu::TextureAspect::All,
3095                        },
3096                        &clear_data,
3097                        wgpu::ImageDataLayout {
3098                            offset: 0,
3099                            bytes_per_row: Some(clear_width * 4),
3100                            rows_per_image: Some(clear_height),
3101                        },
3102                        wgpu::Extent3d {
3103                            width: clear_width,
3104                            height: clear_height,
3105                            depth_or_array_layers: 1,
3106                        },
3107                    );
3108                }
3109
3110                let mut atlas_cursor_x = 0u32;
3111                let mut atlas_cursor_y = 0u32;
3112                let mut next_row_height = 0u32;
3113                let mut atlas_max_x = 0u32;
3114                let mut atlas_max_y = 0u32;
3115                let mut all_text_groups: Vec<(i32, Vec<TextQuadVtx>)> = Vec::new();
3116
3117                // Process each z-index group
3118                for (z_index, glyphs) in text_by_z.iter() {
3119                    let mut vertices: Vec<TextQuadVtx> = Vec::new();
3120                    let force_grayscale = transparent_text_z.contains(z_index);
3121                    // eprintln!("      🔠 Processing z={} with {} glyphs", z_index, glyphs.len());
3122
3123                    let mut local_idx = 0;
3124                    for (_idx, origin, glyph, color) in glyphs.iter() {
3125                        let (w, h, data) = glyph_mask_for_atlas(&glyph.mask, force_grayscale);
3126                        if local_idx == 0 {
3127                            // eprintln!("        🔤 First glyph: origin=[{:.1}, {:.1}], size=[{}, {}], color=[{:.3}, {:.3}, {:.3}, {:.3}]",
3128                            //     origin[0], origin[1], w, h, color.r, color.g, color.b, color.a);
3129                        }
3130                        local_idx += 1;
3131
3132                        if atlas_cursor_x + w >= 4096 {
3133                            atlas_cursor_x = 0;
3134                            atlas_cursor_y += next_row_height;
3135                            next_row_height = 0;
3136                        }
3137                        next_row_height = next_row_height.max(h);
3138
3139                        // Track maximum atlas region used for clearing next frame
3140                        atlas_max_x = atlas_max_x.max(atlas_cursor_x + w);
3141                        atlas_max_y = atlas_max_y.max(atlas_cursor_y + h);
3142
3143                        queue.write_texture(
3144                            wgpu::ImageCopyTexture {
3145                                texture: &self.text_mask_atlas,
3146                                mip_level: 0,
3147                                origin: wgpu::Origin3d {
3148                                    x: atlas_cursor_x,
3149                                    y: atlas_cursor_y,
3150                                    z: 0,
3151                                },
3152                                aspect: wgpu::TextureAspect::All,
3153                            },
3154                            data.as_ref(),
3155                            wgpu::ImageDataLayout {
3156                                offset: 0,
3157                                bytes_per_row: Some(w * 4),
3158                                rows_per_image: Some(h),
3159                            },
3160                            wgpu::Extent3d {
3161                                width: w,
3162                                height: h,
3163                                depth_or_array_layers: 1,
3164                            },
3165                        );
3166
3167                        let u0 = atlas_cursor_x as f32 / 4096.0;
3168                        let v0 = atlas_cursor_y as f32 / 4096.0;
3169                        let u1 = (atlas_cursor_x + w) as f32 / 4096.0;
3170                        let v1 = (atlas_cursor_y + h) as f32 / 4096.0;
3171
3172                        // Glyph masks are rasterized at *physical* pixel size. To avoid
3173                        // scaling them again during composition, convert their size into
3174                        // logical scene units so that the subsequent logical->device
3175                        // scaling in the viewport transform maps them 1:1 to the atlas.
3176                        let quad_w = (w as f32) * inv_logical;
3177                        let quad_h = (h as f32) * inv_logical;
3178
3179                        if local_idx == 1 {
3180                            // eprintln!("        📐 Atlas pos: cursor=({}, {}), uv=[{:.4}, {:.4}] to [{:.4}, {:.4}]",
3181                            //     atlas_cursor_x, atlas_cursor_y, u0, v0, u1, v1);
3182                        }
3183
3184                        vertices.extend_from_slice(&[
3185                            TextQuadVtx {
3186                                pos: [origin[0], origin[1]],
3187                                uv: [u0, v0],
3188                                color: [color.r, color.g, color.b, color.a],
3189                            },
3190                            TextQuadVtx {
3191                                pos: [origin[0] + quad_w, origin[1]],
3192                                uv: [u1, v0],
3193                                color: [color.r, color.g, color.b, color.a],
3194                            },
3195                            TextQuadVtx {
3196                                pos: [origin[0] + quad_w, origin[1] + quad_h],
3197                                uv: [u1, v1],
3198                                color: [color.r, color.g, color.b, color.a],
3199                            },
3200                            TextQuadVtx {
3201                                pos: [origin[0], origin[1] + quad_h],
3202                                uv: [u0, v1],
3203                                color: [color.r, color.g, color.b, color.a],
3204                            },
3205                        ]);
3206
3207                        atlas_cursor_x += w;
3208                    }
3209
3210                    // Store vertices for this z-index group
3211                    if !vertices.is_empty() {
3212                        all_text_groups.push((*z_index, vertices));
3213                    }
3214                }
3215
3216                // Create buffers and bind groups for each text group
3217                // eprintln!("🔧 all_text_groups.len() = {}", all_text_groups.len());
3218                let mut text_resources: Vec<(
3219                    i32,
3220                    wgpu::Buffer,
3221                    wgpu::Buffer,
3222                    u32,
3223                    wgpu::BindGroup,
3224                    wgpu::Buffer,
3225                )> = Vec::new();
3226                for (z_index, vertices) in all_text_groups {
3227                    // eprintln!(
3228                    //     "  🛠️  Creating resources for z={}, vertices={}",
3229                    //     z_index,
3230                    //     vertices.len()
3231                    // );
3232                    let quad_count = vertices.len() / 4;
3233                    let mut indices: Vec<u16> = Vec::with_capacity(quad_count * 6);
3234                    for i in 0..quad_count {
3235                        let base = (i * 4) as u16;
3236                        indices.extend_from_slice(&[
3237                            base,
3238                            base + 1,
3239                            base + 2,
3240                            base,
3241                            base + 2,
3242                            base + 3,
3243                        ]);
3244                    }
3245
3246                    // Create vertex buffer for this group
3247                    let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3248                        label: Some("text-vertex-buffer-group"),
3249                        size: (vertices.len() * std::mem::size_of::<TextQuadVtx>()) as u64,
3250                        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3251                        mapped_at_creation: false,
3252                    });
3253
3254                    // Create index buffer for this group
3255                    let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3256                        label: Some("text-index-buffer-group"),
3257                        size: (indices.len() * std::mem::size_of::<u16>()) as u64,
3258                        usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3259                        mapped_at_creation: false,
3260                    });
3261
3262                    queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&vertices));
3263                    queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&indices));
3264
3265                    // Create z bind group for this text group
3266                    // Pass z_index as float directly - shader will convert to depth
3267                    // eprintln!("    💎 z={} (passing as z-index to shader)", z_index);
3268                    let (z_bg, z_buf) = self.create_group_z_bind_group(z_index as f32, queue);
3269
3270                    text_resources.push((z_index, vbuf, ibuf, indices.len() as u32, z_bg, z_buf));
3271                }
3272
3273                // Store atlas usage for next frame's clearing
3274                self.prev_atlas_max_x = atlas_max_x;
3275                self.prev_atlas_max_y = atlas_max_y;
3276
3277                text_resources
3278            } else {
3279                Vec::new()
3280            };
3281
3282            // Sort text groups by z-index (back to front)
3283            text_groups.sort_by_key(|(z, _, _, _, _, _)| *z);
3284
3285            // Create text bind groups before render pass so they live long enough
3286            let vp_bg_text = self.text.vp_bind_group(&self.device, &self.vp_buffer);
3287
3288            // Prepare image resources (collect all buffers and bind groups so they live long enough)
3289            let mut image_resources: Vec<(
3290                wgpu::Buffer,
3291                wgpu::Buffer,
3292                wgpu::BindGroup,
3293                wgpu::BindGroup,
3294                wgpu::BindGroup,
3295                wgpu::BindGroup,
3296                wgpu::Buffer,
3297                wgpu::Buffer,
3298                Option<crate::Rect>,
3299            )> = Vec::new();
3300            let mut image_z_vals: Vec<i32> = Vec::new();
3301            for (tex_view, origin, size, z_val, clip) in image_views.iter() {
3302                let verts = [
3303                    ImageQuadVtx {
3304                        pos: [origin[0], origin[1]],
3305                        uv: [0.0, 0.0],
3306                    },
3307                    ImageQuadVtx {
3308                        pos: [origin[0] + size[0], origin[1]],
3309                        uv: [1.0, 0.0],
3310                    },
3311                    ImageQuadVtx {
3312                        pos: [origin[0] + size[0], origin[1] + size[1]],
3313                        uv: [1.0, 1.0],
3314                    },
3315                    ImageQuadVtx {
3316                        pos: [origin[0], origin[1] + size[1]],
3317                        uv: [0.0, 1.0],
3318                    },
3319                ];
3320                let idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
3321
3322                let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3323                    label: Some("image-vbuf-unified"),
3324                    size: (verts.len() * std::mem::size_of::<ImageQuadVtx>()) as u64,
3325                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3326                    mapped_at_creation: false,
3327                });
3328                let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3329                    label: Some("image-ibuf-unified"),
3330                    size: (idx.len() * std::mem::size_of::<u16>()) as u64,
3331                    usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3332                    mapped_at_creation: false,
3333                });
3334                queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
3335                queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&idx));
3336
3337                let vp_bg_img = self.image.vp_bind_group(&self.device, &self.vp_buffer);
3338                // Pass z_index as float directly - shader will convert to depth
3339                let (z_bg_img, z_buf_img) = self.create_group_z_bind_group(*z_val as f32, queue);
3340                let tex_bg = self.image.tex_bind_group(&self.device, tex_view);
3341                let (params_bg, params_buf) =
3342                    self.image.params_bind_group(&self.device, 1.0, false);
3343
3344                image_z_vals.push(*z_val as i32);
3345                image_resources.push((
3346                    vbuf, ibuf, vp_bg_img, z_bg_img, tex_bg, params_bg, z_buf_img, params_buf,
3347                    *clip,
3348                ));
3349            }
3350
3351            // Prepare SVG resources
3352            let mut svg_z_vals: Vec<i32> = Vec::new();
3353            let mut svg_resources: Vec<(
3354                wgpu::Buffer,
3355                wgpu::Buffer,
3356                wgpu::BindGroup,
3357                wgpu::BindGroup,
3358                wgpu::BindGroup,
3359                wgpu::BindGroup,
3360                wgpu::Buffer,
3361                wgpu::Buffer,
3362                Option<crate::Rect>,
3363            )> = Vec::new();
3364            for (view_scaled, origin, size, z_val, clip) in svg_views.iter() {
3365                let verts = [
3366                    ImageQuadVtx {
3367                        pos: [origin[0], origin[1]],
3368                        uv: [0.0, 0.0],
3369                    },
3370                    ImageQuadVtx {
3371                        pos: [origin[0] + size[0], origin[1]],
3372                        uv: [1.0, 0.0],
3373                    },
3374                    ImageQuadVtx {
3375                        pos: [origin[0] + size[0], origin[1] + size[1]],
3376                        uv: [1.0, 1.0],
3377                    },
3378                    ImageQuadVtx {
3379                        pos: [origin[0], origin[1] + size[1]],
3380                        uv: [0.0, 1.0],
3381                    },
3382                ];
3383                let idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
3384
3385                let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3386                    label: Some("svg-vbuf-unified"),
3387                    size: (verts.len() * std::mem::size_of::<ImageQuadVtx>()) as u64,
3388                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3389                    mapped_at_creation: false,
3390                });
3391                let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3392                    label: Some("svg-ibuf-unified"),
3393                    size: (idx.len() * std::mem::size_of::<u16>()) as u64,
3394                    usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3395                    mapped_at_creation: false,
3396                });
3397                queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
3398                queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&idx));
3399
3400                let vp_bg_svg = self.image.vp_bind_group(&self.device, &self.vp_buffer);
3401                // Pass z_index as float directly - shader will convert to depth
3402                let (z_bg_svg, z_buf_svg) = self.create_group_z_bind_group(*z_val as f32, queue);
3403                let tex_bg = self.image.tex_bind_group(&self.device, view_scaled);
3404                let (params_bg, params_buf) =
3405                    self.image.params_bind_group(&self.device, 1.0, false);
3406
3407                svg_z_vals.push(*z_val as i32);
3408                svg_resources.push((
3409                    vbuf, ibuf, vp_bg_svg, z_bg_svg, tex_bg, params_bg, z_buf_svg, params_buf,
3410                    *clip,
3411                ));
3412            }
3413
3414            // Prepare external texture resources (e.g., 3D viewports)
3415            let mut ext_z_vals: Vec<i32> = Vec::new();
3416            let mut ext_resources: Vec<(
3417                wgpu::Buffer,
3418                wgpu::Buffer,
3419                wgpu::BindGroup,
3420                wgpu::BindGroup,
3421                wgpu::BindGroup,
3422                wgpu::BindGroup,
3423                wgpu::Buffer,
3424                wgpu::Buffer,
3425            )> = Vec::new();
3426            for etd in external_texture_draws.iter() {
3427                let Some(tex_view) = self.external_textures.get(&etd.texture_id) else {
3428                    continue;
3429                };
3430                let verts = [
3431                    ImageQuadVtx {
3432                        pos: [etd.origin[0], etd.origin[1]],
3433                        uv: [0.0, 0.0],
3434                    },
3435                    ImageQuadVtx {
3436                        pos: [etd.origin[0] + etd.size[0], etd.origin[1]],
3437                        uv: [1.0, 0.0],
3438                    },
3439                    ImageQuadVtx {
3440                        pos: [etd.origin[0] + etd.size[0], etd.origin[1] + etd.size[1]],
3441                        uv: [1.0, 1.0],
3442                    },
3443                    ImageQuadVtx {
3444                        pos: [etd.origin[0], etd.origin[1] + etd.size[1]],
3445                        uv: [0.0, 1.0],
3446                    },
3447                ];
3448                let idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
3449
3450                let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3451                    label: Some("ext-tex-vbuf-unified"),
3452                    size: (verts.len() * std::mem::size_of::<ImageQuadVtx>()) as u64,
3453                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3454                    mapped_at_creation: false,
3455                });
3456                let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3457                    label: Some("ext-tex-ibuf-unified"),
3458                    size: (idx.len() * std::mem::size_of::<u16>()) as u64,
3459                    usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3460                    mapped_at_creation: false,
3461                });
3462                queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
3463                queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&idx));
3464
3465                let vp_bg_ext = self.image.vp_bind_group(&self.device, &self.vp_buffer);
3466                let (z_bg_ext, z_buf_ext) = self.create_group_z_bind_group(etd.z as f32, queue);
3467                let tex_bg = self.image.tex_bind_group(&self.device, tex_view);
3468                let (params_bg, params_buf) =
3469                    self.image
3470                        .params_bind_group(&self.device, etd.opacity, etd.premultiplied);
3471
3472                ext_z_vals.push(etd.z);
3473                ext_resources.push((
3474                    vbuf, ibuf, vp_bg_ext, z_bg_ext, tex_bg, params_bg, z_buf_ext, params_buf,
3475                ));
3476            }
3477
3478            // Build depth attachment after all mutable borrows on self are finished
3479            let depth_attachment = Some(wgpu::RenderPassDepthStencilAttachment {
3480                view: self.depth_view(),
3481                depth_ops: Some(wgpu::Operations {
3482                    load: if preserve_surface {
3483                        wgpu::LoadOp::Load
3484                    } else {
3485                        wgpu::LoadOp::Clear(1.0)
3486                    },
3487                    store: wgpu::StoreOp::Store,
3488                }),
3489                stencil_ops: None,
3490            });
3491
3492            // Begin unified render pass (after all resource preparation)
3493            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3494                label: Some("unified-render-pass"),
3495                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3496                    view: surface_view,
3497                    resolve_target: None,
3498                    ops: wgpu::Operations {
3499                        load: if preserve_surface {
3500                            wgpu::LoadOp::Load
3501                        } else {
3502                            wgpu::LoadOp::Clear(clear)
3503                        },
3504                        store: wgpu::StoreOp::Store,
3505                    },
3506                })],
3507                depth_stencil_attachment: depth_attachment,
3508                occlusion_query_set: None,
3509                timestamp_writes: None,
3510            });
3511
3512            // Render solids first (they're already sorted by z-index in the scene)
3513            self.solid_direct.record(&mut pass, &vp_bg, scene);
3514
3515            // Unified z-sorted rendering: interleave ALL draw types (transparent
3516            // solids, text, images, SVGs, external textures) by z-index so that
3517            // depth ordering is correct across element types. Without this,
3518            // images/SVGs rendered in a flat batch after transparent solids could
3519            // appear on top of higher-z transparent overlays (like a dock scrim).
3520            #[derive(Clone, Copy)]
3521            enum DrawItem {
3522                TransparentBatch(usize),
3523                TextGroup(usize),
3524                Image(usize),
3525                Svg(usize),
3526                ExternalTexture(usize),
3527            }
3528            let mut all_items: Vec<(i32, DrawItem)> = Vec::new();
3529            for (i, batch) in transparent_batches.iter().enumerate() {
3530                all_items.push((batch.z, DrawItem::TransparentBatch(i)));
3531            }
3532            for (i, (z, _, _, _, _, _)) in text_groups.iter().enumerate() {
3533                all_items.push((*z, DrawItem::TextGroup(i)));
3534            }
3535            for (i, z) in image_z_vals.iter().enumerate() {
3536                all_items.push((*z, DrawItem::Image(i)));
3537            }
3538            for (i, z) in svg_z_vals.iter().enumerate() {
3539                all_items.push((*z, DrawItem::Svg(i)));
3540            }
3541            for (i, z) in ext_z_vals.iter().enumerate() {
3542                all_items.push((*z, DrawItem::ExternalTexture(i)));
3543            }
3544            // Stable sort preserves relative order within same z-index
3545            all_items.sort_by_key(|(z, _)| *z);
3546
3547            for &(_, item) in all_items.iter() {
3548                match item {
3549                    DrawItem::TransparentBatch(i) => {
3550                        let batch = &transparent_batches[i];
3551                        self.transparent_solid_direct.record_index_range(
3552                            &mut pass,
3553                            &vp_bg,
3554                            transparent_scene,
3555                            batch.index_start,
3556                            batch.index_count,
3557                        );
3558                    }
3559                    DrawItem::TextGroup(i) => {
3560                        let (_z, vbuf, ibuf, index_count, z_bg, _z_buf) = &text_groups[i];
3561                        if *index_count > 0 {
3562                            pass.set_pipeline(&self.text.pipeline);
3563                            pass.set_bind_group(0, &vp_bg_text, &[]);
3564                            pass.set_bind_group(1, z_bg, &[]);
3565                            pass.set_bind_group(2, &self.text_bind_group, &[]);
3566                            pass.set_vertex_buffer(0, vbuf.slice(..));
3567                            pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint16);
3568                            pass.draw_indexed(0..*index_count, 0, 0..1);
3569                        }
3570                    }
3571                    DrawItem::Image(i) => {
3572                        let (vbuf, ibuf, vp_bg_img, z_bg_img, tex_bg, params_bg, _, _, clip) =
3573                            &image_resources[i];
3574                        if let Some(c) = clip {
3575                            pass.set_scissor_rect(
3576                                c.x.max(0.0) as u32,
3577                                c.y.max(0.0) as u32,
3578                                (c.w.max(1.0) as u32)
3579                                    .min(width.saturating_sub(c.x.max(0.0) as u32)),
3580                                (c.h.max(1.0) as u32)
3581                                    .min(height.saturating_sub(c.y.max(0.0) as u32)),
3582                            );
3583                        }
3584                        self.image.record(
3585                            &mut pass, vp_bg_img, z_bg_img, tex_bg, params_bg, vbuf, ibuf, 6,
3586                        );
3587                        if clip.is_some() {
3588                            pass.set_scissor_rect(0, 0, width, height);
3589                        }
3590                    }
3591                    DrawItem::Svg(i) => {
3592                        let (vbuf, ibuf, vp_bg_svg, z_bg_svg, tex_bg, params_bg, _, _, clip) =
3593                            &svg_resources[i];
3594                        if let Some(c) = clip {
3595                            pass.set_scissor_rect(
3596                                c.x.max(0.0) as u32,
3597                                c.y.max(0.0) as u32,
3598                                (c.w.max(1.0) as u32)
3599                                    .min(width.saturating_sub(c.x.max(0.0) as u32)),
3600                                (c.h.max(1.0) as u32)
3601                                    .min(height.saturating_sub(c.y.max(0.0) as u32)),
3602                            );
3603                        }
3604                        self.image.record(
3605                            &mut pass, vp_bg_svg, z_bg_svg, tex_bg, params_bg, vbuf, ibuf, 6,
3606                        );
3607                        if clip.is_some() {
3608                            pass.set_scissor_rect(0, 0, width, height);
3609                        }
3610                    }
3611                    DrawItem::ExternalTexture(i) => {
3612                        let (vbuf, ibuf, vp_bg_ext, z_bg_ext, tex_bg, params_bg, _, _) =
3613                            &ext_resources[i];
3614                        self.image.record(
3615                            &mut pass, vp_bg_ext, z_bg_ext, tex_bg, params_bg, vbuf, ibuf, 6,
3616                        );
3617                    }
3618                }
3619            }
3620
3621            // NOW drop the pass - all rendering complete
3622            drop(pass);
3623            // eprintln!("✨ Render pass completed successfully (DIRECT)");
3624            return;
3625        }
3626
3627        // Offscreen path - unified rendering to offscreen target
3628        let targets = self.alloc_targets(allocator, width.max(1), height.max(1));
3629
3630        // Pre-fetch (and lazily load) all image views before render pass (to avoid mutable borrow conflicts)
3631        let mut image_views_off: Vec<(
3632            wgpu::TextureView,
3633            [f32; 2],
3634            [f32; 2],
3635            f32,
3636            Option<crate::Rect>,
3637        )> = Vec::new();
3638        // eprintln!("🔍 Pre-fetching {} images for unified offscreen render", image_draws.len());
3639        for (path, origin, size, z, clip) in image_draws.iter() {
3640            // eprintln!("  📦 Image at z={}: {:?}", z, path.file_name().unwrap_or_default());
3641            let tex_opt = if let Some(view) = self.try_get_image_view(std::path::Path::new(path)) {
3642                Some(view)
3643            } else {
3644                self.load_image_to_view(std::path::Path::new(path), queue)
3645            };
3646            if let Some((tex_view, _w, _h)) = tex_opt {
3647                image_views_off.push((tex_view, *origin, *size, *z as f32, *clip));
3648            }
3649        }
3650
3651        // Pre-rasterize all SVGs before creating render pass (to avoid mutable borrow conflicts)
3652        let mut svg_views_off: Vec<(
3653            wgpu::TextureView,
3654            [f32; 2],
3655            [f32; 2],
3656            f32,
3657            Option<crate::Rect>,
3658        )> = Vec::new();
3659        for (path, origin, max_size, style, _z, transform, clip) in svg_draws.iter() {
3660            if let Some((_view, w, h)) =
3661                self.rasterize_svg_to_view(std::path::Path::new(path), 1.0, *style, queue)
3662            {
3663                let base_w = w.max(1) as f32;
3664                let base_h = h.max(1) as f32;
3665                let scale = (max_size[0] / base_w).min(max_size[1] / base_h).max(0.0);
3666
3667                if let Some((view_scaled, _sw, _sh)) =
3668                    self.rasterize_svg_to_view(std::path::Path::new(path), scale, *style, queue)
3669                {
3670                    // Use logical size, not rasterized pixel dimensions (see note above).
3671                    let draw_w = base_w * scale;
3672                    let draw_h = base_h * scale;
3673                    let transformed_origin = apply_transform_to_point(*origin, *transform);
3674                    svg_views_off.push((
3675                        view_scaled,
3676                        transformed_origin,
3677                        [draw_w, draw_h],
3678                        *_z as f32,
3679                        *clip,
3680                    ));
3681                }
3682            }
3683        }
3684
3685        // Group text by z-index for proper depth rendering (offscreen path)
3686        let mut text_by_z_off: std::collections::HashMap<
3687            i32,
3688            Vec<(
3689                usize,
3690                [f32; 2],
3691                &crate::text::RasterizedGlyph,
3692                &crate::ColorLinPremul,
3693            )>,
3694        > = std::collections::HashMap::new();
3695        for (idx, (origin, glyph, color, z)) in glyph_draws.iter().enumerate() {
3696            text_by_z_off
3697                .entry(*z)
3698                .or_insert_with(Vec::new)
3699                .push((idx, *origin, glyph, color));
3700        }
3701
3702        // Prepare text rendering data (same as direct path)
3703        let mut text_groups_off = if !glyph_draws.is_empty() {
3704            // Clear the atlas region used in the previous frame (efficient partial clear)
3705            // Note: Atlas is shared between direct and offscreen paths, so clear here too
3706            if self.prev_atlas_max_x > 0 && self.prev_atlas_max_y > 0 {
3707                let clear_width = self.prev_atlas_max_x.min(4096);
3708                let clear_height = self.prev_atlas_max_y.min(4096);
3709                let clear_size = (clear_width * clear_height * 4) as usize;
3710                let clear_data = vec![0u8; clear_size];
3711                queue.write_texture(
3712                    wgpu::ImageCopyTexture {
3713                        texture: &self.text_mask_atlas,
3714                        mip_level: 0,
3715                        origin: wgpu::Origin3d { x: 0, y: 0, z: 0 },
3716                        aspect: wgpu::TextureAspect::All,
3717                    },
3718                    &clear_data,
3719                    wgpu::ImageDataLayout {
3720                        offset: 0,
3721                        bytes_per_row: Some(clear_width * 4),
3722                        rows_per_image: Some(clear_height),
3723                    },
3724                    wgpu::Extent3d {
3725                        width: clear_width,
3726                        height: clear_height,
3727                        depth_or_array_layers: 1,
3728                    },
3729                );
3730            }
3731
3732            let mut atlas_cursor_x = 0u32;
3733            let mut atlas_cursor_y = 0u32;
3734            let mut next_row_height = 0u32;
3735            let mut atlas_max_x = 0u32;
3736            let mut atlas_max_y = 0u32;
3737            let mut all_text_groups: Vec<(i32, Vec<TextQuadVtx>)> = Vec::new();
3738
3739            // Process each z-index group
3740            for (z_index, glyphs) in text_by_z_off.iter() {
3741                let mut vertices: Vec<TextQuadVtx> = Vec::new();
3742                let force_grayscale = transparent_text_z.contains(z_index);
3743
3744                for (_idx, origin, glyph, color) in glyphs.iter() {
3745                    let (w, h, data) = glyph_mask_for_atlas(&glyph.mask, force_grayscale);
3746
3747                    if atlas_cursor_x + w >= 4096 {
3748                        atlas_cursor_x = 0;
3749                        atlas_cursor_y += next_row_height;
3750                        next_row_height = 0;
3751                    }
3752                    next_row_height = next_row_height.max(h);
3753
3754                    // Track maximum atlas region used for clearing next frame
3755                    atlas_max_x = atlas_max_x.max(atlas_cursor_x + w);
3756                    atlas_max_y = atlas_max_y.max(atlas_cursor_y + h);
3757
3758                    queue.write_texture(
3759                        wgpu::ImageCopyTexture {
3760                            texture: &self.text_mask_atlas,
3761                            mip_level: 0,
3762                            origin: wgpu::Origin3d {
3763                                x: atlas_cursor_x,
3764                                y: atlas_cursor_y,
3765                                z: 0,
3766                            },
3767                            aspect: wgpu::TextureAspect::All,
3768                        },
3769                        data.as_ref(),
3770                        wgpu::ImageDataLayout {
3771                            offset: 0,
3772                            bytes_per_row: Some(w * 4),
3773                            rows_per_image: Some(h),
3774                        },
3775                        wgpu::Extent3d {
3776                            width: w,
3777                            height: h,
3778                            depth_or_array_layers: 1,
3779                        },
3780                    );
3781
3782                    let u0 = atlas_cursor_x as f32 / 4096.0;
3783                    let v0 = atlas_cursor_y as f32 / 4096.0;
3784                    let u1 = (atlas_cursor_x + w) as f32 / 4096.0;
3785                    let v1 = (atlas_cursor_y + h) as f32 / 4096.0;
3786
3787                    // Convert glyph bitmap size from physical pixels into logical
3788                    // scene units so that the viewport scale maps them back to
3789                    // physical pixels without additional filtering.
3790                    let quad_w = (w as f32) * inv_logical;
3791                    let quad_h = (h as f32) * inv_logical;
3792
3793                    vertices.extend_from_slice(&[
3794                        TextQuadVtx {
3795                            pos: [origin[0], origin[1]],
3796                            uv: [u0, v0],
3797                            color: [color.r, color.g, color.b, color.a],
3798                        },
3799                        TextQuadVtx {
3800                            pos: [origin[0] + quad_w, origin[1]],
3801                            uv: [u1, v0],
3802                            color: [color.r, color.g, color.b, color.a],
3803                        },
3804                        TextQuadVtx {
3805                            pos: [origin[0] + quad_w, origin[1] + quad_h],
3806                            uv: [u1, v1],
3807                            color: [color.r, color.g, color.b, color.a],
3808                        },
3809                        TextQuadVtx {
3810                            pos: [origin[0], origin[1] + quad_h],
3811                            uv: [u0, v1],
3812                            color: [color.r, color.g, color.b, color.a],
3813                        },
3814                    ]);
3815
3816                    atlas_cursor_x += w;
3817                }
3818
3819                // Store vertices for this z-index group
3820                if !vertices.is_empty() {
3821                    all_text_groups.push((*z_index, vertices));
3822                }
3823            }
3824
3825            // Create buffers and bind groups for each text group
3826            let mut text_resources: Vec<(
3827                i32,
3828                wgpu::Buffer,
3829                wgpu::Buffer,
3830                u32,
3831                wgpu::BindGroup,
3832                wgpu::Buffer,
3833            )> = Vec::new();
3834            for (z_index, vertices) in all_text_groups {
3835                let quad_count = vertices.len() / 4;
3836                let mut indices: Vec<u16> = Vec::with_capacity(quad_count * 6);
3837                for i in 0..quad_count {
3838                    let base = (i * 4) as u16;
3839                    indices.extend_from_slice(&[
3840                        base,
3841                        base + 1,
3842                        base + 2,
3843                        base,
3844                        base + 2,
3845                        base + 3,
3846                    ]);
3847                }
3848
3849                // Create vertex buffer for this group
3850                let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3851                    label: Some("text-vertex-buffer-group-off"),
3852                    size: (vertices.len() * std::mem::size_of::<TextQuadVtx>()) as u64,
3853                    usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3854                    mapped_at_creation: false,
3855                });
3856
3857                // Create index buffer for this group
3858                let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3859                    label: Some("text-index-buffer-group-off"),
3860                    size: (indices.len() * std::mem::size_of::<u16>()) as u64,
3861                    usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3862                    mapped_at_creation: false,
3863                });
3864
3865                queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&vertices));
3866                queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&indices));
3867
3868                // Create z bind group for this text group
3869                // Pass z_index as float directly - shader will convert to depth
3870                let (z_bg, z_buf) = self.create_group_z_bind_group(z_index as f32, queue);
3871
3872                text_resources.push((z_index, vbuf, ibuf, indices.len() as u32, z_bg, z_buf));
3873            }
3874
3875            // Store atlas usage for next frame's clearing
3876            self.prev_atlas_max_x = atlas_max_x;
3877            self.prev_atlas_max_y = atlas_max_y;
3878
3879            text_resources
3880        } else {
3881            Vec::new()
3882        };
3883
3884        // Sort text groups by z-index (back to front)
3885        text_groups_off.sort_by_key(|(z, _, _, _, _, _)| *z);
3886
3887        // Create text bind groups (use offscreen text renderer for offscreen rendering)
3888        let vp_bg_text_off = self
3889            .text_offscreen
3890            .vp_bind_group(&self.device, &self.vp_buffer);
3891
3892        // Prepare image resources (offscreen: use image_offscreen to match format)
3893        let mut image_z_vals_off: Vec<i32> = Vec::new();
3894        let mut image_resources_off: Vec<(
3895            wgpu::Buffer,
3896            wgpu::Buffer,
3897            wgpu::BindGroup,
3898            wgpu::BindGroup,
3899            wgpu::BindGroup,
3900            wgpu::BindGroup,
3901            wgpu::Buffer,
3902            wgpu::Buffer,
3903            Option<crate::Rect>,
3904        )> = Vec::new();
3905        for (tex_view, origin, size, z_val, clip) in image_views_off.iter() {
3906            let verts = [
3907                ImageQuadVtx {
3908                    pos: [origin[0], origin[1]],
3909                    uv: [0.0, 0.0],
3910                },
3911                ImageQuadVtx {
3912                    pos: [origin[0] + size[0], origin[1]],
3913                    uv: [1.0, 0.0],
3914                },
3915                ImageQuadVtx {
3916                    pos: [origin[0] + size[0], origin[1] + size[1]],
3917                    uv: [1.0, 1.0],
3918                },
3919                ImageQuadVtx {
3920                    pos: [origin[0], origin[1] + size[1]],
3921                    uv: [0.0, 1.0],
3922                },
3923            ];
3924            let idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
3925
3926            let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3927                label: Some("image-vbuf-unified-offscreen"),
3928                size: (verts.len() * std::mem::size_of::<ImageQuadVtx>()) as u64,
3929                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3930                mapped_at_creation: false,
3931            });
3932            let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3933                label: Some("image-ibuf-unified-offscreen"),
3934                size: (idx.len() * std::mem::size_of::<u16>()) as u64,
3935                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3936                mapped_at_creation: false,
3937            });
3938            queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
3939            queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&idx));
3940
3941            let vp_bg_img = self
3942                .image_offscreen
3943                .vp_bind_group(&self.device, &self.vp_buffer);
3944            // Pass z_index as float directly - shader will convert to depth
3945            let (z_bg_img, z_buf_img) = self.create_group_z_bind_group(*z_val as f32, queue);
3946            let tex_bg = self.image_offscreen.tex_bind_group(&self.device, tex_view);
3947            let (params_bg, params_buf) =
3948                self.image_offscreen
3949                    .params_bind_group(&self.device, 1.0, false);
3950
3951            image_z_vals_off.push(*z_val as i32);
3952            image_resources_off.push((
3953                vbuf, ibuf, vp_bg_img, z_bg_img, tex_bg, params_bg, z_buf_img, params_buf, *clip,
3954            ));
3955        }
3956
3957        // Prepare SVG resources (offscreen: use image_offscreen to match format)
3958        let mut svg_z_vals_off: Vec<i32> = Vec::new();
3959        let mut svg_resources_off: Vec<(
3960            wgpu::Buffer,
3961            wgpu::Buffer,
3962            wgpu::BindGroup,
3963            wgpu::BindGroup,
3964            wgpu::BindGroup,
3965            wgpu::BindGroup,
3966            wgpu::Buffer,
3967            wgpu::Buffer,
3968            Option<crate::Rect>,
3969        )> = Vec::new();
3970        for (view_scaled, origin, size, z_val, clip) in svg_views_off.iter() {
3971            let verts = [
3972                ImageQuadVtx {
3973                    pos: [origin[0], origin[1]],
3974                    uv: [0.0, 0.0],
3975                },
3976                ImageQuadVtx {
3977                    pos: [origin[0] + size[0], origin[1]],
3978                    uv: [1.0, 0.0],
3979                },
3980                ImageQuadVtx {
3981                    pos: [origin[0] + size[0], origin[1] + size[1]],
3982                    uv: [1.0, 1.0],
3983                },
3984                ImageQuadVtx {
3985                    pos: [origin[0], origin[1] + size[1]],
3986                    uv: [0.0, 1.0],
3987                },
3988            ];
3989            let idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
3990
3991            let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3992                label: Some("svg-vbuf-unified-offscreen"),
3993                size: (verts.len() * std::mem::size_of::<ImageQuadVtx>()) as u64,
3994                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3995                mapped_at_creation: false,
3996            });
3997            let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
3998                label: Some("svg-ibuf-unified-offscreen"),
3999                size: (idx.len() * std::mem::size_of::<u16>()) as u64,
4000                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
4001                mapped_at_creation: false,
4002            });
4003            queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
4004            queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&idx));
4005
4006            let vp_bg_svg = self
4007                .image_offscreen
4008                .vp_bind_group(&self.device, &self.vp_buffer);
4009            // Pass z_index as float directly - shader will convert to depth
4010            let (z_bg_svg, z_buf_svg) = self.create_group_z_bind_group(*z_val as f32, queue);
4011            let tex_bg = self
4012                .image_offscreen
4013                .tex_bind_group(&self.device, view_scaled);
4014            let (params_bg, params_buf) =
4015                self.image_offscreen
4016                    .params_bind_group(&self.device, 1.0, false);
4017
4018            svg_z_vals_off.push(*z_val as i32);
4019            svg_resources_off.push((
4020                vbuf, ibuf, vp_bg_svg, z_bg_svg, tex_bg, params_bg, z_buf_svg, params_buf, *clip,
4021            ));
4022        }
4023
4024        // Prepare external texture resources (offscreen: use image_offscreen to match format)
4025        let mut ext_z_vals_off: Vec<i32> = Vec::new();
4026        let mut ext_resources_off: Vec<(
4027            wgpu::Buffer,
4028            wgpu::Buffer,
4029            wgpu::BindGroup,
4030            wgpu::BindGroup,
4031            wgpu::BindGroup,
4032            wgpu::BindGroup,
4033            wgpu::Buffer,
4034            wgpu::Buffer,
4035        )> = Vec::new();
4036        for etd in external_texture_draws.iter() {
4037            let Some(tex_view) = self.external_textures.get(&etd.texture_id) else {
4038                continue;
4039            };
4040            let verts = [
4041                ImageQuadVtx {
4042                    pos: [etd.origin[0], etd.origin[1]],
4043                    uv: [0.0, 0.0],
4044                },
4045                ImageQuadVtx {
4046                    pos: [etd.origin[0] + etd.size[0], etd.origin[1]],
4047                    uv: [1.0, 0.0],
4048                },
4049                ImageQuadVtx {
4050                    pos: [etd.origin[0] + etd.size[0], etd.origin[1] + etd.size[1]],
4051                    uv: [1.0, 1.0],
4052                },
4053                ImageQuadVtx {
4054                    pos: [etd.origin[0], etd.origin[1] + etd.size[1]],
4055                    uv: [0.0, 1.0],
4056                },
4057            ];
4058            let idx: [u16; 6] = [0, 1, 2, 0, 2, 3];
4059
4060            let vbuf = self.device.create_buffer(&wgpu::BufferDescriptor {
4061                label: Some("ext-tex-vbuf-unified-offscreen"),
4062                size: (verts.len() * std::mem::size_of::<ImageQuadVtx>()) as u64,
4063                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
4064                mapped_at_creation: false,
4065            });
4066            let ibuf = self.device.create_buffer(&wgpu::BufferDescriptor {
4067                label: Some("ext-tex-ibuf-unified-offscreen"),
4068                size: (idx.len() * std::mem::size_of::<u16>()) as u64,
4069                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
4070                mapped_at_creation: false,
4071            });
4072            queue.write_buffer(&vbuf, 0, bytemuck::cast_slice(&verts));
4073            queue.write_buffer(&ibuf, 0, bytemuck::cast_slice(&idx));
4074
4075            let vp_bg_ext = self
4076                .image_offscreen
4077                .vp_bind_group(&self.device, &self.vp_buffer);
4078            let (z_bg_ext, z_buf_ext) = self.create_group_z_bind_group(etd.z as f32, queue);
4079            let tex_bg = self.image_offscreen.tex_bind_group(&self.device, tex_view);
4080            let (params_bg, params_buf) = self.image_offscreen.params_bind_group(
4081                &self.device,
4082                etd.opacity,
4083                etd.premultiplied,
4084            );
4085
4086            ext_z_vals_off.push(etd.z);
4087            ext_resources_off.push((
4088                vbuf, ibuf, vp_bg_ext, z_bg_ext, tex_bg, params_bg, z_buf_ext, params_buf,
4089            ));
4090        }
4091
4092        let depth_attachment = Some(wgpu::RenderPassDepthStencilAttachment {
4093            view: self.depth_view(),
4094            depth_ops: Some(wgpu::Operations {
4095                load: wgpu::LoadOp::Clear(1.0),
4096                store: wgpu::StoreOp::Store,
4097            }),
4098            stencil_ops: None,
4099        });
4100
4101        let _z_bg = self.create_z_bind_group(0.0, queue);
4102
4103        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
4104            label: Some("unified-offscreen-pass"),
4105            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
4106                view: &targets.color.view,
4107                resolve_target: None,
4108                ops: wgpu::Operations {
4109                    load: wgpu::LoadOp::Clear(clear),
4110                    store: wgpu::StoreOp::Store,
4111                },
4112            })],
4113            depth_stencil_attachment: depth_attachment,
4114            occlusion_query_set: None,
4115            timestamp_writes: None,
4116        });
4117
4118        // Render solids first
4119        // eprintln!("🟢 OFFSCREEN PATH: Rendering {} solid vertices", scene.vertices);
4120        self.solid_offscreen.record(&mut pass, &vp_bg_off, scene);
4121
4122        // Unified z-sorted rendering (offscreen path): interleave ALL draw types
4123        // by z-index for correct depth ordering across element types.
4124        {
4125            #[derive(Clone, Copy)]
4126            enum DrawItemOff {
4127                TransparentBatch(usize),
4128                TextGroup(usize),
4129                Image(usize),
4130                Svg(usize),
4131                ExternalTexture(usize),
4132            }
4133            let mut all_items: Vec<(i32, DrawItemOff)> = Vec::new();
4134            for (i, batch) in transparent_batches.iter().enumerate() {
4135                all_items.push((batch.z, DrawItemOff::TransparentBatch(i)));
4136            }
4137            for (i, (z, _, _, _, _, _)) in text_groups_off.iter().enumerate() {
4138                all_items.push((*z, DrawItemOff::TextGroup(i)));
4139            }
4140            for (i, z) in image_z_vals_off.iter().enumerate() {
4141                all_items.push((*z, DrawItemOff::Image(i)));
4142            }
4143            for (i, z) in svg_z_vals_off.iter().enumerate() {
4144                all_items.push((*z, DrawItemOff::Svg(i)));
4145            }
4146            for (i, z) in ext_z_vals_off.iter().enumerate() {
4147                all_items.push((*z, DrawItemOff::ExternalTexture(i)));
4148            }
4149            all_items.sort_by_key(|(z, _)| *z);
4150
4151            for &(_, item) in all_items.iter() {
4152                match item {
4153                    DrawItemOff::TransparentBatch(i) => {
4154                        let batch = &transparent_batches[i];
4155                        self.transparent_solid_offscreen.record_index_range(
4156                            &mut pass,
4157                            &vp_bg_off,
4158                            transparent_scene,
4159                            batch.index_start,
4160                            batch.index_count,
4161                        );
4162                    }
4163                    DrawItemOff::TextGroup(i) => {
4164                        let (_z, vbuf, ibuf, index_count, z_bg, _z_buf) = &text_groups_off[i];
4165                        if *index_count > 0 {
4166                            pass.set_pipeline(&self.text_offscreen.pipeline);
4167                            pass.set_bind_group(0, &vp_bg_text_off, &[]);
4168                            pass.set_bind_group(1, z_bg, &[]);
4169                            pass.set_bind_group(2, &self.text_bind_group, &[]);
4170                            pass.set_vertex_buffer(0, vbuf.slice(..));
4171                            pass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint16);
4172                            pass.draw_indexed(0..*index_count, 0, 0..1);
4173                        }
4174                    }
4175                    DrawItemOff::Image(i) => {
4176                        let (vbuf, ibuf, vp_bg_img, z_bg_img, tex_bg, params_bg, _, _, clip) =
4177                            &image_resources_off[i];
4178                        if let Some(c) = clip {
4179                            pass.set_scissor_rect(
4180                                c.x.max(0.0) as u32,
4181                                c.y.max(0.0) as u32,
4182                                (c.w.max(1.0) as u32)
4183                                    .min(width.saturating_sub(c.x.max(0.0) as u32)),
4184                                (c.h.max(1.0) as u32)
4185                                    .min(height.saturating_sub(c.y.max(0.0) as u32)),
4186                            );
4187                        }
4188                        self.image_offscreen.record(
4189                            &mut pass, vp_bg_img, z_bg_img, tex_bg, params_bg, vbuf, ibuf, 6,
4190                        );
4191                        if clip.is_some() {
4192                            pass.set_scissor_rect(0, 0, width, height);
4193                        }
4194                    }
4195                    DrawItemOff::Svg(i) => {
4196                        let (vbuf, ibuf, vp_bg_svg, z_bg_svg, tex_bg, params_bg, _, _, clip) =
4197                            &svg_resources_off[i];
4198                        if let Some(c) = clip {
4199                            pass.set_scissor_rect(
4200                                c.x.max(0.0) as u32,
4201                                c.y.max(0.0) as u32,
4202                                (c.w.max(1.0) as u32)
4203                                    .min(width.saturating_sub(c.x.max(0.0) as u32)),
4204                                (c.h.max(1.0) as u32)
4205                                    .min(height.saturating_sub(c.y.max(0.0) as u32)),
4206                            );
4207                        }
4208                        self.image_offscreen.record(
4209                            &mut pass, vp_bg_svg, z_bg_svg, tex_bg, params_bg, vbuf, ibuf, 6,
4210                        );
4211                        if clip.is_some() {
4212                            pass.set_scissor_rect(0, 0, width, height);
4213                        }
4214                    }
4215                    DrawItemOff::ExternalTexture(i) => {
4216                        let (vbuf, ibuf, vp_bg_ext, z_bg_ext, tex_bg, params_bg, _, _) =
4217                            &ext_resources_off[i];
4218                        self.image_offscreen.record(
4219                            &mut pass, vp_bg_ext, z_bg_ext, tex_bg, params_bg, vbuf, ibuf, 6,
4220                        );
4221                    }
4222                }
4223            }
4224        }
4225
4226        // Drop the pass to complete offscreen rendering
4227        drop(pass);
4228
4229        // Composite offscreen target to surface
4230        self.composite_to_surface(encoder, surface_view, &targets, Some(clear));
4231        allocator.release_texture(targets.color);
4232    }
4233}