Skip to main content

frust_gpu/
surface.rs

1//! Swapchain surfaces: creation, cross-thread hand-off, alpha-mode policy and
2//! configuration.
3//!
4//! Three types cover the whole life of a swapchain, in the order a shell meets
5//! them:
6//!
7//! 1. [`SurfaceFactory`] — a cheap clone of the `wgpu::Instance` that creates a
8//!    surface **on the thread the windowing backend requires** (the main/UI
9//!    thread on winit/AppKit).
10//! 2. [`DetachedSurface`] — the created-but-unconfigured surface, opaque and
11//!    `Send`, moved to whichever thread owns the renderer.
12//! 3. [`ConfiguredSurface`] — the surface plus the swapchain configuration it
13//!    was brought up with, and the one derived answer everything downstream
14//!    keys off: whether it *actually* came up translucent
15//!    ([`ConfiguredSurface::resolved_translucent`]).
16//!
17//! Every decision this module makes — which composite alpha mode to resolve
18//! ([`resolve_alpha_mode`]), which swapchain format to pick
19//! ([`select_surface_format`]), what the configuration should be
20//! ([`surface_config`]) — is a pure function over plain values, so it is
21//! host-testable with neither a GPU nor a window server. Only
22//! [`ConfiguredSurface::configure`]/[`ConfiguredSurface::reconfigure`]/
23//! [`ConfiguredSurface::resize`] touch a live device.
24//!
25//! # Device seam
26//!
27//! This module speaks `wgpu` primitives directly — a `&wgpu::Instance` to
28//! create surfaces from, a `&wgpu::Device` to configure them against — rather
29//! than an owning context type. Anything that pools the instance/adapter/device
30//! sits above these entry points and passes its handles in, so nothing here
31//! needs to know how that pooling works.
32//!
33//! # Render-attachment only
34//!
35//! The swapchain is configured with `RENDER_ATTACHMENT` and nothing else: the
36//! engine draws the frame through an ordinary render pass, in whichever of
37//! [`SURFACE_FORMATS`] the platform reports first. There is no
38//! compute-storage-write path here, so no surface has to carry
39//! `STORAGE_BINDING` and no surface is pinned to a single format to satisfy a
40//! compute target.
41
42use anyhow::{Result, anyhow};
43
44/// Every swapchain format [`select_surface_format`] will configure a surface
45/// with — a membership set, not a preference order.
46///
47/// Both are 8-bit-per-channel unorm formats the engine's pipelines are built
48/// for, and the engine warms itself for whichever one the surface hands over,
49/// so neither is privileged. Which of the two a platform reporting both lands
50/// on is the *surface's* own preference order — see [`select_surface_format`].
51pub const SURFACE_FORMATS: [wgpu::TextureFormat; 2] = [
52    wgpu::TextureFormat::Rgba8Unorm,
53    wgpu::TextureFormat::Bgra8Unorm,
54];
55
56/// How many frames the swapchain may have in flight before `get_current_texture`
57/// blocks. Two is wgpu's own default and the value every frust surface has
58/// shipped with: one frame being presented while the next is recorded.
59const DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2;
60
61/// What a caller wants of the surface's alpha, expressed without naming a
62/// `wgpu` type — the public, platform-independent request a shell makes.
63///
64/// **This is a request, not an outcome.** A `TranslucentPreferred` surface on a
65/// platform that advertises no translucent composite mode silently resolves
66/// opaque; read [`ConfiguredSurface::resolved_translucent`] after configuration
67/// rather than keying a paint contract off the request, or a hole-punched view
68/// presents black rectangles on an opaque swapchain.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum SurfaceAlphaRequest {
71    /// An ordinary opaque window: `wgpu::CompositeAlphaMode::Auto`, whatever the
72    /// surface reports.
73    Opaque,
74    /// Prefer a translucent alpha-compositing mode, so content behind the
75    /// surface shows through where the frame's alpha is below 1 — the mode a
76    /// hole-punched native view underneath the frame needs.
77    TranslucentPreferred,
78}
79
80/// Resolves a caller's [`SurfaceAlphaRequest`] against the live surface's
81/// reported `alpha_modes`, choosing the actual `wgpu::CompositeAlphaMode` to
82/// configure with.
83///
84/// `Opaque` resolves to `Auto` without consulting the surface at all.
85/// `TranslucentPreferred` tries, in order, `Inherit` (Android's only reported
86/// translucent mode), `PostMultiplied` (iOS's translucent mode), then
87/// `PreMultiplied` — falling back to `Auto` with a `log::warn!` when none of the
88/// three is in `capabilities.alpha_modes` (translucency is simply unavailable
89/// on that surface).
90pub fn resolve_alpha_mode(
91    request: SurfaceAlphaRequest,
92    capabilities: &wgpu::SurfaceCapabilities,
93) -> wgpu::CompositeAlphaMode {
94    match request {
95        SurfaceAlphaRequest::Opaque => wgpu::CompositeAlphaMode::Auto,
96        SurfaceAlphaRequest::TranslucentPreferred => {
97            const PREFERRED: [wgpu::CompositeAlphaMode; 3] = [
98                wgpu::CompositeAlphaMode::Inherit,
99                wgpu::CompositeAlphaMode::PostMultiplied,
100                wgpu::CompositeAlphaMode::PreMultiplied,
101            ];
102            PREFERRED
103                .into_iter()
104                .find(|mode| capabilities.alpha_modes.contains(mode))
105                .unwrap_or_else(|| {
106                    log::warn!(
107                        "frust-gpu: translucency requested but unavailable \
108                         (alpha_modes={:?}) — falling back to Auto (opaque)",
109                        capabilities.alpha_modes
110                    );
111                    wgpu::CompositeAlphaMode::Auto
112                })
113        }
114    }
115}
116
117/// Whether a **resolved** `wgpu::CompositeAlphaMode` actually composites the
118/// surface's alpha against what is behind it — i.e. whether the surface really
119/// came up translucent, as opposed to what the caller *requested*.
120///
121/// This is the truth behind [`ConfiguredSurface::resolved_translucent`]:
122/// [`resolve_alpha_mode`] can silently degrade a
123/// [`SurfaceAlphaRequest::TranslucentPreferred`] to `Auto` when the platform
124/// advertises no translucent mode, and a shell that kept keying its paint
125/// contract off the *request* would then clear to transparent and punch its
126/// native-view slots against an OPAQUE swapchain — presenting black rectangles.
127///
128/// The three translucent modes are exactly [`resolve_alpha_mode`]'s preference
129/// list — `Inherit` (Android), `PostMultiplied` (iOS), `PreMultiplied`;
130/// `Opaque`/`Auto` ignore the surface's alpha entirely and are therefore *not*
131/// translucent (`Auto` is what every opaque caller and every fallback resolves
132/// to).
133pub fn alpha_mode_is_translucent(mode: wgpu::CompositeAlphaMode) -> bool {
134    matches!(
135        mode,
136        wgpu::CompositeAlphaMode::Inherit
137            | wgpu::CompositeAlphaMode::PostMultiplied
138            | wgpu::CompositeAlphaMode::PreMultiplied
139    )
140}
141
142/// Whether a swapchain configured with this composite alpha mode expects the
143/// pixels it is handed to be **premultiplied**.
144///
145/// The engine writes premultiplied alpha, so this predicate now reads as "no
146/// conversion needed": the two premultiplied-expecting modes are
147/// `PreMultiplied` and — the shipped Android translucent case — `Inherit`, and
148/// both take the engine's output unchanged. On Android the only reported
149/// translucent mode is `Inherit`, under which SurfaceFlinger blends a
150/// `TRANSLUCENT` SurfaceView premultiplied; a straight-alpha frame handed to it
151/// over-brightens every partial-alpha pixel by `1/a` (measured on device: a
152/// 50%-alpha `#FFF176` reaching SurfaceFlinger stored straight `#FFF176@128`
153/// instead of premultiplied `#807B3B@128`).
154///
155/// The cost has therefore moved to the *other* side. `PostMultiplied` — iOS's
156/// translucent mode, and the one mode this returns `false` for while still
157/// being translucent ([`alpha_mode_is_straight_translucent`]) — expects
158/// STRAIGHT alpha, so a translucent iOS surface is the case that needs an
159/// un-premultiply pass over the engine's output before present.
160/// `Opaque`/`Auto` ignore alpha entirely and need nothing either way.
161pub fn alpha_mode_needs_premultiply(mode: wgpu::CompositeAlphaMode) -> bool {
162    matches!(
163        mode,
164        wgpu::CompositeAlphaMode::PreMultiplied | wgpu::CompositeAlphaMode::Inherit
165    )
166}
167
168/// Whether a **resolved** alpha mode means "translucent, and the swapchain
169/// stores STRAIGHT alpha" — the combination the engine's premultiplied output
170/// cannot be presented into unconverted.
171///
172/// True for `PostMultiplied` alone: it is translucent
173/// ([`alpha_mode_is_translucent`]) and does not expect premultiplied pixels
174/// ([`alpha_mode_needs_premultiply`] is false), which is iOS's translucent
175/// mode. `Inherit`/`PreMultiplied` are translucent *and* premultiplied, so they
176/// take the frame as it is written; `Opaque`/`Auto` ignore alpha entirely.
177///
178/// Pure and mode-only, so the decision is host-testable without a surface; the
179/// live per-surface answer is
180/// [`ConfiguredSurface::straight_alpha_translucent`], which also respects a
181/// translucency the surface never actually got.
182pub fn alpha_mode_is_straight_translucent(mode: wgpu::CompositeAlphaMode) -> bool {
183    alpha_mode_is_translucent(mode) && !alpha_mode_needs_premultiply(mode)
184}
185
186/// Whether `backend`'s compositor actually composites `mode` premultiplied,
187/// **despite** [`alpha_mode_is_straight_translucent`] answering true for it —
188/// an upstream wgpu-hal truth bug specific to one backend/mode pair, not a
189/// property of the mode alone.
190///
191/// True for `wgpu::Backend::Metal` with `CompositeAlphaMode::PostMultiplied`
192/// alone. wgpu-hal's Metal adapter advertises `PostMultiplied`
193/// (`wgpu-hal-30.0.1/src/metal/adapter.rs:468-471`,
194/// `composite_alpha_modes: [Opaque, PostMultiplied]`) but its surface
195/// configuration implements the mode as nothing beyond
196/// `render_layer.setOpaque(false)` (`wgpu-hal-30.0.1/src/metal/surface.rs:269-273`)
197/// — it never asks Core Animation to treat the layer's content as straight
198/// alpha, and Core Animation has no such mode: a `CAMetalLayer` ONLY
199/// composites premultiplied (MoltenVK's own equivalent exposes
200/// `OPAQUE | PRE_MULTIPLIED` for the identical layer, never a straight
201/// option). So a Metal `PostMultiplied` swapchain reads back exactly like a
202/// premultiplied one — `display = C·a + BG·(1−a)` — even though the mode's
203/// name and wgpu's advertised contract say straight. Handing it the
204/// spec-correct straight-alpha conversion therefore double-corrects: the
205/// frame is un-premultiplied for a compositor that was going to
206/// premultiply-composite it anyway, over-brightening every partial-alpha
207/// pixel — device-visible only at fractional alpha (an indigo/navy wash,
208/// black frames near a translucent split). See `docs/LIMITATIONS.md`'s
209/// `engine-metal-postmultiplied-truth-bug`. Re-checked at the wgpu 30.0.1
210/// pin: unchanged. Upstream trunk PR gfx-rs/wgpu#9922 (merged 2026-08-18,
211/// unreleased as of 30.0.1) makes Metal advertise `PreMultiplied` instead, so
212/// this predicate retires at the pin bump that carries it.
213///
214/// Every other backend keeps the ordinary reading: a genuinely-straight
215/// `PostMultiplied` compositor exists on at least one other backend (e.g.
216/// Vulkan's own `POST_MULTIPLIED` composite-alpha flag), so this predicate is
217/// Metal-specific rather than blanket-disbelieving the mode everywhere.
218///
219/// A **platform** fact rather than a renderer policy, which is why it sits
220/// beside [`resolve_alpha_mode`] here: which render path a renderer picks in
221/// response is the renderer's own business (see
222/// `frust_render::context::choose_engine_render_path`, the one caller).
223///
224/// Pure and (backend, mode)-only, so the decision is host-testable without a
225/// live adapter.
226pub fn compositor_expects_premultiplied(
227    backend: wgpu::Backend,
228    mode: wgpu::CompositeAlphaMode,
229) -> bool {
230    backend == wgpu::Backend::Metal && mode == wgpu::CompositeAlphaMode::PostMultiplied
231}
232
233/// Picks the swapchain format to configure with: the first format **the
234/// surface reports** that is one of [`SURFACE_FORMATS`].
235///
236/// The preference order is the *surface's*, not this module's — the platform
237/// lists its formats best-first, and taking its first supported entry is the
238/// selection every shipped frust build has configured its swapchain with
239/// (device-gated on Android/Vulkan, iOS/macOS/Metal and Windows). The engine
240/// warms its strip pipelines for whichever of the two it is handed, so neither
241/// is privileged here; reordering this to prefer `Rgba8Unorm` would silently
242/// change the swapchain format on the platforms that report `Bgra8Unorm` first.
243///
244/// Errors when the surface supports neither, which no shipping platform does —
245/// the engine draws through a render pass into whichever of the two is
246/// available and has no third format to fall back on.
247pub fn select_surface_format(
248    capabilities: &wgpu::SurfaceCapabilities,
249) -> Result<wgpu::TextureFormat> {
250    capabilities
251        .formats
252        .iter()
253        .copied()
254        .find(|format| SURFACE_FORMATS.contains(format))
255        .ok_or_else(|| {
256            anyhow!(
257                "frust-gpu: no supported surface format (Rgba8Unorm/Bgra8Unorm) in {:?}",
258                capabilities.formats
259            )
260        })
261}
262
263/// Builds the `wgpu::SurfaceConfiguration` a surface is brought up with.
264///
265/// Split out of [`ConfiguredSurface::configure`] so the configuration a surface
266/// gets is decided by a pure function over plain values and can be asserted
267/// without a device: `RENDER_ATTACHMENT` usage only, the caller's format and
268/// alpha mode verbatim, no view formats, the fixed frame latency
269/// (`DESIRED_MAXIMUM_FRAME_LATENCY`, two frames in flight), and
270/// `SurfaceColorSpace::Auto`.
271///
272/// `Auto` is the deliberate choice for the colour space, not a placeholder.
273/// It is the only value guaranteed to be supported for every format a
274/// surface advertises, and `wgpu` defines it as "reproduce the historical
275/// behaviour": `Srgb` for every non-`Rgba16Float` format, which is exactly
276/// the pair [`select_surface_format`] can return (`Rgba8Unorm`/`Bgra8Unorm`).
277/// Naming `Srgb` explicitly would resolve identically today but would start
278/// failing validation the moment a driver in HDR mode stopped advertising it
279/// for the chosen format, and frust encodes no wide-gamut or HDR output —
280/// the engine writes sRGB-encoded values a non-`*Srgb` swapchain format
281/// carries through untouched. A wide-gamut/HDR surface is a deliberate
282/// feature, not a version-bump side effect.
283///
284/// `size` is `(width, height)` in physical pixels and must be non-zero on both
285/// axes; a zero-sized swapchain is a `wgpu` validation error, and the shell's
286/// own resize path is where a minimized/zero-extent window is filtered out.
287pub fn surface_config(
288    format: wgpu::TextureFormat,
289    alpha_mode: wgpu::CompositeAlphaMode,
290    size: (u32, u32),
291    present_mode: wgpu::PresentMode,
292) -> wgpu::SurfaceConfiguration {
293    let (width, height) = size;
294    wgpu::SurfaceConfiguration {
295        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
296        format,
297        color_space: wgpu::SurfaceColorSpace::Auto,
298        width,
299        height,
300        present_mode,
301        desired_maximum_frame_latency: DESIRED_MAXIMUM_FRAME_LATENCY,
302        alpha_mode,
303        view_formats: vec![],
304    }
305}
306
307/// A cheap, cloneable handle to a `wgpu::Instance` used to create a surface on
308/// a *different* thread than the one that owns the renderer.
309///
310/// # Why this exists
311///
312/// `wgpu` surface creation reads the platform window handle, which several
313/// windowing backends (notably winit on macOS/AppKit) only make available on
314/// the main/UI thread. A render-thread split therefore cannot create the
315/// surface where the renderer lives; instead the UI thread creates a
316/// [`DetachedSurface`] via this factory and hands it across to the render
317/// thread, which configures it there. A `wgpu::Instance` is `Send + Sync +
318/// Clone` (Arc-backed) and a `Surface` it produces stays compatible with any
319/// adapter/device the cloned instance requests, so the two threads share one
320/// instance with no `unsafe`.
321///
322/// The mobile shells do not need this: they receive a platform-created surface
323/// pointer and go through
324/// [`create_android_surface`](crate::lifecycle::create_android_surface) /
325/// `create_metal_surface` instead.
326#[derive(Clone)]
327pub struct SurfaceFactory {
328    instance: wgpu::Instance,
329}
330
331impl SurfaceFactory {
332    /// Clones `instance` into a factory that can be moved to the UI thread.
333    pub fn new(instance: &wgpu::Instance) -> Self {
334        Self {
335            instance: instance.clone(),
336        }
337    }
338
339    /// Create a [`DetachedSurface`] from a window handle **on the calling
340    /// thread** — call this on the thread the windowing backend requires (the
341    /// main/UI thread for winit). The returned surface is `Send` and may then be
342    /// moved to the render thread and configured there.
343    ///
344    /// This performs *only* the window-handle-dependent step (surface creation);
345    /// the device and the swapchain configuration are both built later, on the
346    /// configuring thread, so nothing here touches a GPU device.
347    pub fn create_detached_surface(
348        &self,
349        target: impl Into<wgpu::SurfaceTarget<'static>>,
350    ) -> Result<DetachedSurface> {
351        let surface = self
352            .instance
353            .create_surface(target)
354            .map_err(|e| anyhow!("frust-gpu: failed to create surface: {e}"))?;
355        Ok(DetachedSurface { surface })
356    }
357}
358
359/// A created-but-not-yet-configured `wgpu::Surface`, produced by
360/// [`SurfaceFactory::create_detached_surface`] on the windowing thread and
361/// configured on the render thread.
362///
363/// Opaque on purpose: a shell only ever moves this value across a thread
364/// boundary, so the wrapped `wgpu` type stays out of every signature above this
365/// crate. `Send`, which is the whole point.
366pub struct DetachedSurface {
367    surface: wgpu::Surface<'static>,
368}
369
370impl DetachedSurface {
371    /// Consume the wrapper, yielding the raw surface to configure.
372    ///
373    /// Hidden from the rendered docs rather than made private: the renderer
374    /// crate above this one is the intended (and only) caller, and no layer
375    /// above *it* may re-export this accessor — doing so would make the wrapped
376    /// `wgpu::Surface` nameable from shell and widget code, which is exactly
377    /// what the wrapper exists to prevent.
378    #[doc(hidden)]
379    pub fn into_surface(self) -> wgpu::Surface<'static> {
380        self.surface
381    }
382}
383
384/// A configured swapchain surface: the surface itself, the configuration it was
385/// brought up with, and whether it actually came up translucent.
386pub struct ConfiguredSurface {
387    surface: wgpu::Surface<'static>,
388    config: wgpu::SurfaceConfiguration,
389    /// Whether this surface **actually** came up translucent — the projection of
390    /// `config.alpha_mode` through [`alpha_mode_is_translucent`], computed once
391    /// at configure time (the mode never changes for a live surface; a resize
392    /// reconfigures with the same mode).
393    ///
394    /// Stored as a plain `bool` rather than re-derived from `config.alpha_mode`
395    /// at each read, so the value a shell observes crosses this crate's boundary
396    /// with no `wgpu` type in the signature.
397    resolved_translucent: bool,
398}
399
400impl ConfiguredSurface {
401    /// Configures `surface` against `device` and returns the wrapper.
402    ///
403    /// `format` is the format the caller resolved from what the *surface*
404    /// reports ([`select_surface_format`]) rather than one this layer imposes,
405    /// and `alpha_mode` the one [`resolve_alpha_mode`] resolved from the
406    /// surface's reported modes. `size` is `(width, height)` in physical pixels
407    /// and must be non-zero on both axes.
408    pub fn configure(
409        surface: wgpu::Surface<'static>,
410        device: &wgpu::Device,
411        format: wgpu::TextureFormat,
412        alpha_mode: wgpu::CompositeAlphaMode,
413        size: (u32, u32),
414        present_mode: wgpu::PresentMode,
415    ) -> Self {
416        let config = surface_config(format, alpha_mode, size, present_mode);
417        surface.configure(device, &config);
418        Self {
419            surface,
420            config,
421            resolved_translucent: alpha_mode_is_translucent(alpha_mode),
422        }
423    }
424
425    /// Re-applies the current configuration to the swapchain — the recovery step
426    /// an `Outdated` (or retried `Invalid`) acquire asks for, where the surface
427    /// is stale but its geometry has not changed.
428    pub fn reconfigure(&self, device: &wgpu::Device) {
429        self.surface.configure(device, &self.config);
430    }
431
432    /// Resizes the swapchain in place. `width`/`height` are physical pixels and
433    /// must both be non-zero; the shell's resize path filters a minimized or
434    /// zero-extent window out before reaching here.
435    pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) {
436        self.config.width = width;
437        self.config.height = height;
438        self.reconfigure(device);
439    }
440
441    /// The live surface, for acquiring a swapchain texture.
442    pub fn surface(&self) -> &wgpu::Surface<'static> {
443        &self.surface
444    }
445
446    /// The configuration this surface is currently up with.
447    pub fn config(&self) -> &wgpu::SurfaceConfiguration {
448        &self.config
449    }
450
451    /// The swapchain's `(width, height)` in physical pixels.
452    pub fn size(&self) -> (u32, u32) {
453        (self.config.width, self.config.height)
454    }
455
456    /// Whether this surface really came up translucent — the answer a shell's
457    /// paint contract keys off, never the original
458    /// [`SurfaceAlphaRequest`].
459    pub fn resolved_translucent(&self) -> bool {
460        self.resolved_translucent
461    }
462
463    /// Whether this surface really came up translucent with a swapchain that
464    /// stores STRAIGHT alpha — iOS's `PostMultiplied` translucent mode, the one
465    /// configuration whose present needs the engine's premultiplied output
466    /// converted back.
467    ///
468    /// Reads [`Self::resolved_translucent`] rather than the raw alpha mode, so a
469    /// surface that never got the translucency it asked for answers `false`.
470    pub fn straight_alpha_translucent(&self) -> bool {
471        self.resolved_translucent && alpha_mode_is_straight_translucent(self.config.alpha_mode)
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    /// Builds a synthetic `wgpu::SurfaceCapabilities` reporting only the given
480    /// `alpha_modes` — the rest of the struct is irrelevant to
481    /// `resolve_alpha_mode`, which reads only that one field.
482    fn caps_with_alpha_modes(
483        alpha_modes: &[wgpu::CompositeAlphaMode],
484    ) -> wgpu::SurfaceCapabilities {
485        wgpu::SurfaceCapabilities {
486            alpha_modes: alpha_modes.to_vec(),
487            ..Default::default()
488        }
489    }
490
491    /// The same for the format list `select_surface_format` reads.
492    fn caps_with_formats(formats: &[wgpu::TextureFormat]) -> wgpu::SurfaceCapabilities {
493        wgpu::SurfaceCapabilities {
494            formats: formats.to_vec(),
495            ..Default::default()
496        }
497    }
498
499    #[test]
500    fn opaque_request_always_resolves_to_auto() {
501        // `Opaque` never consults `alpha_modes` — the same answer regardless of
502        // what the surface reports.
503        for modes in [
504            [wgpu::CompositeAlphaMode::Inherit].as_slice(),
505            &[
506                wgpu::CompositeAlphaMode::Opaque,
507                wgpu::CompositeAlphaMode::PostMultiplied,
508            ],
509            &[wgpu::CompositeAlphaMode::Opaque],
510        ] {
511            let caps = caps_with_alpha_modes(modes);
512            assert_eq!(
513                resolve_alpha_mode(SurfaceAlphaRequest::Opaque, &caps),
514                wgpu::CompositeAlphaMode::Auto
515            );
516        }
517    }
518
519    #[test]
520    fn translucent_preferred_picks_inherit_first() {
521        // Android's observed shape: `Inherit` is the only reported mode.
522        let caps = caps_with_alpha_modes(&[wgpu::CompositeAlphaMode::Inherit]);
523        assert_eq!(
524            resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps),
525            wgpu::CompositeAlphaMode::Inherit
526        );
527    }
528
529    #[test]
530    fn translucent_preferred_picks_post_multiplied_when_inherit_absent() {
531        // iOS's observed shape: `[Opaque, PostMultiplied]` — no `Inherit`.
532        let caps = caps_with_alpha_modes(&[
533            wgpu::CompositeAlphaMode::Opaque,
534            wgpu::CompositeAlphaMode::PostMultiplied,
535        ]);
536        assert_eq!(
537            resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps),
538            wgpu::CompositeAlphaMode::PostMultiplied
539        );
540    }
541
542    #[test]
543    fn translucent_preferred_falls_back_to_pre_multiplied_last() {
544        // Neither `Inherit` nor `PostMultiplied` present, but `PreMultiplied`
545        // is — the third preference in the resolution order.
546        let caps = caps_with_alpha_modes(&[
547            wgpu::CompositeAlphaMode::Opaque,
548            wgpu::CompositeAlphaMode::PreMultiplied,
549        ]);
550        assert_eq!(
551            resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps),
552            wgpu::CompositeAlphaMode::PreMultiplied
553        );
554    }
555
556    #[test]
557    fn translucent_preferred_falls_back_to_auto_when_none_available() {
558        // A surface reporting only `Opaque` (no `Inherit`/`PostMultiplied`/
559        // `PreMultiplied`) can't satisfy translucency — fall back to `Auto`
560        // rather than erroring.
561        let caps = caps_with_alpha_modes(&[wgpu::CompositeAlphaMode::Opaque]);
562        assert_eq!(
563            resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps),
564            wgpu::CompositeAlphaMode::Auto
565        );
566    }
567
568    #[test]
569    fn resolved_translucency_is_true_only_for_the_three_translucent_modes() {
570        // The projection a shell's paint contract keys off: exactly
571        // `resolve_alpha_mode`'s preference list.
572        for mode in [
573            wgpu::CompositeAlphaMode::Inherit,
574            wgpu::CompositeAlphaMode::PostMultiplied,
575            wgpu::CompositeAlphaMode::PreMultiplied,
576        ] {
577            assert!(alpha_mode_is_translucent(mode), "{mode:?}");
578        }
579        // `Auto` is what BOTH an opaque request and a failed translucent
580        // resolution land on — neither composites alpha.
581        for mode in [
582            wgpu::CompositeAlphaMode::Auto,
583            wgpu::CompositeAlphaMode::Opaque,
584        ] {
585            assert!(!alpha_mode_is_translucent(mode), "{mode:?}");
586        }
587    }
588
589    #[test]
590    fn premultiplied_expecting_modes_take_engine_output_unchanged() {
591        // The engine writes premultiplied alpha, so these two are the modes that
592        // need no conversion at all.
593        for mode in [
594            wgpu::CompositeAlphaMode::PreMultiplied,
595            wgpu::CompositeAlphaMode::Inherit,
596        ] {
597            assert!(alpha_mode_needs_premultiply(mode), "{mode:?}");
598            assert!(!alpha_mode_is_straight_translucent(mode), "{mode:?}");
599        }
600        // Alpha-ignoring modes never expect premultiplied pixels either.
601        for mode in [
602            wgpu::CompositeAlphaMode::Auto,
603            wgpu::CompositeAlphaMode::Opaque,
604        ] {
605            assert!(!alpha_mode_needs_premultiply(mode), "{mode:?}");
606        }
607    }
608
609    /// Over every mode `resolve_alpha_mode` can produce, `PostMultiplied` alone
610    /// is translucent AND straight-alpha — the one surface that has to convert
611    /// the engine's premultiplied output before present.
612    #[test]
613    fn only_post_multiplied_is_translucent_with_straight_alpha() {
614        assert!(alpha_mode_is_straight_translucent(
615            wgpu::CompositeAlphaMode::PostMultiplied
616        ));
617        assert!(!alpha_mode_needs_premultiply(
618            wgpu::CompositeAlphaMode::PostMultiplied
619        ));
620        // Opaque destinations are not translucent at all, so they are not the
621        // straight-translucent case however their alpha is stored.
622        for mode in [
623            wgpu::CompositeAlphaMode::Auto,
624            wgpu::CompositeAlphaMode::Opaque,
625        ] {
626            assert!(!alpha_mode_is_straight_translucent(mode), "{mode:?}");
627        }
628    }
629
630    #[test]
631    fn the_surfaces_own_order_decides_when_both_formats_are_reported() {
632        // The shipped selection: the platform lists its formats best-first and
633        // the first supported entry wins, whichever of the two that is. This is
634        // what every device-gated build configured its swapchain with; picking
635        // by THIS module's order instead would flip the format on platforms
636        // that report `Bgra8Unorm` first.
637        let bgra_first = caps_with_formats(&[
638            wgpu::TextureFormat::Bgra8Unorm,
639            wgpu::TextureFormat::Rgba8Unorm,
640        ]);
641        assert_eq!(
642            select_surface_format(&bgra_first).unwrap(),
643            wgpu::TextureFormat::Bgra8Unorm,
644            "the surface's own preference order decides, not this module's"
645        );
646        let rgba_first = caps_with_formats(&[
647            wgpu::TextureFormat::Rgba8Unorm,
648            wgpu::TextureFormat::Bgra8Unorm,
649        ]);
650        assert_eq!(
651            select_surface_format(&rgba_first).unwrap(),
652            wgpu::TextureFormat::Rgba8Unorm
653        );
654    }
655
656    #[test]
657    fn unsupported_formats_are_skipped_over_rather_than_taken() {
658        // The shape a Metal/Dx12 surface reports: an sRGB variant first, which
659        // is not one of ours, then the plain `Bgra8Unorm` that is.
660        let caps = caps_with_formats(&[
661            wgpu::TextureFormat::Bgra8UnormSrgb,
662            wgpu::TextureFormat::Bgra8Unorm,
663        ]);
664        assert_eq!(
665            select_surface_format(&caps).unwrap(),
666            wgpu::TextureFormat::Bgra8Unorm
667        );
668    }
669
670    #[test]
671    fn a_surface_supporting_neither_format_is_an_error() {
672        let caps = caps_with_formats(&[wgpu::TextureFormat::Rgba16Float]);
673        assert!(select_surface_format(&caps).is_err());
674    }
675
676    #[test]
677    fn config_asks_for_render_attachment_only() {
678        // The engine draws the frame through a render pass; nothing here writes
679        // the swapchain through a compute storage binding, so no surface has to
680        // carry `STORAGE_BINDING`.
681        let config = surface_config(
682            wgpu::TextureFormat::Bgra8Unorm,
683            wgpu::CompositeAlphaMode::Auto,
684            (800, 600),
685            wgpu::PresentMode::AutoVsync,
686        );
687        assert_eq!(config.usage, wgpu::TextureUsages::RENDER_ATTACHMENT);
688    }
689
690    #[test]
691    fn config_carries_the_callers_format_alpha_mode_and_size() {
692        // Both `SURFACE_FORMATS` entries configure verbatim: the format comes
693        // from what the surface reported, not from a fixed engine target format.
694        for format in SURFACE_FORMATS {
695            let config = surface_config(
696                format,
697                wgpu::CompositeAlphaMode::Inherit,
698                (1080, 2400),
699                wgpu::PresentMode::Fifo,
700            );
701            assert_eq!(config.format, format);
702            assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::Inherit);
703            assert_eq!((config.width, config.height), (1080, 2400));
704            assert_eq!(config.present_mode, wgpu::PresentMode::Fifo);
705            assert!(config.view_formats.is_empty());
706            assert_eq!(
707                config.desired_maximum_frame_latency,
708                DESIRED_MAXIMUM_FRAME_LATENCY
709            );
710            // The colour space is a deliberate constant, not caller-supplied:
711            // `Auto` is supported for every advertised format and resolves to
712            // the sRGB behaviour every shipped frust build has presented in.
713            assert_eq!(config.color_space, wgpu::SurfaceColorSpace::Auto);
714        }
715    }
716
717    #[test]
718    fn forced_mismatch_translucent_request_resolves_not_translucent() {
719        // A surface whose advertised capabilities carry NO translucent mode,
720        // asked for translucency. The request is honoured as far as it can be
721        // (`Auto`), but the RESOLVED translucency — the value
722        // `SurfaceRenderer::surface_resolved_translucent` hands the shells — is
723        // `false`, so the shells keep the opaque (Mode A) paint contract: an
724        // opaque base colour and no `ClearRect` punch.
725        for modes in [
726            [wgpu::CompositeAlphaMode::Opaque].as_slice(),
727            &[wgpu::CompositeAlphaMode::Auto],
728            &[],
729        ] {
730            let caps = caps_with_alpha_modes(modes);
731            let resolved = resolve_alpha_mode(SurfaceAlphaRequest::TranslucentPreferred, &caps);
732            assert_eq!(resolved, wgpu::CompositeAlphaMode::Auto, "{modes:?}");
733            assert!(
734                !alpha_mode_is_translucent(resolved),
735                "a fallback-to-opaque surface must never report translucent ({modes:?})"
736            );
737        }
738    }
739
740    #[test]
741    fn happy_path_translucent_request_resolves_translucent() {
742        // The shipped configs stay unchanged: Android (`Inherit`-only) and iOS
743        // (`[Opaque, PostMultiplied]`) both resolve to a translucent mode, so
744        // the punch + transparent base keep running exactly as today.
745        for modes in [
746            [wgpu::CompositeAlphaMode::Inherit].as_slice(),
747            &[
748                wgpu::CompositeAlphaMode::Opaque,
749                wgpu::CompositeAlphaMode::PostMultiplied,
750            ],
751        ] {
752            let resolved = resolve_alpha_mode(
753                SurfaceAlphaRequest::TranslucentPreferred,
754                &caps_with_alpha_modes(modes),
755            );
756            assert!(alpha_mode_is_translucent(resolved), "{modes:?}");
757        }
758        // An opaque request never reports translucent, whatever the surface
759        // advertises.
760        assert!(!alpha_mode_is_translucent(resolve_alpha_mode(
761            SurfaceAlphaRequest::Opaque,
762            &caps_with_alpha_modes(&[wgpu::CompositeAlphaMode::Inherit]),
763        )));
764    }
765
766    /// Every composite alpha mode a surface can resolve to, so a claim about
767    /// the truth bug is made across the whole space rather than the modes it
768    /// was written for.
769    const EVERY_ALPHA_MODE: [wgpu::CompositeAlphaMode; 5] = [
770        wgpu::CompositeAlphaMode::Auto,
771        wgpu::CompositeAlphaMode::Opaque,
772        wgpu::CompositeAlphaMode::Inherit,
773        wgpu::CompositeAlphaMode::PreMultiplied,
774        wgpu::CompositeAlphaMode::PostMultiplied,
775    ];
776
777    #[test]
778    fn compositor_expects_premultiplied_is_metal_post_multiplied_only() {
779        // Direct guard on the predicate itself, across the whole (backend,
780        // mode) space: the upstream wgpu-hal truth bug is one backend/mode
781        // pair, and must never spread to another backend or another mode.
782        for backend in wgpu::Backend::ALL {
783            for mode in EVERY_ALPHA_MODE {
784                let expected = backend == wgpu::Backend::Metal
785                    && mode == wgpu::CompositeAlphaMode::PostMultiplied;
786                assert_eq!(
787                    compositor_expects_premultiplied(backend, mode),
788                    expected,
789                    "compositor_expects_premultiplied({backend:?}, {mode:?})"
790                );
791            }
792        }
793    }
794
795    #[test]
796    fn the_truth_bug_only_ever_contradicts_a_straight_translucent_mode() {
797        // The predicate exists to override `alpha_mode_is_straight_translucent`
798        // for one pair; anywhere it answers true, that predicate must have
799        // answered true too, or it would be silently redirecting a mode that
800        // never needed conversion in the first place.
801        for backend in wgpu::Backend::ALL {
802            for mode in EVERY_ALPHA_MODE {
803                if compositor_expects_premultiplied(backend, mode) {
804                    assert!(
805                        alpha_mode_is_straight_translucent(mode),
806                        "{backend:?}/{mode:?} is contradicted without being straight-translucent"
807                    );
808                }
809            }
810        }
811    }
812}