Skip to main content

render_recovery/
render_recovery.rs

1//! Demonstrates how to trigger various rendering errors, and how bevy can recover from them.
2
3use bevy::{
4    input::keyboard::Key,
5    prelude::*,
6    render::{
7        error_handler::{RenderErrorHandler, RenderErrorPolicy},
8        extract_resource::{ExtractResource, ExtractResourcePlugin},
9        render_resource::{
10            BufferDescriptor, BufferUsages, CommandEncoderDescriptor, ComputePassDescriptor,
11            Extent3d, PipelineLayoutDescriptor, PollType, RawComputePipelineDescriptor,
12            ShaderModuleDescriptor, ShaderSource, TextureDescriptor, TextureDimension,
13            TextureFormat, TextureUsages,
14        },
15        renderer::{RenderDevice, RenderQueue},
16        Render, RenderApp,
17    },
18};
19
20fn main() {
21    let mut app = App::new();
22    app.add_plugins((
23        DefaultPlugins,
24        ExtractResourcePlugin::<RenderError>::default(),
25    ))
26    .add_systems(Startup, setup)
27    .add_systems(Update, (update_camera, input))
28    .init_resource::<RenderError>()
29    .sub_app_mut(RenderApp)
30    .add_systems(Render, cause_error);
31    app.run();
32}
33
34fn setup(
35    mut commands: Commands,
36    mut meshes: ResMut<Assets<Mesh>>,
37    mut materials: ResMut<Assets<StandardMaterial>>,
38) {
39    // circular base
40    commands.spawn((
41        Mesh3d(meshes.add(Circle::new(4.0))),
42        MeshMaterial3d(materials.add(Color::WHITE)),
43        Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
44    ));
45    // cube
46    commands.spawn((
47        Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
48        MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
49        Transform::from_xyz(0.0, 1.0, 0.0),
50    ));
51    // light
52    commands.spawn((
53        PointLight {
54            shadow_maps_enabled: true,
55            ..default()
56        },
57        Transform::from_xyz(4.0, 8.0, 4.0),
58    ));
59    // camera
60    commands.spawn((
61        Camera3d::default(),
62        Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
63    ));
64    // help text
65    commands.spawn((
66        Text::new(
67            "Test at your own risk: you may need to restart your computer to fully recover\n\
68            Press O to trigger an OutOfMemory error\n\
69            Press V to trigger a Validation error\n\
70            Press D to Destroy the render device (causes device lost error)\n\
71            Press L to Loop infinitely in a compute shader (causes device lost error)\n\
72            \n\
73            Press 1 to ignore errors, pretending nothing happened and continue rendering.\n\
74            Press 2 to panic on error.\n\
75            Press 3 to signals app exit on error.\n\
76            Press 4 to keeps the app alive, but stops rendering further on error.\n\
77            Press 5 to attempt renderer recovery.\n\
78            ",
79        ),
80        Node {
81            position_type: PositionType::Absolute,
82            top: px(12),
83            left: px(12),
84            ..default()
85        },
86    ));
87}
88
89fn update_camera(mut camera: Query<&mut Transform, With<Camera>>, time: Res<Time>) {
90    for mut t in camera.iter_mut() {
91        let (s, c) = ops::sin_cos(time.elapsed_secs() * 0.3);
92        *t = Transform::from_xyz(s * 10.0, 4.5, c * 10.0).looking_at(Vec3::ZERO, Vec3::Y);
93    }
94}
95
96#[derive(Resource, ExtractResource, Clone, Default)]
97enum RenderError {
98    #[default]
99    None,
100    OutOfMemory,
101    Validation,
102    DeviceLost,
103    Loop,
104}
105
106fn input(
107    input: Res<ButtonInput<Key>>,
108    mut error: ResMut<RenderError>,
109    mut handler: ResMut<RenderErrorHandler>,
110) {
111    *error = RenderError::None;
112    if input.just_pressed(Key::Character("o".into())) {
113        *error = RenderError::OutOfMemory;
114    }
115    if input.just_pressed(Key::Character("v".into())) {
116        *error = RenderError::Validation;
117    }
118    if input.just_pressed(Key::Character("d".into())) {
119        *error = RenderError::DeviceLost;
120    }
121    if input.just_pressed(Key::Character("l".into())) {
122        *error = RenderError::Loop;
123    }
124
125    if input.just_pressed(Key::Character("1".into())) {
126        *handler = RenderErrorHandler(|_, _, _| RenderErrorPolicy::Ignore);
127    }
128    if input.just_pressed(Key::Character("2".into())) {
129        *handler = RenderErrorHandler(|error, _, _| panic!("Rendering error {error:?}"));
130    }
131    if input.just_pressed(Key::Character("3".into())) {
132        *handler = RenderErrorHandler(|_, main_world, _| {
133            main_world.write_message(AppExit::error());
134            RenderErrorPolicy::StopRendering
135        });
136    }
137    if input.just_pressed(Key::Character("4".into())) {
138        *handler = RenderErrorHandler(|_, _, _| RenderErrorPolicy::StopRendering);
139    }
140    if input.just_pressed(Key::Character("5".into())) {
141        *handler = RenderErrorHandler(|_, _, _| RenderErrorPolicy::Recover(default()));
142    }
143}
144
145fn cause_error(error: If<Res<RenderError>>, device: Res<RenderDevice>, queue: Res<RenderQueue>) {
146    match **error {
147        RenderError::None => {}
148        RenderError::OutOfMemory => {
149            let mut textures = Vec::new();
150            for _ in 0..64 {
151                textures.push(device.create_texture(&TextureDescriptor {
152                    label: None,
153                    size: Extent3d {
154                        width: 8192,
155                        height: 8192,
156                        depth_or_array_layers: 1,
157                    },
158                    mip_level_count: 1,
159                    sample_count: 1,
160                    dimension: TextureDimension::D2,
161                    format: TextureFormat::Rgba16Float,
162                    usage: TextureUsages::RENDER_ATTACHMENT,
163                    view_formats: &[],
164                }));
165            }
166        }
167        RenderError::Validation => {
168            device.create_buffer(&BufferDescriptor {
169                label: None,
170                size: 1 << 63,
171                usage: BufferUsages::COPY_SRC,
172                mapped_at_creation: false,
173            });
174        }
175        RenderError::DeviceLost => {
176            device.wgpu_device().destroy();
177            device.poll(PollType::wait_indefinitely()).unwrap();
178        }
179        RenderError::Loop => {
180            let sm = device.create_and_validate_shader_module(ShaderModuleDescriptor {
181                label: Some("shader"),
182                source: ShaderSource::Wgsl(
183                    "@compute @workgroup_size(1, 1, 1) fn main() { loop { workgroupBarrier(); } }"
184                        .into(),
185                ),
186            });
187
188            let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
189                label: Some("pipeline_layout"),
190                bind_group_layouts: &[],
191                immediate_size: 0,
192            });
193
194            let pipeline = device.create_compute_pipeline(&RawComputePipelineDescriptor {
195                label: Some("pipeline"),
196                layout: Some(&pipeline_layout),
197                module: &sm,
198                entry_point: Some("main"),
199                compilation_options: Default::default(),
200                cache: None,
201            });
202
203            let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
204            {
205                let mut cpass = encoder.begin_compute_pass(&ComputePassDescriptor::default());
206                cpass.set_pipeline(&pipeline);
207                cpass.dispatch_workgroups(1, 1, 1);
208            }
209            device.poll(PollType::wait_indefinitely()).unwrap();
210            queue.submit([encoder.finish()]);
211        }
212    }
213}