Skip to main content

frust_render/
overlay.rs

1//! Overlay surfaces — a second, translucent wgpu surface positioned above a
2//! hosted native view, drawing a subrange of the frame's commands (render-model
3//! Phase 4, decision D10). The **segmented frame** ([`OverlayCut`],
4//! [`SurfaceRenderer::encode_segmented`], [`SurfaceRenderer::submit_segmented`])
5//! is the general path: one scene cut into the main surface's segment and one
6//! range per overlay, recorded into one encoder and submitted once. The
7//! **spike** ([`OVERLAY_SPIKE_ENV_VAR`], `FRUST_OVERLAY_SPIKE=1`) is its
8//! degenerate single fixed cut, kept as it was. A renderer that is never
9//! handed a cut (and never has the spike's split armed) draws exactly what it
10//! drew before this module existed.
11//!
12//! # What an overlay is
13//!
14//! A frame normally lands on one swapchain. A native view a host places
15//! *above* that swapchain (Mode A: macOS always, Android's default opaque
16//! surface) covers whatever frust paints under it — a tooltip, a popover or a
17//! drag shadow that should paint over the native control cannot. An overlay is
18//! a second swapchain the host stacks above the native view, cleared to
19//! transparent every frame, onto which the commands that paint *after* the
20//! native view (in display-list order) are drawn instead of onto the main
21//! surface. The main surface draws the commands before the cut; the overlay
22//! draws the commands from the cut on.
23//!
24//! # The contract a host attaches to
25//!
26//! A host (an Android `SurfaceView`, an `NSView`/`UIView` hosting a
27//! `CAMetalLayer`) supplies the platform object; this module does the rest.
28//!
29//! 1. **Create the platform object above the hosted view**, transparent and
30//!    input-transparent: it must never take a touch, a click or focus (the
31//!    native view under it and the main surface keep today's input rules).
32//!    On Apple, a `CAMetalLayer` with `opaque = NO` in a view that returns
33//!    `nil` from `hitTest:` and sits above the hosted `NSView`/`UIView`; on
34//!    Android, a translucent `SurfaceView` that is neither focusable nor
35//!    clickable.
36//! 2. **On the render thread**, with the [`RenderContext`] the main surface
37//!    was created from (its device must already exist — create the overlay
38//!    after the main surface), call [`OverlaySurface::new`] with an
39//!    [`OverlayTarget`] naming that object and its size in **physical
40//!    pixels**:
41//!    - Apple: `OverlayTarget::CoreAnimationLayer` with the `CAMetalLayer*`
42//!      (`cfg(target_vendor = "apple")`).
43//!    - Android and any raw-window-handle host:
44//!      [`OverlayTarget::RawHandle`] with the display and window handles
45//!      (Android: [`OverlayTarget::android_ndk_window`] over the
46//!      `ANativeWindow*` the `SurfaceView`'s `Surface` yields).
47//!
48//!    Pass the main [`SurfaceRenderer`] too when there is one: the overlay
49//!    then builds its pipelines through the same persisted driver cache.
50//! 3. **Every frame** — the segmented frame: hand the main surface the
51//!    frame's scene with the overlays' ranges as `cuts`
52//!    ([`SurfaceRenderer::encode_segmented`]; the ranges are the differ's
53//!    `ViewCommand::Overlay.command_range`s, ascending and disjoint), acquire
54//!    ([`SurfaceRenderer::acquire`]), then submit with one [`OverlayCut`] per
55//!    range, in the same order ([`SurfaceRenderer::submit_segmented`]): the
56//!    main segment and every overlay's range are recorded into one encoder and
57//!    submitted once, then presented main first, overlays after, in cut order.
58//!    Where every present must land in one platform transaction (iOS
59//!    `presentsWithTransaction`, a macOS `CATransaction` carrying the native
60//!    views' geometry), [`SurfaceRenderer::submit_segmented_deferred`] hands
61//!    them back as [`SegmentedPresents`] for
62//!    [`SegmentedPresents::present_all`] inside it.
63//!
64//!    The spike's single cut is the older shape and still works: arm the main
65//!    surface with [`SurfaceRenderer::set_overlay_split`] so it stops drawing
66//!    the overlay's commands, call [`OverlaySurface::render`] with
67//!    `spike_cut(scene)..scene.commands().len()` (see [`spike_cut`]) — its own
68//!    encoder and its own submit — and present it with
69//!    [`OverlaySurface::present`] right after the main surface's present (or
70//!    [`OverlaySurface::take_deferred_present`] beside the main frame's
71//!    [`DeferredPresent`]).
72//! 4. **Resize** with [`OverlaySurface::resize`] (physical pixels) whenever
73//!    the platform object's size changes.
74//! 5. **Tear down by dropping the `OverlaySurface` before the platform object
75//!    it was built on** — the same ordering the main surface keeps (drop the
76//!    surface, then release the window/layer); an un-presented frame is
77//!    dropped with it, never presented against a dead window.
78//!
79//! An overlay is full-size — the same extent and origin as the main surface,
80//! so a range's device coordinates need no offset (the differ's v1 overlay
81//! rect is the whole window).
82//!
83//! # Segments
84//!
85//! The main surface draws the complement of the cuts
86//! ([`main_segment_ranges`], projected by [`project_main_segment`]); each
87//! overlay draws its cut. Both go through
88//! [`frust_engine::renderer::project_command_ranges`]'s bracket rule, so every
89//! segment is a well-nested display list: a clip a cut falls inside clips both
90//! sides, and every drawing command of the frame lands in exactly one segment.
91//!
92//! Only a **servable** cut is taken ([`cut_is_servable`]): one whose range
93//! the overlay's engine draws exactly as the main surface would. A range
94//! holding a `DrawRetained`, a `PushRetained` bracket, a `SceneTexture` or a
95//! `BackdropBlur`, or one a retained bracket is open across (opened before
96//! the range and still open at its start, or opened inside it and still open
97//! past its end), is **kept on the main surface**: the main segment draws it
98//! exactly as an uncut frame would, under the native view, and that overlay is
99//! handed an empty range ([`CutServiceability::served_range`]) and clears to
100//! transparent — the same degrade a host whose overlay was refused already
101//! has. [`OverlayCutOutcome::serviceability`] says which cuts were kept and
102//! why. Without the rule such content would draw on no surface at all (the
103//! main segment excludes it, the overlay cannot draw it), or a retained page
104//! would be repainted on the overlay while the main store kept advertising
105//! the old one.
106//! A differ cut opens one command past a leading Mode B hole-punch (the
107//! `ClearRect` recorded at the slot's own index), so the punch stays in the
108//! main segment, where it keeps erasing what lies under the native view; the
109//! overlay's range holds only the content painted above the slot.
110//!
111//! Each target compiles its own segment: the engine's compiled frame is
112//! specific to its target's size and root, so a segmented frame is one compile
113//! per target — the main segment's plus one per overlay — never one shared
114//! compile. Every engine records and frust-render submits once per frame (the
115//! engine's own atlas-growth and glyph-replay submits aside): the overlays'
116//! passes ride the main frame's command buffer.
117//!
118//! # Alpha
119//!
120//! The overlay asks for a translucent composite mode
121//! ([`frust_gpu::surface::resolve_alpha_mode`]'s preference order, `Inherit`
122//! then `PostMultiplied` then `PreMultiplied`) and **refuses** with
123//! [`OverlayError::NoTranslucentMode`] when the platform grants none: an
124//! opaque overlay would black out everything under it. It draws the engine's
125//! premultiplied output straight into the swapchain, which is right for every
126//! mode the platform composites premultiplied — Android's `Inherit` and
127//! `PreMultiplied`, and Metal's `PostMultiplied` (Metal reads that mode
128//! premultiplied despite its name; see `crate::context::choose_engine_render_path`).
129//! A compositor that genuinely reads straight alpha is refused with
130//! [`OverlayError::StraightAlpha`] rather than served half-right.
131//!
132//! # What the overlay renderer is
133//!
134//! Its own [`frust_engine::EngineRenderer`] on the main surface's device and
135//! queue ([`OverlayRenderer`]), warmed for the overlay's format and built with
136//! **retention off**: a retained page belongs to the main surface's store, so
137//! a `DrawRetained` handed to the overlay is dropped (counted in
138//! [`RangeCut::dropped_draw_retained`]) and a `PushRetained` body is painted
139//! inline. The range is cut by [`frust_engine::renderer::project_command_range`]
140//! — read its *bracket rule* for what happens to a clip, layer or retained
141//! bracket a cut falls inside. A backdrop blur handed to the overlay draws
142//! nothing (its swapchain cannot be read back), and an external-pass texture
143//! (`SceneTexture`) is bound in the main renderer's registry only, so it draws
144//! nothing on the overlay either — which is why the segmented frame never
145//! hands an overlay any of them (the servability rule above) and a served
146//! cut's [`RangeCut::dropped_draw_retained`] is always zero. The spike's
147//! single fixed cut ([`OverlaySurface::render`]) applies no such rule.
148//! Shader quads are prepared over the whole scene by every overlay (the
149//! pre-pass is per scene, on the overlay's own engine), so the overlay serves
150//! a quad in its range, and one outside it is rendered and never sampled
151//! there. Each overlay engine schedules its own budget.
152//!
153//! # The sanctioned `unsafe` zone
154//!
155//! [`OverlaySurface::new`] is the one `unsafe` function here: it turns a
156//! caller-owned raw window handle or `CAMetalLayer*` into a `wgpu::Surface`
157//! (`wgpu::Instance::create_surface_unsafe`), the same crossing
158//! `frust_gpu::lifecycle::create_android_surface`/`create_metal_surface` make
159//! for the main surface. Its `# Safety` section is the whole contract.
160
161use core::ffi::c_void;
162use core::ops::Range;
163use core::ptr::NonNull;
164use std::sync::OnceLock;
165use std::sync::atomic::{AtomicBool, Ordering};
166
167use frust_engine::renderer::{RangeCut, open_retained_bracket};
168use frust_gpu::{RenderContext, SurfaceAlphaRequest};
169use frust_scene::Command;
170use kurbo::Affine;
171
172use crate::context::{EngineSurface, RenderPath};
173use crate::renderer::{DeferredPresent, GpuPassTimings, SurfaceRenderer};
174
175/// The spike's switch: `FRUST_OVERLAY_SPIKE`, read at compile time
176/// (`option_env!`, so a `frust build --define` bakes it into an Android or iOS
177/// app with no runtime environment) or at runtime, non-zero meaning on.
178///
179/// Off — the default — nothing in this module is reached by any shipped
180/// path: [`SurfaceRenderer::set_overlay_split`] refuses to arm and a shell
181/// creates no overlay.
182pub const OVERLAY_SPIKE_ENV_VAR: &str = "FRUST_OVERLAY_SPIKE";
183
184/// Whether the overlay spike is switched on ([`OVERLAY_SPIKE_ENV_VAR`]).
185///
186/// Read once per process and cached, like every `FRUST_*` knob; a runtime
187/// value wins over neither — either half set non-zero switches it on.
188pub fn overlay_spike_enabled() -> bool {
189    static ENABLED: OnceLock<bool> = OnceLock::new();
190    *ENABLED.get_or_init(|| spike_value().is_some())
191}
192
193/// The spike switch's raw value when it is on, else `None` — a shell that
194/// offers more than one host arrangement under the spike (Android's z-order
195/// A/B, see `frust-shell-android`) reads its variant from here.
196pub fn overlay_spike_value() -> Option<String> {
197    spike_value()
198}
199
200fn spike_value() -> Option<String> {
201    let set = |value: &str| {
202        let value = value.trim();
203        (!value.is_empty() && value != "0").then(|| value.to_string())
204    };
205    std::env::var(OVERLAY_SPIKE_ENV_VAR)
206        .ok()
207        .as_deref()
208        .and_then(set)
209        .or_else(|| option_env!("FRUST_OVERLAY_SPIKE").and_then(set))
210}
211
212/// The spike's fixed cut: the command index just past the scene's first
213/// `ClearRect`, or `None` when it records none.
214///
215/// A `ClearRect` is the one command a platform view leaves in the display
216/// list: on a translucent (Mode B) surface it is the slot's own hole punch, so
217/// everything after it paints above the first native view. An opaque (Mode A)
218/// surface records no punch, so a host page that wants an overlay under the
219/// spike records a zero-area `ClearRect` right after its native view as the
220/// marker (the native-widgets demo's Controls page does) — a zero-area clear
221/// erases nothing on either surface.
222///
223/// Fixed and deliberately crude: the production cut is the platform view's
224/// own paint-order index (p4-02), and a frame may cut more than once (p4-03).
225pub fn spike_cut(scene: &frust_scene::Scene) -> Option<usize> {
226    scene
227        .commands()
228        .iter()
229        .position(|command| matches!(command, frust_scene::Command::ClearRect { .. }))
230        .map(|at| at + 1)
231}
232
233/// The platform object an [`OverlaySurface`] presents into.
234///
235/// The variants mirror `wgpu::SurfaceTargetUnsafe`'s raw ones; the pointer
236/// and handles are the caller's, borrowed for the surface's whole life (see
237/// [`OverlaySurface::new`]'s `# Safety`).
238#[derive(Debug, Clone, Copy)]
239pub enum OverlayTarget {
240    /// A raw display and window handle — Android's `ANativeWindow`
241    /// ([`Self::android_ndk_window`]) and any other raw-window-handle host.
242    RawHandle {
243        /// The display the window lives on.
244        display: wgpu::rwh::RawDisplayHandle,
245        /// The window to present into.
246        window: wgpu::rwh::RawWindowHandle,
247    },
248    /// A `CAMetalLayer*` an `NSView`/`UIView` hosts above the native view —
249    /// macOS and iOS (`p4-01-mac`).
250    #[cfg(target_vendor = "apple")]
251    CoreAnimationLayer(*mut c_void),
252}
253
254impl OverlayTarget {
255    /// The Android target for an acquired `ANativeWindow*`.
256    pub fn android_ndk_window(window: NonNull<c_void>) -> Self {
257        use wgpu::rwh::{
258            AndroidDisplayHandle, AndroidNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
259        };
260        Self::RawHandle {
261            display: RawDisplayHandle::Android(AndroidDisplayHandle::new()),
262            window: RawWindowHandle::AndroidNdk(AndroidNdkWindowHandle::new(window)),
263        }
264    }
265}
266
267/// Why an overlay could not be created or could not draw a frame.
268#[derive(Debug)]
269#[non_exhaustive]
270pub enum OverlayError {
271    /// The context has no device yet: an overlay shares the main surface's
272    /// device, so it is created after the main surface.
273    NoDevice,
274    /// The platform object could not be turned into a surface, or the
275    /// surface could not be configured.
276    Surface(String),
277    /// The main surface's adapter cannot present to this surface. Refused
278    /// rather than answered with a second device: the overlay must share the
279    /// main frame's device.
280    UnsupportedSurface,
281    /// The platform granted no translucent composite mode — an opaque overlay
282    /// would cover everything under it.
283    NoTranslucentMode,
284    /// The compositor reads the overlay's alpha straight, which the spike
285    /// does not serve (no un-premultiplying pass).
286    StraightAlpha,
287    /// The overlay's engine renderer could not be built or refused the frame.
288    Engine(frust_engine::EngineError),
289}
290
291impl std::fmt::Display for OverlayError {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        match self {
294            Self::NoDevice => f.write_str("overlay: no device yet (create the main surface first)"),
295            Self::Surface(reason) => write!(f, "overlay: surface creation failed: {reason}"),
296            Self::UnsupportedSurface => {
297                f.write_str("overlay: the main surface's adapter cannot present to this surface")
298            }
299            Self::NoTranslucentMode => {
300                f.write_str("overlay: the platform granted no translucent composite mode")
301            }
302            Self::StraightAlpha => {
303                f.write_str("overlay: the compositor reads straight alpha, which the spike refuses")
304            }
305            Self::Engine(error) => write!(f, "overlay: engine: {error}"),
306        }
307    }
308}
309
310impl std::error::Error for OverlayError {}
311
312/// The overlay's renderer, free of any surface: a retention-off
313/// [`frust_engine::EngineRenderer`] that records a command range into a
314/// caller-supplied target cleared to transparent.
315///
316/// [`OverlaySurface`] wraps one around a swapchain; the spike's host tests
317/// drive one directly against an offscreen target.
318pub struct OverlayRenderer {
319    engine: Box<frust_engine::EngineRenderer>,
320    shader_quads: Box<frust_engine::ShaderQuadPass>,
321    timestamps: frust_gpu::diag::TimestampRing,
322    last_cut: Option<RangeCut>,
323    refused: u32,
324}
325
326impl OverlayRenderer {
327    /// Builds an overlay renderer for `format` targets on `device`.
328    ///
329    /// `pipeline_cache` is the main surface's persisted driver cache, when it
330    /// has one, so the overlay's pipelines compile as cache hits.
331    ///
332    /// # Errors
333    ///
334    /// [`OverlayError::Engine`] when the engine cannot be built on `caps`.
335    pub fn new(
336        device: &wgpu::Device,
337        queue: &wgpu::Queue,
338        caps: &frust_gpu::TierCaps,
339        format: wgpu::TextureFormat,
340        pipeline_cache: Option<&wgpu::PipelineCache>,
341    ) -> Result<Self, OverlayError> {
342        let mut engine = frust_engine::EngineRenderer::new(device, caps, format, pipeline_cache)
343            .map_err(OverlayError::Engine)?;
344        // The retained store is the main surface's: the overlay records no page
345        // and draws none (see the module docs).
346        engine.set_retained_layers(false);
347        Ok(Self {
348            engine: Box::new(engine),
349            shader_quads: Box::new(frust_engine::ShaderQuadPass::new(pipeline_cache.cloned())),
350            timestamps: frust_gpu::diag::TimestampRing::new(
351                device,
352                queue,
353                frust_engine::EngineSpan::COUNT,
354                "frust-render overlay timestamps",
355            ),
356            last_cut: None,
357            refused: 0,
358        })
359    }
360
361    /// Records the commands of `scene` in `range` into `encoder`, targeting
362    /// `target` cleared to transparent, and returns what the cut did.
363    ///
364    /// Shader quads are prepared over the whole `scene` (the pre-pass is
365    /// per-scene), so a quad outside the range is rendered to its own
366    /// offscreen target too and simply never sampled — a spike cost.
367    ///
368    /// # Errors
369    ///
370    /// [`OverlayError::Engine`] when the engine refuses the frame; nothing
371    /// was recorded into `encoder` then.
372    pub fn record(
373        &mut self,
374        device: &wgpu::Device,
375        queue: &wgpu::Queue,
376        encoder: &mut wgpu::CommandEncoder,
377        scene: &frust_scene::Scene,
378        range: Range<usize>,
379        target: frust_engine::EngineTarget<'_>,
380    ) -> Result<RangeCut, OverlayError> {
381        self.shader_quads.prepare(
382            device,
383            queue,
384            encoder,
385            scene,
386            Affine::IDENTITY,
387            &mut self.engine,
388        );
389        self.timestamps.begin_frame(device);
390        let result = self.engine.record_range(
391            device,
392            queue,
393            encoder,
394            scene,
395            range,
396            target,
397            peniko::Color::TRANSPARENT,
398            Affine::IDENTITY,
399            frust_engine::FrameTimestamps::new(&self.timestamps),
400        );
401        match result {
402            Ok(cut) => {
403                self.timestamps.resolve(encoder);
404                self.last_cut = Some(cut);
405                Ok(cut)
406            }
407            Err(error) => {
408                self.timestamps.abandon_frame();
409                self.refused = self.refused.saturating_add(1);
410                Err(OverlayError::Engine(error))
411            }
412        }
413    }
414
415    /// Closes the frame [`Self::record`] recorded, once its encoder was
416    /// submitted (`submitted`) or dropped: the engine's end-of-frame
417    /// maintenance and the timestamp ring's frame close — or, for an encoder
418    /// that was never submitted, the ring's abandon, so no slot is left
419    /// waiting on a resolve that will never run.
420    pub fn end_frame(&mut self, queue: &wgpu::Queue, submitted: bool) {
421        if submitted {
422            self.timestamps.end_frame();
423        } else {
424            self.timestamps.abandon_frame();
425        }
426        self.engine.end_frame(queue);
427    }
428
429    /// What the last recorded range's cut did, `None` before the first.
430    pub fn last_cut(&self) -> Option<RangeCut> {
431        self.last_cut
432    }
433
434    /// How many frames the engine has refused on this overlay.
435    pub fn refused_frames(&self) -> u32 {
436        self.refused
437    }
438
439    /// The overlay's most recent per-pass GPU time, on a device with
440    /// `TIMESTAMP_QUERY` (a `perf-trace` build); `None` otherwise — the same
441    /// reading [`SurfaceRenderer::gpu_pass_timings`] gives the main surface.
442    pub fn gpu_pass_timings(&self) -> Option<GpuPassTimings> {
443        let reading = self.timestamps.latest()?;
444        let span = |which: frust_engine::EngineSpan| reading.span(which.index());
445        Some(GpuPassTimings {
446            prepass: span(frust_engine::EngineSpan::Prepass),
447            main: span(frust_engine::EngineSpan::Main),
448            composite: span(frust_engine::EngineSpan::Composite),
449            blit: span(frust_engine::EngineSpan::Blit),
450        })
451    }
452}
453
454/// What one [`OverlaySurface::record`] (or [`OverlaySurface::render`]) did.
455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456pub enum OverlayFrame {
457    /// The range was drawn and its frame is waiting for
458    /// [`OverlaySurface::present`] (or [`OverlaySurface::take_deferred_present`])
459    /// — after [`OverlaySurface::record`], once its encoder was submitted and
460    /// [`OverlaySurface::end_frame`] told so.
461    Rendered(RangeCut),
462    /// The swapchain was stale and has been reconfigured; nothing to present
463    /// this frame.
464    Reconfigured,
465    /// No frame this time (a transient acquire failure, or a lost surface the
466    /// host recreates through its own lifecycle).
467    Skipped,
468}
469
470/// A translucent swapchain above a hosted native view, drawing one command
471/// range of the frame — see the [module docs](self) for the host contract.
472pub struct OverlaySurface {
473    surface: EngineSurface,
474    renderer: OverlayRenderer,
475    device: wgpu::Device,
476    queue: wgpu::Queue,
477    /// The frame [`Self::record`] acquired and recorded, until
478    /// [`Self::end_frame`] learns whether its encoder was submitted.
479    recorded: Option<wgpu::SurfaceTexture>,
480    /// The submitted frame waiting for [`Self::present`].
481    pending: Option<wgpu::SurfaceTexture>,
482}
483
484impl OverlaySurface {
485    /// Creates the overlay's swapchain on `target`, configured translucent at
486    /// `width` x `height` physical pixels, sharing `ctx`'s device and queue.
487    ///
488    /// `main` is the main surface's renderer when there is one; the overlay
489    /// then compiles its pipelines through the same persisted driver cache.
490    ///
491    /// # Safety
492    ///
493    /// The one `unsafe` crossing of this module (a sanctioned zone, like
494    /// `frust_gpu::lifecycle::create_android_surface`): `target` must name a
495    /// valid, live platform object — an acquired `ANativeWindow*` behind an
496    /// Android NDK handle, a valid display/window pair, or a live
497    /// `CAMetalLayer*` — that **outlives** the returned `OverlaySurface` and
498    /// every frame it acquires. Drop the `OverlaySurface` before releasing
499    /// the window or tearing down the layer.
500    ///
501    /// # Errors
502    ///
503    /// [`OverlayError::NoDevice`] before the main surface created the device;
504    /// [`OverlayError::Surface`] when the platform object yields no surface or
505    /// the swapchain cannot be configured; [`OverlayError::UnsupportedSurface`]
506    /// when the shared adapter cannot present to it;
507    /// [`OverlayError::NoTranslucentMode`]/[`OverlayError::StraightAlpha`] for
508    /// an alpha mode the overlay cannot use; [`OverlayError::Engine`] when the
509    /// overlay's renderer cannot be built.
510    pub unsafe fn new(
511        ctx: &mut RenderContext,
512        main: Option<&SurfaceRenderer>,
513        target: OverlayTarget,
514        width: u32,
515        height: u32,
516    ) -> Result<Self, OverlayError> {
517        if ctx.caps().is_none() {
518            return Err(OverlayError::NoDevice);
519        }
520        let raw = match target {
521            OverlayTarget::RawHandle { display, window } => wgpu::SurfaceTargetUnsafe::RawHandle {
522                raw_display_handle: Some(display),
523                raw_window_handle: window,
524            },
525            #[cfg(target_vendor = "apple")]
526            OverlayTarget::CoreAnimationLayer(layer) => {
527                if layer.is_null() {
528                    return Err(OverlayError::Surface("null CAMetalLayer pointer".into()));
529                }
530                wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(layer)
531            }
532        };
533        // SAFETY: upheld by this function's own `# Safety` contract — the
534        // platform object `target` names is valid and outlives the surface.
535        let surface = unsafe { ctx.instance().create_surface_unsafe(raw) }
536            .map_err(|error| OverlayError::Surface(error.to_string()))?;
537        // Asked before configuring: `create_engine_surface` would otherwise
538        // answer an unsupported surface with a NEW device, pulling the device
539        // out from under the main surface.
540        if !ctx.device_handle().adapter.is_surface_supported(&surface) {
541            return Err(OverlayError::UnsupportedSurface);
542        }
543        let surface = pollster_block_on(crate::context::create_engine_surface(
544            ctx,
545            surface,
546            width.max(1),
547            height.max(1),
548            wgpu::PresentMode::AutoVsync,
549            SurfaceAlphaRequest::TranslucentPreferred,
550        ))
551        .map_err(|error| OverlayError::Surface(format!("{error:#}")))?;
552        if !surface.resolved_translucent() {
553            return Err(OverlayError::NoTranslucentMode);
554        }
555        if !matches!(surface.path, RenderPath::EngineDirect { .. }) {
556            return Err(OverlayError::StraightAlpha);
557        }
558        let handle = ctx.device_handle();
559        let cache = main.and_then(SurfaceRenderer::shared_pipeline_cache);
560        let renderer = OverlayRenderer::new(
561            &handle.device,
562            &handle.queue,
563            &handle.caps,
564            surface.config().format,
565            cache.as_ref(),
566        )?;
567        log::info!(
568            "frust-render overlay spike: overlay surface up ({:?}, {:?}, {}x{})",
569            surface.config().format,
570            surface.config().alpha_mode,
571            surface.config().width,
572            surface.config().height
573        );
574        Ok(Self {
575            surface,
576            renderer,
577            device: handle.device.clone(),
578            queue: handle.queue.clone(),
579            recorded: None,
580            pending: None,
581        })
582    }
583
584    /// The swapchain's `(width, height)` in physical pixels.
585    pub fn size(&self) -> (u32, u32) {
586        (self.surface.config().width, self.surface.config().height)
587    }
588
589    /// Resizes the swapchain (physical pixels); a zero extent is ignored, and
590    /// an un-presented frame of the old size is dropped.
591    pub fn resize(&mut self, width: u32, height: u32) {
592        if width == 0 || height == 0 || (width, height) == self.size() {
593            return;
594        }
595        self.pending = None;
596        if self.recorded.take().is_some() {
597            self.renderer.end_frame(&self.queue, false);
598        }
599        self.surface.resize(&self.device, width, height);
600    }
601
602    /// Acquires the overlay's next frame and records the commands of `scene`
603    /// in `range` into it over a transparent clear — into the **caller's**
604    /// `encoder`, so the overlay's passes land in the same command buffer as
605    /// the main frame's and one queue submit carries both (the segmented
606    /// frame, [`SurfaceRenderer::submit_segmented`]).
607    ///
608    /// Nothing is submitted and nothing is presentable yet: the acquired
609    /// frame waits, recorded, until [`Self::end_frame`] is told whether the
610    /// encoder was submitted — then (and only then) it becomes the frame
611    /// [`Self::present`]/[`Self::take_deferred_present`] hand out. A frame
612    /// still waiting from an earlier call (presentable or merely recorded) is
613    /// dropped un-presented first (latest wins).
614    ///
615    /// [`OverlayFrame::Reconfigured`]/[`OverlayFrame::Skipped`] recorded
616    /// nothing and leave nothing for [`Self::end_frame`] to close.
617    ///
618    /// # Errors
619    ///
620    /// [`OverlayError::Engine`] when the engine refuses the frame: the
621    /// acquired frame is dropped and the renderer's frame already closed, so
622    /// there is nothing for [`Self::end_frame`] to do (the previous overlay
623    /// frame stays on screen). Shader-quad pre-passes the overlay recorded
624    /// ahead of the refusal stay in `encoder`, harmlessly — their targets are
625    /// the overlay's own.
626    pub fn record(
627        &mut self,
628        encoder: &mut wgpu::CommandEncoder,
629        scene: &frust_scene::Scene,
630        range: Range<usize>,
631    ) -> Result<OverlayFrame, OverlayError> {
632        self.pending = None;
633        if self.recorded.take().is_some() {
634            // Recorded by an earlier call whose encoder never reported back:
635            // close that frame as unsubmitted before opening this one.
636            self.renderer.end_frame(&self.queue, false);
637        }
638        use wgpu::CurrentSurfaceTexture as Cst;
639        let texture = match self.surface.gpu.surface().get_current_texture() {
640            Cst::Success(texture) | Cst::Suboptimal(texture) => texture,
641            Cst::Outdated | Cst::Validation => {
642                self.surface.reconfigure(&self.device);
643                return Ok(OverlayFrame::Reconfigured);
644            }
645            Cst::Lost | Cst::Timeout | Cst::Occluded => return Ok(OverlayFrame::Skipped),
646        };
647        let view = texture
648            .texture
649            .create_view(&wgpu::TextureViewDescriptor::default());
650        let (width, height) = self.size();
651        let format = self.surface.config().format;
652        let depth = match &self.surface.path {
653            RenderPath::EngineDirect { depth, .. } => Some(depth.view()),
654            RenderPath::EngineDirectUnpremultiply { .. } => None,
655        };
656        let result = self.renderer.record(
657            &self.device,
658            &self.queue,
659            encoder,
660            scene,
661            range,
662            frust_engine::EngineTarget {
663                view: &view,
664                format,
665                width,
666                height,
667                depth,
668                output: frust_engine::OutputAlpha::Premultiplied,
669            },
670        );
671        match result {
672            Ok(cut) => {
673                self.recorded = Some(texture);
674                Ok(OverlayFrame::Rendered(cut))
675            }
676            Err(error) => {
677                self.renderer.end_frame(&self.queue, false);
678                Err(error)
679            }
680        }
681    }
682
683    /// Closes the frame [`Self::record`] recorded, once the encoder it was
684    /// recorded into has been submitted (`submitted`) or dropped: the
685    /// renderer's end-of-frame maintenance, and — when submitted — the
686    /// recorded frame becomes the one [`Self::present`] presents. Answers
687    /// whether a frame is now waiting to be presented; a no-op answering
688    /// `false` when nothing was recorded.
689    pub fn end_frame(&mut self, submitted: bool) -> bool {
690        let Some(texture) = self.recorded.take() else {
691            return false;
692        };
693        self.renderer.end_frame(&self.queue, submitted);
694        if submitted {
695            self.pending = Some(texture);
696        }
697        submitted
698    }
699
700    /// **The spike's path only** — [`Self::record`] into an encoder of the
701    /// overlay's own, submitted on its own: a second queue submit per frame,
702    /// which the segmented frame exists to remove
703    /// ([`SurfaceRenderer::submit_segmented`] records every overlay into the
704    /// main frame's encoder and submits once). Kept for the single-cut spike
705    /// host (`FRUST_OVERLAY_SPIKE`), which renders its overlay ahead of the
706    /// main frame.
707    ///
708    /// Leaves the frame un-presented until [`Self::present`] (or
709    /// [`Self::take_deferred_present`]), so the host can land it right beside
710    /// the main frame's present. A frame still waiting from the last call is
711    /// dropped un-presented first (latest wins).
712    ///
713    /// # Errors
714    ///
715    /// [`OverlayError::Engine`] when the engine refuses the frame (nothing is
716    /// presented; the previous overlay frame stays on screen).
717    pub fn render(
718        &mut self,
719        scene: &frust_scene::Scene,
720        range: Range<usize>,
721    ) -> Result<OverlayFrame, OverlayError> {
722        let mut encoder = self
723            .device
724            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
725                label: Some("frust-render overlay"),
726            });
727        let frame = self.record(&mut encoder, scene, range)?;
728        if matches!(frame, OverlayFrame::Rendered(_)) {
729            self.queue.submit([encoder.finish()]);
730            self.end_frame(true);
731        }
732        Ok(frame)
733    }
734
735    /// Presents the frame [`Self::render`] (or a submitted [`Self::record`])
736    /// left waiting, on the calling thread; `false` when there was none.
737    pub fn present(&mut self) -> bool {
738        match self.pending.take() {
739            Some(texture) => {
740                self.queue.present(texture);
741                true
742            }
743            None => false,
744        }
745    }
746
747    /// Hands the waiting frame out as a [`DeferredPresent`], for a host that
748    /// presents the main frame and the overlay inside one platform
749    /// transaction (iOS `presentsWithTransaction`); `None` when there was
750    /// none.
751    pub fn take_deferred_present(&mut self) -> Option<DeferredPresent> {
752        self.pending
753            .take()
754            .map(|texture| DeferredPresent::new(texture, self.queue.clone()))
755    }
756
757    /// The overlay's renderer — its cut report, refusal count and GPU time.
758    pub fn renderer(&self) -> &OverlayRenderer {
759        &self.renderer
760    }
761}
762
763impl std::fmt::Debug for OverlaySurface {
764    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
765        f.debug_struct("OverlaySurface")
766            .field("size", &self.size())
767            .field("pending", &self.pending.is_some())
768            .finish_non_exhaustive()
769    }
770}
771
772/// One overlay of a segmented frame: the [`OverlaySurface`] that draws the
773/// command range `range` of the frame's scene, named by the differ's
774/// `overlay_id` (`frust_shell_common`'s `ViewCommand::Overlay`, whose
775/// `command_range` is this `range`).
776///
777/// Handed to [`SurfaceRenderer::submit_segmented`] (or
778/// [`SurfaceRenderer::submit_segmented_deferred`]) with the same ranges
779/// [`SurfaceRenderer::encode_segmented`] was given as `cuts`, in the same
780/// order.
781#[derive(Debug)]
782pub struct OverlayCut<'a> {
783    /// The overlay drawing this range.
784    pub overlay: &'a mut OverlaySurface,
785    /// The half-open command range of the frame's scene it draws.
786    pub range: Range<usize>,
787    /// The differ's id for the overlay, carried back in
788    /// [`OverlayCutOutcome::overlay_id`] and [`SegmentedPresents::overlays`].
789    pub overlay_id: u64,
790}
791
792/// What one [`OverlayCut`] of a segmented frame did.
793#[derive(Debug)]
794pub struct OverlayCutOutcome {
795    /// The cut's [`OverlayCut::overlay_id`].
796    pub overlay_id: u64,
797    /// [`OverlaySurface::record`]'s answer for this frame —
798    /// [`OverlayFrame::Skipped`] for every cut of a frame whose main segment
799    /// was not rendered (nothing was recorded for any overlay then).
800    pub result: Result<OverlayFrame, OverlayError>,
801    /// Whether the cut's range was served on the overlay or kept on the main
802    /// surface, and why ([`cut_is_servable`]): a kept cut's overlay recorded
803    /// an empty range, so its `result` is a transparent frame. The
804    /// classification [`SurfaceRenderer::encode_segmented`] made for the
805    /// frame — [`CutServiceability::Servable`] for a cut the frame's encode
806    /// never classified (a plain [`SurfaceRenderer::encode`]).
807    pub serviceability: CutServiceability,
808}
809
810/// Why a cut's range cannot be drawn on an overlay and stays on the main
811/// surface — [`CutServiceability::Unservable`]'s reason.
812#[derive(Debug, Clone, Copy, PartialEq, Eq)]
813#[non_exhaustive]
814pub enum UnservableReason {
815    /// A `DrawRetained`: the page lives in the main surface's retained store,
816    /// which the overlay's engine cannot read.
817    RetainedComposite,
818    /// A `PushRetained` bracket inside the range: painted inline on the
819    /// overlay, it would never record the page the main surface's store then
820    /// keeps advertising, and the same content would move between surfaces
821    /// as the widget turns clean (drawn from its page) and dirty again.
822    RetainedRecording,
823    /// A retained bracket open across one of the range's ends: the projection
824    /// would demote it to inline paint on both sides of the cut.
825    RetainedBracketCrossing,
826    /// A `SceneTexture`: external textures are bound in the main renderer's
827    /// registry only.
828    SceneTexture,
829    /// A `BackdropBlur`: it reads back what is painted beneath it, which an
830    /// overlay's swapchain does not hold.
831    BackdropBlur,
832}
833
834impl std::fmt::Display for UnservableReason {
835    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
836        f.write_str(match self {
837            Self::RetainedComposite => "retained content (a page composite)",
838            Self::RetainedRecording => "retained content (a page recording)",
839            Self::RetainedBracketCrossing => "retained content (a bracket crossing the cut)",
840            Self::SceneTexture => "an external texture",
841            Self::BackdropBlur => "a backdrop blur",
842        })
843    }
844}
845
846/// Whether an overlay can draw a cut's range exactly as the main surface
847/// would — [`cut_is_servable`]'s answer.
848#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
849pub enum CutServiceability {
850    /// The overlay draws the range; the main segment excludes it.
851    #[default]
852    Servable,
853    /// The range stays on the main surface and the overlay draws nothing.
854    Unservable {
855        /// Why.
856        reason: UnservableReason,
857        /// The command index that decided it: the offending command, or the
858        /// `PushRetained` opening the crossing bracket.
859        at: usize,
860    },
861}
862
863impl CutServiceability {
864    /// Whether the overlay serves the cut.
865    pub fn is_servable(self) -> bool {
866        matches!(self, Self::Servable)
867    }
868
869    /// The range the overlay records for `cut`, and the main segment
870    /// excludes: `cut` itself when servable, otherwise the empty range `0..0`
871    /// — nothing reopened, nothing drawn, the overlay cleared to transparent.
872    pub fn served_range(self, cut: &Range<usize>) -> Range<usize> {
873        match self {
874            Self::Servable => cut.clone(),
875            Self::Unservable { .. } => 0..0,
876        }
877    }
878}
879
880impl std::fmt::Display for CutServiceability {
881    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
882        match self {
883            Self::Servable => f.write_str("served on the overlay"),
884            Self::Unservable { reason, at } => {
885                write!(f, "kept on main: {reason} at command {at}")
886            }
887        }
888    }
889}
890
891/// Classifies the cut `range` of `commands`: whether an overlay can draw it
892/// exactly as the main surface would (see the module docs' *Segments*).
893///
894/// Unservable, at the first offending command in display-list order: a
895/// retained bracket open at the range's start
896/// ([`frust_engine::renderer::open_retained_bracket`]); then, inside the
897/// range, a `DrawRetained`, a `PushRetained` (whose bracket crosses the
898/// range's end when it is still open there short of the scene's end), a
899/// `SceneTexture` or a `BackdropBlur`. Everything else — fills, paths, text,
900/// images, blurred rects, hole punches, clips, layers and shader quads (the
901/// overlay prepares its own) — is servable. `range` is clamped to the list
902/// the way the projection clamps it, and an empty range is servable (there is
903/// nothing to draw on either surface). Pure and read-only: one pass over the
904/// commands up to the range's end.
905pub fn cut_is_servable(commands: &[Command], range: Range<usize>) -> CutServiceability {
906    let len = commands.len();
907    let end = range.end.min(len);
908    let start = range.start.min(end);
909    if start == end {
910        return CutServiceability::Servable;
911    }
912    let unservable = |reason, at| CutServiceability::Unservable { reason, at };
913    if let Some(at) = open_retained_bracket(commands, start) {
914        return unservable(UnservableReason::RetainedBracketCrossing, at);
915    }
916    for (at, command) in commands.iter().enumerate().take(end).skip(start) {
917        let reason = match command {
918            Command::DrawRetained { .. } => UnservableReason::RetainedComposite,
919            Command::PushRetained { .. } => {
920                if end < len && !retained_closes_before(commands, at, end) {
921                    UnservableReason::RetainedBracketCrossing
922                } else {
923                    UnservableReason::RetainedRecording
924                }
925            }
926            Command::SceneTexture { .. } => UnservableReason::SceneTexture,
927            Command::BackdropBlur { .. } => UnservableReason::BackdropBlur,
928            _ => continue,
929        };
930        return unservable(reason, at);
931    }
932    CutServiceability::Servable
933}
934
935/// Whether the retained bracket `commands[push]` opens is closed before
936/// `end` — pops matched to the most recent retained push, as the projection
937/// matches them.
938fn retained_closes_before(commands: &[Command], push: usize, end: usize) -> bool {
939    let mut depth = 0_usize;
940    for command in commands.iter().take(end).skip(push) {
941        match command {
942            Command::PushRetained { .. } => depth += 1,
943            Command::PopRetained => {
944                depth -= 1;
945                if depth == 0 {
946                    return true;
947                }
948            }
949            _ => {}
950        }
951    }
952    false
953}
954
955/// What [`SurfaceRenderer::submit_segmented`] did with one frame.
956#[derive(Debug)]
957pub struct SegmentedOutcome {
958    /// The main surface's outcome — exactly what [`SurfaceRenderer::submit`]
959    /// would have answered.
960    pub frame: frust_gpu::FrameOutcome,
961    /// One entry per [`OverlayCut`], in the order given.
962    pub overlays: Vec<OverlayCutOutcome>,
963    /// Command encoders the frame created: `1` whenever it reached the GPU
964    /// encode — the main segment and every overlay share it (an engine
965    /// refusal drops it un-submitted) — `0` when it never got that far.
966    pub encoders: u32,
967    /// Queue submits the frame issued: `1` for a rendered frame, however many
968    /// overlays it carried, `0` otherwise. The segmented frame's invariant.
969    pub submits: u32,
970}
971
972/// The un-presented frames of one segmented frame —
973/// [`SurfaceRenderer::submit_segmented_deferred`]'s answer for a host that
974/// pairs the presents itself (iOS/macOS inside one `CATransaction`).
975///
976/// Dropping it presents nothing: every frame returns to its swapchain
977/// un-presented, the same skip a dropped [`DeferredPresent`] is.
978#[derive(Debug, Default)]
979pub struct SegmentedPresents {
980    /// The main surface's frame, `None` when the main frame did not render.
981    pub main: Option<DeferredPresent>,
982    /// Each overlay that rendered, with its [`OverlayCut::overlay_id`], in cut
983    /// order.
984    pub overlays: Vec<(u64, DeferredPresent)>,
985}
986
987impl SegmentedPresents {
988    /// Presents every frame on the calling thread: the main surface first,
989    /// then each overlay in cut order (bottom to top).
990    pub fn present_all(self) {
991        if let Some(main) = self.main {
992            main.present();
993        }
994        for (_, overlay) in self.overlays {
995            overlay.present();
996        }
997    }
998
999    /// The frames as one list — the main surface's first, then each overlay
1000    /// in cut order — for a host that presents them through a list of its own.
1001    pub fn into_vec(self) -> Vec<DeferredPresent> {
1002        self.main
1003            .into_iter()
1004            .chain(self.overlays.into_iter().map(|(_, present)| present))
1005            .collect()
1006    }
1007
1008    /// How many frames are waiting.
1009    pub fn len(&self) -> usize {
1010        usize::from(self.main.is_some()) + self.overlays.len()
1011    }
1012
1013    /// Whether no frame is waiting.
1014    pub fn is_empty(&self) -> bool {
1015        self.len() == 0
1016    }
1017}
1018
1019/// The ranges of a `len`-command scene the **main** surface draws when the
1020/// overlays draw `cuts`: the complement of the cuts, ascending, empty gaps
1021/// left out — `0..first.start`, the gap between each cut and the next, and
1022/// whatever follows the last cut.
1023///
1024/// `cuts` are the differ's overlay ranges: ascending and disjoint
1025/// (`ViewCommand::Overlay.command_range`). Each is clamped to the scene; a cut
1026/// that starts before the previous one ended is treated as starting where it
1027/// ended, so a malformed list never makes the main surface draw a command
1028/// twice or go backwards. An empty cut (an overlay whose content disappeared,
1029/// `command_range` empty) removes nothing and splits no gap.
1030pub fn main_segment_ranges(len: usize, cuts: &[Range<usize>]) -> Vec<Range<usize>> {
1031    let mut ranges = Vec::with_capacity(cuts.len() + 1);
1032    let mut from = 0;
1033    for cut in cuts {
1034        let start = cut.start.clamp(from, len);
1035        let end = cut.end.clamp(start, len);
1036        if start == end {
1037            // An empty cut (an overlay whose content disappeared) removes
1038            // nothing, so it splits nothing either.
1039            continue;
1040        }
1041        if from < start {
1042            ranges.push(from..start);
1043        }
1044        from = end;
1045    }
1046    if from < len {
1047        ranges.push(from..len);
1048    }
1049    ranges
1050}
1051
1052/// Projects the main surface's segment of `scene` — every command outside the
1053/// overlay `cuts` ([`main_segment_ranges`]) — into `out`, through
1054/// [`frust_engine::renderer::project_command_ranges`]: each gap between cuts
1055/// is projected on its own, so its brackets are rebalanced (a clip open across
1056/// a cut closes before it and reopens after it) and the concatenation is a
1057/// well-nested display list. What [`SurfaceRenderer::encode_segmented`] keeps
1058/// for the main surface.
1059pub fn project_main_segment(
1060    scene: &frust_scene::Scene,
1061    cuts: &[Range<usize>],
1062    options: frust_engine::renderer::RangeOptions,
1063    out: &mut frust_scene::Scene,
1064) -> RangeCut {
1065    let ranges = main_segment_ranges(scene.commands().len(), cuts);
1066    frust_engine::renderer::project_command_ranges(scene, &ranges, options, out)
1067}
1068
1069/// Records every cut's served range of `scene` into `encoder` — the overlay
1070/// half of [`SurfaceRenderer::submit_segmented`], after the main segment's
1071/// passes and before the one submit.
1072///
1073/// `service` is the frame's classification, one per cut in order, as the
1074/// encode cut the main segment by; a cut past its end is classified here.
1075/// A kept cut's overlay records the empty range
1076/// ([`CutServiceability::served_range`]).
1077pub(crate) fn record_cuts(
1078    encoder: &mut wgpu::CommandEncoder,
1079    scene: &frust_scene::Scene,
1080    cuts: &mut [OverlayCut<'_>],
1081    service: &[CutServiceability],
1082) -> Vec<OverlayCutOutcome> {
1083    cuts.iter_mut()
1084        .enumerate()
1085        .map(|(index, cut)| {
1086            let serviceability = service
1087                .get(index)
1088                .copied()
1089                .unwrap_or_else(|| cut_is_servable(scene.commands(), cut.range.clone()));
1090            let result =
1091                cut.overlay
1092                    .record(encoder, scene, serviceability.served_range(&cut.range));
1093            if let Ok(OverlayFrame::Rendered(range_cut)) = &result {
1094                note_dropped_composites(range_cut);
1095            }
1096            OverlayCutOutcome {
1097                overlay_id: cut.overlay_id,
1098                result,
1099                serviceability,
1100            }
1101        })
1102        .collect()
1103}
1104
1105/// A served cut never hands the overlay a `DrawRetained` ([`cut_is_servable`]
1106/// keeps such a range on the main surface), so the overlay's projection drops
1107/// none: asserted in a debug build, logged once otherwise — a dropped one is
1108/// content drawn on no surface.
1109fn note_dropped_composites(cut: &RangeCut) {
1110    static LOGGED: AtomicBool = AtomicBool::new(false);
1111    debug_assert_eq!(
1112        cut.dropped_draw_retained, 0,
1113        "an overlay dropped a retained composite of a served cut"
1114    );
1115    debug_assert_eq!(
1116        cut.demoted_retained, 0,
1117        "an overlay demoted a retained bracket of a served cut: the classifier \
1118         and the projection disagree on where the bracket closes"
1119    );
1120    if cut.dropped_draw_retained > 0 && !LOGGED.swap(true, Ordering::Relaxed) {
1121        log::error!(
1122            "frust-render overlay: a served cut dropped {} retained composite(s) \
1123             (commands {}..{}); that content drew on no surface (logged once)",
1124            cut.dropped_draw_retained,
1125            cut.start,
1126            cut.end
1127        );
1128    }
1129}
1130
1131/// One [`OverlayFrame::Skipped`] per cut — the overlays of a frame whose main
1132/// segment did not render — each carrying the frame's classification of it
1133/// (`service`, one per cut in order; [`CutServiceability::Servable`] past its
1134/// end).
1135pub(crate) fn skipped_cuts(
1136    cuts: &[OverlayCut<'_>],
1137    service: &[CutServiceability],
1138) -> Vec<OverlayCutOutcome> {
1139    cuts.iter()
1140        .enumerate()
1141        .map(|(index, cut)| OverlayCutOutcome {
1142            overlay_id: cut.overlay_id,
1143            result: Ok(OverlayFrame::Skipped),
1144            serviceability: service.get(index).copied().unwrap_or_default(),
1145        })
1146        .collect()
1147}
1148
1149/// Closes every cut's recorded frame after the encoder was submitted (or
1150/// dropped) — [`OverlaySurface::end_frame`] on each.
1151pub(crate) fn end_cuts(cuts: &mut [OverlayCut<'_>], submitted: bool) {
1152    for cut in cuts {
1153        cut.overlay.end_frame(submitted);
1154    }
1155}
1156
1157/// Drives `future` to completion on the calling thread. Surface configuration
1158/// is `async` only because device creation can be; the overlay never creates a
1159/// device (it refused above when there was none), so this resolves without an
1160/// executor driving it.
1161fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
1162    use std::task::{Context, Poll, Waker};
1163
1164    let mut cx = Context::from_waker(Waker::noop());
1165    let mut future = std::pin::pin!(future);
1166    loop {
1167        match future.as_mut().poll(&mut cx) {
1168            Poll::Ready(value) => return value,
1169            Poll::Pending => std::thread::yield_now(),
1170        }
1171    }
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176    use super::*;
1177
1178    #[test]
1179    fn the_overlay_types_cross_threads() {
1180        fn send<T: Send>() {}
1181        send::<OverlaySurface>();
1182        send::<OverlayRenderer>();
1183        send::<OverlayError>();
1184    }
1185
1186    #[test]
1187    fn spike_cut_is_past_the_first_clear() {
1188        let mut scene = frust_scene::Scene::new();
1189        assert_eq!(spike_cut(&scene), None);
1190        let mut builder = frust_scene::SceneBuilder::new(&mut scene);
1191        builder.fill_rect(
1192            kurbo::Rect::new(0.0, 0.0, 1.0, 1.0),
1193            peniko::Brush::Solid(peniko::Color::BLACK),
1194        );
1195        builder.clear_rect(kurbo::Rect::ZERO);
1196        builder.clear_rect(kurbo::Rect::ZERO);
1197        assert_eq!(spike_cut(&scene), Some(2));
1198    }
1199
1200    #[test]
1201    fn the_spike_reads_off_by_default() {
1202        // The test process sets no `FRUST_OVERLAY_SPIKE`; a build that baked
1203        // one in is the operator's own A/B, not this suite's.
1204        if option_env!("FRUST_OVERLAY_SPIKE").is_none()
1205            && std::env::var(OVERLAY_SPIKE_ENV_VAR).is_err()
1206        {
1207            assert!(!overlay_spike_enabled());
1208            assert_eq!(overlay_spike_value(), None);
1209        }
1210    }
1211}