Skip to main content

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}