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::{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}