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::{
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;
type WindowHandle = Arc<Window>;
const DESTROY_SURFACE_BARRIER_DEADLINE: Duration = Duration::from_secs(5);
pub(crate) struct PaintedScene {
scene: Scene,
base_color: peniko::Color,
}
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 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) => inline.renderer.on_surface_destroyed(),
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)
}
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;
}
}
pub(crate) fn submit_frame(
&mut self,
window: &WindowHandle,
scene: &mut Scene,
base_color: peniko::Color,
ui_spans: UiSpans,
frame_time: FrameTime,
size: SurfaceSize,
) {
match self {
FrameExecutor::Inline(inline) => {
inline.submit_frame(window, scene, base_color, ui_spans)
}
FrameExecutor::Split(split) => {
let replacement = split.take_reusable_scene();
let painted = PaintedScene {
scene: std::mem::replace(scene, replacement),
base_color,
};
split.submit_frame(painted, ui_spans, frame_time, size);
}
}
}
}
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>,
}
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)),
}
}
fn ensure_surface(&mut self, window: &WindowHandle, size: SurfaceSize) -> Result<()> {
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")?;
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,
) {
let outcome = render_frame(
&mut self.renderer,
&self.render_cx,
window,
scene,
base_color,
ui_spans,
&mut self.frame_stats,
&mut self.startup,
&mut self.first_encode_recorded,
&mut self.first_frame_recorded,
);
match outcome {
Ok(FrameOutcome::Redraw) => window.request_redraw(),
Ok(FrameOutcome::SurfaceLost) => {
let size = window.inner_size();
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) => {}
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,
) {
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,
},
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;
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;
}
window = Some(w);
}
RenderCommand::SurfaceChanged { size } => {
renderer.on_surface_changed(&render_cx, size.width, size.height);
}
RenderCommand::SurfaceDestroyed { ack } => {
renderer.on_surface_destroyed();
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 outcome = render_frame(
&mut renderer,
&render_cx,
win,
&frame.scene.scene,
frame.scene.base_color,
frame.ui_spans,
&mut frame_stats,
&mut startup,
&mut first_encode_recorded,
&mut first_frame_recorded,
);
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) => {}
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,
})
}
#[allow(clippy::too_many_arguments)]
fn render_frame(
renderer: &mut SurfaceRenderer,
render_cx: &RenderContext,
window: &Window,
scene: &Scene,
base_color: peniko::Color,
ui_spans: UiSpans,
frame_stats: &mut FrameStats,
startup: &mut StartupSpans,
first_encode_recorded: &mut bool,
first_frame_recorded: &mut bool,
) -> Result<FrameOutcome> {
window.pre_present_notify();
let encode_start = Instant::now();
let encode_outcome = renderer.encode(render_cx, scene, base_color);
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) => renderer.submit(render_cx),
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 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
}