use core::ffi::c_void;
use core::ops::Range;
use anyhow::{Result, anyhow};
use std::sync::OnceLock;
use std::time::Duration;
use kurbo::Affine;
use crate::context::{EngineSurface, RenderPath};
use crate::overlay::{
CutServiceability, OverlayCut, OverlayCutOutcome, SegmentedOutcome, SegmentedPresents,
};
use frust_gpu::lifecycle::{
AcquireAction, AcquireOutcome, AcquireStatus, EncodeOutcome, FrameOutcome, SurfaceEvent,
SurfacePhase, decide_acquire, next_invalid_streak, next_phase,
};
use frust_gpu::{DetachedSurface, RenderContext, SurfaceAlphaRequest};
struct ReadySurface {
surface: EngineSurface,
backend: TierBackend,
pipeline_cache: Option<(wgpu::PipelineCache, String)>,
}
enum TierBackend {
Engine {
engine: Box<frust_engine::EngineRenderer>,
refused_frames: u32,
shader_quads: Box<frust_engine::ShaderQuadPass>,
timestamps: frust_gpu::diag::TimestampRing,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct GpuPassTimings {
pub prepass: Duration,
pub main: Duration,
pub composite: Duration,
pub blit: Duration,
}
impl GpuPassTimings {
#[must_use]
pub fn total(&self) -> Duration {
self.prepass
.saturating_add(self.main)
.saturating_add(self.composite)
.saturating_add(self.blit)
}
}
enum SurfaceState {
NoSurface,
Ready(Box<ReadySurface>),
Lost,
}
pub struct DeferredPresent {
texture: wgpu::SurfaceTexture,
queue: wgpu::Queue,
}
impl std::fmt::Debug for DeferredPresent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeferredPresent").finish_non_exhaustive()
}
}
impl DeferredPresent {
pub(crate) fn new(texture: wgpu::SurfaceTexture, queue: wgpu::Queue) -> Self {
Self { texture, queue }
}
pub fn present(self) {
self.queue.present(self.texture);
}
}
pub struct SurfaceRenderer {
state: SurfaceState,
consecutive_invalid: u8,
initial_cache_data: Option<Vec<u8>>,
pending_present: Option<wgpu::SurfaceTexture>,
pending_base_color: Option<peniko::Color>,
engine_scene: frust_scene::Scene,
#[cfg(feature = "perf-trace")]
first_submit: Option<FirstSubmitBreakdown>,
#[cfg(feature = "perf-trace")]
first_submit_seen: bool,
retained_published: Option<Option<u64>>,
scene_serial: u64,
pending_serial: Option<(u64, bool)>,
committed_serial: u64,
recording_committed: bool,
device_scale: Option<u64>,
overlay_split: bool,
segment_scene: frust_scene::Scene,
segment_pending: bool,
segment_service: Vec<CutServiceability>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ResidentPage {
pub key: u64,
pub extent_px: (u32, u32),
pub scale: f32,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ResidentPages {
pub pages: Vec<ResidentPage>,
pub frame: u64,
}
fn resident_pages(engine: &frust_engine::EngineRenderer, frame: u64) -> ResidentPages {
ResidentPages {
pages: engine
.retained_ledger()
.map(|entry| ResidentPage {
key: entry.key,
extent_px: entry.extent_px,
scale: entry.scale,
})
.collect(),
frame,
}
}
fn scene_records_retained(scene: &frust_scene::Scene) -> bool {
scene
.commands()
.iter()
.any(|command| matches!(command, frust_scene::Command::PushRetained { .. }))
}
#[cfg(feature = "perf-trace")]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FirstSubmitBreakdown {
pub pipeline_wait: std::time::Duration,
pub encode: std::time::Duration,
pub queue: std::time::Duration,
}
#[cfg(feature = "perf-trace")]
struct SubmitProbe {
armed: bool,
encode_start: Option<std::time::Instant>,
queue_start: Option<std::time::Instant>,
breakdown: FirstSubmitBreakdown,
}
#[cfg(feature = "perf-trace")]
impl SubmitProbe {
fn new(armed: bool) -> Self {
Self {
armed,
encode_start: None,
queue_start: None,
breakdown: FirstSubmitBreakdown::default(),
}
}
fn encode_begin(&mut self, engine: &mut frust_engine::EngineRenderer) {
if self.armed {
let _ = engine.take_pipeline_wait();
self.encode_start = Some(std::time::Instant::now());
}
}
fn encode_end(&mut self, engine: &mut frust_engine::EngineRenderer) {
if let Some(start) = self.encode_start.take() {
let total = start.elapsed();
self.breakdown.pipeline_wait = engine.take_pipeline_wait();
self.breakdown.encode = total.saturating_sub(self.breakdown.pipeline_wait);
}
}
fn queue_begin(&mut self) {
if self.armed {
self.queue_start = Some(std::time::Instant::now());
}
}
fn queue_end(&mut self) {
if let Some(start) = self.queue_start.take() {
self.breakdown.queue = start.elapsed();
}
}
}
#[cfg(not(feature = "perf-trace"))]
struct SubmitProbe;
#[cfg(not(feature = "perf-trace"))]
impl SubmitProbe {
fn new(_armed: bool) -> Self {
Self
}
fn encode_begin(&mut self, _engine: &mut frust_engine::EngineRenderer) {}
fn encode_end(&mut self, _engine: &mut frust_engine::EngineRenderer) {}
fn queue_begin(&mut self) {}
fn queue_end(&mut self) {}
}
impl Default for SurfaceRenderer {
fn default() -> Self {
Self::new()
}
}
impl SurfaceRenderer {
pub fn new() -> Self {
Self {
state: SurfaceState::NoSurface,
consecutive_invalid: 0,
initial_cache_data: None,
pending_present: None,
pending_base_color: None,
engine_scene: frust_scene::Scene::default(),
#[cfg(feature = "perf-trace")]
first_submit: None,
#[cfg(feature = "perf-trace")]
first_submit_seen: false,
retained_published: None,
scene_serial: 0,
pending_serial: None,
committed_serial: 0,
recording_committed: false,
device_scale: None,
overlay_split: false,
segment_scene: frust_scene::Scene::default(),
segment_pending: false,
segment_service: Vec::new(),
}
}
#[cfg(feature = "perf-trace")]
pub fn take_first_submit_breakdown(&mut self) -> Option<FirstSubmitBreakdown> {
self.first_submit.take()
}
fn clear_pending_frame(&mut self) {
self.pending_base_color = None;
self.pending_serial = None;
self.segment_pending = false;
}
fn adopt_surface_state(&mut self, state: SurfaceState) {
self.state = state;
self.consecutive_invalid = 0;
self.clear_pending_frame();
self.retained_published = None;
}
pub fn set_initial_pipeline_cache_data(&mut self, data: Option<Vec<u8>>) {
self.initial_cache_data = data;
}
pub fn pipeline_cache_data(&self) -> Option<Vec<u8>> {
let SurfaceState::Ready(ready) = &self.state else {
return None;
};
let (cache, key) = ready.pipeline_cache.as_ref()?;
let data = cache.get_data()?;
Some(frust_gpu::pipeline_cache::frame(key, &data))
}
pub fn pipeline_warm_up_finished(&self) -> Option<u64> {
let SurfaceState::Ready(ready) = &self.state else {
return None;
};
let TierBackend::Engine { engine, .. } = &ready.backend;
Some(engine.warm_up_finished())
}
pub fn retained_ledger(&self) -> ResidentPages {
let SurfaceState::Ready(ready) = &self.state else {
return ResidentPages {
pages: Vec::new(),
frame: self.committed_serial,
};
};
let TierBackend::Engine { engine, .. } = &ready.backend;
resident_pages(engine, self.committed_serial)
}
pub fn take_retained_ledger(&mut self) -> Option<ResidentPages> {
let (generation, holds_pages) = match &self.state {
SurfaceState::Ready(ready) => {
let TierBackend::Engine { engine, .. } = &ready.backend;
(
Some(engine.retained_generation()),
engine.retained_ledger().next().is_some(),
)
}
SurfaceState::NoSurface | SurfaceState::Lost => (None, false),
};
let recorded = std::mem::take(&mut self.recording_committed) && holds_pages;
if self.retained_published == Some(generation) && !recorded {
return None;
}
self.retained_published = Some(generation);
Some(self.retained_ledger())
}
pub fn note_scene_serial(&mut self, serial: u64) {
self.scene_serial = serial;
}
pub fn take_retained_refusal(&mut self) -> bool {
match &mut self.state {
SurfaceState::Ready(ready) => {
let TierBackend::Engine { engine, .. } = &mut ready.backend;
engine.take_retained_refusal()
}
SurfaceState::NoSurface | SurfaceState::Lost => false,
}
}
pub fn clear_retained(&mut self) {
if let SurfaceState::Ready(ready) = &mut self.state {
let TierBackend::Engine { engine, .. } = &mut ready.backend;
engine.clear_retained();
}
}
pub fn note_device_scale(&mut self, scale: f64) -> bool {
let bits = scale.to_bits();
if self.device_scale == Some(bits) {
return false;
}
self.device_scale = Some(bits);
self.clear_retained();
self.retained_published = None;
true
}
pub fn surface_resolved_translucent(&self) -> bool {
match &self.state {
SurfaceState::Ready(ready) => ready.surface.resolved_translucent(),
SurfaceState::NoSurface | SurfaceState::Lost => false,
}
}
pub fn gpu_pass_timings(&self) -> Option<GpuPassTimings> {
let SurfaceState::Ready(ready) = &self.state else {
return None;
};
let TierBackend::Engine { timestamps, .. } = &ready.backend;
let reading = 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),
})
}
pub fn set_overlay_split(&mut self, armed: bool) -> bool {
self.overlay_split = armed && crate::overlay::overlay_spike_enabled();
self.overlay_split
}
pub(crate) fn shared_pipeline_cache(&self) -> Option<wgpu::PipelineCache> {
match &self.state {
SurfaceState::Ready(ready) => ready
.pipeline_cache
.as_ref()
.map(|(cache, _)| cache.clone()),
SurfaceState::NoSurface | SurfaceState::Lost => None,
}
}
pub fn phase(&self) -> SurfacePhase {
match self.state {
SurfaceState::NoSurface => SurfacePhase::NoSurface,
SurfaceState::Ready(_) => SurfacePhase::SurfaceReady,
SurfaceState::Lost => SurfacePhase::SurfaceLost,
}
}
pub async fn on_surface_created(
&mut self,
ctx: &mut RenderContext,
window: impl Into<wgpu::SurfaceTarget<'static>>,
width: u32,
height: u32,
alpha: SurfaceAlphaRequest,
) -> Result<()> {
let surface = crate::context::create_engine_surface_from_target(
ctx,
window,
width.max(1),
height.max(1),
wgpu::PresentMode::AutoVsync,
alpha,
)
.await
.map_err(|e| anyhow!("frust-render: failed to create render surface: {e}"))?;
self.install_surface(ctx, surface)
}
pub async fn on_surface_installed(
&mut self,
ctx: &mut RenderContext,
surface: DetachedSurface,
width: u32,
height: u32,
alpha: SurfaceAlphaRequest,
) -> Result<()> {
let surface = crate::context::create_engine_surface(
ctx,
surface.into_surface(),
width.max(1),
height.max(1),
wgpu::PresentMode::AutoVsync,
alpha,
)
.await
.map_err(|e| anyhow!("frust-render: failed to configure detached surface: {e}"))?;
self.install_surface(ctx, surface)
}
pub async unsafe fn on_surface_created_from_android_window(
&mut self,
ctx: &mut RenderContext,
window_ptr: *mut c_void,
width: u32,
height: u32,
alpha: SurfaceAlphaRequest,
) -> Result<()> {
let raw =
unsafe { frust_gpu::lifecycle::create_android_surface(ctx.instance(), window_ptr) }?;
let surface = crate::context::create_engine_surface(
ctx,
raw,
width.max(1),
height.max(1),
wgpu::PresentMode::AutoVsync,
alpha,
)
.await
.map_err(|e| anyhow!("frust-render: failed to configure Android surface: {e}"))?;
self.install_surface(ctx, surface)
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub async unsafe fn on_surface_created_from_metal_layer(
&mut self,
ctx: &mut RenderContext,
layer_ptr: *mut c_void,
width: u32,
height: u32,
alpha: SurfaceAlphaRequest,
) -> Result<()> {
let raw = unsafe { frust_gpu::lifecycle::create_metal_surface(ctx.instance(), layer_ptr) }?;
let surface = crate::context::create_engine_surface(
ctx,
raw,
width.max(1),
height.max(1),
wgpu::PresentMode::Fifo,
alpha,
)
.await
.map_err(|e| anyhow!("frust-render: failed to configure Metal surface: {e}"))?;
self.install_surface(ctx, surface)
}
fn install_surface(&mut self, ctx: &RenderContext, surface: EngineSurface) -> Result<()> {
let pipeline_cache =
unsafe { ctx.create_pipeline_cache(self.initial_cache_data.as_deref()) };
let backend = {
let device = &ctx.device_handle().device;
let caps = frust_gpu::TierCaps::probe(&ctx.device_handle().adapter);
let engine_format = engine_target_format(&surface.path, surface.config().format);
let engine = frust_engine::EngineRenderer::new(
device,
&caps,
engine_format,
pipeline_cache.as_ref(),
)
.map_err(|e| anyhow!("frust-render: failed to create engine renderer: {e}"))?;
static ENGINE_LOGGED: OnceLock<()> = OnceLock::new();
ENGINE_LOGGED.get_or_init(|| {
log::info!(
"frust-render tier=engine (frust-engine strip pipeline, format={:?} into \
a {:?} swapchain, {}x{}, adapter `{}`)",
engine_format,
surface.config().format,
surface.config().width,
surface.config().height,
caps.adapter_name
);
});
let timestamps = frust_gpu::diag::TimestampRing::new(
device,
&ctx.device_handle().queue,
frust_engine::EngineSpan::COUNT,
frust_engine::diag::TIMESTAMP_RING_LABEL,
);
TierBackend::Engine {
engine: Box::new(engine),
refused_frames: 0,
shader_quads: Box::new(frust_engine::ShaderQuadPass::new(pipeline_cache.clone())),
timestamps,
}
};
debug_assert_eq!(
next_phase(self.phase(), SurfaceEvent::Created),
SurfacePhase::SurfaceReady,
"Created must reach SurfaceReady"
);
let pipeline_cache = pipeline_cache.map(|cache| (cache, ctx.adapter_cache_key()));
self.adopt_surface_state(SurfaceState::Ready(Box::new(ReadySurface {
surface,
backend,
pipeline_cache,
})));
Ok(())
}
pub fn on_surface_changed(&mut self, ctx: &RenderContext, width: u32, height: u32) {
if width == 0 || height == 0 {
return;
}
if let SurfaceState::Ready(ready) = &mut self.state {
ready
.surface
.resize(&ctx.device_handle().device, width, height);
match &mut ready.backend {
TierBackend::Engine { engine, .. } => {
let expected =
engine_target_format(&ready.surface.path, ready.surface.config().format);
if engine.format() != expected {
log::warn!(
"frust-render: engine renderer warmed for {:?} but this surface's \
frames now target {:?} — a format change must arrive as a fresh \
surface, not a resize",
engine.format(),
expected
);
}
engine.resize(&ctx.device_handle().device, width, height);
}
}
}
}
pub fn on_surface_destroyed(&mut self) {
debug_assert_eq!(
next_phase(self.phase(), SurfaceEvent::Destroyed),
SurfacePhase::NoSurface,
"Destroyed must reach NoSurface"
);
self.clear_pending_frame();
if let SurfaceState::Ready(ready) = &mut self.state {
match &mut ready.backend {
TierBackend::Engine { .. } => {}
}
}
self.adopt_surface_state(SurfaceState::NoSurface);
}
pub fn render(
&mut self,
ctx: &RenderContext,
scene: &frust_scene::Scene,
base_color: peniko::Color,
) -> Result<FrameOutcome> {
match self.encode(ctx, scene, base_color)? {
EncodeOutcome::Skipped => Ok(FrameOutcome::Skipped),
EncodeOutcome::Encoded => self.present(ctx),
}
}
#[allow(unused_variables)]
pub fn encode(
&mut self,
ctx: &RenderContext,
scene: &frust_scene::Scene,
base_color: peniko::Color,
) -> Result<EncodeOutcome> {
if !self.phase().can_render() {
return Ok(EncodeOutcome::Skipped);
}
let Self {
state,
pending_base_color,
engine_scene,
..
} = self;
let SurfaceState::Ready(ready) = state else {
return Ok(EncodeOutcome::Skipped);
};
let ready: &mut ReadySurface = ready;
match self
.overlay_split
.then(|| crate::overlay::spike_cut(scene))
.flatten()
{
Some(cut) => {
let TierBackend::Engine { engine, .. } = &ready.backend;
frust_engine::renderer::project_command_range(
scene,
0..cut,
frust_engine::renderer::RangeOptions {
keep_draw_retained: engine.retained_layers(),
},
engine_scene,
);
}
None => engine_scene.clone_from(scene),
}
*pending_base_color = Some(base_color);
self.pending_serial = Some((self.scene_serial, scene_records_retained(scene)));
self.segment_pending = false;
Ok(EncodeOutcome::Encoded)
}
pub fn encode_segmented(
&mut self,
ctx: &RenderContext,
scene: &frust_scene::Scene,
base_color: peniko::Color,
cuts: &[Range<usize>],
) -> Result<EncodeOutcome> {
if cuts.is_empty() {
return self.encode(ctx, scene, base_color);
}
if !self.phase().can_render() {
return Ok(EncodeOutcome::Skipped);
}
let Self {
state,
pending_base_color,
engine_scene,
segment_scene,
segment_service,
..
} = self;
let SurfaceState::Ready(ready) = state else {
return Ok(EncodeOutcome::Skipped);
};
let TierBackend::Engine { engine, .. } = &ready.backend;
segment_service.clear();
segment_service.extend(
cuts.iter()
.map(|cut| crate::overlay::cut_is_servable(scene.commands(), cut.clone())),
);
let served: Vec<Range<usize>> = cuts
.iter()
.zip(segment_service.iter())
.map(|(cut, service)| service.served_range(cut))
.collect();
crate::overlay::project_main_segment(
scene,
&served,
frust_engine::renderer::RangeOptions {
keep_draw_retained: engine.retained_layers(),
},
engine_scene,
);
segment_scene.clone_from(scene);
*pending_base_color = Some(base_color);
self.pending_serial = Some((self.scene_serial, scene_records_retained(scene)));
self.segment_pending = true;
Ok(EncodeOutcome::Encoded)
}
pub fn present(&mut self, ctx: &RenderContext) -> Result<FrameOutcome> {
match self.acquire(ctx)? {
AcquireOutcome::Acquired => self.submit(ctx),
AcquireOutcome::Reconfigured => Ok(FrameOutcome::Redraw),
AcquireOutcome::Lost => Ok(FrameOutcome::SurfaceLost),
AcquireOutcome::Skipped => Ok(FrameOutcome::Skipped),
}
}
pub fn acquire(&mut self, ctx: &RenderContext) -> Result<AcquireOutcome> {
if !self.phase().can_render() {
return Ok(AcquireOutcome::Skipped);
}
let Self {
state,
consecutive_invalid,
pending_present,
..
} = self;
let SurfaceState::Ready(ready) = state else {
return Ok(AcquireOutcome::Skipped);
};
let ready: &mut ReadySurface = ready;
use wgpu::CurrentSurfaceTexture as Cst;
let acquired = ready.surface.gpu.surface().get_current_texture();
let status = match &acquired {
Cst::Success(_) | Cst::Suboptimal(_) => AcquireStatus::Usable,
Cst::Outdated => AcquireStatus::Outdated,
Cst::Lost => AcquireStatus::Lost,
Cst::Timeout | Cst::Occluded => AcquireStatus::Transient,
Cst::Validation => AcquireStatus::Invalid,
};
let action = decide_acquire(status, *consecutive_invalid);
if status == AcquireStatus::Invalid && action == AcquireAction::Lose {
log::warn!(
"frust-render: giving up on Invalid-acquire reconfigure after \
{consecutive_invalid} consecutive attempts; surface lost"
);
}
*consecutive_invalid = next_invalid_streak(status, action, *consecutive_invalid);
let outcome = match action {
AcquireAction::Present => {
let surface_texture = match acquired {
Cst::Success(t) | Cst::Suboptimal(t) => t,
_ => {
return Err(anyhow!("frust-render: acquire classification desync"));
}
};
*pending_present = Some(surface_texture);
AcquireOutcome::Acquired
}
AcquireAction::Reconfigure => {
ready.surface.reconfigure(&ctx.device_handle().device);
AcquireOutcome::Reconfigured
}
AcquireAction::Lose => {
debug_assert_eq!(
next_phase(SurfacePhase::SurfaceReady, SurfaceEvent::Lost),
SurfacePhase::SurfaceLost,
"Lost must reach SurfaceLost from SurfaceReady"
);
AcquireOutcome::Lost
}
AcquireAction::Skip => AcquireOutcome::Skipped,
};
if matches!(outcome, AcquireOutcome::Lost) {
self.adopt_surface_state(SurfaceState::Lost);
}
Ok(outcome)
}
pub fn submit(&mut self, ctx: &RenderContext) -> Result<FrameOutcome> {
#[cfg(feature = "perf-trace")]
let measuring = !self.first_submit_seen;
let SubmitParts {
outcome,
texture: presentable,
..
} = self.submit_impl(ctx, &mut [])?;
if let Some(surface_texture) = presentable {
#[cfg(feature = "perf-trace")]
let present_start =
(measuring && self.first_submit.is_some()).then(std::time::Instant::now);
ctx.device_handle().queue.present(surface_texture);
#[cfg(feature = "perf-trace")]
if let (Some(start), Some(breakdown)) = (present_start, self.first_submit.as_mut()) {
breakdown.queue = breakdown.queue.saturating_add(start.elapsed());
}
}
Ok(outcome)
}
pub fn submit_deferred(
&mut self,
ctx: &RenderContext,
) -> Result<(FrameOutcome, Option<DeferredPresent>)> {
let SubmitParts {
outcome,
texture: presentable,
..
} = self.submit_impl(ctx, &mut [])?;
Ok((
outcome,
presentable.map(|texture| DeferredPresent {
texture,
queue: ctx.device_handle().queue.clone(),
}),
))
}
pub fn submit_segmented(
&mut self,
ctx: &RenderContext,
overlays: &mut [OverlayCut<'_>],
) -> Result<SegmentedOutcome> {
let parts = self.submit_impl(ctx, overlays)?;
if let Some(texture) = parts.texture {
ctx.device_handle().queue.present(texture);
}
for (cut, outcome) in overlays.iter_mut().zip(&parts.overlays) {
if matches!(
outcome.result,
Ok(crate::overlay::OverlayFrame::Rendered(_))
) {
cut.overlay.present();
}
}
Ok(SegmentedOutcome {
frame: parts.outcome,
overlays: parts.overlays,
encoders: parts.encoders,
submits: parts.submits,
})
}
pub fn submit_segmented_deferred(
&mut self,
ctx: &RenderContext,
overlays: &mut [OverlayCut<'_>],
) -> Result<(SegmentedOutcome, SegmentedPresents)> {
let parts = self.submit_impl(ctx, overlays)?;
let mut presents = SegmentedPresents {
main: parts.texture.map(|texture| DeferredPresent {
texture,
queue: ctx.device_handle().queue.clone(),
}),
overlays: Vec::new(),
};
for (cut, outcome) in overlays.iter_mut().zip(&parts.overlays) {
if matches!(
outcome.result,
Ok(crate::overlay::OverlayFrame::Rendered(_))
) && let Some(present) = cut.overlay.take_deferred_present()
{
presents.overlays.push((cut.overlay_id, present));
}
}
Ok((
SegmentedOutcome {
frame: parts.outcome,
overlays: parts.overlays,
encoders: parts.encoders,
submits: parts.submits,
},
presents,
))
}
fn submit_impl(
&mut self,
ctx: &RenderContext,
cuts: &mut [OverlayCut<'_>],
) -> Result<SubmitParts> {
let segmented = std::mem::take(&mut self.segment_pending);
let Some(surface_texture) = self.pending_present.take() else {
return Ok(SubmitParts::skipped(
cuts,
frame_service(segmented, &self.segment_service),
));
};
#[cfg(feature = "perf-trace")]
let mut probe = SubmitProbe::new(!self.first_submit_seen);
#[cfg(not(feature = "perf-trace"))]
let mut probe = SubmitProbe::new(false);
let Self {
state,
pending_base_color,
engine_scene,
segment_scene,
segment_service,
..
} = self;
let service = frame_service(segmented, segment_service);
let SurfaceState::Ready(ready) = state else {
return Ok(SubmitParts::skipped(cuts, service));
};
let overlays;
let mut encoders = 0_u32;
let mut submits = 0_u32;
let ReadySurface {
surface, backend, ..
} = &mut **ready;
let device_handle = ctx.device_handle();
let swapchain_view = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let surface_format = surface.config().format;
let (surface_width, surface_height) = (surface.config().width, surface.config().height);
let engine_format = engine_target_format(&surface.path, surface_format);
match &mut surface.path {
RenderPath::EngineDirect { depth, backdrop } => {
let TierBackend::Engine {
engine,
refused_frames,
shader_quads,
timestamps,
} = backend;
let root = crate::context::frame_renders_via_intermediate(
crate::context::RenderPathKind::EngineDirect,
crate::context::scene_carries_backdrop(engine_scene),
)
.then(|| {
backdrop.prepare(
&device_handle.device,
surface_format,
surface_width,
surface_height,
)
});
let engine_view = root.map_or(&swapchain_view, |(view, _)| view);
let base_color = pending_base_color.take().unwrap_or(peniko::Color::BLACK);
let mut encoder =
device_handle
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-render engine"),
});
encoders += 1;
crate::external_pass::run_external_passes(
&device_handle.device,
&device_handle.queue,
&mut encoder,
engine,
);
shader_quads.prepare(
&device_handle.device,
&device_handle.queue,
&mut encoder,
engine_scene,
Affine::IDENTITY,
engine,
);
timestamps.begin_frame(&device_handle.device);
probe.encode_begin(engine);
let result = engine.encode_traced(
&device_handle.device,
&device_handle.queue,
&mut encoder,
engine_scene,
frust_engine::EngineTarget {
view: engine_view,
format: surface_format,
width: surface_width,
height: surface_height,
depth: Some(depth.view()),
output: frust_engine::OutputAlpha::Premultiplied,
},
base_color,
Affine::IDENTITY,
frust_engine::FrameTimestamps::new(timestamps),
);
probe.encode_end(engine);
match result {
Ok(()) => {
if let Some((view, present)) = root {
present.record(
&device_handle.device,
&device_handle.queue,
&mut encoder,
view,
&swapchain_view,
(surface_width, surface_height),
frust_engine::FrameTimestamps::new(timestamps)
.writes(frust_engine::EngineSpan::Blit),
);
}
overlays =
record_overlays(&mut encoder, segmented, segment_scene, cuts, service);
timestamps.resolve(&mut encoder);
probe.queue_begin();
device_handle.queue.submit([encoder.finish()]);
submits += 1;
probe.queue_end();
timestamps.end_frame();
engine.end_frame(&device_handle.queue);
crate::overlay::end_cuts(cuts, true);
}
Err(error) => {
timestamps.abandon_frame();
*refused_frames = refused_frames.saturating_add(1);
log_engine_refusal(*refused_frames, &error);
engine.end_frame(&device_handle.queue);
return Ok(SubmitParts {
encoders,
..SubmitParts::skipped(cuts, service)
});
}
}
}
RenderPath::EngineDirectUnpremultiply {
depth,
intermediate_view,
present,
} => {
let TierBackend::Engine {
engine,
refused_frames,
shader_quads,
timestamps,
} = backend;
let base_color = pending_base_color.take().unwrap_or(peniko::Color::BLACK);
let mut encoder =
device_handle
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-render engine unpremultiply"),
});
encoders += 1;
crate::external_pass::run_external_passes(
&device_handle.device,
&device_handle.queue,
&mut encoder,
engine,
);
shader_quads.prepare(
&device_handle.device,
&device_handle.queue,
&mut encoder,
engine_scene,
Affine::IDENTITY,
engine,
);
timestamps.begin_frame(&device_handle.device);
probe.encode_begin(engine);
let result = engine.encode_traced(
&device_handle.device,
&device_handle.queue,
&mut encoder,
engine_scene,
frust_engine::EngineTarget {
view: intermediate_view,
format: engine_format,
width: surface_width,
height: surface_height,
depth: Some(depth.view()),
output: frust_engine::OutputAlpha::Premultiplied,
},
base_color,
Affine::IDENTITY,
frust_engine::FrameTimestamps::new(timestamps),
);
probe.encode_end(engine);
match result {
Ok(()) => {
present.record(
&device_handle.device,
&mut encoder,
intermediate_view,
&swapchain_view,
frust_engine::FrameTimestamps::new(timestamps)
.writes(frust_engine::EngineSpan::Blit),
);
overlays =
record_overlays(&mut encoder, segmented, segment_scene, cuts, service);
timestamps.resolve(&mut encoder);
probe.queue_begin();
device_handle.queue.submit([encoder.finish()]);
submits += 1;
probe.queue_end();
timestamps.end_frame();
engine.end_frame(&device_handle.queue);
crate::overlay::end_cuts(cuts, true);
}
Err(error) => {
timestamps.abandon_frame();
*refused_frames = refused_frames.saturating_add(1);
log_engine_refusal(*refused_frames, &error);
engine.end_frame(&device_handle.queue);
return Ok(SubmitParts {
encoders,
..SubmitParts::skipped(cuts, service)
});
}
}
}
}
#[cfg(feature = "perf-trace")]
if probe.armed && !self.first_submit_seen {
self.first_submit_seen = true;
self.first_submit = Some(probe.breakdown);
}
if let Some((serial, records)) = self.pending_serial.take() {
self.committed_serial = serial;
self.recording_committed |= records;
}
Ok(SubmitParts {
outcome: FrameOutcome::Rendered,
texture: Some(surface_texture),
overlays,
encoders,
submits,
})
}
}
struct SubmitParts {
outcome: FrameOutcome,
texture: Option<wgpu::SurfaceTexture>,
overlays: Vec<OverlayCutOutcome>,
encoders: u32,
submits: u32,
}
impl SubmitParts {
fn skipped(cuts: &[OverlayCut<'_>], service: &[CutServiceability]) -> Self {
Self {
outcome: FrameOutcome::Skipped,
texture: None,
overlays: crate::overlay::skipped_cuts(cuts, service),
encoders: 0,
submits: 0,
}
}
}
fn record_overlays(
encoder: &mut wgpu::CommandEncoder,
segmented: bool,
scene: &frust_scene::Scene,
cuts: &mut [OverlayCut<'_>],
service: &[CutServiceability],
) -> Vec<OverlayCutOutcome> {
if segmented {
crate::overlay::record_cuts(encoder, scene, cuts, service)
} else {
crate::overlay::skipped_cuts(cuts, service)
}
}
fn frame_service(segmented: bool, service: &[CutServiceability]) -> &[CutServiceability] {
if segmented { service } else { &[] }
}
fn engine_target_format(
path: &RenderPath,
surface_format: wgpu::TextureFormat,
) -> wgpu::TextureFormat {
match path {
RenderPath::EngineDirectUnpremultiply { .. } => {
frust_engine::gpu::pipelines::INTERMEDIATE_FORMAT
}
RenderPath::EngineDirect { .. } => surface_format,
}
}
fn log_engine_refusal(count: u32, error: &frust_engine::EngineError) {
match frust_gpu::context::decide_log_action(count) {
frust_gpu::context::LogAction::Log => {
log::warn!(
"frust-render: engine refused frame {count} on this surface — {error}; the \
frame is dropped rather than presented"
);
}
frust_gpu::context::LogAction::SuppressionNotice => {
log::warn!(
"frust-render: further engine frame refusals suppressed (total so far: {count})"
);
}
frust_gpu::context::LogAction::Silent { debug_bump } => {
if debug_bump {
log::debug!(
"frust-render: engine frame refusals now {count} on this surface (still \
suppressed)"
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
static RENDER_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn test_device() -> (wgpu::Device, wgpu::Queue, frust_gpu::TierCaps) {
pollster::block_on(async {
let instance = wgpu::Instance::new(
wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
);
let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
.await
.expect("no compatible GPU adapter");
println!("frust-render engine arm adapter: {:?}", adapter.get_info());
let caps = frust_gpu::TierCaps::probe(&adapter);
let limits = crate::context::effective_limits(
adapter.limits(),
crate::context::is_ios_simulator(),
);
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-render engine arm test"),
required_features: wgpu::Features::empty(),
required_limits: limits,
..Default::default()
})
.await
.expect("failed to create device");
(device, queue, caps)
})
}
#[test]
fn starts_with_no_surface() {
let renderer = SurfaceRenderer::new();
assert_eq!(renderer.phase(), SurfacePhase::NoSurface);
}
#[test]
fn adopting_a_surface_state_drops_the_previous_frames_stashes() {
let mut renderer = SurfaceRenderer::new();
renderer.pending_base_color = Some(peniko::Color::WHITE);
renderer.consecutive_invalid = 3;
renderer.adopt_surface_state(SurfaceState::NoSurface);
assert!(renderer.pending_base_color.is_none());
assert_eq!(renderer.consecutive_invalid, 0);
assert_eq!(renderer.phase(), SurfacePhase::NoSurface);
renderer.adopt_surface_state(SurfaceState::NoSurface);
assert!(renderer.pending_base_color.is_none());
assert_eq!(renderer.consecutive_invalid, 0);
}
#[test]
fn adopting_the_lost_state_drops_the_dying_surfaces_stashes() {
let mut renderer = SurfaceRenderer::new();
renderer.pending_base_color = Some(peniko::Color::WHITE);
renderer.adopt_surface_state(SurfaceState::Lost);
assert_eq!(renderer.phase(), SurfacePhase::SurfaceLost);
assert!(
renderer.pending_base_color.is_none(),
"a lost surface's base colour must not outlive it"
);
}
#[test]
fn resolved_translucent_is_false_without_a_live_surface() {
let renderer = SurfaceRenderer::new();
assert!(!renderer.surface_resolved_translucent());
}
#[test]
fn deferred_present_is_send() {
fn assert_send<T: Send>() {}
assert_send::<DeferredPresent>();
}
#[test]
fn submit_deferred_yields_no_frame_without_a_surface() {
let ctx = RenderContext::new();
let mut renderer = SurfaceRenderer::new();
let (outcome, deferred) = renderer
.submit_deferred(&ctx)
.expect("submit_deferred in NoSurface must not error");
assert_eq!(outcome, FrameOutcome::Skipped);
assert!(deferred.is_none());
}
#[test]
fn render_is_skipped_without_a_surface() {
let ctx = RenderContext::new();
let mut renderer = SurfaceRenderer::new();
let scene = frust_scene::Scene::new();
let outcome = renderer
.render(&ctx, &scene, peniko::Color::WHITE)
.expect("render in NoSurface must not error");
assert_eq!(outcome, FrameOutcome::Skipped);
}
#[test]
fn destroy_is_idempotent_and_resets_to_no_surface() {
let mut renderer = SurfaceRenderer::new();
renderer.pending_base_color = Some(peniko::Color::WHITE);
renderer.on_surface_destroyed();
assert_eq!(renderer.phase(), SurfacePhase::NoSurface);
assert!(
renderer.pending_base_color.is_none(),
"the dying surface's base colour must not reach the next submit"
);
renderer.on_surface_destroyed();
assert_eq!(renderer.phase(), SurfacePhase::NoSurface);
assert!(renderer.pending_base_color.is_none());
}
#[test]
fn resize_without_surface_is_a_no_op() {
let ctx = RenderContext::new();
let mut renderer = SurfaceRenderer::new();
renderer.on_surface_changed(&ctx, 800, 600);
assert_eq!(renderer.phase(), SurfacePhase::NoSurface);
}
#[test]
fn pipeline_cache_data_is_none_without_a_surface() {
let mut renderer = SurfaceRenderer::new();
assert_eq!(renderer.pipeline_cache_data(), None);
renderer.set_initial_pipeline_cache_data(Some(vec![1, 2, 3, 4]));
assert_eq!(renderer.pipeline_cache_data(), None);
}
#[test]
fn set_initial_pipeline_cache_data_none_clears_the_blob() {
let mut renderer = SurfaceRenderer::new();
renderer.set_initial_pipeline_cache_data(Some(vec![9, 9, 9]));
renderer.set_initial_pipeline_cache_data(None);
assert_eq!(renderer.phase(), SurfacePhase::NoSurface);
assert_eq!(renderer.pipeline_cache_data(), None);
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-render -- --ignored`"]
fn engine_arm_records_a_frame_into_its_target_without_validation_errors() {
let _serialized = RENDER_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
const SIZE: u32 = 64;
let (device, queue, caps) = test_device();
let mut scene = frust_scene::Scene::new();
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
builder.fill_rect(
kurbo::Rect::new(0.0, 0.0, SIZE as f64, SIZE as f64),
peniko::Brush::Solid(peniko::color::palette::css::RED),
);
}
for format in frust_gpu::SURFACE_FORMATS {
let target = frust_gpu::HeadlessTarget::new(&device, SIZE, SIZE, format);
let depth = frust_engine::DepthTexture::new(&device, SIZE, SIZE);
let mut engine = frust_engine::EngineRenderer::new(&device, &caps, format, None)
.expect("failed to create the engine renderer");
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
engine
.encode(
&device,
&queue,
&mut encoder,
&scene,
frust_engine::EngineTarget {
view: target.view(),
format,
width: SIZE,
height: SIZE,
depth: Some(depth.view()),
output: frust_engine::OutputAlpha::Premultiplied,
},
peniko::Color::BLACK,
Affine::IDENTITY,
)
.expect("the engine refused a plain opaque frame");
queue.submit([encoder.finish()]);
engine.end_frame(&queue);
let error = {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut pop = std::pin::pin!(scope.pop());
loop {
match pop.as_mut().poll(&mut cx) {
Poll::Ready(error) => break error,
Poll::Pending => {
let _ = device.poll(wgpu::PollType::wait_indefinitely());
}
}
}
};
assert!(
error.is_none(),
"the engine arm's {format:?} target raised a validation error: {error:?}"
);
let pixels = target.read_back(&device, &queue);
let index = (((SIZE / 2) * SIZE + (SIZE / 2)) * 4) as usize;
let centre: [u8; 4] = pixels[index..index + 4]
.try_into()
.expect("a read-back row holds four bytes per pixel");
assert_eq!(centre[3], 255, "{format:?}: the frame is not opaque");
assert!(
centre[0] != 0 || centre[1] != 0 || centre[2] != 0,
"{format:?}: the target still holds its clear colour ({centre:?})"
);
}
}
#[test]
fn the_retained_ledger_is_published_once_per_change_without_a_surface() {
let mut renderer = SurfaceRenderer::new();
assert_eq!(renderer.retained_ledger(), ResidentPages::default());
assert_eq!(
renderer.take_retained_ledger(),
Some(ResidentPages::default()),
"the first take always publishes"
);
assert_eq!(
renderer.take_retained_ledger(),
None,
"nothing changed since"
);
renderer.adopt_surface_state(SurfaceState::NoSurface);
assert_eq!(
renderer.take_retained_ledger(),
Some(ResidentPages::default()),
"an episode ending publishes the empty ledger again"
);
assert!(!renderer.take_retained_refusal());
renderer.clear_retained();
assert_eq!(renderer.take_retained_ledger(), None);
renderer.on_surface_destroyed();
assert_eq!(
renderer.take_retained_ledger(),
Some(ResidentPages::default()),
"a surface destroy publishes the empty ledger, once"
);
assert_eq!(renderer.take_retained_ledger(), None);
}
#[test]
fn only_a_rendered_frame_commits_its_scene_serial() {
let mut renderer = SurfaceRenderer::new();
let ctx = RenderContext::new();
renderer.note_scene_serial(9);
let outcome = renderer
.encode(&ctx, &frust_scene::Scene::new(), peniko::Color::BLACK)
.expect("no surface is not an error");
assert_eq!(outcome, EncodeOutcome::Skipped);
assert_eq!(renderer.pending_serial, None, "nothing was encoded");
assert_eq!(renderer.take_retained_ledger().map(|l| l.frame), Some(0));
renderer.committed_serial = 4;
renderer.pending_serial = Some((5, true));
renderer.on_surface_destroyed();
assert_eq!(renderer.pending_serial, None, "the in-flight scene is gone");
assert_eq!(
renderer.take_retained_ledger(),
Some(ResidentPages {
pages: Vec::new(),
frame: 4,
}),
"the empty ledger names the last committed scene"
);
}
#[test]
fn a_scene_records_a_retained_page_only_through_a_push_retained_bracket() {
let rect = kurbo::Rect::new(0.0, 0.0, 4.0, 4.0);
let mut scene = frust_scene::Scene::new();
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
builder.draw_retained(3, rect, 1.0, 1.0);
}
assert!(!scene_records_retained(&scene), "a draw records nothing");
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
builder.push_retained(3, rect, 1.0, 1.0);
builder.pop_retained();
}
assert!(scene_records_retained(&scene));
}
#[test]
fn a_device_scale_change_clears_and_republishes_the_empty_ledger() {
let mut renderer = SurfaceRenderer::new();
assert_eq!(
renderer.take_retained_ledger(),
Some(ResidentPages::default())
);
assert!(renderer.note_device_scale(2.0), "the first scale noted");
assert_eq!(
renderer.take_retained_ledger(),
Some(ResidentPages::default()),
"a scale change publishes the empty ledger"
);
assert!(!renderer.note_device_scale(2.0), "same scale: a no-op");
assert_eq!(renderer.take_retained_ledger(), None, "and owes nothing");
assert!(renderer.note_device_scale(3.0));
assert_eq!(
renderer.take_retained_ledger(),
Some(ResidentPages::default())
);
assert_eq!(renderer.take_retained_ledger(), None);
renderer.on_surface_destroyed();
assert_eq!(
renderer.take_retained_ledger(),
Some(ResidentPages::default())
);
assert!(!renderer.note_device_scale(3.0));
assert_eq!(renderer.take_retained_ledger(), None);
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-render -- --ignored`"]
fn the_retained_ledger_reports_what_the_engine_holds() {
let _serialized = RENDER_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
const SIZE: u32 = 64;
const KEY: u64 = 41;
let (device, queue, caps) = test_device();
let format = wgpu::TextureFormat::Rgba8Unorm;
let target = frust_gpu::HeadlessTarget::new(&device, SIZE, SIZE, format);
let mut engine = frust_engine::EngineRenderer::new(&device, &caps, format, None)
.expect("failed to create the engine renderer");
let mut scene = frust_scene::Scene::new();
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
builder.push_transform(Affine::translate((8.0, 8.0)) * Affine::scale(2.0));
builder.push_retained(KEY, kurbo::Rect::new(0.0, 0.0, 16.0, 12.0), 0.5, 1.0);
builder.fill_rect(
kurbo::Rect::new(0.0, 0.0, 16.0, 12.0),
peniko::Brush::Solid(peniko::color::palette::css::RED),
);
builder.pop_retained();
builder.pop_transform();
}
assert!(resident_pages(&engine, 0).pages.is_empty());
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
engine
.encode(
&device,
&queue,
&mut encoder,
&scene,
frust_engine::EngineTarget {
view: target.view(),
format,
width: SIZE,
height: SIZE,
depth: None,
output: frust_engine::OutputAlpha::Premultiplied,
},
peniko::Color::BLACK,
Affine::IDENTITY,
)
.expect("the engine refused a retained frame");
queue.submit([encoder.finish()]);
engine.end_frame(&queue);
let resident = resident_pages(&engine, 7);
assert_eq!(
resident.pages,
vec![ResidentPage {
key: KEY,
extent_px: (32, 24),
scale: 2.0,
}],
"the body's page is reported at its rasterized extent and scale"
);
assert_eq!(resident.frame, 7, "stamped with the committed serial");
}
}