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}