bevy_render/error_handler.rs
1use alloc::sync::Arc;
2use bevy_app::AppExit;
3use bevy_ecs::{
4 resource::Resource,
5 world::{Mut, World},
6};
7use std::sync::Mutex;
8use wgpu::ErrorSource;
9pub use wgpu_types::error::ErrorType;
10
11use crate::{
12 insert_future_resources,
13 render_resource::PipelineCache,
14 renderer::{wgpu_wrapper, RenderDevice},
15 settings::RenderCreation,
16 FutureRenderResources, RenderStartup,
17};
18
19/// Resource to indicate renderer behavior upon error.
20pub enum RenderErrorPolicy {
21 /// Pretends nothing happened and continues rendering.
22 /// This discards the error after logging it to console.
23 /// WARNING: Using this policy could cause hazardous rapid flashing
24 /// if the conditions causing the error remain unaddressed, since
25 /// rendering will attempt to continue executing.
26 /// When choosing to use this policy, be sure to test that the application
27 /// remains safe to use.
28 Ignore,
29 /// Keeps the app alive, but stops rendering further.
30 /// This keeps the error state, and will continue polling the [`RenderErrorHandler`]
31 /// every frame until some other policy is returned.
32 StopRendering,
33 /// Attempt renderer recovery with the given [`RenderCreation`].
34 Recover(RenderCreation),
35}
36
37/// Determines what [`RenderErrorPolicy`] should be used to respond to a given [`RenderError`].
38///
39/// The handler has access to both the main world and the render world in that order.
40/// By the time this is invoked, the error has already been logged. The error is provided
41/// for the decision-making reason of how to appropriately respond to it.
42///
43/// Note that failing to address the source of an error and continuing to render may cause rapid flashing.
44/// Be sure to thoroughly test your error handler to ensure you application remains safe
45/// to use.
46#[derive(Resource)]
47pub struct RenderErrorHandler(
48 pub for<'a> fn(&'a RenderError, &'a mut World, &'a mut World) -> RenderErrorPolicy,
49);
50
51impl RenderErrorHandler {
52 fn handle(&self, error: &RenderError, main_world: &mut World, render_world: &mut World) {
53 match self.0(error, main_world, render_world) {
54 RenderErrorPolicy::Ignore => {
55 // Pretend that didn't happen.
56 render_world.insert_resource(RenderState::Ready);
57 }
58 RenderErrorPolicy::StopRendering => {
59 // do nothing
60 }
61 RenderErrorPolicy::Recover(render_creation) => {
62 assert!(insert_future_resources(&render_creation, main_world));
63 render_world.insert_resource(RenderState::Reinitializing);
64 }
65 }
66 }
67}
68
69impl Default for RenderErrorHandler {
70 fn default() -> Self {
71 // Quit the application for any RenderError. This is overzealous at the moment,
72 // but requires more extensive use of the non-fatal error handling pattern in
73 // upstream wgpu. RenderErrors are issued when wgpu is used incorrectly.
74 // Ignoring a wgpu OutOfMemory or Validation error without addressing the
75 // root cause (via hiding or deleting entities or changing rendering settings) will
76 // likely hit the same error repeatedly, resulting in hazardous strobing effects.
77 // The parameters to this function are (error, main_world, render_world).
78 Self(|error, main_world, _| {
79 bevy_log::error!("Quitting the application due to {:?} RenderError", error.ty);
80 main_world.write_message(AppExit::error());
81 RenderErrorPolicy::StopRendering
82 })
83 }
84}
85
86wgpu_wrapper! {
87 #[derive(Debug)]
88 pub struct WgpuErrorSource(ErrorSource);
89}
90
91/// An error encountered during rendering. These are errors reported by wgpu validation layers,
92/// and typically indicate problems in the way it is being used.
93#[derive(Debug)]
94pub struct RenderError {
95 pub ty: ErrorType,
96 pub description: String,
97 pub source: Option<WgpuErrorSource>,
98}
99
100/// The current state of the renderer.
101#[derive(Resource, Debug)]
102pub(crate) enum RenderState {
103 /// Just started, [`crate::RenderStartup`] will run in this state.
104 Initializing,
105 /// Everything is okay and we are rendering stuff every frame.
106 Ready,
107 /// An error was encountered, and we may decide how to handle it.
108 Errored(RenderError),
109 /// We are recreating the render context after an error to recover.
110 Reinitializing,
111}
112
113wgpu_wrapper!(struct WgpuError(wgpu::Error));
114
115/// Resource to allow polling wgpu error handlers.
116#[derive(Resource)]
117pub(crate) struct DeviceErrorHandler {
118 device_lost: Arc<Mutex<Option<(wgpu::DeviceLostReason, String)>>>,
119 uncaptured: Arc<Mutex<Option<WgpuError>>>,
120}
121
122impl DeviceErrorHandler {
123 /// Creates and registers error handlers on the given device and stores them to later be polled.
124 pub(crate) fn new(device: &RenderDevice) -> Self {
125 let device_lost = Arc::new(Mutex::new(None));
126 let uncaptured = Arc::new(Mutex::new(None));
127 {
128 // scoped clone to move into closures
129 let device_lost = device_lost.clone();
130 let uncaptured = uncaptured.clone();
131 let device = device.wgpu_device();
132 // we log errors as soon as they are captured so they stay chronological in logs
133 // and only keep the first error, as it often causes other errors downstream
134 device.set_device_lost_callback(move |reason, str| {
135 bevy_log::error!("Caught DeviceLost error: {reason:?} {str}");
136 assert!(device_lost.lock().unwrap().replace((reason, str)).is_none());
137 });
138 device.on_uncaptured_error(Arc::new(move |e| {
139 bevy_log::error!("Caught rendering error: {e}");
140 uncaptured.lock().unwrap().get_or_insert(WgpuError::new(e));
141 }));
142 }
143 Self {
144 device_lost,
145 uncaptured,
146 }
147 }
148
149 /// Checks to see if any errors have been caught, and returns an appropriate `RenderState`
150 pub(crate) fn poll(&self) -> Option<RenderError> {
151 // Device lost is more important so we let it take precedence; every error gets logged anyways.
152 if let Some((_, description)) = self.device_lost.lock().unwrap().take() {
153 return Some(RenderError {
154 ty: ErrorType::DeviceLost,
155 description,
156 source: None,
157 });
158 }
159 if let Some(error) = self.uncaptured.lock().unwrap().take() {
160 let (ty, description, source) = match error.into_inner() {
161 wgpu::Error::OutOfMemory { source } => {
162 (ErrorType::OutOfMemory, "".to_string(), source)
163 }
164 wgpu::Error::Validation {
165 source,
166 description,
167 } => (ErrorType::Validation, description, source),
168 wgpu::Error::Internal {
169 source,
170 description,
171 } => (ErrorType::Internal, description, source),
172 };
173 return Some(RenderError {
174 ty,
175 description,
176 source: Some(WgpuErrorSource::new(source)),
177 });
178 }
179 None
180 }
181}
182
183/// Updates the state machine that handles the renderer and device lifecycle.
184/// Polls the [`DeviceErrorHandler`] and fires the [`RenderErrorHandler`] if needed.
185///
186/// Runs [`crate::RenderStartup`] after every time a [`RenderDevice`] is acquired.
187///
188/// We need both the main and render world to properly handle errors, so we wedge ourselves into [extract](bevy_app::SubApp::set_extract).
189pub(crate) fn update_state(main_world: &mut World, render_world: &mut World) {
190 if let Some(error) = render_world.resource::<DeviceErrorHandler>().poll() {
191 render_world.insert_resource(RenderState::Errored(error));
192 };
193
194 // Remove the render state so we can provide both worlds to the `RenderErrorHandler`.
195 let state = render_world.remove_resource::<RenderState>().unwrap();
196
197 match &state {
198 RenderState::Initializing => {
199 render_world.run_schedule(RenderStartup);
200 render_world.insert_resource(RenderState::Ready);
201 }
202 RenderState::Ready => {
203 // all is well
204 }
205 RenderState::Errored(error) => {
206 main_world.resource_scope(|main_world, error_handler: Mut<RenderErrorHandler>| {
207 error_handler.handle(error, main_world, render_world);
208 });
209 }
210 RenderState::Reinitializing => {
211 if let Some(render_resources) = main_world
212 .get_resource::<FutureRenderResources>()
213 .unwrap()
214 .clone()
215 .lock()
216 .unwrap()
217 .take()
218 {
219 let synchronous_pipeline_compilation = render_world
220 .resource::<PipelineCache>()
221 .synchronous_pipeline_compilation;
222 render_resources.unpack_into(
223 main_world,
224 render_world,
225 synchronous_pipeline_compilation,
226 );
227 render_world.insert_resource(RenderState::Initializing);
228 }
229 }
230 }
231
232 // Put the state back if we didn't set a new one
233 if render_world.get_resource::<RenderState>().is_none() {
234 render_world.insert_resource(state);
235 }
236}