pebble/rendering/
render_plugin.rs1use crate::{
2 prelude::{Backend, Commands, CurrentFrame, Plugin, Res, ResMut, SystemStage},
3 rendering::errors::AcquireError,
4};
5
6pub struct RenderFailure {
28 pub message: String,
29}
30
31pub struct RenderPlugin<B: Backend> {
42 _marker: std::marker::PhantomData<B>,
43}
44
45impl<B: Backend> RenderPlugin<B> {
46 pub fn new() -> Self {
47 Self {
48 _marker: std::marker::PhantomData,
49 }
50 }
51}
52
53impl<B: Backend> Plugin for RenderPlugin<B> {
54 fn build(&self, app: &mut crate::prelude::App) {
55 app.add_resource(CurrentFrame::<B> { frame: None })
56 .add_system(SystemStage::PreRender, begin_frame::<B>)
57 .add_system(SystemStage::PostRender, end_frame::<B>);
58 }
59}
60
61fn begin_frame<B: Backend>(
65 backend: Option<ResMut<B>>,
66 mut frame: ResMut<CurrentFrame<B>>,
67 already_failed: Option<Res<RenderFailure>>,
68 mut commands: Commands,
69) {
70 if already_failed.is_some() {
74 return;
75 }
76
77 let Some(mut backend) = backend else { return };
78
79 match backend.acquire() {
80 Ok(f) => frame.frame = Some(f),
81 Err(AcquireError::Transient) => frame.frame = None,
82 Err(AcquireError::Fatal(msg)) => {
83 tracing::error!("Fatal frame acquisition error — rendering has permanently stopped: {msg}");
84 frame.frame = None;
85 commands.insert_resource(RenderFailure { message: msg });
86 }
87 }
88}
89
90pub(crate) fn end_frame<B: Backend>(backend: Option<ResMut<B>>, mut current: ResMut<CurrentFrame<B>>) {
99 let Some(mut backend) = backend else { return };
100 if let Some(frame) = current.frame.take() {
101 backend.present(frame);
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::app::App;
109 use crate::rendering::backend::{FrameOperations, Pass};
110 use crate::rendering::sync::InitSender;
111 use crate::rendering::window::GPUSurfaceHandle;
112 use std::sync::Arc;
113 use std::sync::atomic::{AtomicU32, Ordering};
114
115 struct FakeFrame;
116 impl FrameOperations for FakeFrame {
117 type Context<'a> = ();
118 type Attachment = ();
119 type DepthAttachment = ();
120 fn begin(&mut self, _pass: Pass<'_, Self>) -> Self::Context<'_> {}
121 }
122
123 struct FakeBackend {
124 acquire_calls: Arc<AtomicU32>,
125 }
126
127 impl Backend for FakeBackend {
128 type Frame = FakeFrame;
129
130 fn init(_handle: impl GPUSurfaceHandle, _width: u32, _height: u32, _sender: InitSender<Self>) {
131 unreachable!("not exercised by this test — the backend is inserted directly")
132 }
133
134 fn acquire(&mut self) -> Result<Self::Frame, AcquireError> {
135 self.acquire_calls.fetch_add(1, Ordering::SeqCst);
136 Err(AcquireError::Fatal("simulated device loss".to_string()))
137 }
138
139 fn present(&mut self, _frame: Self::Frame) {}
140 }
141
142 #[test]
143 fn a_fatal_acquire_error_reports_failure_and_stops_retrying() {
144 let acquire_calls = Arc::new(AtomicU32::new(0));
145 let backend = FakeBackend { acquire_calls: acquire_calls.clone() };
146
147 let mut app = App::new();
148 app.add_resource(backend);
149 app.add_plugin(RenderPlugin::<FakeBackend>::new());
150 app.build();
151
152 app.update();
153 assert_eq!(acquire_calls.load(Ordering::SeqCst), 1);
154 assert_eq!(app.get_resource::<RenderFailure>().message, "simulated device loss");
155
156 app.update();
160 app.update();
161 assert_eq!(
162 acquire_calls.load(Ordering::SeqCst),
163 1,
164 "begin_frame kept calling acquire() after a Fatal error instead of stopping"
165 );
166 }
167}