use crate::{
prelude::{Backend, Commands, CurrentFrame, Plugin, Res, ResMut, SystemStage},
rendering::errors::AcquireError,
};
pub struct RenderFailure {
pub message: String,
}
pub struct RenderPlugin<B: Backend> {
_marker: std::marker::PhantomData<B>,
}
impl<B: Backend> RenderPlugin<B> {
pub fn new() -> Self {
Self {
_marker: std::marker::PhantomData,
}
}
}
impl<B: Backend> Plugin for RenderPlugin<B> {
fn build(&self, app: &mut crate::prelude::App) {
app.add_resource(CurrentFrame::<B> { frame: None })
.provides::<RenderFailure>()
.add_system(SystemStage::PreRender, begin_frame::<B>)
.add_system(SystemStage::PostRender, end_frame::<B>);
}
}
fn begin_frame<B: Backend>(
backend: Option<ResMut<B>>,
mut frame: ResMut<CurrentFrame<B>>,
already_failed: Option<Res<RenderFailure>>,
mut commands: Commands,
) {
if already_failed.is_some() {
return;
}
let Some(mut backend) = backend else { return };
match backend.acquire() {
Ok(f) => frame.frame = Some(f),
Err(AcquireError::Transient) => frame.frame = None,
Err(AcquireError::Fatal(msg)) => {
tracing::error!("Fatal frame acquisition error — rendering has permanently stopped: {msg}");
frame.frame = None;
commands.insert_resource(RenderFailure { message: msg });
}
}
}
pub(crate) fn end_frame<B: Backend>(backend: Option<ResMut<B>>, mut current: ResMut<CurrentFrame<B>>) {
let Some(mut backend) = backend else { return };
if let Some(frame) = current.frame.take() {
backend.present(frame);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::App;
use crate::rendering::backend::{FrameOperations, Pass};
use crate::rendering::sync::InitSender;
use crate::rendering::window::GPUSurfaceHandle;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
struct FakeFrame;
impl FrameOperations for FakeFrame {
type Context<'a> = ();
type Attachment = ();
type DepthAttachment = ();
fn begin(&mut self, _pass: Pass<'_, Self>) -> Self::Context<'_> {}
}
struct FakeBackend {
acquire_calls: Arc<AtomicU32>,
}
impl Backend for FakeBackend {
type Frame = FakeFrame;
fn init(_handle: impl GPUSurfaceHandle, _width: u32, _height: u32, _sender: InitSender<Self>) {
unreachable!("not exercised by this test — the backend is inserted directly")
}
fn acquire(&mut self) -> Result<Self::Frame, AcquireError> {
self.acquire_calls.fetch_add(1, Ordering::SeqCst);
Err(AcquireError::Fatal("simulated device loss".to_string()))
}
fn present(&mut self, _frame: Self::Frame) {}
}
#[test]
fn a_fatal_acquire_error_reports_failure_and_stops_retrying() {
let acquire_calls = Arc::new(AtomicU32::new(0));
let backend = FakeBackend { acquire_calls: acquire_calls.clone() };
let mut app = App::new();
app.add_resource(backend);
app.add_plugin(RenderPlugin::<FakeBackend>::new());
app.build();
app.update();
assert_eq!(acquire_calls.load(Ordering::SeqCst), 1);
assert_eq!(app.get_resource::<RenderFailure>().message, "simulated device loss");
app.update();
app.update();
assert_eq!(
acquire_calls.load(Ordering::SeqCst),
1,
"begin_frame kept calling acquire() after a Fatal error instead of stopping"
);
}
}