use core::ffi::c_void;
use anyhow::{Result, anyhow};
use std::sync::OnceLock;
use std::time::Duration;
use kurbo::Affine;
use crate::context::{EngineSurface, RenderPath};
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 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,
}
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(),
}
}
fn clear_pending_frame(&mut self) {
self.pending_base_color = None;
}
fn adopt_surface_state(&mut self, state: SurfaceState) {
self.state = state;
self.consecutive_invalid = 0;
self.clear_pending_frame();
}
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 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 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;
engine_scene.clone_from(scene);
*pending_base_color = Some(base_color);
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> {
let (outcome, presentable) = self.submit_impl(ctx)?;
if let Some(surface_texture) = presentable {
ctx.device_handle().queue.present(surface_texture);
}
Ok(outcome)
}
pub fn submit_deferred(
&mut self,
ctx: &RenderContext,
) -> Result<(FrameOutcome, Option<DeferredPresent>)> {
let (outcome, presentable) = self.submit_impl(ctx)?;
Ok((
outcome,
presentable.map(|texture| DeferredPresent {
texture,
queue: ctx.device_handle().queue.clone(),
}),
))
}
fn submit_impl(
&mut self,
ctx: &RenderContext,
) -> Result<(FrameOutcome, Option<wgpu::SurfaceTexture>)> {
let Some(surface_texture) = self.pending_present.take() else {
return Ok((FrameOutcome::Skipped, None));
};
let Self {
state,
pending_base_color,
engine_scene,
..
} = self;
let SurfaceState::Ready(ready) = state else {
return Ok((FrameOutcome::Skipped, None));
};
let ReadySurface {
surface, backend, ..
} = &mut **ready;
let device_handle = ctx.device_handle();
let swapchain_view = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
match &surface.path {
RenderPath::EngineDirect { depth } => {
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"),
});
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);
let result = engine.encode_traced(
&device_handle.device,
&device_handle.queue,
&mut encoder,
engine_scene,
frust_engine::EngineTarget {
view: &swapchain_view,
format: surface.config().format,
width: surface.config().width,
height: surface.config().height,
depth: Some(depth.view()),
output: frust_engine::OutputAlpha::Premultiplied,
},
base_color,
Affine::IDENTITY,
frust_engine::FrameTimestamps::new(timestamps),
);
match result {
Ok(()) => {
timestamps.resolve(&mut encoder);
device_handle.queue.submit([encoder.finish()]);
timestamps.end_frame();
engine.end_frame(&device_handle.queue);
}
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((FrameOutcome::Skipped, None));
}
}
}
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"),
});
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);
let result = engine.encode_traced(
&device_handle.device,
&device_handle.queue,
&mut encoder,
engine_scene,
frust_engine::EngineTarget {
view: intermediate_view,
format: engine_target_format(&surface.path, surface.config().format),
width: surface.config().width,
height: surface.config().height,
depth: Some(depth.view()),
output: frust_engine::OutputAlpha::Premultiplied,
},
base_color,
Affine::IDENTITY,
frust_engine::FrameTimestamps::new(timestamps),
);
match result {
Ok(()) => {
present.record(
&device_handle.device,
&mut encoder,
intermediate_view,
&swapchain_view,
frust_engine::FrameTimestamps::new(timestamps)
.writes(frust_engine::EngineSpan::Blit),
);
timestamps.resolve(&mut encoder);
device_handle.queue.submit([encoder.finish()]);
timestamps.end_frame();
engine.end_frame(&device_handle.queue);
}
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((FrameOutcome::Skipped, None));
}
}
}
}
Ok((FrameOutcome::Rendered, Some(surface_texture)))
}
}
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::*;
#[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() {
static RENDER_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _serialized = RENDER_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
const SIZE: u32 = 64;
let (device, queue, caps) = 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)
});
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:?})"
);
}
}
}