use core::ffi::c_void;
use core::ops::Range;
use core::ptr::NonNull;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use frust_engine::renderer::{RangeCut, open_retained_bracket};
use frust_gpu::{RenderContext, SurfaceAlphaRequest};
use frust_scene::Command;
use kurbo::Affine;
use crate::context::{EngineSurface, RenderPath};
use crate::renderer::{DeferredPresent, GpuPassTimings, SurfaceRenderer};
pub const OVERLAY_SPIKE_ENV_VAR: &str = "FRUST_OVERLAY_SPIKE";
pub fn overlay_spike_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| spike_value().is_some())
}
pub fn overlay_spike_value() -> Option<String> {
spike_value()
}
fn spike_value() -> Option<String> {
let set = |value: &str| {
let value = value.trim();
(!value.is_empty() && value != "0").then(|| value.to_string())
};
std::env::var(OVERLAY_SPIKE_ENV_VAR)
.ok()
.as_deref()
.and_then(set)
.or_else(|| option_env!("FRUST_OVERLAY_SPIKE").and_then(set))
}
pub fn spike_cut(scene: &frust_scene::Scene) -> Option<usize> {
scene
.commands()
.iter()
.position(|command| matches!(command, frust_scene::Command::ClearRect { .. }))
.map(|at| at + 1)
}
#[derive(Debug, Clone, Copy)]
pub enum OverlayTarget {
RawHandle {
display: wgpu::rwh::RawDisplayHandle,
window: wgpu::rwh::RawWindowHandle,
},
#[cfg(target_vendor = "apple")]
CoreAnimationLayer(*mut c_void),
}
impl OverlayTarget {
pub fn android_ndk_window(window: NonNull<c_void>) -> Self {
use wgpu::rwh::{
AndroidDisplayHandle, AndroidNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
};
Self::RawHandle {
display: RawDisplayHandle::Android(AndroidDisplayHandle::new()),
window: RawWindowHandle::AndroidNdk(AndroidNdkWindowHandle::new(window)),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum OverlayError {
NoDevice,
Surface(String),
UnsupportedSurface,
NoTranslucentMode,
StraightAlpha,
Engine(frust_engine::EngineError),
}
impl std::fmt::Display for OverlayError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoDevice => f.write_str("overlay: no device yet (create the main surface first)"),
Self::Surface(reason) => write!(f, "overlay: surface creation failed: {reason}"),
Self::UnsupportedSurface => {
f.write_str("overlay: the main surface's adapter cannot present to this surface")
}
Self::NoTranslucentMode => {
f.write_str("overlay: the platform granted no translucent composite mode")
}
Self::StraightAlpha => {
f.write_str("overlay: the compositor reads straight alpha, which the spike refuses")
}
Self::Engine(error) => write!(f, "overlay: engine: {error}"),
}
}
}
impl std::error::Error for OverlayError {}
pub struct OverlayRenderer {
engine: Box<frust_engine::EngineRenderer>,
shader_quads: Box<frust_engine::ShaderQuadPass>,
timestamps: frust_gpu::diag::TimestampRing,
last_cut: Option<RangeCut>,
refused: u32,
}
impl OverlayRenderer {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
caps: &frust_gpu::TierCaps,
format: wgpu::TextureFormat,
pipeline_cache: Option<&wgpu::PipelineCache>,
) -> Result<Self, OverlayError> {
let mut engine = frust_engine::EngineRenderer::new(device, caps, format, pipeline_cache)
.map_err(OverlayError::Engine)?;
engine.set_retained_layers(false);
Ok(Self {
engine: Box::new(engine),
shader_quads: Box::new(frust_engine::ShaderQuadPass::new(pipeline_cache.cloned())),
timestamps: frust_gpu::diag::TimestampRing::new(
device,
queue,
frust_engine::EngineSpan::COUNT,
"frust-render overlay timestamps",
),
last_cut: None,
refused: 0,
})
}
pub fn record(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
scene: &frust_scene::Scene,
range: Range<usize>,
target: frust_engine::EngineTarget<'_>,
) -> Result<RangeCut, OverlayError> {
self.shader_quads.prepare(
device,
queue,
encoder,
scene,
Affine::IDENTITY,
&mut self.engine,
);
self.timestamps.begin_frame(device);
let result = self.engine.record_range(
device,
queue,
encoder,
scene,
range,
target,
peniko::Color::TRANSPARENT,
Affine::IDENTITY,
frust_engine::FrameTimestamps::new(&self.timestamps),
);
match result {
Ok(cut) => {
self.timestamps.resolve(encoder);
self.last_cut = Some(cut);
Ok(cut)
}
Err(error) => {
self.timestamps.abandon_frame();
self.refused = self.refused.saturating_add(1);
Err(OverlayError::Engine(error))
}
}
}
pub fn end_frame(&mut self, queue: &wgpu::Queue, submitted: bool) {
if submitted {
self.timestamps.end_frame();
} else {
self.timestamps.abandon_frame();
}
self.engine.end_frame(queue);
}
pub fn last_cut(&self) -> Option<RangeCut> {
self.last_cut
}
pub fn refused_frames(&self) -> u32 {
self.refused
}
pub fn gpu_pass_timings(&self) -> Option<GpuPassTimings> {
let reading = self.timestamps.latest()?;
let span = |which: frust_engine::EngineSpan| reading.span(which.index());
Some(GpuPassTimings {
prepass: span(frust_engine::EngineSpan::Prepass),
main: span(frust_engine::EngineSpan::Main),
composite: span(frust_engine::EngineSpan::Composite),
blit: span(frust_engine::EngineSpan::Blit),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverlayFrame {
Rendered(RangeCut),
Reconfigured,
Skipped,
}
pub struct OverlaySurface {
surface: EngineSurface,
renderer: OverlayRenderer,
device: wgpu::Device,
queue: wgpu::Queue,
recorded: Option<wgpu::SurfaceTexture>,
pending: Option<wgpu::SurfaceTexture>,
}
impl OverlaySurface {
pub unsafe fn new(
ctx: &mut RenderContext,
main: Option<&SurfaceRenderer>,
target: OverlayTarget,
width: u32,
height: u32,
) -> Result<Self, OverlayError> {
if ctx.caps().is_none() {
return Err(OverlayError::NoDevice);
}
let raw = match target {
OverlayTarget::RawHandle { display, window } => wgpu::SurfaceTargetUnsafe::RawHandle {
raw_display_handle: Some(display),
raw_window_handle: window,
},
#[cfg(target_vendor = "apple")]
OverlayTarget::CoreAnimationLayer(layer) => {
if layer.is_null() {
return Err(OverlayError::Surface("null CAMetalLayer pointer".into()));
}
wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(layer)
}
};
let surface = unsafe { ctx.instance().create_surface_unsafe(raw) }
.map_err(|error| OverlayError::Surface(error.to_string()))?;
if !ctx.device_handle().adapter.is_surface_supported(&surface) {
return Err(OverlayError::UnsupportedSurface);
}
let surface = pollster_block_on(crate::context::create_engine_surface(
ctx,
surface,
width.max(1),
height.max(1),
wgpu::PresentMode::AutoVsync,
SurfaceAlphaRequest::TranslucentPreferred,
))
.map_err(|error| OverlayError::Surface(format!("{error:#}")))?;
if !surface.resolved_translucent() {
return Err(OverlayError::NoTranslucentMode);
}
if !matches!(surface.path, RenderPath::EngineDirect { .. }) {
return Err(OverlayError::StraightAlpha);
}
let handle = ctx.device_handle();
let cache = main.and_then(SurfaceRenderer::shared_pipeline_cache);
let renderer = OverlayRenderer::new(
&handle.device,
&handle.queue,
&handle.caps,
surface.config().format,
cache.as_ref(),
)?;
log::info!(
"frust-render overlay spike: overlay surface up ({:?}, {:?}, {}x{})",
surface.config().format,
surface.config().alpha_mode,
surface.config().width,
surface.config().height
);
Ok(Self {
surface,
renderer,
device: handle.device.clone(),
queue: handle.queue.clone(),
recorded: None,
pending: None,
})
}
pub fn size(&self) -> (u32, u32) {
(self.surface.config().width, self.surface.config().height)
}
pub fn resize(&mut self, width: u32, height: u32) {
if width == 0 || height == 0 || (width, height) == self.size() {
return;
}
self.pending = None;
if self.recorded.take().is_some() {
self.renderer.end_frame(&self.queue, false);
}
self.surface.resize(&self.device, width, height);
}
pub fn record(
&mut self,
encoder: &mut wgpu::CommandEncoder,
scene: &frust_scene::Scene,
range: Range<usize>,
) -> Result<OverlayFrame, OverlayError> {
self.pending = None;
if self.recorded.take().is_some() {
self.renderer.end_frame(&self.queue, false);
}
use wgpu::CurrentSurfaceTexture as Cst;
let texture = match self.surface.gpu.surface().get_current_texture() {
Cst::Success(texture) | Cst::Suboptimal(texture) => texture,
Cst::Outdated | Cst::Validation => {
self.surface.reconfigure(&self.device);
return Ok(OverlayFrame::Reconfigured);
}
Cst::Lost | Cst::Timeout | Cst::Occluded => return Ok(OverlayFrame::Skipped),
};
let view = texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let (width, height) = self.size();
let format = self.surface.config().format;
let depth = match &self.surface.path {
RenderPath::EngineDirect { depth, .. } => Some(depth.view()),
RenderPath::EngineDirectUnpremultiply { .. } => None,
};
let result = self.renderer.record(
&self.device,
&self.queue,
encoder,
scene,
range,
frust_engine::EngineTarget {
view: &view,
format,
width,
height,
depth,
output: frust_engine::OutputAlpha::Premultiplied,
},
);
match result {
Ok(cut) => {
self.recorded = Some(texture);
Ok(OverlayFrame::Rendered(cut))
}
Err(error) => {
self.renderer.end_frame(&self.queue, false);
Err(error)
}
}
}
pub fn end_frame(&mut self, submitted: bool) -> bool {
let Some(texture) = self.recorded.take() else {
return false;
};
self.renderer.end_frame(&self.queue, submitted);
if submitted {
self.pending = Some(texture);
}
submitted
}
pub fn render(
&mut self,
scene: &frust_scene::Scene,
range: Range<usize>,
) -> Result<OverlayFrame, OverlayError> {
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-render overlay"),
});
let frame = self.record(&mut encoder, scene, range)?;
if matches!(frame, OverlayFrame::Rendered(_)) {
self.queue.submit([encoder.finish()]);
self.end_frame(true);
}
Ok(frame)
}
pub fn present(&mut self) -> bool {
match self.pending.take() {
Some(texture) => {
self.queue.present(texture);
true
}
None => false,
}
}
pub fn take_deferred_present(&mut self) -> Option<DeferredPresent> {
self.pending
.take()
.map(|texture| DeferredPresent::new(texture, self.queue.clone()))
}
pub fn renderer(&self) -> &OverlayRenderer {
&self.renderer
}
}
impl std::fmt::Debug for OverlaySurface {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OverlaySurface")
.field("size", &self.size())
.field("pending", &self.pending.is_some())
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct OverlayCut<'a> {
pub overlay: &'a mut OverlaySurface,
pub range: Range<usize>,
pub overlay_id: u64,
}
#[derive(Debug)]
pub struct OverlayCutOutcome {
pub overlay_id: u64,
pub result: Result<OverlayFrame, OverlayError>,
pub serviceability: CutServiceability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnservableReason {
RetainedComposite,
RetainedRecording,
RetainedBracketCrossing,
SceneTexture,
BackdropBlur,
}
impl std::fmt::Display for UnservableReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::RetainedComposite => "retained content (a page composite)",
Self::RetainedRecording => "retained content (a page recording)",
Self::RetainedBracketCrossing => "retained content (a bracket crossing the cut)",
Self::SceneTexture => "an external texture",
Self::BackdropBlur => "a backdrop blur",
})
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum CutServiceability {
#[default]
Servable,
Unservable {
reason: UnservableReason,
at: usize,
},
}
impl CutServiceability {
pub fn is_servable(self) -> bool {
matches!(self, Self::Servable)
}
pub fn served_range(self, cut: &Range<usize>) -> Range<usize> {
match self {
Self::Servable => cut.clone(),
Self::Unservable { .. } => 0..0,
}
}
}
impl std::fmt::Display for CutServiceability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Servable => f.write_str("served on the overlay"),
Self::Unservable { reason, at } => {
write!(f, "kept on main: {reason} at command {at}")
}
}
}
}
pub fn cut_is_servable(commands: &[Command], range: Range<usize>) -> CutServiceability {
let len = commands.len();
let end = range.end.min(len);
let start = range.start.min(end);
if start == end {
return CutServiceability::Servable;
}
let unservable = |reason, at| CutServiceability::Unservable { reason, at };
if let Some(at) = open_retained_bracket(commands, start) {
return unservable(UnservableReason::RetainedBracketCrossing, at);
}
for (at, command) in commands.iter().enumerate().take(end).skip(start) {
let reason = match command {
Command::DrawRetained { .. } => UnservableReason::RetainedComposite,
Command::PushRetained { .. } => {
if end < len && !retained_closes_before(commands, at, end) {
UnservableReason::RetainedBracketCrossing
} else {
UnservableReason::RetainedRecording
}
}
Command::SceneTexture { .. } => UnservableReason::SceneTexture,
Command::BackdropBlur { .. } => UnservableReason::BackdropBlur,
_ => continue,
};
return unservable(reason, at);
}
CutServiceability::Servable
}
fn retained_closes_before(commands: &[Command], push: usize, end: usize) -> bool {
let mut depth = 0_usize;
for command in commands.iter().take(end).skip(push) {
match command {
Command::PushRetained { .. } => depth += 1,
Command::PopRetained => {
depth -= 1;
if depth == 0 {
return true;
}
}
_ => {}
}
}
false
}
#[derive(Debug)]
pub struct SegmentedOutcome {
pub frame: frust_gpu::FrameOutcome,
pub overlays: Vec<OverlayCutOutcome>,
pub encoders: u32,
pub submits: u32,
}
#[derive(Debug, Default)]
pub struct SegmentedPresents {
pub main: Option<DeferredPresent>,
pub overlays: Vec<(u64, DeferredPresent)>,
}
impl SegmentedPresents {
pub fn present_all(self) {
if let Some(main) = self.main {
main.present();
}
for (_, overlay) in self.overlays {
overlay.present();
}
}
pub fn into_vec(self) -> Vec<DeferredPresent> {
self.main
.into_iter()
.chain(self.overlays.into_iter().map(|(_, present)| present))
.collect()
}
pub fn len(&self) -> usize {
usize::from(self.main.is_some()) + self.overlays.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub fn main_segment_ranges(len: usize, cuts: &[Range<usize>]) -> Vec<Range<usize>> {
let mut ranges = Vec::with_capacity(cuts.len() + 1);
let mut from = 0;
for cut in cuts {
let start = cut.start.clamp(from, len);
let end = cut.end.clamp(start, len);
if start == end {
continue;
}
if from < start {
ranges.push(from..start);
}
from = end;
}
if from < len {
ranges.push(from..len);
}
ranges
}
pub fn project_main_segment(
scene: &frust_scene::Scene,
cuts: &[Range<usize>],
options: frust_engine::renderer::RangeOptions,
out: &mut frust_scene::Scene,
) -> RangeCut {
let ranges = main_segment_ranges(scene.commands().len(), cuts);
frust_engine::renderer::project_command_ranges(scene, &ranges, options, out)
}
pub(crate) fn record_cuts(
encoder: &mut wgpu::CommandEncoder,
scene: &frust_scene::Scene,
cuts: &mut [OverlayCut<'_>],
service: &[CutServiceability],
) -> Vec<OverlayCutOutcome> {
cuts.iter_mut()
.enumerate()
.map(|(index, cut)| {
let serviceability = service
.get(index)
.copied()
.unwrap_or_else(|| cut_is_servable(scene.commands(), cut.range.clone()));
let result =
cut.overlay
.record(encoder, scene, serviceability.served_range(&cut.range));
if let Ok(OverlayFrame::Rendered(range_cut)) = &result {
note_dropped_composites(range_cut);
}
OverlayCutOutcome {
overlay_id: cut.overlay_id,
result,
serviceability,
}
})
.collect()
}
fn note_dropped_composites(cut: &RangeCut) {
static LOGGED: AtomicBool = AtomicBool::new(false);
debug_assert_eq!(
cut.dropped_draw_retained, 0,
"an overlay dropped a retained composite of a served cut"
);
debug_assert_eq!(
cut.demoted_retained, 0,
"an overlay demoted a retained bracket of a served cut: the classifier \
and the projection disagree on where the bracket closes"
);
if cut.dropped_draw_retained > 0 && !LOGGED.swap(true, Ordering::Relaxed) {
log::error!(
"frust-render overlay: a served cut dropped {} retained composite(s) \
(commands {}..{}); that content drew on no surface (logged once)",
cut.dropped_draw_retained,
cut.start,
cut.end
);
}
}
pub(crate) fn skipped_cuts(
cuts: &[OverlayCut<'_>],
service: &[CutServiceability],
) -> Vec<OverlayCutOutcome> {
cuts.iter()
.enumerate()
.map(|(index, cut)| OverlayCutOutcome {
overlay_id: cut.overlay_id,
result: Ok(OverlayFrame::Skipped),
serviceability: service.get(index).copied().unwrap_or_default(),
})
.collect()
}
pub(crate) fn end_cuts(cuts: &mut [OverlayCut<'_>], submitted: bool) {
for cut in cuts {
cut.overlay.end_frame(submitted);
}
}
fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let mut cx = Context::from_waker(Waker::noop());
let mut future = std::pin::pin!(future);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(value) => return value,
Poll::Pending => std::thread::yield_now(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_overlay_types_cross_threads() {
fn send<T: Send>() {}
send::<OverlaySurface>();
send::<OverlayRenderer>();
send::<OverlayError>();
}
#[test]
fn spike_cut_is_past_the_first_clear() {
let mut scene = frust_scene::Scene::new();
assert_eq!(spike_cut(&scene), None);
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
builder.fill_rect(
kurbo::Rect::new(0.0, 0.0, 1.0, 1.0),
peniko::Brush::Solid(peniko::Color::BLACK),
);
builder.clear_rect(kurbo::Rect::ZERO);
builder.clear_rect(kurbo::Rect::ZERO);
assert_eq!(spike_cut(&scene), Some(2));
}
#[test]
fn the_spike_reads_off_by_default() {
if option_env!("FRUST_OVERLAY_SPIKE").is_none()
&& std::env::var(OVERLAY_SPIKE_ENV_VAR).is_err()
{
assert!(!overlay_spike_enabled());
assert_eq!(overlay_spike_value(), None);
}
}
}