use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use frust_core::FrameTime;
use frust_render::{
AcquireOutcome, DetachedSurface, EncodeOutcome, FrameOutcome, RenderContext,
SurfaceAlphaRequest, SurfaceFactory, SurfacePhase, SurfaceRenderer,
};
use frust_scene::Scene;
use frust_shell_common::perf::{
FramePasses, FrameStats, GpuPasses, RenderSpans, SPAN_ADAPTER_READY, SPAN_DEVICE_READY,
SPAN_FIRST_ENCODE_DONE, SPAN_FIRST_FRAME_PRESENTED, SPAN_FIRST_REBUILD_DONE, SPAN_INIT_ENTRY,
SPAN_PIPELINE_CACHE_RESTORED, SPAN_RENDERER_READY, StartupSpans, UiSpans,
};
use frust_shell_common::render_split::publish_resident_pages;
use frust_shell_common::{
FrameMeta, RenderCommand, RenderPhase, RenderReceiver, RenderSender, SceneFrame,
SceneReturnReceiver, SceneReturnSender, SurfaceSize, next_render_phase, render_channel,
scene_return_channel,
};
use winit::event_loop::EventLoopProxy;
use winit::window::Window;
use crate::app_handler::ShellUserEvent;
use crate::platform_view::OverlayCutSpec;
type WindowHandle = Arc<Window>;
const DESTROY_SURFACE_BARRIER_DEADLINE: Duration = Duration::from_secs(5);
pub(crate) struct PaintedScene {
scene: Scene,
base_color: peniko::Color,
overlay_cuts: Vec<OverlayCutSpec>,
}
pub(crate) struct DesktopSurface {
detached: DetachedSurface,
window: WindowHandle,
}
pub(crate) enum FrameExecutor {
Inline(Box<InlineExecutor>),
Split(SplitExecutor),
}
impl FrameExecutor {
pub(crate) fn has_surface(&self) -> bool {
match self {
FrameExecutor::Inline(inline) => inline.renderer.phase() == SurfacePhase::SurfaceReady,
FrameExecutor::Split(split) => split.surface_requested,
}
}
pub(crate) fn presented_frames(&self) -> u64 {
match self {
FrameExecutor::Inline(inline) => inline.presented.load(Ordering::Relaxed),
FrameExecutor::Split(split) => split.presented.load(Ordering::Relaxed),
}
}
pub(crate) fn take_present_retry(&mut self) -> bool {
match self {
FrameExecutor::Inline(inline) => std::mem::take(&mut inline.present_retry),
FrameExecutor::Split(_) => false,
}
}
pub(crate) fn ensure_surface(
&mut self,
window: &WindowHandle,
size: SurfaceSize,
) -> Result<()> {
match self {
FrameExecutor::Inline(inline) => inline.ensure_surface(window, size),
FrameExecutor::Split(split) => split.send_surface_created(window, size),
}
}
pub(crate) fn recreate_surface(&mut self, window: &WindowHandle, size: SurfaceSize) {
if let FrameExecutor::Split(split) = self {
if let Err(err) = split.send_surface_created(window, size) {
log::error!("frust: failed to recreate surface: {err}");
}
}
}
pub(crate) fn destroy_surface(&mut self) {
match self {
FrameExecutor::Inline(inline) => {
drop_overlay_surfaces();
inline.renderer.on_surface_destroyed();
publish_ledger(&mut inline.renderer);
}
FrameExecutor::Split(split) => split.destroy_surface(),
}
}
pub(crate) fn resize_surface(&mut self, size: SurfaceSize) {
match self {
FrameExecutor::Inline(inline) => {
inline
.renderer
.on_surface_changed(&inline.render_cx, size.width, size.height);
note_scale(&mut inline.renderer, size.scale);
}
FrameExecutor::Split(split) => split.resize_surface(size),
}
}
pub(crate) fn record_first_rebuild(&mut self) {
if let FrameExecutor::Inline(inline) = self
&& !inline.first_rebuild_recorded
{
inline.startup.record(SPAN_FIRST_REBUILD_DONE);
inline.first_rebuild_recorded = true;
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn submit_frame(
&mut self,
window: &WindowHandle,
scene: &mut Scene,
base_color: peniko::Color,
ui_spans: UiSpans,
frame_time: FrameTime,
size: SurfaceSize,
paint_serial: u64,
overlay_cuts: Vec<OverlayCutSpec>,
) {
match self {
FrameExecutor::Inline(inline) => inline.submit_frame(
window,
scene,
base_color,
ui_spans,
paint_serial,
&overlay_cuts,
),
FrameExecutor::Split(split) => {
let replacement = split.take_reusable_scene();
let painted = PaintedScene {
scene: std::mem::replace(scene, replacement),
base_color,
overlay_cuts,
};
split.submit_frame(painted, ui_spans, frame_time, size, paint_serial);
}
}
}
}
pub(crate) fn spawn_render_thread(proxy: EventLoopProxy<ShellUserEvent>) -> SplitExecutor {
let render_cx = RenderContext::new();
let factory = render_cx.surface_factory();
let (sender, receiver) = render_channel::<PaintedScene, DesktopSurface>();
let (scene_return_tx, scene_return_rx) = scene_return_channel::<Scene>();
let presented = Arc::new(AtomicU64::new(0));
let presented_render = Arc::clone(&presented);
let join = std::thread::Builder::new()
.name("frust-render".to_string())
.spawn(move || {
frust_shell_common::run_guarded_thread("frust-render (desktop)", move || {
render_loop(
receiver,
proxy,
render_cx,
scene_return_tx,
presented_render,
)
})
})
.expect("frust: failed to spawn render thread");
SplitExecutor::new(sender, join, factory, scene_return_rx, presented)
}
pub(crate) struct InlineExecutor {
render_cx: RenderContext,
renderer: SurfaceRenderer,
startup: StartupSpans,
frame_stats: FrameStats,
renderer_spans_recorded: bool,
first_rebuild_recorded: bool,
first_encode_recorded: bool,
first_frame_recorded: bool,
presented: Arc<AtomicU64>,
present_retry: bool,
}
impl InlineExecutor {
pub(crate) fn new() -> Self {
let mut startup = StartupSpans::begin();
startup.record(SPAN_INIT_ENTRY);
Self {
render_cx: RenderContext::new(),
renderer: SurfaceRenderer::new(),
startup,
frame_stats: FrameStats::new(),
renderer_spans_recorded: false,
first_rebuild_recorded: false,
first_encode_recorded: false,
first_frame_recorded: false,
presented: Arc::new(AtomicU64::new(0)),
present_retry: false,
}
}
fn ensure_surface(&mut self, window: &WindowHandle, size: SurfaceSize) -> Result<()> {
drop_overlay_surfaces();
let hit = load_pipeline_cache(&mut self.renderer);
pollster::block_on(self.renderer.on_surface_created(
&mut self.render_cx,
window.clone(),
size.width.max(1),
size.height.max(1),
SurfaceAlphaRequest::Opaque,
))
.context("frust: failed to create render surface")?;
note_scale(&mut self.renderer, size.scale);
persist_and_record(
&self.renderer,
&mut self.startup,
&mut self.renderer_spans_recorded,
hit,
);
publish_gpu_handle(&self.render_cx);
Ok(())
}
fn submit_frame(
&mut self,
window: &WindowHandle,
scene: &Scene,
base_color: peniko::Color,
ui_spans: UiSpans,
paint_serial: u64,
overlay_cuts: &[OverlayCutSpec],
) {
let mut overlays = take_frame_overlays(
&self.renderer,
&mut self.render_cx,
overlay_cuts,
scene.commands().len(),
);
let outcome = render_frame(
&mut self.renderer,
&self.render_cx,
window,
scene,
paint_serial,
base_color,
ui_spans,
&mut self.frame_stats,
&mut self.startup,
&mut self.first_encode_recorded,
&mut self.first_frame_recorded,
overlays.as_mut(),
);
if let Some(overlays) = overlays {
land_frame_overlays(overlays);
}
match outcome {
Ok(FrameOutcome::Redraw) => window.request_redraw(),
Ok(FrameOutcome::SurfaceLost) => {
let size = window.inner_size();
drop_overlay_surfaces();
match pollster::block_on(self.renderer.on_surface_created(
&mut self.render_cx,
window.clone(),
size.width.max(1),
size.height.max(1),
SurfaceAlphaRequest::Opaque,
)) {
Ok(()) => window.request_redraw(),
Err(err) => eprintln!("frust: failed to recreate surface: {err}"),
}
}
Ok(FrameOutcome::Rendered) => {
self.presented.fetch_add(1, Ordering::Relaxed);
}
Ok(FrameOutcome::Skipped) => {
self.present_retry = self.renderer.phase() == SurfacePhase::SurfaceReady;
}
Err(err) => eprintln!("frust: render error: {err}"),
}
}
}
impl Default for InlineExecutor {
fn default() -> Self {
Self::new()
}
}
pub(crate) struct SplitExecutor {
sender: Option<RenderSender<PaintedScene, DesktopSurface>>,
join: Option<JoinHandle<()>>,
factory: SurfaceFactory,
surface_requested: bool,
frame_id: u64,
scene_return: SceneReturnReceiver<Scene>,
spare_scene: Option<Scene>,
presented: Arc<AtomicU64>,
}
impl SplitExecutor {
pub(crate) fn new(
sender: RenderSender<PaintedScene, DesktopSurface>,
join: JoinHandle<()>,
factory: SurfaceFactory,
scene_return: SceneReturnReceiver<Scene>,
presented: Arc<AtomicU64>,
) -> Self {
Self {
sender: Some(sender),
join: Some(join),
factory,
surface_requested: false,
frame_id: 0,
scene_return,
spare_scene: None,
presented,
}
}
fn take_reusable_scene(&mut self) -> Scene {
if let Some(spare) = self.spare_scene.take() {
return spare;
}
if let Some(mut returned) = self.scene_return.try_recv() {
returned.reset();
return returned;
}
Scene::new()
}
fn send_surface_created(&mut self, window: &WindowHandle, size: SurfaceSize) -> Result<()> {
let detached = self
.factory
.create_detached_surface(window.clone())
.context("frust: failed to create render surface")?;
if let Some(sender) = self.sender.as_ref() {
sender.send_command(RenderCommand::SurfaceCreated {
window: DesktopSurface {
detached,
window: window.clone(),
},
size,
});
}
self.surface_requested = true;
Ok(())
}
fn destroy_surface(&mut self) {
if let Some(sender) = self.sender.as_ref() {
if !sender
.destroy_surface()
.wait_timeout(DESTROY_SURFACE_BARRIER_DEADLINE)
{
log::error!(
"frust: surface-destroy barrier timed out after \
{DESTROY_SURFACE_BARRIER_DEADLINE:?}; proceeding (degraded)"
);
}
}
self.surface_requested = false;
}
fn resize_surface(&mut self, size: SurfaceSize) {
if let Some(sender) = self.sender.as_ref() {
sender.send_command(RenderCommand::SurfaceChanged { size });
}
}
fn submit_frame(
&mut self,
painted: PaintedScene,
ui_spans: UiSpans,
frame_time: FrameTime,
size: SurfaceSize,
paint_serial: u64,
) {
self.frame_id += 1;
if let Some(sender) = self.sender.as_ref() {
let stale = sender.send_scene(SceneFrame {
scene: painted,
meta: FrameMeta {
frame_time,
size,
frame_id: self.frame_id,
paint_serial,
},
ui_spans,
});
if let Some(stale_frame) = stale {
let mut reclaimed = stale_frame.scene.scene;
reclaimed.reset();
self.spare_scene = Some(reclaimed);
}
}
}
}
impl Drop for SplitExecutor {
fn drop(&mut self) {
self.sender.take();
if let Some(join) = self.join.take() {
let _ = join.join();
}
}
}
fn render_loop(
receiver: RenderReceiver<PaintedScene, DesktopSurface>,
proxy: EventLoopProxy<ShellUserEvent>,
mut render_cx: RenderContext,
scene_return: SceneReturnSender<Scene>,
presented: Arc<AtomicU64>,
) {
let mut renderer = SurfaceRenderer::new();
let mut window: Option<WindowHandle> = None;
let mut phase = RenderPhase::NoSurface;
let mut frame_stats = FrameStats::new();
let mut startup = StartupSpans::begin();
startup.record(SPAN_INIT_ENTRY);
let mut renderer_spans_recorded = false;
let mut first_rebuild_recorded = false;
let mut first_encode_recorded = false;
let mut first_frame_recorded = false;
loop {
let batch = receiver.wait_next();
for command in batch.commands {
phase = next_render_phase(phase, command.event());
match command {
RenderCommand::SurfaceCreated {
window: surface,
size,
} => {
let DesktopSurface {
detached,
window: w,
} = surface;
drop_overlay_surfaces();
if let Err(err) = install_detached(
&mut renderer,
&mut render_cx,
detached,
size.width,
size.height,
&mut startup,
&mut renderer_spans_recorded,
) {
let _ = proxy.send_event(ShellUserEvent::RenderFatal(err));
return;
}
note_scale(&mut renderer, size.scale);
window = Some(w);
}
RenderCommand::SurfaceChanged { size } => {
renderer.on_surface_changed(&render_cx, size.width, size.height);
note_scale(&mut renderer, size.scale);
}
RenderCommand::SurfaceDestroyed { ack } => {
drop_overlay_surfaces();
renderer.on_surface_destroyed();
publish_ledger(&mut renderer);
window = None;
ack.acknowledge();
}
RenderCommand::Pause { ack } => {
ack.acknowledge();
}
RenderCommand::Resume => {}
}
}
if let Some(frame) = batch.scene {
if phase.can_render()
&& let Some(win) = window.as_ref()
{
if !first_rebuild_recorded {
startup.record(SPAN_FIRST_REBUILD_DONE);
first_rebuild_recorded = true;
}
let mut overlays = take_frame_overlays(
&renderer,
&mut render_cx,
&frame.scene.overlay_cuts,
frame.scene.scene.commands().len(),
);
let outcome = render_frame(
&mut renderer,
&render_cx,
win,
&frame.scene.scene,
frame.meta.paint_serial,
frame.scene.base_color,
frame.ui_spans,
&mut frame_stats,
&mut startup,
&mut first_encode_recorded,
&mut first_frame_recorded,
overlays.as_mut(),
);
if let Some(overlays) = overlays {
land_frame_overlays(overlays);
}
match outcome {
Ok(FrameOutcome::Redraw) => {
let _ = proxy.send_event(ShellUserEvent::RenderNeedsRedraw);
}
Ok(FrameOutcome::SurfaceLost) => {
let _ = proxy.send_event(ShellUserEvent::RenderRecreateSurface);
}
Ok(FrameOutcome::Rendered) => {
presented.fetch_add(1, Ordering::Relaxed);
}
Ok(FrameOutcome::Skipped) => {
if renderer.phase() == SurfacePhase::SurfaceReady {
let _ = proxy.send_event(ShellUserEvent::RenderRetry);
}
}
Err(err) => log::error!("frust: render error: {err}"),
}
}
scene_return.give_back(frame.scene.scene);
}
if batch.disconnected {
break;
}
}
}
fn load_pipeline_cache(renderer: &mut SurfaceRenderer) -> bool {
let cache_data = crate::cache::load_cache();
let hit = cache_data.is_some();
if cache_data.is_some() {
renderer.set_initial_pipeline_cache_data(cache_data);
}
hit
}
fn persist_and_record(
renderer: &SurfaceRenderer,
startup: &mut StartupSpans,
renderer_spans_recorded: &mut bool,
pipeline_cache_hit: bool,
) {
if let Some(cache) = renderer.pipeline_cache_data() {
std::thread::spawn(move || crate::cache::save_cache(&cache));
}
if !*renderer_spans_recorded {
startup.record(SPAN_ADAPTER_READY);
startup.record(SPAN_DEVICE_READY);
if pipeline_cache_hit {
startup.record(SPAN_PIPELINE_CACHE_RESTORED);
}
startup.record(SPAN_RENDERER_READY);
*renderer_spans_recorded = true;
}
}
fn install_detached(
renderer: &mut SurfaceRenderer,
render_cx: &mut RenderContext,
detached: DetachedSurface,
width: u32,
height: u32,
startup: &mut StartupSpans,
renderer_spans_recorded: &mut bool,
) -> Result<()> {
let hit = load_pipeline_cache(renderer);
pollster::block_on(renderer.on_surface_installed(
render_cx,
detached,
width,
height,
SurfaceAlphaRequest::Opaque,
))
.context("frust: failed to install render surface")?;
persist_and_record(renderer, startup, renderer_spans_recorded, hit);
publish_gpu_handle(render_cx);
Ok(())
}
#[cfg(feature = "gpu")]
fn publish_gpu_handle(render_cx: &RenderContext) {
frust_shell_common::gpu::install_gpu_handle(render_cx.device_handle().clone());
}
#[cfg(not(feature = "gpu"))]
fn publish_gpu_handle(_render_cx: &RenderContext) {}
fn gpu_passes(renderer: &SurfaceRenderer) -> Option<GpuPasses> {
renderer.gpu_pass_timings().map(|timings| GpuPasses {
prepass: timings.prepass,
main: timings.main,
composite: timings.composite,
blit: timings.blit,
})
}
fn publish_ledger(renderer: &mut SurfaceRenderer) {
if let Some(resident) = renderer.take_retained_ledger() {
publish_resident_pages(
resident.frame,
resident
.pages
.iter()
.map(|page| (page.key, page.extent_px, page.scale)),
);
}
}
fn note_scale(renderer: &mut SurfaceRenderer, scale: f64) {
if renderer.note_device_scale(scale) {
publish_ledger(renderer);
}
}
fn publish_retained(renderer: &mut SurfaceRenderer) -> bool {
publish_ledger(renderer);
renderer.take_retained_refusal()
}
#[allow(clippy::too_many_arguments)]
fn render_frame(
renderer: &mut SurfaceRenderer,
render_cx: &RenderContext,
window: &Window,
scene: &Scene,
paint_serial: u64,
base_color: peniko::Color,
ui_spans: UiSpans,
frame_stats: &mut FrameStats,
startup: &mut StartupSpans,
first_encode_recorded: &mut bool,
first_frame_recorded: &mut bool,
mut overlays: Option<&mut FrameOverlays>,
) -> Result<FrameOutcome> {
#[cfg(target_os = "macos")]
frust_render::native_import::install(render_cx);
window.pre_present_notify();
renderer.note_scene_serial(paint_serial);
let encode_start = Instant::now();
let encode_outcome = match overlays.as_deref() {
None => renderer.encode(render_cx, scene, base_color),
Some(overlays) => {
renderer.encode_segmented(render_cx, scene, base_color, &overlays.ranges())
}
};
let encode_dur = encode_start.elapsed();
if matches!(encode_outcome, Ok(EncodeOutcome::Encoded)) && !*first_encode_recorded {
startup.record(SPAN_FIRST_ENCODE_DONE);
*first_encode_recorded = true;
}
let acquire_start = Instant::now();
let acquire_outcome = match encode_outcome {
Ok(EncodeOutcome::Encoded) => renderer.acquire(render_cx),
Ok(EncodeOutcome::Skipped) => Ok(AcquireOutcome::Skipped),
Err(err) => Err(err),
};
let acquire_dur = acquire_start.elapsed();
let submit_start = Instant::now();
let render_outcome = match acquire_outcome {
Ok(AcquireOutcome::Acquired) => match overlays.as_mut() {
None => renderer.submit(render_cx),
Some(overlays) => submit_with_overlays(renderer, render_cx, overlays),
},
Ok(AcquireOutcome::Reconfigured) => Ok(FrameOutcome::Redraw),
Ok(AcquireOutcome::Lost) => Ok(FrameOutcome::SurfaceLost),
Ok(AcquireOutcome::Skipped) => Ok(FrameOutcome::Skipped),
Err(err) => Err(err),
};
let submit_dur = submit_start.elapsed();
let refused_retained = publish_retained(renderer);
let render_outcome = match render_outcome {
Ok(FrameOutcome::Skipped) if refused_retained => Ok(FrameOutcome::Redraw),
other => other,
};
let mut passes = FramePasses::from_split(
ui_spans,
RenderSpans {
encode: encode_dur,
acquire: acquire_dur,
submit: submit_dur,
},
);
if let Some(gpu) = gpu_passes(renderer) {
passes = passes.with_gpu(gpu);
}
frame_stats.record(passes);
if frame_stats.should_emit() {
frame_stats.emit_log();
}
if matches!(render_outcome, Ok(FrameOutcome::Rendered)) && !*first_frame_recorded {
startup.record(SPAN_FIRST_FRAME_PRESENTED);
startup.emit_log();
*first_frame_recorded = true;
}
render_outcome
}
const OVERLAYS_ENV_VAR: &str = "FRUST_OVERLAYS";
const OVERLAY_ERRORS_LOGGED: u32 = 3;
const OVERLAY_SKEW_EVERY: u32 = 120;
const OVERLAY_LANDING_WAIT: Duration = Duration::from_secs(2);
pub(crate) fn overlay_switch_value_on(value: &str) -> bool {
let value = value.trim();
!(value.is_empty()
|| value == "0"
|| value.eq_ignore_ascii_case("off")
|| value.eq_ignore_ascii_case("false"))
}
pub(crate) fn overlay_switch_from<'a>(sources: impl IntoIterator<Item = Option<&'a str>>) -> bool {
sources
.into_iter()
.flatten()
.find(|value| !value.trim().is_empty())
.is_some_and(overlay_switch_value_on)
}
pub(crate) fn overlays_enabled() -> bool {
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ON.get_or_init(|| {
if !cfg!(target_os = "macos") {
return false;
}
let runtime = std::env::var(OVERLAYS_ENV_VAR).ok();
let alias = frust_render::overlay::overlay_spike_value();
overlay_switch_from([
runtime.as_deref(),
option_env!("FRUST_OVERLAYS"),
alias.as_deref(),
])
})
}
#[derive(Debug)]
struct OverlayEntry<S> {
surface: Option<S>,
layer: usize,
size: (u32, u32),
refused: bool,
in_flight: bool,
errors: u32,
unservable_logged: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LayerChange {
Adopted,
Resized,
Unchanged,
Busy,
}
#[derive(Debug)]
pub(crate) struct OverlayTable<S> {
entries: std::collections::BTreeMap<u64, OverlayEntry<S>>,
}
impl<S> OverlayTable<S> {
pub(crate) const fn new() -> Self {
Self {
entries: std::collections::BTreeMap::new(),
}
}
pub(crate) fn publish(&mut self, id: u64, layer: usize, size: (u32, u32)) -> LayerChange {
if let Some(entry) = self.entries.get_mut(&id) {
if entry.layer == layer {
if entry.size == size {
return LayerChange::Unchanged;
}
entry.size = size;
return LayerChange::Resized;
}
if entry.in_flight {
return LayerChange::Busy;
}
entry.surface = None;
}
self.entries.insert(
id,
OverlayEntry {
surface: None,
layer,
size,
refused: false,
in_flight: false,
errors: 0,
unservable_logged: false,
},
);
LayerChange::Adopted
}
pub(crate) fn is_in_flight(&self, id: u64) -> bool {
self.entries.get(&id).is_some_and(|entry| entry.in_flight)
}
pub(crate) fn withdraw(&mut self, id: u64) -> bool {
if self.is_in_flight(id) {
return false;
}
match self.entries.remove(&id) {
Some(mut entry) => {
entry.surface = None;
true
}
None => false,
}
}
pub(crate) fn drop_surfaces(&mut self) -> usize {
let mut dropped = 0;
for entry in self.entries.values_mut() {
if entry.surface.take().is_some() {
dropped += 1;
}
entry.refused = false;
}
dropped
}
pub(crate) fn take_for_frame<E>(
&mut self,
mut build: impl FnMut(usize, (u32, u32)) -> Result<S, E>,
mut resize: impl FnMut(&mut S, (u32, u32)),
) -> OverlayTake<S, E> {
let mut flying = Vec::new();
let mut refusals = Vec::new();
for (&id, entry) in &mut self.entries {
if entry.in_flight {
continue;
}
match entry.surface.as_mut() {
Some(surface) => resize(surface, entry.size),
None if !entry.refused => match build(entry.layer, entry.size) {
Ok(surface) => entry.surface = Some(surface),
Err(error) => {
entry.refused = true;
refusals.push((id, error));
}
},
None => {}
}
if let Some(surface) = entry.surface.take() {
entry.in_flight = true;
flying.push((id, surface));
}
}
(flying, refusals)
}
pub(crate) fn land(&mut self, flying: Vec<(u64, S)>) -> usize {
let mut orphaned = 0;
for (id, surface) in flying {
match self.entries.get_mut(&id) {
Some(entry) => {
entry.surface = Some(surface);
entry.in_flight = false;
}
None => {
drop(surface);
orphaned += 1;
}
}
}
orphaned
}
pub(crate) fn note_error(&mut self, id: u64, log_limit: u32) -> Option<u32> {
let entry = self.entries.get_mut(&id)?;
entry.errors = entry.errors.saturating_add(1);
(entry.errors <= log_limit).then_some(entry.errors)
}
pub(crate) fn note_unservable(&mut self, id: u64) -> bool {
self.entries
.get_mut(&id)
.is_some_and(|entry| !std::mem::replace(&mut entry.unservable_logged, true))
}
pub(crate) fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub(crate) type OverlayTake<S, E> = (Vec<(u64, S)>, Vec<(u64, E)>);
static OVERLAY_TABLE: std::sync::Mutex<OverlayTable<frust_render::overlay::OverlaySurface>> =
std::sync::Mutex::new(OverlayTable::new());
static OVERLAY_LANDED: std::sync::Condvar = std::sync::Condvar::new();
static OVERLAY_SKEW: std::sync::Mutex<(u32, u64, u64)> = std::sync::Mutex::new((0, 0, 0));
fn overlay_table()
-> std::sync::MutexGuard<'static, OverlayTable<frust_render::overlay::OverlaySurface>> {
OVERLAY_TABLE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn publish_overlay_layer(id: u64, layer: usize, size: (u32, u32)) -> bool {
let size = (size.0.max(1), size.1.max(1));
let mut table = overlay_table();
let mut change = table.publish(id, layer, size);
if change == LayerChange::Busy {
table = wait_overlay_landed(table, id);
change = table.publish(id, layer, size);
}
match change {
LayerChange::Adopted => {
log::info!(
"frust-overlay: overlay {id} layer published ({}x{} px)",
size.0,
size.1
);
true
}
LayerChange::Resized => true,
LayerChange::Unchanged => false,
LayerChange::Busy => {
log::error!("frust-overlay: overlay {id} could not replace its layer");
false
}
}
}
pub(crate) fn withdraw_overlay_layer(id: u64) {
let mut table = wait_overlay_landed(overlay_table(), id);
if table.withdraw(id) {
log::info!("frust-overlay: overlay {id} layer withdrawn");
}
}
fn wait_overlay_landed(
mut table: std::sync::MutexGuard<'static, OverlayTable<frust_render::overlay::OverlaySurface>>,
id: u64,
) -> std::sync::MutexGuard<'static, OverlayTable<frust_render::overlay::OverlaySurface>> {
let deadline = Instant::now() + OVERLAY_LANDING_WAIT;
while table.is_in_flight(id) {
let Some(left) = deadline.checked_duration_since(Instant::now()) else {
log::error!(
"frust-overlay: overlay {id} still in flight after {OVERLAY_LANDING_WAIT:?}; \
proceeding without it"
);
break;
};
table = OVERLAY_LANDED
.wait_timeout(table, left)
.map(|(guard, _)| guard)
.unwrap_or_else(|poisoned| poisoned.into_inner().0);
}
table
}
fn drop_overlay_surfaces() {
if !overlays_enabled() {
return;
}
let dropped = overlay_table().drop_surfaces();
if dropped > 0 {
log::debug!("frust-overlay: dropped {dropped} overlay surface(s) with the main surface");
}
}
struct FrameOverlays {
surfaces: Vec<(u64, frust_render::overlay::OverlaySurface)>,
cuts: Vec<OverlayCutSpec>,
refused: Vec<(u64, String)>,
unservable: Vec<(u64, String)>,
}
impl FrameOverlays {
fn ranges(&self) -> Vec<std::ops::Range<usize>> {
self.cuts.iter().map(|(_, range)| range.clone()).collect()
}
fn overlay_cuts(&mut self) -> Vec<frust_render::overlay::OverlayCut<'_>> {
let mut live: std::collections::BTreeMap<u64, &mut frust_render::overlay::OverlaySurface> =
self.surfaces
.iter_mut()
.map(|(id, surface)| (*id, surface))
.collect();
self.cuts
.iter()
.filter_map(|(overlay_id, range)| {
live.remove(overlay_id)
.map(|overlay| frust_render::overlay::OverlayCut {
overlay,
range: range.clone(),
overlay_id: *overlay_id,
})
})
.collect()
}
fn note_outcomes(&mut self, outcomes: &[frust_render::overlay::OverlayCutOutcome]) {
for outcome in outcomes {
if let Err(error) = &outcome.result {
self.refused.push((outcome.overlay_id, error.to_string()));
}
if !outcome.serviceability.is_servable() {
self.unservable
.push((outcome.overlay_id, outcome.serviceability.to_string()));
}
}
}
}
fn assemble_overlay_cuts(
frame: &[OverlayCutSpec],
live: impl IntoIterator<Item = u64>,
len: usize,
) -> Vec<OverlayCutSpec> {
let mut cuts: Vec<OverlayCutSpec> = live
.into_iter()
.map(|id| {
let range = frame
.iter()
.find(|(cut, _)| *cut == id)
.map(|(_, range)| range.start.min(len)..range.end.min(len))
.filter(|range| range.start < range.end)
.unwrap_or(len..len);
(id, range)
})
.collect();
cuts.sort_by_key(|(id, range)| (range.start, range.end, *id));
cuts
}
fn take_frame_overlays(
renderer: &SurfaceRenderer,
render_cx: &mut RenderContext,
frame_cuts: &[OverlayCutSpec],
len: usize,
) -> Option<FrameOverlays> {
if !overlays_enabled() {
return None;
}
let mut table = overlay_table();
if table.is_empty() {
return None;
}
let (surfaces, refusals) = table.take_for_frame(
|layer, size| build_overlay_surface(render_cx, renderer, layer, size),
|surface, (width, height)| surface.resize(width, height),
);
drop(table);
for (overlay_id, error) in refusals {
log::warn!(
"frust-overlay: overlay {overlay_id} refused ({error}); its content stays on the main \
surface"
);
}
if surfaces.is_empty() {
return None;
}
let cuts = assemble_overlay_cuts(frame_cuts, surfaces.iter().map(|(id, _)| *id), len);
Some(FrameOverlays {
surfaces,
cuts,
refused: Vec::new(),
unservable: Vec::new(),
})
}
fn log_overlay_cost(surfaces: &[(u64, frust_render::overlay::OverlaySurface)]) {
static FRAMES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let frame = FRAMES.fetch_add(1, Ordering::Relaxed) + 1;
if !frame.is_multiple_of(OVERLAY_SKEW_EVERY) {
return;
}
for (overlay_id, surface) in surfaces {
if let Some(gpu) = surface.renderer().gpu_pass_timings() {
log::info!(
"frust-overlay cost overlay={overlay_id} gpu_prepass_us={} gpu_main_us={} \
gpu_composite_us={} size={:?}",
gpu.prepass.as_micros(),
gpu.main.as_micros(),
gpu.composite.as_micros(),
surface.size(),
);
}
}
}
fn land_frame_overlays(overlays: FrameOverlays) {
let FrameOverlays {
surfaces,
refused,
unservable,
..
} = overlays;
log_overlay_cost(&surfaces);
let mut table = overlay_table();
table.land(surfaces);
for (overlay_id, error) in refused {
if let Some(count) = table.note_error(overlay_id, OVERLAY_ERRORS_LOGGED) {
log::warn!("frust-overlay: overlay {overlay_id} frame refused ({count}): {error}");
}
}
for (overlay_id, reason) in unservable {
if table.note_unservable(overlay_id) {
log::info!(
"frust-overlay: overlay {overlay_id} content {reason}; it draws under the native \
view while that lasts (logged once)"
);
}
}
drop(table);
OVERLAY_LANDED.notify_all();
}
fn record_overlay_presents(delta: Duration, overlays: usize) {
let delta = u64::try_from(delta.as_micros()).unwrap_or(u64::MAX);
let mut window = OVERLAY_SKEW
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (frames, sum, max) = &mut *window;
*frames += 1;
*sum = sum.saturating_add(delta);
*max = (*max).max(delta);
if *frames >= OVERLAY_SKEW_EVERY {
log::info!(
"frust-overlay skew frames={} avg_us={} max_us={} overlays={overlays}",
*frames,
*sum / u64::from(*frames),
*max,
);
*window = (0, 0, 0);
}
}
#[cfg(target_os = "macos")]
fn build_overlay_surface(
render_cx: &mut RenderContext,
renderer: &SurfaceRenderer,
layer: usize,
(width, height): (u32, u32),
) -> Result<frust_render::overlay::OverlaySurface, frust_render::overlay::OverlayError> {
let layer = std::ptr::with_exposed_provenance_mut::<std::ffi::c_void>(layer);
let surface = unsafe {
frust_render::overlay::OverlaySurface::new(
render_cx,
Some(renderer),
frust_render::overlay::OverlayTarget::CoreAnimationLayer(layer),
width,
height,
)
};
if surface.is_ok() {
log::info!(
"frust-overlay: overlay surface up on CAMetalLayer {layer:p} ({width}x{height} px)"
);
}
surface
}
#[cfg(not(target_os = "macos"))]
fn build_overlay_surface(
_render_cx: &mut RenderContext,
_renderer: &SurfaceRenderer,
_layer: usize,
_size: (u32, u32),
) -> Result<frust_render::overlay::OverlaySurface, frust_render::overlay::OverlayError> {
Err(frust_render::overlay::OverlayError::UnsupportedSurface)
}
fn submit_with_overlays(
renderer: &mut SurfaceRenderer,
render_cx: &RenderContext,
overlays: &mut FrameOverlays,
) -> Result<FrameOutcome> {
let (outcome, presents) = {
let mut cuts = overlays.overlay_cuts();
renderer.submit_segmented_deferred(render_cx, &mut cuts)?
};
overlays.note_outcomes(&outcome.overlays);
let frust_render::overlay::SegmentedPresents {
main,
overlays: overlay_frames,
} = presents;
let main_at = main.map(|frame| {
frame.present();
Instant::now()
});
let count = overlay_frames.len();
for (_, frame) in overlay_frames {
frame.present();
}
if let Some(main_at) = main_at
&& count > 0
{
static FIRST: std::sync::Once = std::sync::Once::new();
FIRST.call_once(|| {
log::info!(
"frust-overlay: first segmented frame presented: main + {count} overlay(s), \
cuts {:?}, {} submit(s)",
overlays.cuts,
outcome.submits
);
});
record_overlay_presents(main_at.elapsed(), count);
}
Ok(outcome.frame)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_overlay_switch_reads_the_first_non_empty_source() {
assert!(!overlay_switch_from([None, None, None]));
assert!(!overlay_switch_from([Some("0"), Some("1"), None]));
assert!(overlay_switch_from([Some(" "), None, Some("1")]));
for off in ["off", "OFF", "false", " False ", "0"] {
assert!(!overlay_switch_value_on(off), "{off:?}");
}
for on in ["1", "on", "media", "3"] {
assert!(overlay_switch_value_on(on), "{on:?}");
}
}
#[test]
fn overlays_stay_off_without_a_macos_host() {
if !cfg!(target_os = "macos") {
assert!(!overlays_enabled());
}
}
struct Tracked(String, std::rc::Rc<std::cell::RefCell<Vec<String>>>);
impl Drop for Tracked {
fn drop(&mut self) {
self.1.borrow_mut().push(format!("drop {}", self.0));
}
}
type Log = std::rc::Rc<std::cell::RefCell<Vec<String>>>;
fn build(log: &Log) -> impl FnMut(usize, (u32, u32)) -> Result<Tracked, String> + '_ {
move |layer, _| {
if layer == 0 {
return Err("refused".to_string());
}
Ok(Tracked(format!("{layer:#x}"), log.clone()))
}
}
#[test]
fn a_withdraw_waits_for_the_frame_then_drops_the_surface_first() {
let log = Log::default();
let mut table: OverlayTable<Tracked> = OverlayTable::new();
assert_eq!(table.publish(1, 0x10, (8, 6)), LayerChange::Adopted);
assert_eq!(table.publish(1, 0x10, (8, 6)), LayerChange::Unchanged);
let (flying, refusals) = table.take_for_frame(build(&log), |_, _| {});
assert!(refusals.is_empty());
assert_eq!(table.publish(1, 0x10, (16, 12)), LayerChange::Resized);
assert_eq!(table.publish(1, 0x20, (8, 6)), LayerChange::Busy);
assert!(!table.withdraw(1));
assert!(log.borrow().is_empty());
assert_eq!(table.land(flying), 0);
assert!(table.withdraw(1));
assert_eq!(log.borrow().as_slice(), ["drop 0x10"]);
assert!(table.is_empty());
}
#[test]
fn the_main_surface_going_away_keeps_the_layers_and_retries_refusals() {
let log = Log::default();
let mut table: OverlayTable<Tracked> = OverlayTable::new();
table.publish(1, 0x10, (1, 1));
table.publish(2, 0, (1, 1));
let (flying, refusals) = table.take_for_frame(build(&log), |_, _| {});
assert_eq!(refusals.len(), 1);
table.land(flying);
let (flying, refusals) = table.take_for_frame(build(&log), |_, _| {});
assert!(refusals.is_empty(), "a refusal is not retried every frame");
table.land(flying);
assert_eq!(table.drop_surfaces(), 1);
assert_eq!(log.borrow().as_slice(), ["drop 0x10"]);
let (flying, refusals) = table.take_for_frame(build(&log), |_, _| {});
assert_eq!((flying.len(), refusals.len()), (1, 1));
table.land(flying);
assert_eq!(table.note_error(1, 1), Some(1));
assert_eq!(table.note_error(1, 1), None);
assert!(table.note_unservable(1));
assert!(!table.note_unservable(1));
}
#[test]
fn a_frame_cuts_only_live_surfaces_with_its_own_ranges() {
let frame = [(1, 10..20), (2, 30..90), (3, 5..8)];
assert_eq!(
assemble_overlay_cuts(&frame, [1, 2, 4], 60),
vec![(1, 10..20), (2, 30..60), (4, 60..60)]
);
}
}