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::{RenderDevice, WgpuWrapper},
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
86/// An error encountered during rendering. These are errors reported by wgpu validation layers,
87/// and typically indicate problems in the way it is being used.
88#[derive(Debug)]
89pub struct RenderError {
90 pub ty: ErrorType,
91 pub description: String,
92 pub source: Option<WgpuWrapper<ErrorSource>>,
93}
94
95/// The current state of the renderer.
96#[derive(Resource, Debug)]
97pub(crate) enum RenderState {
98 /// Just started, [`crate::RenderStartup`] will run in this state.
99 Initializing,
100 /// Everything is okay and we are rendering stuff every frame.
101 Ready,
102 /// An error was encountered, and we may decide how to handle it.
103 Errored(RenderError),
104 /// We are recreating the render context after an error to recover.
105 Reinitializing,
106}
107
108/// Resource to allow polling wgpu error handlers.
109#[derive(Resource)]
110pub(crate) struct DeviceErrorHandler {
111 device_lost: Arc<Mutex<Option<(wgpu::DeviceLostReason, String)>>>,
112 uncaptured: Arc<Mutex<Option<WgpuWrapper<wgpu::Error>>>>,
113}
114
115impl DeviceErrorHandler {
116 /// Creates and registers error handlers on the given device and stores them to later be polled.
117 pub(crate) fn new(device: &RenderDevice) -> Self {
118 let device_lost = Arc::new(Mutex::new(None));
119 let uncaptured = Arc::new(Mutex::new(None));
120 {
121 // scoped clone to move into closures
122 let device_lost = device_lost.clone();
123 let uncaptured = uncaptured.clone();
124 let device = device.wgpu_device();
125 // we log errors as soon as they are captured so they stay chronological in logs
126 // and only keep the first error, as it often causes other errors downstream
127 device.set_device_lost_callback(move |reason, str| {
128 bevy_log::error!("Caught DeviceLost error: {reason:?} {str}");
129 assert!(device_lost.lock().unwrap().replace((reason, str)).is_none());
130 });
131 device.on_uncaptured_error(Arc::new(move |e| {
132 bevy_log::error!("Caught rendering error: {e}");
133 uncaptured
134 .lock()
135 .unwrap()
136 .get_or_insert(WgpuWrapper::new(e));
137 }));
138 }
139 Self {
140 device_lost,
141 uncaptured,
142 }
143 }
144
145 /// Checks to see if any errors have been caught, and returns an appropriate `RenderState`
146 pub(crate) fn poll(&self) -> Option<RenderError> {
147 // Device lost is more important so we let it take precedence; every error gets logged anyways.
148 if let Some((_, description)) = self.device_lost.lock().unwrap().take() {
149 return Some(RenderError {
150 ty: ErrorType::DeviceLost,
151 description,
152 source: None,
153 });
154 }
155 if let Some(error) = self.uncaptured.lock().unwrap().take() {
156 let (ty, description, source) = match error.into_inner() {
157 wgpu::Error::OutOfMemory { source } => {
158 (ErrorType::OutOfMemory, "".to_string(), source)
159 }
160 wgpu::Error::Validation {
161 source,
162 description,
163 } => (ErrorType::Validation, description, source),
164 wgpu::Error::Internal {
165 source,
166 description,
167 } => (ErrorType::Internal, description, source),
168 };
169 return Some(RenderError {
170 ty,
171 description,
172 source: Some(WgpuWrapper::new(source)),
173 });
174 }
175 None
176 }
177}
178
179/// Updates the state machine that handles the renderer and device lifecycle.
180/// Polls the [`DeviceErrorHandler`] and fires the [`RenderErrorHandler`] if needed.
181///
182/// Runs [`crate::RenderStartup`] after every time a [`RenderDevice`] is acquired.
183///
184/// We need both the main and render world to properly handle errors, so we wedge ourselves into [extract](bevy_app::SubApp::set_extract).
185pub(crate) fn update_state(main_world: &mut World, render_world: &mut World) {
186 if let Some(error) = render_world.resource::<DeviceErrorHandler>().poll() {
187 render_world.insert_resource(RenderState::Errored(error));
188 };
189
190 // Remove the render state so we can provide both worlds to the `RenderErrorHandler`.
191 let state = render_world.remove_resource::<RenderState>().unwrap();
192
193 match &state {
194 RenderState::Initializing => {
195 render_world.run_schedule(RenderStartup);
196 render_world.insert_resource(RenderState::Ready);
197 }
198 RenderState::Ready => {
199 // all is well
200 }
201 RenderState::Errored(error) => {
202 main_world.resource_scope(|main_world, error_handler: Mut<RenderErrorHandler>| {
203 error_handler.handle(error, main_world, render_world);
204 });
205 }
206 RenderState::Reinitializing => {
207 if let Some(render_resources) = main_world
208 .get_resource::<FutureRenderResources>()
209 .unwrap()
210 .clone()
211 .lock()
212 .unwrap()
213 .take()
214 {
215 let synchronous_pipeline_compilation = render_world
216 .resource::<PipelineCache>()
217 .synchronous_pipeline_compilation;
218 render_resources.unpack_into(
219 main_world,
220 render_world,
221 synchronous_pipeline_compilation,
222 );
223 render_world.insert_resource(RenderState::Initializing);
224 }
225 }
226 }
227
228 // Put the state back if we didn't set a new one
229 if render_world.get_resource::<RenderState>().is_none() {
230 render_world.insert_resource(state);
231 }
232}