ailloli_ui_render_wgpu/
pipeline_cache.rs

1//! Cached render pipelines, GPU bootstrap policy, and presentation-surface state.
2//!
3//! This module separates reusable device-bound resources from the native
4//! surface attachment so a host can detach and later reattach a window.
5
6use std::panic::{catch_unwind, AssertUnwindSafe};
7
8use crate::error::RendererError;
9use crate::render_target::{PhysicalExtent, RenderFrame, RenderTarget};
10use crate::vertices::{
11    BorderRRectVertex, BoxShadowVertex, RRectVertex, RingProgressVertex, StrokeVertex, TexVertex,
12    Vertex,
13};
14
15/// Why surface configuration was deferred during resize/bootstrap.
16///
17/// # Examples
18///
19/// ```
20/// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceConfigDeferredReason;
21///
22/// let reason = SurfaceConfigDeferredReason::NoPresentModes;
23/// assert_eq!(reason.as_str(), "surface capabilities reported no present modes");
24/// ```
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum SurfaceConfigDeferredReason {
27    /// No native surface is currently attached to the reusable GPU context.
28    Detached,
29    /// The surface currently advertises no usable texture formats.
30    NoFormats,
31    /// The surface currently advertises no concrete presentation modes.
32    NoPresentModes,
33}
34
35impl SurfaceConfigDeferredReason {
36    /// Returns a stable diagnostic phrase for the deferred condition.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceConfigDeferredReason;
42    ///
43    /// assert_eq!(
44    ///     SurfaceConfigDeferredReason::NoFormats.as_str(),
45    ///     "surface capabilities reported no formats"
46    /// );
47    /// ```
48    pub fn as_str(self) -> &'static str {
49        match self {
50            Self::Detached => "presentation surface is detached",
51            Self::NoFormats => "surface capabilities reported no formats",
52            Self::NoPresentModes => "surface capabilities reported no present modes",
53        }
54    }
55}
56
57/// Whether a reusable surface renderer currently owns a native attachment.
58///
59/// # Examples
60///
61/// ```
62/// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceAttachmentState;
63///
64/// assert_ne!(SurfaceAttachmentState::Attached, SurfaceAttachmentState::Detached);
65/// ```
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum SurfaceAttachmentState {
68    /// A native presentation surface is available.
69    Attached,
70    /// The GPU context is retained without a presentation surface.
71    Detached,
72}
73
74/// Why a newly-created surface could not reuse the current GPU context.
75///
76/// These conditions are not fatal by themselves: reattachment falls back to
77/// selecting another adapter and rebuilding device-bound renderer resources.
78///
79/// # Examples
80///
81/// ```
82/// use ailloli_ui_render_wgpu::pipeline_cache::{
83///     SurfaceConfigDeferredReason, SurfaceContextReuseFailure,
84/// };
85///
86/// let failure = SurfaceContextReuseFailure::CapabilitiesDeferred(
87///     SurfaceConfigDeferredReason::NoFormats,
88/// );
89/// assert!(matches!(failure, SurfaceContextReuseFailure::CapabilitiesDeferred(_)));
90/// ```
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92#[non_exhaustive]
93pub enum SurfaceContextReuseFailure {
94    /// The retained adapter cannot present to the new surface.
95    AdapterUnsupported,
96    /// The new surface does not yet advertise usable capabilities.
97    CapabilitiesDeferred(SurfaceConfigDeferredReason),
98    /// The surface cannot use the format baked into the retained pipelines.
99    FormatUnsupported,
100    /// `wgpu` rejected or panicked while configuring the surface.
101    ConfigureFailed,
102}
103
104/// Result of attaching a new native surface to an existing renderer.
105///
106/// # Examples
107///
108/// ```
109/// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceReattachOutcome;
110///
111/// let outcome = SurfaceReattachOutcome::ReusedGpuContext;
112/// assert!(matches!(outcome, SurfaceReattachOutcome::ReusedGpuContext));
113/// ```
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115#[non_exhaustive]
116pub enum SurfaceReattachOutcome {
117    /// Instance, adapter, device, queue, pipelines, atlases, and caches were reused.
118    ReusedGpuContext,
119    /// The retained adapter was incompatible, so the GPU context was rebuilt.
120    RebuiltGpuContext {
121        /// The incompatibility that required rebuilding device-bound resources.
122        reason: SurfaceContextReuseFailure,
123    },
124}
125
126/// Optional native attachment with explicit detach/reattach transitions.
127#[derive(Debug)]
128struct SurfaceAttachmentSlot<T> {
129    /// Current attachment value, absent while detached or after extraction.
130    value: Option<T>,
131}
132
133impl<T> SurfaceAttachmentSlot<T> {
134    /// Creates a slot containing `value`.
135    fn attached(value: T) -> Self {
136        Self { value: Some(value) }
137    }
138
139    /// Reports whether the slot currently contains an attachment.
140    fn state(&self) -> SurfaceAttachmentState {
141        if self.value.is_some() {
142            SurfaceAttachmentState::Attached
143        } else {
144            SurfaceAttachmentState::Detached
145        }
146    }
147
148    /// Borrows the attachment, returning `None` while detached.
149    fn as_ref(&self) -> Option<&T> {
150        self.value.as_ref()
151    }
152
153    /// Mutably borrows the attachment, returning `None` while detached.
154    fn as_mut(&mut self) -> Option<&mut T> {
155        self.value.as_mut()
156    }
157
158    /// Removes and returns the current attachment, if any.
159    fn detach(&mut self) -> Option<T> {
160        self.value.take()
161    }
162
163    /// Installs an attachment and returns the value it replaced, if any.
164    fn attach(&mut self, value: T) -> Option<T> {
165        self.value.replace(value)
166    }
167}
168
169/// Result of applying a pending surface resize.
170///
171/// # Examples
172///
173/// ```
174/// use ailloli_ui_render_wgpu::pipeline_cache::ResizeOutcome;
175///
176/// assert_eq!(ResizeOutcome::SkippedZero, ResizeOutcome::SkippedZero);
177/// ```
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum ResizeOutcome {
180    /// The target configuration was changed and applied.
181    Applied,
182    /// The requested extent already matched the current extent.
183    Unchanged,
184    /// Configuration was skipped because either physical dimension was zero.
185    SkippedZero,
186    /// The surface temporarily lacked the capabilities needed to configure it.
187    Deferred(SurfaceConfigDeferredReason),
188}
189
190/// Whether equal extents may bypass native surface configuration.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192enum SurfaceConfigureMode {
193    /// Configure only when the requested extent changed.
194    Resize,
195    /// Configure even when the requested extent is unchanged.
196    Force,
197}
198
199/// Returns whether `requested` requires configuration under `mode`.
200fn surface_configure_required(
201    current: PhysicalExtent,
202    requested: PhysicalExtent,
203    mode: SurfaceConfigureMode,
204) -> bool {
205    mode == SurfaceConfigureMode::Force || current != requested
206}
207
208/// GPU bundle for rendering when a presentation surface is managed externally.
209///
210/// This keeps the renderer core device/queue/pipelines/configuration decoupled
211/// from swapchain ownership, and is intended for host integrations (OpenXR,
212/// custom swapchains, tests with mock targets).
213///
214/// # Examples
215///
216/// ```
217/// use ailloli_ui_render_wgpu::pipeline_cache::WgpuRenderContext;
218///
219/// assert!(std::mem::size_of::<WgpuRenderContext>() > 0);
220/// ```
221#[derive(Debug)]
222pub struct WgpuRenderContext {
223    /// Logical device used to allocate and encode renderer resources.
224    pub device: wgpu::Device,
225    /// Submission queue paired with [`Self::device`].
226    pub queue: wgpu::Queue,
227    /// Virtual target configuration; width and height are always at least one.
228    pub config: wgpu::SurfaceConfiguration,
229    /// Pipelines compiled for [`wgpu::SurfaceConfiguration::format`].
230    pub pipelines: PipelineCache,
231    /// Whether target composition requests a non-opaque alpha mode.
232    pub transparent: bool,
233}
234
235impl WgpuRenderContext {
236    /// Builds a detached rendering context from an existing device/queue pair.
237    ///
238    /// Zero dimensions are clamped to one physical pixel. The detached config
239    /// uses FIFO presentation metadata and a maximum frame latency of one even
240    /// though it owns no swapchain.
241    ///
242    /// # Panics
243    ///
244    /// May panic if `wgpu` rejects shader or pipeline creation for `device`.
245    ///
246    /// # Examples
247    ///
248    /// ```no_run
249    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuRenderContext;
250    ///
251    /// fn make(device: wgpu::Device, queue: wgpu::Queue) -> WgpuRenderContext {
252    ///     WgpuRenderContext::new_with_size(
253    ///         device,
254    ///         queue,
255    ///         wgpu::TextureFormat::Bgra8UnormSrgb,
256    ///         1280,
257    ///         720,
258    ///         false,
259    ///     )
260    /// }
261    /// ```
262    pub fn new_with_size(
263        device: wgpu::Device,
264        queue: wgpu::Queue,
265        format: wgpu::TextureFormat,
266        width: u32,
267        height: u32,
268        transparent: bool,
269    ) -> Self {
270        let config = wgpu::SurfaceConfiguration {
271            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
272            format,
273            width: width.max(1),
274            height: height.max(1),
275            present_mode: wgpu::PresentMode::Fifo,
276            alpha_mode: if transparent {
277                wgpu::CompositeAlphaMode::PreMultiplied
278            } else {
279                wgpu::CompositeAlphaMode::Opaque
280            },
281            view_formats: vec![],
282            desired_maximum_frame_latency: 1,
283        };
284        let pipelines = PipelineCache::new(&device, format);
285
286        Self {
287            device,
288            queue,
289            config,
290            pipelines,
291            transparent,
292        }
293    }
294
295    /// Resize the virtual config (no swapchain reconfigure side effects).
296    ///
297    /// Zero-sized requests leave the configuration unchanged. Nonzero extents
298    /// are expressed in physical pixels.
299    ///
300    /// # Examples
301    ///
302    /// ```no_run
303    /// use ailloli_ui_render_wgpu::{
304    ///     pipeline_cache::{ResizeOutcome, WgpuRenderContext},
305    ///     PhysicalExtent,
306    /// };
307    ///
308    /// fn resize(context: &mut WgpuRenderContext) {
309    ///     assert_eq!(
310    ///         context.try_resize(PhysicalExtent::new(1920, 1080)),
311    ///         ResizeOutcome::Applied
312    ///     );
313    /// }
314    /// ```
315    pub fn try_resize(&mut self, new_size: PhysicalExtent) -> ResizeOutcome {
316        if new_size.width == 0 || new_size.height == 0 {
317            return ResizeOutcome::SkippedZero;
318        }
319        if self.config.width == new_size.width && self.config.height == new_size.height {
320            return ResizeOutcome::Unchanged;
321        }
322        self.config.width = new_size.width.max(1);
323        self.config.height = new_size.height.max(1);
324        ResizeOutcome::Applied
325    }
326
327    /// No-op equivalent to `Surface::pre_present_notify` for detached contexts.
328    ///
329    /// # Examples
330    ///
331    /// ```no_run
332    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuRenderContext;
333    ///
334    /// fn notify(context: &WgpuRenderContext) {
335    ///     context.pre_present_notify();
336    /// }
337    /// ```
338    pub fn pre_present_notify(&self) {}
339
340    /// Returns `None` because this context has no managed presentation surface.
341    ///
342    /// # Examples
343    ///
344    /// ```no_run
345    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuRenderContext;
346    ///
347    /// fn is_ready(context: &WgpuRenderContext) -> bool {
348    ///     context.surface_config_deferred_reason().is_none()
349    /// }
350    /// ```
351    pub fn surface_config_deferred_reason(&self) -> Option<SurfaceConfigDeferredReason> {
352        None
353    }
354}
355
356/// Configuration used when bootstrapping GPU instances/adapters.
357///
358/// Defaults request all backends, prefer primary backends and high performance,
359/// allow backend fallback, and do not force a software adapter.
360///
361/// # Examples
362///
363/// ```
364/// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceBootstrapConfig;
365///
366/// let config = SurfaceBootstrapConfig::default();
367/// assert_eq!(config.requested_backends, wgpu::Backends::all());
368/// assert!(config.allow_fallback_backends);
369/// ```
370#[derive(Debug, Clone, Copy)]
371pub struct SurfaceBootstrapConfig {
372    /// Backends to request explicitly from wgpu.
373    pub requested_backends: wgpu::Backends,
374    /// Allow falling back to non-requested backends.
375    pub allow_fallback_backends: bool,
376    /// Preferred backend for ranking candidates.
377    pub preferred_backends: wgpu::Backends,
378    /// Power preference used when requesting devices.
379    pub power_preference: wgpu::PowerPreference,
380    /// Whether to request a fallback adapter if the preferred backend cannot be opened.
381    pub force_fallback_adapter: bool,
382}
383
384impl Default for SurfaceBootstrapConfig {
385    fn default() -> Self {
386        Self {
387            requested_backends: wgpu::Backends::all(),
388            allow_fallback_backends: true,
389            preferred_backends: wgpu::Backends::PRIMARY,
390            power_preference: wgpu::PowerPreference::HighPerformance,
391            force_fallback_adapter: false,
392        }
393    }
394}
395
396impl SurfaceBootstrapConfig {
397    /// Explicit backend selection by environment (no fallback to non-requested backends).
398    ///
399    /// # Examples
400    ///
401    /// ```
402    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceBootstrapConfig;
403    ///
404    /// let config = SurfaceBootstrapConfig::with_requested_backends(wgpu::Backends::GL);
405    /// assert_eq!(config.requested_backends, wgpu::Backends::GL);
406    /// assert!(!config.allow_fallback_backends);
407    /// ```
408    pub fn with_requested_backends(requested: wgpu::Backends) -> Self {
409        Self {
410            requested_backends: requested,
411            preferred_backends: requested,
412            allow_fallback_backends: false,
413            ..Self::default()
414        }
415    }
416
417    /// Vulkan-first bootstrap with fallback to other available backends.
418    ///
419    /// # Examples
420    ///
421    /// ```
422    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceBootstrapConfig;
423    ///
424    /// let config = SurfaceBootstrapConfig::vulkan_first();
425    /// assert_eq!(config.requested_backends, wgpu::Backends::VULKAN);
426    /// assert!(config.allow_fallback_backends);
427    /// ```
428    pub fn vulkan_first() -> Self {
429        Self {
430            requested_backends: wgpu::Backends::VULKAN,
431            allow_fallback_backends: true,
432            preferred_backends: wgpu::Backends::VULKAN,
433            ..Self::default()
434        }
435    }
436
437    /// Vulkan-only bootstrap (strict, no fallback).
438    ///
439    /// # Examples
440    ///
441    /// ```
442    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceBootstrapConfig;
443    ///
444    /// let config = SurfaceBootstrapConfig::vulkan_only();
445    /// assert_eq!(config.preferred_backends, wgpu::Backends::VULKAN);
446    /// assert!(!config.allow_fallback_backends);
447    /// ```
448    pub fn vulkan_only() -> Self {
449        Self {
450            requested_backends: wgpu::Backends::VULKAN,
451            allow_fallback_backends: false,
452            preferred_backends: wgpu::Backends::VULKAN,
453            ..Self::default()
454        }
455    }
456
457    /// Returns whether `backend` belongs to the preferred backend set.
458    fn preferred_backend_matches(&self, backend: wgpu::Backend) -> bool {
459        self.preferred_backends.contains(backend_to_flags(backend))
460    }
461}
462
463/// Converts one adapter backend into its corresponding backend bit flag.
464fn backend_to_flags(backend: wgpu::Backend) -> wgpu::Backends {
465    match backend {
466        wgpu::Backend::Empty => wgpu::Backends::empty(),
467        wgpu::Backend::Vulkan => wgpu::Backends::VULKAN,
468        wgpu::Backend::Metal => wgpu::Backends::METAL,
469        wgpu::Backend::Dx12 => wgpu::Backends::DX12,
470        wgpu::Backend::Gl => wgpu::Backends::GL,
471        wgpu::Backend::BrowserWebGpu => wgpu::Backends::BROWSER_WEBGPU,
472    }
473}
474
475/// Computes the stable `(device, backend, name)` adapter selection key.
476fn bootstrap_adapter_rank(
477    adapter_info: &wgpu::AdapterInfo,
478    cfg: &SurfaceBootstrapConfig,
479) -> (u8, u8, &'static str) {
480    let device_type_rank = adapter_bootstrap_rank(adapter_info.device_type);
481    let backend_rank = if cfg.preferred_backend_matches(adapter_info.backend) {
482        0
483    } else {
484        1
485    };
486    let backend_name = adapter_info.backend.to_str();
487    (device_type_rank, backend_rank, backend_name)
488}
489
490impl ResizeOutcome {
491    /// Returns `true` only for [`Self::Deferred`].
492    ///
493    /// # Examples
494    ///
495    /// ```
496    /// use ailloli_ui_render_wgpu::pipeline_cache::{
497    ///     ResizeOutcome, SurfaceConfigDeferredReason,
498    /// };
499    ///
500    /// assert!(ResizeOutcome::Deferred(SurfaceConfigDeferredReason::NoFormats).is_deferred());
501    /// assert!(!ResizeOutcome::Applied.is_deferred());
502    /// ```
503    pub fn is_deferred(self) -> bool {
504        matches!(self, Self::Deferred(_))
505    }
506}
507
508/// Cached wgpu render pipelines and shared bind group layouts.
509///
510/// A cache is bound to one device and one color target format. It contains
511/// plain, stencil-compatible, stencil-tested, and mask/edge variants used by
512/// planned batches.
513///
514/// # Examples
515///
516/// ```
517/// use ailloli_ui_render_wgpu::pipeline_cache::PipelineCache;
518///
519/// assert!(std::mem::size_of::<PipelineCache>() > 0);
520/// ```
521#[derive(Debug)]
522pub struct PipelineCache {
523    /// Solid rectangle pipeline without a stencil attachment.
524    pub rect: wgpu::RenderPipeline,
525    /// Polyline stroke pipeline without a stencil attachment.
526    pub stroke: wgpu::RenderPipeline,
527    /// Sampled-texture pipeline without a stencil attachment.
528    pub textured: wgpu::RenderPipeline,
529    /// Antialiased rounded-rectangle fill pipeline without stencil.
530    pub rounded_rect: wgpu::RenderPipeline,
531    /// Rounded-rectangle border pipeline without stencil.
532    pub border_rounded_rect: wgpu::RenderPipeline,
533    /// Analytic box-shadow pipeline without stencil.
534    pub box_shadow: wgpu::RenderPipeline,
535    /// Analytic circular progress-ring pipeline without stencil.
536    pub ring_progress: wgpu::RenderPipeline,
537    /// single-pass compositing: same as `rect`, but with a stencil-compatible depth_stencil
538    /// (compare=Always, op=Keep). Used inside a single `RenderPass` that has a
539    /// stencil attachment, for layers that are NOT in stencil mode.
540    pub rect_passthrough_stencil: wgpu::RenderPipeline,
541    /// Stroke pipeline compatible with, but not constrained by, attached stencil.
542    pub stroke_passthrough_stencil: wgpu::RenderPipeline,
543    /// Textured pipeline compatible with, but not constrained by, attached stencil.
544    pub textured_passthrough_stencil: wgpu::RenderPipeline,
545    /// Rounded fill pipeline compatible with, but not constrained by, attached stencil.
546    pub rounded_rect_passthrough_stencil: wgpu::RenderPipeline,
547    /// Rounded border pipeline compatible with, but not constrained by, attached stencil.
548    pub border_rounded_rect_passthrough_stencil: wgpu::RenderPipeline,
549    /// Box-shadow pipeline compatible with, but not constrained by, attached stencil.
550    pub box_shadow_passthrough_stencil: wgpu::RenderPipeline,
551    /// Progress-ring pipeline compatible with, but not constrained by, attached stencil.
552    pub ring_progress_passthrough_stencil: wgpu::RenderPipeline,
553    /// Writes only to the stencil buffer (rounded mask).
554    pub rounded_rect_stencil_mask: wgpu::RenderPipeline,
555    /// Solid rectangle pipeline restricted to the active stencil mask.
556    pub rect_stencil: wgpu::RenderPipeline,
557    /// Stroke pipeline restricted to the active stencil mask.
558    pub stroke_stencil: wgpu::RenderPipeline,
559    /// Textured pipeline restricted to the active stencil mask.
560    pub textured_stencil: wgpu::RenderPipeline,
561    /// Rounded fill pipeline restricted to the active stencil mask.
562    pub rounded_rect_stencil: wgpu::RenderPipeline,
563    /// Rounded border pipeline restricted to the active stencil mask.
564    pub border_rounded_rect_stencil: wgpu::RenderPipeline,
565    /// Box-shadow pipeline restricted to the active stencil mask.
566    pub box_shadow_stencil: wgpu::RenderPipeline,
567    /// Progress-ring pipeline restricted to the active stencil mask.
568    pub ring_progress_stencil: wgpu::RenderPipeline,
569    /// AA edge band: stencil `NotEqual` + rounded `clip_alpha` outside the hard mask.
570    pub rounded_rect_stencil_edge: wgpu::RenderPipeline,
571    /// Uniform-buffer layout used by all clip-aware pipelines.
572    pub clip_bind_group_layout: wgpu::BindGroupLayout,
573    /// Sampled 2D texture and filtering-sampler layout used by textured pipelines.
574    pub texture_bind_group_layout: wgpu::BindGroupLayout,
575}
576
577impl PipelineCache {
578    /// Compiles the complete renderer pipeline family for `surface_format`.
579    ///
580    /// Every cached color pipeline is format-specific. A cache must therefore
581    /// be rebuilt when the presentation format changes.
582    ///
583    /// # Panics
584    ///
585    /// May panic if `wgpu` validation rejects a shader, layout, or pipeline.
586    ///
587    /// # Examples
588    ///
589    /// ```no_run
590    /// use ailloli_ui_render_wgpu::pipeline_cache::PipelineCache;
591    ///
592    /// fn compile(device: &wgpu::Device) -> PipelineCache {
593    ///     PipelineCache::new(device, wgpu::TextureFormat::Bgra8UnormSrgb)
594    /// }
595    /// ```
596    pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
597        let clip_bind_group_layout =
598            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
599                label: Some("clip bind group layout"),
600                entries: &[wgpu::BindGroupLayoutEntry {
601                    binding: 0,
602                    visibility: wgpu::ShaderStages::FRAGMENT,
603                    ty: wgpu::BindingType::Buffer {
604                        ty: wgpu::BufferBindingType::Uniform,
605                        has_dynamic_offset: false,
606                        min_binding_size: None,
607                    },
608                    count: None,
609                }],
610            });
611
612        let shader_rect = device.create_shader_module(wgpu::ShaderModuleDescriptor {
613            label: Some("rect shader"),
614            source: wgpu::ShaderSource::Wgsl(crate::shaders::rect_shader_source().into()),
615        });
616        let shader_stroke = device.create_shader_module(wgpu::ShaderModuleDescriptor {
617            label: Some("stroke shader"),
618            source: wgpu::ShaderSource::Wgsl(crate::shaders::stroke_shader_source().into()),
619        });
620
621        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
622            label: Some("pipeline layout"),
623            bind_group_layouts: &[&clip_bind_group_layout],
624            push_constant_ranges: &[],
625        });
626
627        let rect = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
628            label: Some("rect pipeline"),
629            layout: Some(&pipeline_layout),
630            vertex: wgpu::VertexState {
631                module: &shader_rect,
632                entry_point: "vs_main",
633                compilation_options: Default::default(),
634                buffers: &[Vertex::desc()],
635            },
636            fragment: Some(wgpu::FragmentState {
637                module: &shader_rect,
638                entry_point: "fs_main",
639                compilation_options: Default::default(),
640                targets: &[Some(wgpu::ColorTargetState {
641                    format: surface_format,
642                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
643                    write_mask: wgpu::ColorWrites::ALL,
644                })],
645            }),
646            primitive: wgpu::PrimitiveState {
647                topology: wgpu::PrimitiveTopology::TriangleList,
648                ..Default::default()
649            },
650            depth_stencil: None,
651            multisample: wgpu::MultisampleState::default(),
652            multiview: None,
653        });
654
655        let stroke = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
656            label: Some("stroke pipeline"),
657            layout: Some(&pipeline_layout),
658            vertex: wgpu::VertexState {
659                module: &shader_stroke,
660                entry_point: "vs_main",
661                compilation_options: Default::default(),
662                buffers: &[StrokeVertex::desc()],
663            },
664            fragment: Some(wgpu::FragmentState {
665                module: &shader_stroke,
666                entry_point: "fs_main",
667                compilation_options: Default::default(),
668                targets: &[Some(wgpu::ColorTargetState {
669                    format: surface_format,
670                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
671                    write_mask: wgpu::ColorWrites::ALL,
672                })],
673            }),
674            primitive: wgpu::PrimitiveState {
675                topology: wgpu::PrimitiveTopology::TriangleList,
676                ..Default::default()
677            },
678            depth_stencil: None,
679            multisample: wgpu::MultisampleState::default(),
680            multiview: None,
681        });
682
683        let texture_bind_group_layout =
684            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
685                label: Some("tex bind group layout"),
686                entries: &[
687                    wgpu::BindGroupLayoutEntry {
688                        binding: 0,
689                        visibility: wgpu::ShaderStages::FRAGMENT,
690                        ty: wgpu::BindingType::Texture {
691                            multisampled: false,
692                            view_dimension: wgpu::TextureViewDimension::D2,
693                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
694                        },
695                        count: None,
696                    },
697                    wgpu::BindGroupLayoutEntry {
698                        binding: 1,
699                        visibility: wgpu::ShaderStages::FRAGMENT,
700                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
701                        count: None,
702                    },
703                ],
704            });
705
706        let tex_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
707            label: Some("tex pipeline layout"),
708            bind_group_layouts: &[&clip_bind_group_layout, &texture_bind_group_layout],
709            push_constant_ranges: &[],
710        });
711
712        let shader_textured = device.create_shader_module(wgpu::ShaderModuleDescriptor {
713            label: Some("tex shader"),
714            source: wgpu::ShaderSource::Wgsl(crate::shaders::textured_shader_source().into()),
715        });
716
717        let textured = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
718            label: Some("tex pipeline"),
719            layout: Some(&tex_pipeline_layout),
720            vertex: wgpu::VertexState {
721                module: &shader_textured,
722                entry_point: "vs_main",
723                compilation_options: Default::default(),
724                buffers: &[TexVertex::desc()],
725            },
726            fragment: Some(wgpu::FragmentState {
727                module: &shader_textured,
728                entry_point: "fs_main",
729                compilation_options: Default::default(),
730                targets: &[Some(wgpu::ColorTargetState {
731                    format: surface_format,
732                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
733                    write_mask: wgpu::ColorWrites::ALL,
734                })],
735            }),
736            primitive: wgpu::PrimitiveState {
737                topology: wgpu::PrimitiveTopology::TriangleList,
738                ..Default::default()
739            },
740            depth_stencil: None,
741            multisample: wgpu::MultisampleState::default(),
742            multiview: None,
743        });
744
745        let shader_rrect = device.create_shader_module(wgpu::ShaderModuleDescriptor {
746            label: Some("rrect shader"),
747            source: wgpu::ShaderSource::Wgsl(crate::shaders::rounded_rect_shader_source().into()),
748        });
749        let shader_border_rrect = device.create_shader_module(wgpu::ShaderModuleDescriptor {
750            label: Some("border rrect shader"),
751            source: wgpu::ShaderSource::Wgsl(
752                crate::shaders::border_rounded_rect_shader_source().into(),
753            ),
754        });
755        let shader_box_shadow = device.create_shader_module(wgpu::ShaderModuleDescriptor {
756            label: Some("box shadow shader"),
757            source: wgpu::ShaderSource::Wgsl(crate::shaders::box_shadow_shader_source().into()),
758        });
759        let shader_ring_progress = device.create_shader_module(wgpu::ShaderModuleDescriptor {
760            label: Some("ring progress shader"),
761            source: wgpu::ShaderSource::Wgsl(crate::shaders::ring_progress_shader_source().into()),
762        });
763
764        let rrect_pipeline_layout =
765            device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
766                label: Some("rrect pipeline layout"),
767                bind_group_layouts: &[&clip_bind_group_layout],
768                push_constant_ranges: &[],
769            });
770
771        let rounded_rect = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
772            label: Some("rrect pipeline"),
773            layout: Some(&rrect_pipeline_layout),
774            vertex: wgpu::VertexState {
775                module: &shader_rrect,
776                entry_point: "vs_main",
777                compilation_options: Default::default(),
778                buffers: &[RRectVertex::desc()],
779            },
780            fragment: Some(wgpu::FragmentState {
781                module: &shader_rrect,
782                entry_point: "fs_main",
783                compilation_options: Default::default(),
784                targets: &[Some(wgpu::ColorTargetState {
785                    format: surface_format,
786                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
787                    write_mask: wgpu::ColorWrites::ALL,
788                })],
789            }),
790            primitive: wgpu::PrimitiveState {
791                topology: wgpu::PrimitiveTopology::TriangleList,
792                ..Default::default()
793            },
794            depth_stencil: None,
795            multisample: wgpu::MultisampleState::default(),
796            multiview: None,
797        });
798
799        let border_rounded_rect = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
800            label: Some("border rrect pipeline"),
801            layout: Some(&rrect_pipeline_layout),
802            vertex: wgpu::VertexState {
803                module: &shader_border_rrect,
804                entry_point: "vs_main",
805                compilation_options: Default::default(),
806                buffers: &[BorderRRectVertex::desc()],
807            },
808            fragment: Some(wgpu::FragmentState {
809                module: &shader_border_rrect,
810                entry_point: "fs_main",
811                compilation_options: Default::default(),
812                targets: &[Some(wgpu::ColorTargetState {
813                    format: surface_format,
814                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
815                    write_mask: wgpu::ColorWrites::ALL,
816                })],
817            }),
818            primitive: wgpu::PrimitiveState {
819                topology: wgpu::PrimitiveTopology::TriangleList,
820                ..Default::default()
821            },
822            depth_stencil: None,
823            multisample: wgpu::MultisampleState::default(),
824            multiview: None,
825        });
826
827        let box_shadow = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
828            label: Some("box shadow pipeline"),
829            layout: Some(&rrect_pipeline_layout),
830            vertex: wgpu::VertexState {
831                module: &shader_box_shadow,
832                entry_point: "vs_main",
833                compilation_options: Default::default(),
834                buffers: &[BoxShadowVertex::desc()],
835            },
836            fragment: Some(wgpu::FragmentState {
837                module: &shader_box_shadow,
838                entry_point: "fs_main",
839                compilation_options: Default::default(),
840                targets: &[Some(wgpu::ColorTargetState {
841                    format: surface_format,
842                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
843                    write_mask: wgpu::ColorWrites::ALL,
844                })],
845            }),
846            primitive: wgpu::PrimitiveState {
847                topology: wgpu::PrimitiveTopology::TriangleList,
848                ..Default::default()
849            },
850            depth_stencil: None,
851            multisample: wgpu::MultisampleState::default(),
852            multiview: None,
853        });
854
855        let ring_progress = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
856            label: Some("ring progress pipeline"),
857            layout: Some(&rrect_pipeline_layout),
858            vertex: wgpu::VertexState {
859                module: &shader_ring_progress,
860                entry_point: "vs_main",
861                compilation_options: Default::default(),
862                buffers: &[RingProgressVertex::desc()],
863            },
864            fragment: Some(wgpu::FragmentState {
865                module: &shader_ring_progress,
866                entry_point: "fs_main",
867                compilation_options: Default::default(),
868                targets: &[Some(wgpu::ColorTargetState {
869                    format: surface_format,
870                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
871                    write_mask: wgpu::ColorWrites::ALL,
872                })],
873            }),
874            primitive: wgpu::PrimitiveState {
875                topology: wgpu::PrimitiveTopology::TriangleList,
876                ..Default::default()
877            },
878            depth_stencil: None,
879            multisample: wgpu::MultisampleState::default(),
880            multiview: None,
881        });
882
883        let stencil_write = wgpu::StencilState {
884            front: wgpu::StencilFaceState {
885                compare: wgpu::CompareFunction::Always,
886                fail_op: wgpu::StencilOperation::Keep,
887                depth_fail_op: wgpu::StencilOperation::Keep,
888                pass_op: wgpu::StencilOperation::Replace,
889            },
890            back: wgpu::StencilFaceState::default(),
891            read_mask: 0xff,
892            write_mask: 0xff,
893        };
894        let stencil_test = wgpu::StencilState {
895            front: wgpu::StencilFaceState {
896                compare: wgpu::CompareFunction::Equal,
897                fail_op: wgpu::StencilOperation::Keep,
898                depth_fail_op: wgpu::StencilOperation::Keep,
899                pass_op: wgpu::StencilOperation::Keep,
900            },
901            back: wgpu::StencilFaceState::default(),
902            read_mask: 0xff,
903            write_mask: 0x00,
904        };
905        let stencil_edge = wgpu::StencilState {
906            front: wgpu::StencilFaceState {
907                compare: wgpu::CompareFunction::NotEqual,
908                fail_op: wgpu::StencilOperation::Keep,
909                depth_fail_op: wgpu::StencilOperation::Keep,
910                pass_op: wgpu::StencilOperation::Keep,
911            },
912            back: wgpu::StencilFaceState::default(),
913            read_mask: 0xff,
914            write_mask: 0x00,
915        };
916        // single-pass compositing: passthrough stencil state with compare=Always and no write.
917        // Used by `*_passthrough_stencil` pipelines so that non-stencil-mode
918        // layers can coexist inside a single RenderPass that already has a
919        // depth_stencil_attachment (required when any other layer is stencil).
920        let stencil_passthrough = wgpu::StencilState {
921            front: wgpu::StencilFaceState {
922                compare: wgpu::CompareFunction::Always,
923                fail_op: wgpu::StencilOperation::Keep,
924                depth_fail_op: wgpu::StencilOperation::Keep,
925                pass_op: wgpu::StencilOperation::Keep,
926            },
927            back: wgpu::StencilFaceState::default(),
928            read_mask: 0xff,
929            write_mask: 0x00,
930        };
931        let depth_stencil_passthrough = Some(wgpu::DepthStencilState {
932            format: crate::stencil::StencilTarget::FORMAT,
933            depth_write_enabled: false,
934            depth_compare: wgpu::CompareFunction::Always,
935            stencil: stencil_passthrough,
936            bias: wgpu::DepthBiasState::default(),
937        });
938        let depth_stencil_write = Some(wgpu::DepthStencilState {
939            format: crate::stencil::StencilTarget::FORMAT,
940            depth_write_enabled: false,
941            depth_compare: wgpu::CompareFunction::Always,
942            stencil: stencil_write,
943            bias: wgpu::DepthBiasState::default(),
944        });
945        let depth_stencil_test = Some(wgpu::DepthStencilState {
946            format: crate::stencil::StencilTarget::FORMAT,
947            depth_write_enabled: false,
948            depth_compare: wgpu::CompareFunction::Always,
949            stencil: stencil_test,
950            bias: wgpu::DepthBiasState::default(),
951        });
952        let depth_stencil_edge = Some(wgpu::DepthStencilState {
953            format: crate::stencil::StencilTarget::FORMAT,
954            depth_write_enabled: false,
955            depth_compare: wgpu::CompareFunction::Always,
956            stencil: stencil_edge,
957            bias: wgpu::DepthBiasState::default(),
958        });
959
960        let rounded_rect_stencil_mask =
961            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
962                label: Some("rounded rect stencil mask"),
963                layout: Some(&rrect_pipeline_layout),
964                vertex: wgpu::VertexState {
965                    module: &shader_rrect,
966                    entry_point: "vs_main",
967                    compilation_options: Default::default(),
968                    buffers: &[RRectVertex::desc()],
969                },
970                fragment: Some(wgpu::FragmentState {
971                    module: &shader_rrect,
972                    entry_point: "fs_main",
973                    compilation_options: Default::default(),
974                    targets: &[Some(wgpu::ColorTargetState {
975                        format: surface_format,
976                        blend: None,
977                        write_mask: wgpu::ColorWrites::empty(),
978                    })],
979                }),
980                primitive: wgpu::PrimitiveState::default(),
981                depth_stencil: depth_stencil_write,
982                multisample: wgpu::MultisampleState::default(),
983                multiview: None,
984            });
985
986        let rect_stencil = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
987            label: Some("rect stencil content"),
988            layout: Some(&pipeline_layout),
989            vertex: wgpu::VertexState {
990                module: &shader_rect,
991                entry_point: "vs_main",
992                compilation_options: Default::default(),
993                buffers: &[Vertex::desc()],
994            },
995            fragment: Some(wgpu::FragmentState {
996                module: &shader_rect,
997                entry_point: "fs_main",
998                compilation_options: Default::default(),
999                targets: &[Some(wgpu::ColorTargetState {
1000                    format: surface_format,
1001                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1002                    write_mask: wgpu::ColorWrites::ALL,
1003                })],
1004            }),
1005            primitive: wgpu::PrimitiveState::default(),
1006            depth_stencil: depth_stencil_test.clone(),
1007            multisample: wgpu::MultisampleState::default(),
1008            multiview: None,
1009        });
1010
1011        let stroke_stencil = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1012            label: Some("stroke stencil content"),
1013            layout: Some(&pipeline_layout),
1014            vertex: wgpu::VertexState {
1015                module: &shader_stroke,
1016                entry_point: "vs_main",
1017                compilation_options: Default::default(),
1018                buffers: &[StrokeVertex::desc()],
1019            },
1020            fragment: Some(wgpu::FragmentState {
1021                module: &shader_stroke,
1022                entry_point: "fs_main",
1023                compilation_options: Default::default(),
1024                targets: &[Some(wgpu::ColorTargetState {
1025                    format: surface_format,
1026                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1027                    write_mask: wgpu::ColorWrites::ALL,
1028                })],
1029            }),
1030            primitive: wgpu::PrimitiveState::default(),
1031            depth_stencil: depth_stencil_test.clone(),
1032            multisample: wgpu::MultisampleState::default(),
1033            multiview: None,
1034        });
1035
1036        let textured_stencil = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1037            label: Some("textured stencil content"),
1038            layout: Some(&tex_pipeline_layout),
1039            vertex: wgpu::VertexState {
1040                module: &shader_textured,
1041                entry_point: "vs_main",
1042                compilation_options: Default::default(),
1043                buffers: &[TexVertex::desc()],
1044            },
1045            fragment: Some(wgpu::FragmentState {
1046                module: &shader_textured,
1047                entry_point: "fs_main",
1048                compilation_options: Default::default(),
1049                targets: &[Some(wgpu::ColorTargetState {
1050                    format: surface_format,
1051                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1052                    write_mask: wgpu::ColorWrites::ALL,
1053                })],
1054            }),
1055            primitive: wgpu::PrimitiveState::default(),
1056            depth_stencil: depth_stencil_test.clone(),
1057            multisample: wgpu::MultisampleState::default(),
1058            multiview: None,
1059        });
1060
1061        let rounded_rect_stencil = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1062            label: Some("rrect stencil content"),
1063            layout: Some(&rrect_pipeline_layout),
1064            vertex: wgpu::VertexState {
1065                module: &shader_rrect,
1066                entry_point: "vs_main",
1067                compilation_options: Default::default(),
1068                buffers: &[RRectVertex::desc()],
1069            },
1070            fragment: Some(wgpu::FragmentState {
1071                module: &shader_rrect,
1072                entry_point: "fs_main",
1073                compilation_options: Default::default(),
1074                targets: &[Some(wgpu::ColorTargetState {
1075                    format: surface_format,
1076                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1077                    write_mask: wgpu::ColorWrites::ALL,
1078                })],
1079            }),
1080            primitive: wgpu::PrimitiveState::default(),
1081            depth_stencil: depth_stencil_test.clone(),
1082            multisample: wgpu::MultisampleState::default(),
1083            multiview: None,
1084        });
1085
1086        let border_rounded_rect_stencil =
1087            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1088                label: Some("border rrect stencil content"),
1089                layout: Some(&rrect_pipeline_layout),
1090                vertex: wgpu::VertexState {
1091                    module: &shader_border_rrect,
1092                    entry_point: "vs_main",
1093                    compilation_options: Default::default(),
1094                    buffers: &[BorderRRectVertex::desc()],
1095                },
1096                fragment: Some(wgpu::FragmentState {
1097                    module: &shader_border_rrect,
1098                    entry_point: "fs_main",
1099                    compilation_options: Default::default(),
1100                    targets: &[Some(wgpu::ColorTargetState {
1101                        format: surface_format,
1102                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1103                        write_mask: wgpu::ColorWrites::ALL,
1104                    })],
1105                }),
1106                primitive: wgpu::PrimitiveState::default(),
1107                depth_stencil: depth_stencil_test.clone(),
1108                multisample: wgpu::MultisampleState::default(),
1109                multiview: None,
1110            });
1111
1112        let box_shadow_stencil = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1113            label: Some("box shadow stencil content"),
1114            layout: Some(&rrect_pipeline_layout),
1115            vertex: wgpu::VertexState {
1116                module: &shader_box_shadow,
1117                entry_point: "vs_main",
1118                compilation_options: Default::default(),
1119                buffers: &[BoxShadowVertex::desc()],
1120            },
1121            fragment: Some(wgpu::FragmentState {
1122                module: &shader_box_shadow,
1123                entry_point: "fs_main",
1124                compilation_options: Default::default(),
1125                targets: &[Some(wgpu::ColorTargetState {
1126                    format: surface_format,
1127                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1128                    write_mask: wgpu::ColorWrites::ALL,
1129                })],
1130            }),
1131            primitive: wgpu::PrimitiveState::default(),
1132            depth_stencil: depth_stencil_test.clone(),
1133            multisample: wgpu::MultisampleState::default(),
1134            multiview: None,
1135        });
1136
1137        let ring_progress_stencil =
1138            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1139                label: Some("ring progress stencil content"),
1140                layout: Some(&rrect_pipeline_layout),
1141                vertex: wgpu::VertexState {
1142                    module: &shader_ring_progress,
1143                    entry_point: "vs_main",
1144                    compilation_options: Default::default(),
1145                    buffers: &[RingProgressVertex::desc()],
1146                },
1147                fragment: Some(wgpu::FragmentState {
1148                    module: &shader_ring_progress,
1149                    entry_point: "fs_main",
1150                    compilation_options: Default::default(),
1151                    targets: &[Some(wgpu::ColorTargetState {
1152                        format: surface_format,
1153                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1154                        write_mask: wgpu::ColorWrites::ALL,
1155                    })],
1156                }),
1157                primitive: wgpu::PrimitiveState::default(),
1158                depth_stencil: depth_stencil_test.clone(),
1159                multisample: wgpu::MultisampleState::default(),
1160                multiview: None,
1161            });
1162
1163        let rounded_rect_stencil_edge =
1164            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1165                label: Some("rrect stencil edge aa"),
1166                layout: Some(&rrect_pipeline_layout),
1167                vertex: wgpu::VertexState {
1168                    module: &shader_rrect,
1169                    entry_point: "vs_main",
1170                    compilation_options: Default::default(),
1171                    buffers: &[RRectVertex::desc()],
1172                },
1173                fragment: Some(wgpu::FragmentState {
1174                    module: &shader_rrect,
1175                    entry_point: "fs_main",
1176                    compilation_options: Default::default(),
1177                    targets: &[Some(wgpu::ColorTargetState {
1178                        format: surface_format,
1179                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1180                        write_mask: wgpu::ColorWrites::ALL,
1181                    })],
1182                }),
1183                primitive: wgpu::PrimitiveState::default(),
1184                depth_stencil: depth_stencil_edge,
1185                multisample: wgpu::MultisampleState::default(),
1186                multiview: None,
1187            });
1188
1189        let rect_passthrough_stencil =
1190            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1191                label: Some("rect pipeline (passthrough stencil)"),
1192                layout: Some(&pipeline_layout),
1193                vertex: wgpu::VertexState {
1194                    module: &shader_rect,
1195                    entry_point: "vs_main",
1196                    compilation_options: Default::default(),
1197                    buffers: &[Vertex::desc()],
1198                },
1199                fragment: Some(wgpu::FragmentState {
1200                    module: &shader_rect,
1201                    entry_point: "fs_main",
1202                    compilation_options: Default::default(),
1203                    targets: &[Some(wgpu::ColorTargetState {
1204                        format: surface_format,
1205                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1206                        write_mask: wgpu::ColorWrites::ALL,
1207                    })],
1208                }),
1209                primitive: wgpu::PrimitiveState {
1210                    topology: wgpu::PrimitiveTopology::TriangleList,
1211                    ..Default::default()
1212                },
1213                depth_stencil: depth_stencil_passthrough.clone(),
1214                multisample: wgpu::MultisampleState::default(),
1215                multiview: None,
1216            });
1217
1218        let stroke_passthrough_stencil =
1219            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1220                label: Some("stroke pipeline (passthrough stencil)"),
1221                layout: Some(&pipeline_layout),
1222                vertex: wgpu::VertexState {
1223                    module: &shader_stroke,
1224                    entry_point: "vs_main",
1225                    compilation_options: Default::default(),
1226                    buffers: &[StrokeVertex::desc()],
1227                },
1228                fragment: Some(wgpu::FragmentState {
1229                    module: &shader_stroke,
1230                    entry_point: "fs_main",
1231                    compilation_options: Default::default(),
1232                    targets: &[Some(wgpu::ColorTargetState {
1233                        format: surface_format,
1234                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1235                        write_mask: wgpu::ColorWrites::ALL,
1236                    })],
1237                }),
1238                primitive: wgpu::PrimitiveState {
1239                    topology: wgpu::PrimitiveTopology::TriangleList,
1240                    ..Default::default()
1241                },
1242                depth_stencil: depth_stencil_passthrough.clone(),
1243                multisample: wgpu::MultisampleState::default(),
1244                multiview: None,
1245            });
1246
1247        let textured_passthrough_stencil =
1248            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1249                label: Some("tex pipeline (passthrough stencil)"),
1250                layout: Some(&tex_pipeline_layout),
1251                vertex: wgpu::VertexState {
1252                    module: &shader_textured,
1253                    entry_point: "vs_main",
1254                    compilation_options: Default::default(),
1255                    buffers: &[TexVertex::desc()],
1256                },
1257                fragment: Some(wgpu::FragmentState {
1258                    module: &shader_textured,
1259                    entry_point: "fs_main",
1260                    compilation_options: Default::default(),
1261                    targets: &[Some(wgpu::ColorTargetState {
1262                        format: surface_format,
1263                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1264                        write_mask: wgpu::ColorWrites::ALL,
1265                    })],
1266                }),
1267                primitive: wgpu::PrimitiveState {
1268                    topology: wgpu::PrimitiveTopology::TriangleList,
1269                    ..Default::default()
1270                },
1271                depth_stencil: depth_stencil_passthrough.clone(),
1272                multisample: wgpu::MultisampleState::default(),
1273                multiview: None,
1274            });
1275
1276        let rounded_rect_passthrough_stencil =
1277            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1278                label: Some("rrect pipeline (passthrough stencil)"),
1279                layout: Some(&rrect_pipeline_layout),
1280                vertex: wgpu::VertexState {
1281                    module: &shader_rrect,
1282                    entry_point: "vs_main",
1283                    compilation_options: Default::default(),
1284                    buffers: &[RRectVertex::desc()],
1285                },
1286                fragment: Some(wgpu::FragmentState {
1287                    module: &shader_rrect,
1288                    entry_point: "fs_main",
1289                    compilation_options: Default::default(),
1290                    targets: &[Some(wgpu::ColorTargetState {
1291                        format: surface_format,
1292                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1293                        write_mask: wgpu::ColorWrites::ALL,
1294                    })],
1295                }),
1296                primitive: wgpu::PrimitiveState {
1297                    topology: wgpu::PrimitiveTopology::TriangleList,
1298                    ..Default::default()
1299                },
1300                depth_stencil: depth_stencil_passthrough.clone(),
1301                multisample: wgpu::MultisampleState::default(),
1302                multiview: None,
1303            });
1304
1305        let border_rounded_rect_passthrough_stencil =
1306            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1307                label: Some("border rrect pipeline (passthrough stencil)"),
1308                layout: Some(&rrect_pipeline_layout),
1309                vertex: wgpu::VertexState {
1310                    module: &shader_border_rrect,
1311                    entry_point: "vs_main",
1312                    compilation_options: Default::default(),
1313                    buffers: &[BorderRRectVertex::desc()],
1314                },
1315                fragment: Some(wgpu::FragmentState {
1316                    module: &shader_border_rrect,
1317                    entry_point: "fs_main",
1318                    compilation_options: Default::default(),
1319                    targets: &[Some(wgpu::ColorTargetState {
1320                        format: surface_format,
1321                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1322                        write_mask: wgpu::ColorWrites::ALL,
1323                    })],
1324                }),
1325                primitive: wgpu::PrimitiveState {
1326                    topology: wgpu::PrimitiveTopology::TriangleList,
1327                    ..Default::default()
1328                },
1329                depth_stencil: depth_stencil_passthrough.clone(),
1330                multisample: wgpu::MultisampleState::default(),
1331                multiview: None,
1332            });
1333
1334        let box_shadow_passthrough_stencil =
1335            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1336                label: Some("box shadow pipeline (passthrough stencil)"),
1337                layout: Some(&rrect_pipeline_layout),
1338                vertex: wgpu::VertexState {
1339                    module: &shader_box_shadow,
1340                    entry_point: "vs_main",
1341                    compilation_options: Default::default(),
1342                    buffers: &[BoxShadowVertex::desc()],
1343                },
1344                fragment: Some(wgpu::FragmentState {
1345                    module: &shader_box_shadow,
1346                    entry_point: "fs_main",
1347                    compilation_options: Default::default(),
1348                    targets: &[Some(wgpu::ColorTargetState {
1349                        format: surface_format,
1350                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1351                        write_mask: wgpu::ColorWrites::ALL,
1352                    })],
1353                }),
1354                primitive: wgpu::PrimitiveState {
1355                    topology: wgpu::PrimitiveTopology::TriangleList,
1356                    ..Default::default()
1357                },
1358                depth_stencil: depth_stencil_passthrough.clone(),
1359                multisample: wgpu::MultisampleState::default(),
1360                multiview: None,
1361            });
1362
1363        let ring_progress_passthrough_stencil =
1364            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1365                label: Some("ring progress pipeline (passthrough stencil)"),
1366                layout: Some(&rrect_pipeline_layout),
1367                vertex: wgpu::VertexState {
1368                    module: &shader_ring_progress,
1369                    entry_point: "vs_main",
1370                    compilation_options: Default::default(),
1371                    buffers: &[RingProgressVertex::desc()],
1372                },
1373                fragment: Some(wgpu::FragmentState {
1374                    module: &shader_ring_progress,
1375                    entry_point: "fs_main",
1376                    compilation_options: Default::default(),
1377                    targets: &[Some(wgpu::ColorTargetState {
1378                        format: surface_format,
1379                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
1380                        write_mask: wgpu::ColorWrites::ALL,
1381                    })],
1382                }),
1383                primitive: wgpu::PrimitiveState {
1384                    topology: wgpu::PrimitiveTopology::TriangleList,
1385                    ..Default::default()
1386                },
1387                depth_stencil: depth_stencil_passthrough,
1388                multisample: wgpu::MultisampleState::default(),
1389                multiview: None,
1390            });
1391
1392        Self {
1393            rect,
1394            stroke,
1395            textured,
1396            rounded_rect,
1397            border_rounded_rect,
1398            box_shadow,
1399            ring_progress,
1400            rect_passthrough_stencil,
1401            stroke_passthrough_stencil,
1402            textured_passthrough_stencil,
1403            rounded_rect_passthrough_stencil,
1404            border_rounded_rect_passthrough_stencil,
1405            box_shadow_passthrough_stencil,
1406            ring_progress_passthrough_stencil,
1407            rounded_rect_stencil_mask,
1408            rect_stencil,
1409            stroke_stencil,
1410            textured_stencil,
1411            rounded_rect_stencil,
1412            border_rounded_rect_stencil,
1413            box_shadow_stencil,
1414            ring_progress_stencil,
1415            rounded_rect_stencil_edge,
1416            clip_bind_group_layout,
1417            texture_bind_group_layout,
1418        }
1419    }
1420}
1421
1422/// Returns whether stderr GPU diagnostics are enabled.
1423///
1424/// `AILLOLI_UI_GPU_DEBUG=1` or `true` enables diagnostics. The legacy
1425/// `OCTAVUI_GPU_DEBUG` variable is consulted only when the primary variable is
1426/// unset.
1427///
1428/// # Examples
1429///
1430/// ```
1431/// use ailloli_ui_render_wgpu::pipeline_cache::gpu_debug_enabled;
1432///
1433/// let enabled: bool = gpu_debug_enabled();
1434/// let _ = enabled;
1435/// ```
1436pub fn gpu_debug_enabled() -> bool {
1437    crate::env_control::truthy("AILLOLI_UI_GPU_DEBUG", "OCTAVUI_GPU_DEBUG")
1438}
1439
1440/// Adapter try order: discrete GPU first, then integrated, then others.
1441///
1442/// Lower values have higher priority; the mapping is deterministic and never
1443/// depends on the adapter name.
1444fn adapter_bootstrap_rank(device_type: wgpu::DeviceType) -> u8 {
1445    match device_type {
1446        wgpu::DeviceType::DiscreteGpu => 0,
1447        wgpu::DeviceType::IntegratedGpu => 1,
1448        wgpu::DeviceType::Other => 2,
1449        wgpu::DeviceType::VirtualGpu => 3,
1450        wgpu::DeviceType::Cpu => 4,
1451    }
1452}
1453
1454/// Device-bound state that can outlive a native presentation surface.
1455///
1456/// Keeping the instance and adapter here lets a suspended host discard only
1457/// [`SurfaceAttachment`] while retaining the device, queue, and pipeline cache.
1458///
1459/// # Examples
1460///
1461/// ```
1462/// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceGpuContext;
1463///
1464/// assert!(std::mem::size_of::<SurfaceGpuContext>() > 0);
1465/// ```
1466pub struct SurfaceGpuContext {
1467    /// WGPU instance that created the adapter and any attached surfaces.
1468    instance: wgpu::Instance,
1469    /// Physical or software adapter selected for this context.
1470    adapter: wgpu::Adapter,
1471    /// Logical device owning pipelines, textures, and command encoders.
1472    device: wgpu::Device,
1473    /// Submission queue paired with `device`.
1474    queue: wgpu::Queue,
1475    /// Lazily populated render-pipeline and bind-layout cache.
1476    pipelines: PipelineCache,
1477    /// Color format used by surface and compatible intermediate pipelines.
1478    format: wgpu::TextureFormat,
1479}
1480
1481impl SurfaceGpuContext {
1482    /// Returns the instance that created the retained adapter and surfaces.
1483    ///
1484    /// # Examples
1485    ///
1486    /// ```no_run
1487    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceGpuContext;
1488    ///
1489    /// fn instance(context: &SurfaceGpuContext) -> &wgpu::Instance {
1490    ///     context.instance()
1491    /// }
1492    /// ```
1493    pub fn instance(&self) -> &wgpu::Instance {
1494        &self.instance
1495    }
1496
1497    /// Returns the adapter selected for the current presentation format.
1498    ///
1499    /// # Examples
1500    ///
1501    /// ```no_run
1502    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceGpuContext;
1503    ///
1504    /// fn adapter(context: &SurfaceGpuContext) -> &wgpu::Adapter {
1505    ///     context.adapter()
1506    /// }
1507    /// ```
1508    pub fn adapter(&self) -> &wgpu::Adapter {
1509        &self.adapter
1510    }
1511
1512    /// Returns the logical device retained across surface detachments.
1513    ///
1514    /// # Examples
1515    ///
1516    /// ```no_run
1517    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceGpuContext;
1518    ///
1519    /// fn device(context: &SurfaceGpuContext) -> &wgpu::Device {
1520    ///     context.device()
1521    /// }
1522    /// ```
1523    pub fn device(&self) -> &wgpu::Device {
1524        &self.device
1525    }
1526
1527    /// Returns the submission queue paired with [`Self::device`].
1528    ///
1529    /// # Examples
1530    ///
1531    /// ```no_run
1532    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceGpuContext;
1533    ///
1534    /// fn queue(context: &SurfaceGpuContext) -> &wgpu::Queue {
1535    ///     context.queue()
1536    /// }
1537    /// ```
1538    pub fn queue(&self) -> &wgpu::Queue {
1539        &self.queue
1540    }
1541
1542    /// Returns pipelines compiled for [`Self::format`].
1543    ///
1544    /// # Examples
1545    ///
1546    /// ```no_run
1547    /// use ailloli_ui_render_wgpu::pipeline_cache::{PipelineCache, SurfaceGpuContext};
1548    ///
1549    /// fn pipelines(context: &SurfaceGpuContext) -> &PipelineCache {
1550    ///     context.pipelines()
1551    /// }
1552    /// ```
1553    pub fn pipelines(&self) -> &PipelineCache {
1554        &self.pipelines
1555    }
1556
1557    /// Returns the texture format baked into the retained pipeline cache.
1558    ///
1559    /// # Examples
1560    ///
1561    /// ```no_run
1562    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceGpuContext;
1563    ///
1564    /// fn format(context: &SurfaceGpuContext) -> wgpu::TextureFormat {
1565    ///     context.format()
1566    /// }
1567    /// ```
1568    pub fn format(&self) -> wgpu::TextureFormat {
1569        self.format
1570    }
1571
1572    /// Queries descriptive information for the retained adapter.
1573    ///
1574    /// # Examples
1575    ///
1576    /// ```no_run
1577    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceGpuContext;
1578    ///
1579    /// fn name(context: &SurfaceGpuContext) -> String {
1580    ///     context.adapter_info().name
1581    /// }
1582    /// ```
1583    pub fn adapter_info(&self) -> wgpu::AdapterInfo {
1584        self.adapter.get_info()
1585    }
1586}
1587
1588/// Native presentation resources that are discarded on host suspension.
1589///
1590/// The surface keeps the owned raw-window-handle provider alive. Configuration,
1591/// capabilities, and the host pre-present callback therefore share exactly the
1592/// same lifetime and are dropped before the native target owner can be released.
1593///
1594/// # Examples
1595///
1596/// ```
1597/// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceAttachment;
1598///
1599/// assert!(std::mem::size_of::<SurfaceAttachment>() > 0);
1600/// ```
1601pub struct SurfaceAttachment {
1602    /// Native presentation surface tied to the source window lifetime contract.
1603    surface: wgpu::Surface<'static>,
1604    /// Last successfully applied physical extent, format, and present policy.
1605    config: wgpu::SurfaceConfiguration,
1606    /// Adapter-reported formats, present modes, and alpha modes.
1607    capabilities: wgpu::SurfaceCapabilities,
1608    /// Optional callback invoked immediately before each present operation.
1609    pre_present: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
1610}
1611
1612impl std::fmt::Debug for SurfaceAttachment {
1613    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1614        formatter
1615            .debug_struct("SurfaceAttachment")
1616            .field("surface", &self.surface)
1617            .field("config", &self.config)
1618            .field("capabilities", &self.capabilities)
1619            .field("has_pre_present", &self.pre_present.is_some())
1620            .finish()
1621    }
1622}
1623
1624impl SurfaceAttachment {
1625    /// Returns the last configuration successfully applied to the surface.
1626    ///
1627    /// Width and height are physical pixels and are nonzero for a configured
1628    /// attachment.
1629    ///
1630    /// # Examples
1631    ///
1632    /// ```no_run
1633    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceAttachment;
1634    ///
1635    /// fn width(attachment: &SurfaceAttachment) -> u32 {
1636    ///     attachment.config().width
1637    /// }
1638    /// ```
1639    pub fn config(&self) -> &wgpu::SurfaceConfiguration {
1640        &self.config
1641    }
1642
1643    /// Returns the capabilities snapshot captured at the last configuration.
1644    ///
1645    /// Callers needing a fresh snapshot should use
1646    /// [`WgpuSurfaceBundle::surface_capabilities`].
1647    ///
1648    /// # Examples
1649    ///
1650    /// ```no_run
1651    /// use ailloli_ui_render_wgpu::pipeline_cache::SurfaceAttachment;
1652    ///
1653    /// fn formats(attachment: &SurfaceAttachment) -> usize {
1654    ///     attachment.capabilities().formats.len()
1655    /// }
1656    /// ```
1657    pub fn capabilities(&self) -> &wgpu::SurfaceCapabilities {
1658        &self.capabilities
1659    }
1660
1661    /// Invokes the optional host callback immediately before presentation.
1662    fn pre_present_notify(&self) {
1663        if let Some(pre_present) = &self.pre_present {
1664            pre_present();
1665        }
1666    }
1667}
1668
1669/// Internal distinction between unrecoverable attachment errors and cases
1670/// that require selecting another adapter and rebuilding the GPU context.
1671enum SurfaceAttachAttemptError {
1672    /// The attachment cannot be created by rebuilding the context.
1673    Fatal(RendererError),
1674    /// The current context is incompatible but a fresh bootstrap may work.
1675    RequiresRebuild(SurfaceContextReuseFailure),
1676}
1677
1678/// Reusable GPU context plus an optional host-owned surface attachment.
1679///
1680/// The target passed to [`Self::new_with_surface_target`] or
1681/// [`Self::reattach_surface_target`] is stored by wgpu's surface so its raw
1682/// handles remain valid for the entire attachment lifetime.
1683///
1684/// # Examples
1685///
1686/// ```
1687/// use ailloli_ui_render_wgpu::pipeline_cache::WgpuSurfaceBundle;
1688///
1689/// assert!(std::mem::size_of::<WgpuSurfaceBundle>() > 0);
1690/// ```
1691pub struct WgpuSurfaceBundle {
1692    // Declared first so Rust's field drop order releases the surface (and its
1693    // owned raw-handle target) before the reusable GPU context on full teardown.
1694    /// Detachable native surface and its attachment lifecycle state.
1695    attachment: SurfaceAttachmentSlot<SurfaceAttachment>,
1696    /// Reusable device, queue, adapter, and pipeline context.
1697    context: SurfaceGpuContext,
1698    /// Most recent requested physical extent, including deferred zero extents.
1699    last_extent: PhysicalExtent,
1700    /// Whether surface composition requests transparent alpha handling.
1701    transparent: bool,
1702    /// Power, fallback, present, and alpha preferences used for reattachment.
1703    bootstrap: SurfaceBootstrapConfig,
1704}
1705
1706impl WgpuSurfaceBundle {
1707    /// Creates a renderer bundle from any owned raw-window-handle provider.
1708    ///
1709    /// The physical size is clamped to at least one pixel per dimension in the
1710    /// resulting surface configuration. Adapter candidates are tried in stable
1711    /// device/backend/name order.
1712    ///
1713    /// # Errors
1714    ///
1715    /// Returns an error when no compatible adapter/device can be opened, the
1716    /// raw handles cannot create a surface, or every candidate rejects surface
1717    /// configuration.
1718    ///
1719    /// # Panics
1720    ///
1721    /// Pipeline creation may panic if the selected device rejects the renderer
1722    /// shaders or layouts. Native surface-configuration panics are caught and
1723    /// cause the next adapter candidate to be tried.
1724    ///
1725    /// # Examples
1726    ///
1727    /// ```no_run
1728    /// use std::sync::Arc;
1729    /// use ailloli_ui_render_wgpu::{
1730    ///     pipeline_cache::{SurfaceBootstrapConfig, WgpuSurfaceBundle},
1731    ///     PhysicalExtent, RendererError,
1732    /// };
1733    ///
1734    /// fn create<T>(target: Arc<T>) -> Result<WgpuSurfaceBundle, RendererError>
1735    /// where
1736    ///     T: wgpu::rwh::HasWindowHandle
1737    ///         + wgpu::rwh::HasDisplayHandle
1738    ///         + Send
1739    ///         + Sync
1740    ///         + 'static,
1741    /// {
1742    ///     WgpuSurfaceBundle::new_with_surface_target(
1743    ///         target,
1744    ///         PhysicalExtent::new(1280, 720),
1745    ///         false,
1746    ///         SurfaceBootstrapConfig::default(),
1747    ///         None,
1748    ///     )
1749    /// }
1750    /// ```
1751    pub fn new_with_surface_target<T>(
1752        target: std::sync::Arc<T>,
1753        size: PhysicalExtent,
1754        transparent: bool,
1755        bootstrap: SurfaceBootstrapConfig,
1756        pre_present: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
1757    ) -> Result<Self, RendererError>
1758    where
1759        T: wgpu::rwh::HasWindowHandle + wgpu::rwh::HasDisplayHandle + Send + Sync + 'static,
1760    {
1761        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::default());
1762        let surface = instance
1763            .create_surface(target)
1764            .map_err(|_| RendererError::SurfaceConfigFailed)?;
1765        let gpu_debug = gpu_debug_enabled();
1766
1767        let mut candidates: Vec<wgpu::Adapter> = instance
1768            .enumerate_adapters(bootstrap.requested_backends)
1769            .into_iter()
1770            .filter(|adapter| adapter.is_surface_supported(&surface))
1771            .collect();
1772
1773        if candidates.is_empty() && bootstrap.allow_fallback_backends {
1774            candidates = instance
1775                .enumerate_adapters(wgpu::Backends::all())
1776                .into_iter()
1777                .filter(|adapter| adapter.is_surface_supported(&surface))
1778                .collect();
1779        }
1780
1781        if candidates.is_empty() {
1782            if let Some(adapter) =
1783                pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1784                    compatible_surface: Some(&surface),
1785                    power_preference: bootstrap.power_preference,
1786                    force_fallback_adapter: bootstrap.force_fallback_adapter,
1787                }))
1788            {
1789                if adapter.is_surface_supported(&surface) {
1790                    candidates.push(adapter);
1791                }
1792            }
1793        }
1794
1795        if candidates.is_empty() {
1796            return Err(RendererError::RequestAdapterFailed);
1797        }
1798
1799        candidates.sort_by(|a, b| {
1800            let ia = a.get_info();
1801            let ib = b.get_info();
1802            bootstrap_adapter_rank(&ia, &bootstrap)
1803                .cmp(&bootstrap_adapter_rank(&ib, &bootstrap))
1804                .then_with(|| ia.name.cmp(&ib.name))
1805        });
1806
1807        let device_desc = wgpu::DeviceDescriptor {
1808            label: Some("device"),
1809            required_features: wgpu::Features::empty(),
1810            required_limits: wgpu::Limits::default(),
1811        };
1812
1813        for adapter in candidates {
1814            let info = adapter.get_info();
1815            if gpu_debug {
1816                eprintln!(
1817                    "ailloli_ui_render_wgpu: trying adapter name={} backend={:?} device_type={:?}",
1818                    info.name, info.backend, info.device_type
1819                );
1820            }
1821
1822            let (device, queue) =
1823                match pollster::block_on(adapter.request_device(&device_desc, None)) {
1824                    Ok(pair) => pair,
1825                    Err(error) => {
1826                        if gpu_debug {
1827                            eprintln!(
1828                                "ailloli_ui_render_wgpu: request_device failed for {}: {error:?}",
1829                                info.name
1830                            );
1831                        }
1832                        continue;
1833                    }
1834                };
1835
1836            let capabilities = surface.get_capabilities(&adapter);
1837            let surface_config = match build_surface_config(&capabilities, size, 1, transparent) {
1838                Ok(config) => config,
1839                Err(reason) => {
1840                    if gpu_debug {
1841                        eprintln!(
1842                            "ailloli_ui_render_wgpu: build_surface_config deferred {:?} for {}",
1843                            reason, info.name
1844                        );
1845                    }
1846                    continue;
1847                }
1848            };
1849
1850            if gpu_debug {
1851                eprintln!(
1852                    "ailloli_ui_render_wgpu: configuring surface for adapter name={} backend={:?} device_type={:?}",
1853                    info.name, info.backend, info.device_type
1854                );
1855            }
1856
1857            // wgpu 0.20 may panic on a fatal WSI validation failure. Trying the
1858            // next compatible adapter keeps this failure at the adapter boundary.
1859            let configured_ok = catch_unwind(AssertUnwindSafe(|| {
1860                surface.configure(&device, &surface_config);
1861            }))
1862            .is_ok();
1863
1864            if !configured_ok {
1865                if gpu_debug {
1866                    eprintln!(
1867                        "ailloli_ui_render_wgpu: surface.configure panicked for adapter name={}",
1868                        info.name
1869                    );
1870                }
1871                continue;
1872            }
1873
1874            let format = surface_config.format;
1875            let pipelines = PipelineCache::new(&device, format);
1876            let context = SurfaceGpuContext {
1877                instance,
1878                adapter,
1879                device,
1880                queue,
1881                pipelines,
1882                format,
1883            };
1884            let attachment = SurfaceAttachment {
1885                surface,
1886                config: surface_config,
1887                capabilities,
1888                pre_present,
1889            };
1890
1891            return Ok(Self {
1892                attachment: SurfaceAttachmentSlot::attached(attachment),
1893                context,
1894                last_extent: size,
1895                transparent,
1896                bootstrap,
1897            });
1898        }
1899
1900        Err(RendererError::SurfaceConfigureExhausted)
1901    }
1902
1903    /// Returns the reusable device-bound context.
1904    ///
1905    /// # Examples
1906    ///
1907    /// ```no_run
1908    /// use ailloli_ui_render_wgpu::pipeline_cache::{SurfaceGpuContext, WgpuSurfaceBundle};
1909    ///
1910    /// fn context(bundle: &WgpuSurfaceBundle) -> &SurfaceGpuContext {
1911    ///     bundle.context()
1912    /// }
1913    /// ```
1914    pub fn context(&self) -> &SurfaceGpuContext {
1915        &self.context
1916    }
1917
1918    /// Borrows the native attachment, or returns `None` while detached.
1919    ///
1920    /// # Examples
1921    ///
1922    /// ```no_run
1923    /// use ailloli_ui_render_wgpu::pipeline_cache::{SurfaceAttachment, WgpuSurfaceBundle};
1924    ///
1925    /// fn attachment(bundle: &WgpuSurfaceBundle) -> Option<&SurfaceAttachment> {
1926    ///     bundle.attachment()
1927    /// }
1928    /// ```
1929    pub fn attachment(&self) -> Option<&SurfaceAttachment> {
1930        self.attachment.as_ref()
1931    }
1932
1933    /// Reports whether a native surface is attached.
1934    ///
1935    /// # Examples
1936    ///
1937    /// ```no_run
1938    /// use ailloli_ui_render_wgpu::pipeline_cache::{
1939    ///     SurfaceAttachmentState, WgpuSurfaceBundle,
1940    /// };
1941    ///
1942    /// fn attached(bundle: &WgpuSurfaceBundle) -> bool {
1943    ///     bundle.attachment_state() == SurfaceAttachmentState::Attached
1944    /// }
1945    /// ```
1946    pub fn attachment_state(&self) -> SurfaceAttachmentState {
1947        self.attachment.state()
1948    }
1949
1950    /// Returns the logical device, including while the surface is detached.
1951    ///
1952    /// # Examples
1953    ///
1954    /// ```no_run
1955    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuSurfaceBundle;
1956    ///
1957    /// fn device(bundle: &WgpuSurfaceBundle) -> &wgpu::Device {
1958    ///     bundle.device()
1959    /// }
1960    /// ```
1961    pub fn device(&self) -> &wgpu::Device {
1962        self.context.device()
1963    }
1964
1965    /// Returns the submission queue, including while the surface is detached.
1966    ///
1967    /// # Examples
1968    ///
1969    /// ```no_run
1970    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuSurfaceBundle;
1971    ///
1972    /// fn queue(bundle: &WgpuSurfaceBundle) -> &wgpu::Queue {
1973    ///     bundle.queue()
1974    /// }
1975    /// ```
1976    pub fn queue(&self) -> &wgpu::Queue {
1977        self.context.queue()
1978    }
1979
1980    /// Returns the format-specific pipeline cache retained across detachments.
1981    ///
1982    /// # Examples
1983    ///
1984    /// ```no_run
1985    /// use ailloli_ui_render_wgpu::pipeline_cache::{PipelineCache, WgpuSurfaceBundle};
1986    ///
1987    /// fn pipelines(bundle: &WgpuSurfaceBundle) -> &PipelineCache {
1988    ///     bundle.pipelines()
1989    /// }
1990    /// ```
1991    pub fn pipelines(&self) -> &PipelineCache {
1992        self.context.pipelines()
1993    }
1994
1995    /// Returns the color format expected by all cached pipelines.
1996    ///
1997    /// # Examples
1998    ///
1999    /// ```no_run
2000    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuSurfaceBundle;
2001    ///
2002    /// fn format(bundle: &WgpuSurfaceBundle) -> wgpu::TextureFormat {
2003    ///     bundle.format()
2004    /// }
2005    /// ```
2006    pub fn format(&self) -> wgpu::TextureFormat {
2007        self.context.format()
2008    }
2009
2010    /// Returns the configured physical-pixel extent or the last attached extent.
2011    ///
2012    /// The remembered value remains available while detached and may contain a
2013    /// zero dimension only if the caller originally supplied one before any
2014    /// successful surface configuration.
2015    ///
2016    /// # Examples
2017    ///
2018    /// ```no_run
2019    /// use ailloli_ui_render_wgpu::{pipeline_cache::WgpuSurfaceBundle, PhysicalExtent};
2020    ///
2021    /// fn extent(bundle: &WgpuSurfaceBundle) -> PhysicalExtent {
2022    ///     bundle.extent()
2023    /// }
2024    /// ```
2025    pub fn extent(&self) -> PhysicalExtent {
2026        self.attachment
2027            .as_ref()
2028            .map(|attachment| {
2029                PhysicalExtent::new(attachment.config.width, attachment.config.height)
2030            })
2031            .unwrap_or(self.last_extent)
2032    }
2033
2034    /// Returns the active surface configuration, or `None` while detached.
2035    ///
2036    /// # Examples
2037    ///
2038    /// ```no_run
2039    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuSurfaceBundle;
2040    ///
2041    /// fn configured_width(bundle: &WgpuSurfaceBundle) -> Option<u32> {
2042    ///     bundle.config().map(|config| config.width)
2043    /// }
2044    /// ```
2045    pub fn config(&self) -> Option<&wgpu::SurfaceConfiguration> {
2046        self.attachment.as_ref().map(SurfaceAttachment::config)
2047    }
2048
2049    /// Drops only native presentation resources while retaining the GPU context.
2050    ///
2051    /// Returns `true` when an attachment was dropped and `false` when the
2052    /// bundle was already detached. The last configured physical extent is
2053    /// retained.
2054    ///
2055    /// # Examples
2056    ///
2057    /// ```no_run
2058    /// use ailloli_ui_render_wgpu::pipeline_cache::{
2059    ///     SurfaceAttachmentState, WgpuSurfaceBundle,
2060    /// };
2061    ///
2062    /// fn detach(bundle: &mut WgpuSurfaceBundle) {
2063    ///     let _had_surface = bundle.detach_surface();
2064    ///     assert_eq!(bundle.attachment_state(), SurfaceAttachmentState::Detached);
2065    /// }
2066    /// ```
2067    pub fn detach_surface(&mut self) -> bool {
2068        let Some(attachment) = self.attachment.detach() else {
2069            return false;
2070        };
2071        self.last_extent = PhysicalExtent::new(attachment.config.width, attachment.config.height);
2072        drop(attachment);
2073        true
2074    }
2075
2076    /// Attaches a new raw-window-handle target.
2077    ///
2078    /// The existing context is tried first. If its adapter or pipeline format
2079    /// cannot present to the new surface, a complete context bootstrap is used
2080    /// as a correctness fallback and the outcome tells the renderer to rebuild
2081    /// every device-bound cache.
2082    ///
2083    /// The existing attachment, if any, is dropped before the new target is
2084    /// tried. `size` is in physical pixels.
2085    ///
2086    /// # Errors
2087    ///
2088    /// Returns an error when the raw handles cannot create a surface or neither
2089    /// reuse nor full GPU bootstrap can produce a configured attachment.
2090    ///
2091    /// # Panics
2092    ///
2093    /// Rebuild may panic if `wgpu` rejects renderer pipeline creation.
2094    ///
2095    /// # Examples
2096    ///
2097    /// ```no_run
2098    /// use std::sync::Arc;
2099    /// use ailloli_ui_render_wgpu::{
2100    ///     pipeline_cache::{SurfaceReattachOutcome, WgpuSurfaceBundle},
2101    ///     PhysicalExtent, RendererError,
2102    /// };
2103    ///
2104    /// fn reattach<T>(
2105    ///     bundle: &mut WgpuSurfaceBundle,
2106    ///     target: Arc<T>,
2107    /// ) -> Result<SurfaceReattachOutcome, RendererError>
2108    /// where
2109    ///     T: wgpu::rwh::HasWindowHandle
2110    ///         + wgpu::rwh::HasDisplayHandle
2111    ///         + Send
2112    ///         + Sync
2113    ///         + 'static,
2114    /// {
2115    ///     bundle.reattach_surface_target(target, PhysicalExtent::new(800, 600), None)
2116    /// }
2117    /// ```
2118    pub fn reattach_surface_target<T>(
2119        &mut self,
2120        target: std::sync::Arc<T>,
2121        size: PhysicalExtent,
2122        pre_present: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
2123    ) -> Result<SurfaceReattachOutcome, RendererError>
2124    where
2125        T: wgpu::rwh::HasWindowHandle + wgpu::rwh::HasDisplayHandle + Send + Sync + 'static,
2126    {
2127        self.detach_surface();
2128
2129        match Self::create_attachment_with_context(
2130            &self.context,
2131            target.clone(),
2132            size,
2133            self.transparent,
2134            pre_present.clone(),
2135        ) {
2136            Ok(attachment) => {
2137                let previous_attachment = self.attachment.attach(attachment);
2138                debug_assert!(previous_attachment.is_none());
2139                drop(previous_attachment);
2140                self.last_extent = size;
2141                Ok(SurfaceReattachOutcome::ReusedGpuContext)
2142            }
2143            Err(SurfaceAttachAttemptError::Fatal(error)) => Err(error),
2144            Err(SurfaceAttachAttemptError::RequiresRebuild(reason)) => {
2145                let replacement = Self::new_with_surface_target(
2146                    target,
2147                    size,
2148                    self.transparent,
2149                    self.bootstrap,
2150                    pre_present,
2151                )?;
2152                *self = replacement;
2153                Ok(SurfaceReattachOutcome::RebuiltGpuContext { reason })
2154            }
2155        }
2156    }
2157
2158    /// Attempts to configure `target` using the retained adapter and format.
2159    ///
2160    /// Capability or configuration incompatibilities request a full rebuild;
2161    /// failure to create the native surface is fatal for this target.
2162    ///
2163    /// # Errors
2164    ///
2165    /// Returns [`SurfaceAttachAttemptError::Fatal`] when native surface creation
2166    /// fails. Returns [`SurfaceAttachAttemptError::RequiresRebuild`] when the
2167    /// retained adapter or format is incompatible, capabilities are incomplete,
2168    /// or wgpu panics while configuring the surface.
2169    fn create_attachment_with_context<T>(
2170        context: &SurfaceGpuContext,
2171        target: std::sync::Arc<T>,
2172        size: PhysicalExtent,
2173        transparent: bool,
2174        pre_present: Option<std::sync::Arc<dyn Fn() + Send + Sync>>,
2175    ) -> Result<SurfaceAttachment, SurfaceAttachAttemptError>
2176    where
2177        T: wgpu::rwh::HasWindowHandle + wgpu::rwh::HasDisplayHandle + Send + Sync + 'static,
2178    {
2179        let surface = context
2180            .instance
2181            .create_surface(target)
2182            .map_err(|_| SurfaceAttachAttemptError::Fatal(RendererError::SurfaceConfigFailed))?;
2183        let adapter_supported = context.adapter.is_surface_supported(&surface);
2184        if !adapter_supported {
2185            return Err(SurfaceAttachAttemptError::RequiresRebuild(
2186                SurfaceContextReuseFailure::AdapterUnsupported,
2187            ));
2188        }
2189        let capabilities = surface.get_capabilities(&context.adapter);
2190        if let Some(reason) = surface_context_reuse_failure(true, context.format, &capabilities) {
2191            return Err(SurfaceAttachAttemptError::RequiresRebuild(reason));
2192        }
2193
2194        let config =
2195            build_surface_config_for_format(&capabilities, size, 1, transparent, context.format)
2196                .map_err(|reason| {
2197                    SurfaceAttachAttemptError::RequiresRebuild(
2198                        SurfaceContextReuseFailure::CapabilitiesDeferred(reason),
2199                    )
2200                })?;
2201        let configured = catch_unwind(AssertUnwindSafe(|| {
2202            surface.configure(&context.device, &config);
2203        }));
2204        if configured.is_err() {
2205            return Err(SurfaceAttachAttemptError::RequiresRebuild(
2206                SurfaceContextReuseFailure::ConfigureFailed,
2207            ));
2208        }
2209
2210        Ok(SurfaceAttachment {
2211            surface,
2212            config,
2213            capabilities,
2214            pre_present,
2215        })
2216    }
2217
2218    /// Best-effort resize that discards the detailed outcome or error.
2219    ///
2220    /// Prefer [`Self::try_resize`] when the host must react to deferred surface
2221    /// capabilities or request surface recreation. `new_size` is in physical
2222    /// pixels; a zero dimension is skipped.
2223    ///
2224    /// # Examples
2225    ///
2226    /// ```no_run
2227    /// use ailloli_ui_render_wgpu::{pipeline_cache::WgpuSurfaceBundle, PhysicalExtent};
2228    ///
2229    /// fn resize(bundle: &mut WgpuSurfaceBundle) {
2230    ///     bundle.resize(PhysicalExtent::new(1024, 768));
2231    /// }
2232    /// ```
2233    pub fn resize(&mut self, new_size: PhysicalExtent) {
2234        let _ = self.try_resize(new_size);
2235    }
2236
2237    /// Reconfigures an attached surface only when its physical extent changed.
2238    ///
2239    /// # Errors
2240    ///
2241    /// Returns [`RendererError::RenderTargetUnavailable`] while detached,
2242    /// [`RendererError::SurfaceRecreationRequired`] if the retained pipeline
2243    /// format is no longer supported, or a configuration error if wgpu rejects
2244    /// the update. Zero-sized requests return [`ResizeOutcome::SkippedZero`]
2245    /// before attachment state is inspected.
2246    ///
2247    /// # Examples
2248    ///
2249    /// ```no_run
2250    /// use ailloli_ui_render_wgpu::{
2251    ///     pipeline_cache::{ResizeOutcome, WgpuSurfaceBundle},
2252    ///     PhysicalExtent, RendererError,
2253    /// };
2254    ///
2255    /// fn resize(bundle: &mut WgpuSurfaceBundle) -> Result<ResizeOutcome, RendererError> {
2256    ///     bundle.try_resize(PhysicalExtent::new(1024, 768))
2257    /// }
2258    /// ```
2259    pub fn try_resize(&mut self, new_size: PhysicalExtent) -> Result<ResizeOutcome, RendererError> {
2260        self.configure_surface(new_size, SurfaceConfigureMode::Resize)
2261    }
2262
2263    /// Reconfigures the presentation surface even when its physical extent is
2264    /// unchanged.
2265    ///
2266    /// # Errors
2267    ///
2268    /// Returns the same attachment, format, and wgpu configuration errors as
2269    /// [`Self::try_resize`]. A zero dimension is still skipped.
2270    ///
2271    /// # Examples
2272    ///
2273    /// ```no_run
2274    /// use ailloli_ui_render_wgpu::{
2275    ///     pipeline_cache::{ResizeOutcome, WgpuSurfaceBundle},
2276    ///     PhysicalExtent, RendererError,
2277    /// };
2278    ///
2279    /// fn recover(bundle: &mut WgpuSurfaceBundle) -> Result<ResizeOutcome, RendererError> {
2280    ///     bundle.try_reconfigure(PhysicalExtent::new(1024, 768))
2281    /// }
2282    /// ```
2283    pub fn try_reconfigure(
2284        &mut self,
2285        new_size: PhysicalExtent,
2286    ) -> Result<ResizeOutcome, RendererError> {
2287        self.configure_surface(new_size, SurfaceConfigureMode::Force)
2288    }
2289
2290    /// Shared resize/recovery path that refreshes capabilities before configure.
2291    ///
2292    /// Surface configuration panics from wgpu are caught and converted to
2293    /// [`RendererError::SurfaceConfigFailed`]. Successful configuration records
2294    /// a benchmark event with a saturating wall-clock timestamp.
2295    ///
2296    /// # Errors
2297    ///
2298    /// Returns [`RendererError::RenderTargetUnavailable`] while detached,
2299    /// [`RendererError::SurfaceRecreationRequired`] when the retained format is
2300    /// no longer advertised, or [`RendererError::SurfaceConfigFailed`] when
2301    /// wgpu panics during configuration. Incomplete capabilities yield a
2302    /// successful [`ResizeOutcome::Deferred`] instead.
2303    fn configure_surface(
2304        &mut self,
2305        new_size: PhysicalExtent,
2306        mode: SurfaceConfigureMode,
2307    ) -> Result<ResizeOutcome, RendererError> {
2308        if new_size.is_zero() {
2309            return Ok(ResizeOutcome::SkippedZero);
2310        }
2311        let Some(attachment) = self.attachment.as_mut() else {
2312            return Err(RendererError::RenderTargetUnavailable(
2313                "presentation surface is detached",
2314            ));
2315        };
2316        let capabilities = attachment.surface.get_capabilities(&self.context.adapter);
2317        if let Some(reason) = surface_config_deferred_reason(&capabilities) {
2318            attachment.capabilities = capabilities;
2319            return Ok(ResizeOutcome::Deferred(reason));
2320        }
2321        if !capabilities.formats.contains(&self.context.format) {
2322            attachment.capabilities = capabilities;
2323            return Err(RendererError::SurfaceRecreationRequired(
2324                "surface no longer supports the pipeline format",
2325            ));
2326        }
2327        let current_size = PhysicalExtent::new(attachment.config.width, attachment.config.height);
2328        if !surface_configure_required(current_size, new_size, mode) {
2329            return Ok(ResizeOutcome::Unchanged);
2330        }
2331        let next_config = match build_surface_config_for_format(
2332            &capabilities,
2333            new_size,
2334            attachment.config.desired_maximum_frame_latency,
2335            self.transparent,
2336            self.context.format,
2337        ) {
2338            Ok(config) => config,
2339            Err(reason) => {
2340                attachment.capabilities = capabilities;
2341                return Ok(ResizeOutcome::Deferred(reason));
2342            }
2343        };
2344        let start = std::time::Instant::now();
2345        let configured = catch_unwind(AssertUnwindSafe(|| {
2346            attachment
2347                .surface
2348                .configure(&self.context.device, &next_config);
2349        }));
2350        if configured.is_err() {
2351            return Err(RendererError::SurfaceConfigFailed);
2352        }
2353        attachment.config = next_config;
2354        attachment.capabilities = attachment.surface.get_capabilities(&self.context.adapter);
2355        self.last_extent = new_size;
2356        ailloli_ui_bench::record(ailloli_ui_bench::Event::SurfaceConfigure {
2357            ts_ms: now_ms(),
2358            w: new_size.width,
2359            h: new_size.height,
2360            dur_us: start.elapsed().as_micros(),
2361        });
2362        if let Some(reason) = surface_config_deferred_reason(&attachment.capabilities) {
2363            return Ok(ResizeOutcome::Deferred(reason));
2364        }
2365        Ok(ResizeOutcome::Applied)
2366    }
2367
2368    /// Forces recovery configuration using the current remembered extent.
2369    ///
2370    /// # Errors
2371    ///
2372    /// Returns the same errors as [`Self::try_reconfigure`].
2373    ///
2374    /// # Examples
2375    ///
2376    /// ```no_run
2377    /// use ailloli_ui_render_wgpu::{
2378    ///     pipeline_cache::{ResizeOutcome, WgpuSurfaceBundle},
2379    ///     RendererError,
2380    /// };
2381    ///
2382    /// fn recover(bundle: &mut WgpuSurfaceBundle) -> Result<ResizeOutcome, RendererError> {
2383    ///     bundle.reconfigure()
2384    /// }
2385    /// ```
2386    pub fn reconfigure(&mut self) -> Result<ResizeOutcome, RendererError> {
2387        self.try_reconfigure(self.extent())
2388    }
2389
2390    /// Queries current capabilities or returns an empty set while detached.
2391    ///
2392    /// The empty detached value is a sentinel; use [`Self::attachment_state`]
2393    /// when that distinction matters.
2394    ///
2395    /// # Examples
2396    ///
2397    /// ```no_run
2398    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuSurfaceBundle;
2399    ///
2400    /// fn format_count(bundle: &WgpuSurfaceBundle) -> usize {
2401    ///     bundle.surface_capabilities().formats.len()
2402    /// }
2403    /// ```
2404    pub fn surface_capabilities(&self) -> wgpu::SurfaceCapabilities {
2405        self.attachment
2406            .as_ref()
2407            .map(|attachment| attachment.surface.get_capabilities(&self.context.adapter))
2408            .unwrap_or_default()
2409    }
2410
2411    /// Queries information about the retained adapter.
2412    ///
2413    /// # Examples
2414    ///
2415    /// ```no_run
2416    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuSurfaceBundle;
2417    ///
2418    /// fn backend(bundle: &WgpuSurfaceBundle) -> wgpu::Backend {
2419    ///     bundle.adapter_info().backend
2420    /// }
2421    /// ```
2422    pub fn adapter_info(&self) -> wgpu::AdapterInfo {
2423        self.context.adapter_info()
2424    }
2425
2426    /// Returns the current reason configuration must wait, if any.
2427    ///
2428    /// Detachment is reported explicitly. An attached surface returns
2429    /// [`SurfaceConfigDeferredReason::NoFormats`] or
2430    /// [`SurfaceConfigDeferredReason::NoPresentModes`] for transient empty
2431    /// capabilities, otherwise `None`.
2432    ///
2433    /// # Examples
2434    ///
2435    /// ```no_run
2436    /// use ailloli_ui_render_wgpu::pipeline_cache::{
2437    ///     SurfaceConfigDeferredReason, WgpuSurfaceBundle,
2438    /// };
2439    ///
2440    /// fn deferred(bundle: &WgpuSurfaceBundle) -> Option<SurfaceConfigDeferredReason> {
2441    ///     bundle.surface_config_deferred_reason()
2442    /// }
2443    /// ```
2444    pub fn surface_config_deferred_reason(&self) -> Option<SurfaceConfigDeferredReason> {
2445        let Some(attachment) = self.attachment.as_ref() else {
2446            return Some(SurfaceConfigDeferredReason::Detached);
2447        };
2448        let capabilities = attachment.surface.get_capabilities(&self.context.adapter);
2449        surface_config_deferred_reason(&capabilities)
2450    }
2451
2452    /// Invokes the host callback associated with the active attachment.
2453    ///
2454    /// This is a no-op while detached or when no callback was supplied.
2455    ///
2456    /// # Examples
2457    ///
2458    /// ```no_run
2459    /// use ailloli_ui_render_wgpu::pipeline_cache::WgpuSurfaceBundle;
2460    ///
2461    /// fn notify(bundle: &WgpuSurfaceBundle) {
2462    ///     bundle.pre_present_notify();
2463    /// }
2464    /// ```
2465    pub fn pre_present_notify(&self) {
2466        if let Some(attachment) = self.attachment.as_ref() {
2467            attachment.pre_present_notify();
2468        }
2469    }
2470}
2471
2472impl RenderTarget for WgpuSurfaceBundle {
2473    fn size(&self) -> PhysicalExtent {
2474        self.extent()
2475    }
2476
2477    fn format(&self) -> wgpu::TextureFormat {
2478        self.context.format
2479    }
2480
2481    fn acquire_frame(&mut self) -> Result<RenderFrame, RendererError> {
2482        let size = self.extent();
2483        let format = self.format();
2484        let Some(attachment) = self.attachment.as_mut() else {
2485            return Err(RendererError::RenderTargetUnavailable(
2486                "presentation surface is detached",
2487            ));
2488        };
2489        let acquire_start = std::time::Instant::now();
2490        let frame = match attachment.surface.get_current_texture() {
2491            Ok(frame) => frame,
2492            Err(error) => {
2493                ailloli_ui_bench::metric(
2494                    "get_current_texture_us",
2495                    acquire_start.elapsed().as_micros() as f64,
2496                );
2497                ailloli_ui_bench::record(ailloli_ui_bench::Event::GetCurrentTextureErr {
2498                    ts_ms: now_ms(),
2499                    err: format!("{error:?}"),
2500                });
2501                return Err(RendererError::from_surface_error(error));
2502            }
2503        };
2504        ailloli_ui_bench::metric(
2505            "get_current_texture_us",
2506            acquire_start.elapsed().as_micros() as f64,
2507        );
2508        let view = frame
2509            .texture
2510            .create_view(&wgpu::TextureViewDescriptor::default());
2511        Ok(RenderFrame::from_surface_texture(
2512            view, frame, size, format, None,
2513        ))
2514    }
2515
2516    fn pre_present_notify(&self) {
2517        self.pre_present_notify();
2518    }
2519}
2520
2521/// Chooses the first sRGB format, falling back to the first advertised format.
2522///
2523/// Returns `None` only when `caps.formats` is empty. Input ordering is retained
2524/// within each preference class.
2525///
2526/// # Examples
2527///
2528/// ```
2529/// use ailloli_ui_render_wgpu::pipeline_cache::choose_surface_format;
2530///
2531/// let mut caps = wgpu::SurfaceCapabilities::default();
2532/// caps.formats = vec![
2533///     wgpu::TextureFormat::Bgra8Unorm,
2534///     wgpu::TextureFormat::Bgra8UnormSrgb,
2535/// ];
2536/// assert_eq!(choose_surface_format(&caps), Some(wgpu::TextureFormat::Bgra8UnormSrgb));
2537/// ```
2538pub fn choose_surface_format(caps: &wgpu::SurfaceCapabilities) -> Option<wgpu::TextureFormat> {
2539    caps.formats
2540        .iter()
2541        .copied()
2542        .find(|format| format.is_srgb())
2543        .or_else(|| caps.formats.first().copied())
2544}
2545
2546/// Chooses FIFO when available, otherwise the first concrete present mode.
2547///
2548/// Automatic modes are deliberately excluded because surface configuration
2549/// requires a concrete capability. Returns `None` for an empty or all-automatic
2550/// slice.
2551///
2552/// # Examples
2553///
2554/// ```
2555/// use ailloli_ui_render_wgpu::pipeline_cache::choose_present_mode;
2556///
2557/// let modes = [wgpu::PresentMode::Immediate, wgpu::PresentMode::Fifo];
2558/// assert_eq!(choose_present_mode(&modes), Some(wgpu::PresentMode::Fifo));
2559/// assert_eq!(choose_present_mode(&[wgpu::PresentMode::AutoVsync]), None);
2560/// ```
2561pub fn choose_present_mode(modes: &[wgpu::PresentMode]) -> Option<wgpu::PresentMode> {
2562    modes
2563        .iter()
2564        .copied()
2565        .filter(is_concrete_present_mode)
2566        .find(|mode| *mode == wgpu::PresentMode::Fifo)
2567        .or_else(|| modes.iter().copied().find(is_concrete_present_mode))
2568}
2569
2570/// Returns `false` for wgpu's automatic present-mode sentinels.
2571fn is_concrete_present_mode(mode: &wgpu::PresentMode) -> bool {
2572    !matches!(
2573        mode,
2574        wgpu::PresentMode::AutoVsync | wgpu::PresentMode::AutoNoVsync
2575    )
2576}
2577
2578/// Selects renderer surface usages from the capabilities bit set.
2579///
2580/// `RENDER_ATTACHMENT` is always requested. `COPY_SRC` is added only when the
2581/// surface advertises it, enabling frame capture without requiring that feature.
2582/// Other advertised bits are intentionally ignored.
2583///
2584/// # Examples
2585///
2586/// ```
2587/// use ailloli_ui_render_wgpu::pipeline_cache::choose_surface_usage;
2588///
2589/// let supported = wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC;
2590/// assert_eq!(choose_surface_usage(supported), supported);
2591/// ```
2592pub fn choose_surface_usage(usages: wgpu::TextureUsages) -> wgpu::TextureUsages {
2593    let mut usage = wgpu::TextureUsages::RENDER_ATTACHMENT;
2594    if usages.contains(wgpu::TextureUsages::COPY_SRC) {
2595        usage |= wgpu::TextureUsages::COPY_SRC;
2596    }
2597    usage
2598}
2599
2600/// Chooses an advertised surface-composition alpha mode.
2601///
2602/// Transparent surfaces prefer premultiplied, then postmultiplied composition;
2603/// opaque surfaces prefer opaque composition. If no preferred value matches,
2604/// the first advertised mode is used; an empty slice falls back to `Opaque`.
2605///
2606/// # Examples
2607///
2608/// ```
2609/// use ailloli_ui_render_wgpu::pipeline_cache::choose_alpha_mode;
2610///
2611/// let modes = [
2612///     wgpu::CompositeAlphaMode::Opaque,
2613///     wgpu::CompositeAlphaMode::PreMultiplied,
2614/// ];
2615/// assert_eq!(
2616///     choose_alpha_mode(&modes, true),
2617///     wgpu::CompositeAlphaMode::PreMultiplied
2618/// );
2619/// ```
2620pub fn choose_alpha_mode(
2621    modes: &[wgpu::CompositeAlphaMode],
2622    transparent: bool,
2623) -> wgpu::CompositeAlphaMode {
2624    let preferred: &[wgpu::CompositeAlphaMode] = if transparent {
2625        &[
2626            wgpu::CompositeAlphaMode::PreMultiplied,
2627            wgpu::CompositeAlphaMode::PostMultiplied,
2628            wgpu::CompositeAlphaMode::Inherit,
2629            wgpu::CompositeAlphaMode::Auto,
2630            wgpu::CompositeAlphaMode::Opaque,
2631        ]
2632    } else {
2633        &[
2634            wgpu::CompositeAlphaMode::Opaque,
2635            wgpu::CompositeAlphaMode::Auto,
2636            wgpu::CompositeAlphaMode::Inherit,
2637            wgpu::CompositeAlphaMode::PreMultiplied,
2638            wgpu::CompositeAlphaMode::PostMultiplied,
2639        ]
2640    };
2641
2642    preferred
2643        .iter()
2644        .copied()
2645        .find(|mode| modes.contains(mode))
2646        .or_else(|| modes.first().copied())
2647        .unwrap_or(wgpu::CompositeAlphaMode::Opaque)
2648}
2649
2650/// Diagnoses whether the retained adapter/format can serve a new surface.
2651///
2652/// Adapter support is checked first, then transient capabilities, then the
2653/// format baked into the retained pipeline cache.
2654fn surface_context_reuse_failure(
2655    adapter_supported: bool,
2656    pipeline_format: wgpu::TextureFormat,
2657    capabilities: &wgpu::SurfaceCapabilities,
2658) -> Option<SurfaceContextReuseFailure> {
2659    if !adapter_supported {
2660        return Some(SurfaceContextReuseFailure::AdapterUnsupported);
2661    }
2662    if let Some(reason) = surface_config_deferred_reason(capabilities) {
2663        return Some(SurfaceContextReuseFailure::CapabilitiesDeferred(reason));
2664    }
2665    if !capabilities.formats.contains(&pipeline_format) {
2666        return Some(SurfaceContextReuseFailure::FormatUnsupported);
2667    }
2668    None
2669}
2670
2671/// Builds a surface config while preserving the retained pipeline format.
2672///
2673/// The compatibility caller must first establish that `format` appears in the
2674/// advertised capabilities.
2675///
2676/// # Errors
2677///
2678/// Propagates [`SurfaceConfigDeferredReason::NoFormats`] or
2679/// [`SurfaceConfigDeferredReason::NoPresentModes`] from
2680/// [`build_surface_config`]. Format compatibility itself is a caller invariant.
2681fn build_surface_config_for_format(
2682    capabilities: &wgpu::SurfaceCapabilities,
2683    size: PhysicalExtent,
2684    desired_maximum_frame_latency: u32,
2685    transparent: bool,
2686    format: wgpu::TextureFormat,
2687) -> Result<wgpu::SurfaceConfiguration, SurfaceConfigDeferredReason> {
2688    let mut config = build_surface_config(
2689        capabilities,
2690        size,
2691        desired_maximum_frame_latency,
2692        transparent,
2693    )?;
2694    config.format = format;
2695    Ok(config)
2696}
2697
2698/// Builds a concrete presentation configuration from advertised capabilities.
2699///
2700/// `size` is in physical pixels and each zero dimension is clamped to one.
2701/// `desired_maximum_frame_latency` is forwarded verbatim to wgpu; this helper
2702/// does not impose a minimum. The output uses no additional view formats.
2703///
2704/// # Errors
2705///
2706/// Returns [`SurfaceConfigDeferredReason::NoFormats`] when no formats are
2707/// advertised, or [`SurfaceConfigDeferredReason::NoPresentModes`] when no
2708/// concrete presentation mode is advertised.
2709///
2710/// # Examples
2711///
2712/// ```
2713/// use ailloli_ui_render_wgpu::{
2714///     pipeline_cache::build_surface_config,
2715///     PhysicalExtent,
2716/// };
2717///
2718/// let caps = wgpu::SurfaceCapabilities {
2719///     formats: vec![wgpu::TextureFormat::Bgra8UnormSrgb],
2720///     present_modes: vec![wgpu::PresentMode::Fifo],
2721///     alpha_modes: vec![wgpu::CompositeAlphaMode::Opaque],
2722///     usages: wgpu::TextureUsages::RENDER_ATTACHMENT,
2723/// };
2724/// let config = build_surface_config(&caps, PhysicalExtent::new(0, 720), 2, false)?;
2725/// assert_eq!((config.width, config.height), (1, 720));
2726/// assert_eq!(config.desired_maximum_frame_latency, 2);
2727/// # Ok::<(), ailloli_ui_render_wgpu::SurfaceConfigDeferredReason>(())
2728/// ```
2729pub fn build_surface_config(
2730    caps: &wgpu::SurfaceCapabilities,
2731    size: PhysicalExtent,
2732    desired_maximum_frame_latency: u32,
2733    transparent: bool,
2734) -> Result<wgpu::SurfaceConfiguration, SurfaceConfigDeferredReason> {
2735    if let Some(reason) = surface_config_deferred_reason(caps) {
2736        return Err(reason);
2737    }
2738    let format = choose_surface_format(caps).ok_or(SurfaceConfigDeferredReason::NoFormats)?;
2739    let present_mode = choose_present_mode(&caps.present_modes)
2740        .ok_or(SurfaceConfigDeferredReason::NoPresentModes)?;
2741    let alpha_mode = choose_alpha_mode(&caps.alpha_modes, transparent);
2742
2743    Ok(wgpu::SurfaceConfiguration {
2744        usage: choose_surface_usage(caps.usages),
2745        format,
2746        width: size.width.max(1),
2747        height: size.height.max(1),
2748        present_mode,
2749        alpha_mode,
2750        view_formats: vec![],
2751        desired_maximum_frame_latency,
2752    })
2753}
2754
2755/// Reports whether surface capabilities are temporarily insufficient.
2756///
2757/// Empty formats take precedence over absent concrete presentation modes.
2758/// Alpha modes and usages do not defer configuration because selection helpers
2759/// provide safe fallbacks for those fields.
2760///
2761/// # Examples
2762///
2763/// ```
2764/// use ailloli_ui_render_wgpu::pipeline_cache::surface_config_deferred_reason;
2765/// use ailloli_ui_render_wgpu::SurfaceConfigDeferredReason;
2766///
2767/// assert_eq!(
2768///     surface_config_deferred_reason(&wgpu::SurfaceCapabilities::default()),
2769///     Some(SurfaceConfigDeferredReason::NoFormats)
2770/// );
2771/// ```
2772pub fn surface_config_deferred_reason(
2773    caps: &wgpu::SurfaceCapabilities,
2774) -> Option<SurfaceConfigDeferredReason> {
2775    if caps.formats.is_empty() {
2776        return Some(SurfaceConfigDeferredReason::NoFormats);
2777    }
2778    if choose_present_mode(&caps.present_modes).is_none() {
2779        return Some(SurfaceConfigDeferredReason::NoPresentModes);
2780    }
2781    None
2782}
2783
2784/// Returns milliseconds since the Unix epoch for benchmark event timestamps.
2785///
2786/// Clocks before the epoch map to zero rather than panicking.
2787fn now_ms() -> u128 {
2788    use std::time::{SystemTime, UNIX_EPOCH};
2789    SystemTime::now()
2790        .duration_since(UNIX_EPOCH)
2791        .unwrap_or_default()
2792        .as_millis()
2793}
2794
2795#[cfg(test)]
2796/// Exercises pure surface-policy selection and attachment state transitions.
2797mod tests {
2798    use super::*;
2799
2800    /// Creates a representative capability set with caller-selected present modes.
2801    fn caps_with_present_modes(modes: Vec<wgpu::PresentMode>) -> wgpu::SurfaceCapabilities {
2802        wgpu::SurfaceCapabilities {
2803            formats: vec![
2804                wgpu::TextureFormat::Bgra8Unorm,
2805                wgpu::TextureFormat::Bgra8UnormSrgb,
2806            ],
2807            present_modes: modes,
2808            alpha_modes: vec![wgpu::CompositeAlphaMode::Opaque],
2809            usages: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2810        }
2811    }
2812
2813    #[test]
2814    fn present_mode_empty_capabilities_defer_instead_of_autovsync() {
2815        let caps = caps_with_present_modes(Vec::new());
2816
2817        let result = build_surface_config(&caps, PhysicalExtent::new(100, 100), 1, false);
2818
2819        assert_eq!(result, Err(SurfaceConfigDeferredReason::NoPresentModes));
2820    }
2821
2822    #[test]
2823    fn present_mode_prefers_fifo_when_available() {
2824        let modes = vec![wgpu::PresentMode::Immediate, wgpu::PresentMode::Fifo];
2825
2826        assert_eq!(choose_present_mode(&modes), Some(wgpu::PresentMode::Fifo));
2827    }
2828
2829    #[test]
2830    fn surface_usage_includes_copy_src_only_when_supported() {
2831        assert_eq!(
2832            choose_surface_usage(wgpu::TextureUsages::RENDER_ATTACHMENT),
2833            wgpu::TextureUsages::RENDER_ATTACHMENT
2834        );
2835        assert_eq!(
2836            choose_surface_usage(
2837                wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
2838            ),
2839            wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
2840        );
2841    }
2842
2843    #[test]
2844    fn alpha_mode_prefers_opaque_for_non_transparent_surfaces() {
2845        let modes = vec![
2846            wgpu::CompositeAlphaMode::PreMultiplied,
2847            wgpu::CompositeAlphaMode::Opaque,
2848        ];
2849
2850        assert_eq!(
2851            choose_alpha_mode(&modes, false),
2852            wgpu::CompositeAlphaMode::Opaque
2853        );
2854    }
2855
2856    #[test]
2857    fn alpha_mode_prefers_premultiplied_for_transparent_surfaces() {
2858        let mut caps = caps_with_present_modes(vec![wgpu::PresentMode::Fifo]);
2859        caps.alpha_modes = vec![
2860            wgpu::CompositeAlphaMode::Opaque,
2861            wgpu::CompositeAlphaMode::PostMultiplied,
2862            wgpu::CompositeAlphaMode::PreMultiplied,
2863        ];
2864
2865        let config =
2866            build_surface_config(&caps, PhysicalExtent::new(100, 100), 1, true).expect("config");
2867
2868        assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::PreMultiplied);
2869    }
2870
2871    #[test]
2872    fn alpha_mode_falls_back_to_opaque_when_transparency_is_not_supported() {
2873        let caps = caps_with_present_modes(vec![wgpu::PresentMode::Fifo]);
2874
2875        let config =
2876            build_surface_config(&caps, PhysicalExtent::new(100, 100), 1, true).expect("config");
2877
2878        assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::Opaque);
2879    }
2880
2881    #[test]
2882    fn present_mode_ignores_automatic_modes() {
2883        let modes = vec![wgpu::PresentMode::AutoVsync, wgpu::PresentMode::Mailbox];
2884
2885        assert_eq!(
2886            choose_present_mode(&modes),
2887            Some(wgpu::PresentMode::Mailbox)
2888        );
2889        assert_eq!(choose_present_mode(&[wgpu::PresentMode::AutoVsync]), None);
2890    }
2891
2892    #[test]
2893    fn adapter_bootstrap_rank_prefers_discrete_then_integrated_then_cpu() {
2894        assert!(
2895            super::adapter_bootstrap_rank(wgpu::DeviceType::DiscreteGpu)
2896                < super::adapter_bootstrap_rank(wgpu::DeviceType::IntegratedGpu)
2897        );
2898        assert!(
2899            super::adapter_bootstrap_rank(wgpu::DeviceType::IntegratedGpu)
2900                < super::adapter_bootstrap_rank(wgpu::DeviceType::VirtualGpu)
2901        );
2902        assert!(
2903            super::adapter_bootstrap_rank(wgpu::DeviceType::VirtualGpu)
2904                < super::adapter_bootstrap_rank(wgpu::DeviceType::Cpu)
2905        );
2906    }
2907
2908    #[test]
2909    fn same_size_resize_skips_but_surface_recovery_forces_configure() {
2910        let size = PhysicalExtent::new(1280, 720);
2911
2912        assert!(!surface_configure_required(
2913            size,
2914            size,
2915            SurfaceConfigureMode::Resize
2916        ));
2917        assert!(surface_configure_required(
2918            size,
2919            size,
2920            SurfaceConfigureMode::Force
2921        ));
2922    }
2923
2924    #[test]
2925    fn attachment_slot_transitions_detach_and_reattach_without_native_handles() {
2926        let mut slot = SurfaceAttachmentSlot::attached("first");
2927
2928        assert_eq!(slot.state(), SurfaceAttachmentState::Attached);
2929        assert_eq!(slot.detach(), Some("first"));
2930        assert_eq!(slot.state(), SurfaceAttachmentState::Detached);
2931        assert_eq!(slot.detach(), None);
2932        assert_eq!(slot.attach("second"), None);
2933        assert_eq!(slot.state(), SurfaceAttachmentState::Attached);
2934        assert_eq!(slot.as_ref(), Some(&"second"));
2935    }
2936
2937    #[test]
2938    fn surface_context_reuse_requires_adapter_and_pipeline_format_compatibility() {
2939        let caps = caps_with_present_modes(vec![wgpu::PresentMode::Fifo]);
2940
2941        assert_eq!(
2942            surface_context_reuse_failure(true, wgpu::TextureFormat::Bgra8UnormSrgb, &caps),
2943            None
2944        );
2945        assert_eq!(
2946            surface_context_reuse_failure(false, wgpu::TextureFormat::Bgra8UnormSrgb, &caps),
2947            Some(SurfaceContextReuseFailure::AdapterUnsupported)
2948        );
2949        assert_eq!(
2950            surface_context_reuse_failure(true, wgpu::TextureFormat::Rgba16Float, &caps),
2951            Some(SurfaceContextReuseFailure::FormatUnsupported)
2952        );
2953    }
2954
2955    #[test]
2956    fn surface_context_reuse_defers_empty_capabilities_before_format_check() {
2957        let caps = wgpu::SurfaceCapabilities::default();
2958
2959        assert_eq!(
2960            surface_context_reuse_failure(true, wgpu::TextureFormat::Bgra8UnormSrgb, &caps),
2961            Some(SurfaceContextReuseFailure::CapabilitiesDeferred(
2962                SurfaceConfigDeferredReason::NoFormats
2963            ))
2964        );
2965    }
2966
2967    #[test]
2968    fn reusable_surface_config_keeps_the_existing_pipeline_format() {
2969        let caps = caps_with_present_modes(vec![wgpu::PresentMode::Fifo]);
2970        let config = build_surface_config_for_format(
2971            &caps,
2972            PhysicalExtent::new(640, 480),
2973            1,
2974            false,
2975            wgpu::TextureFormat::Bgra8Unorm,
2976        )
2977        .expect("surface config");
2978
2979        assert_eq!(config.format, wgpu::TextureFormat::Bgra8Unorm);
2980        assert_eq!((config.width, config.height), (640, 480));
2981    }
2982}