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)]
97#[extract_app(RenderApp)]
98enum RenderError {
99    #[default]
100    None,
101    OutOfMemory,
102    Validation,
103    DeviceLost,
104    Loop,
105}
106
107fn input(
108    input: Res<ButtonInput<Key>>,
109    mut error: ResMut<RenderError>,
110    mut handler: ResMut<RenderErrorHandler>,
111) {
112    *error = RenderError::None;
113    if input.just_pressed(Key::Character("o".into())) {
114        *error = RenderError::OutOfMemory;
115    }
116    if input.just_pressed(Key::Character("v".into())) {
117        *error = RenderError::Validation;
118    }
119    if input.just_pressed(Key::Character("d".into())) {
120        *error = RenderError::DeviceLost;
121    }
122    if input.just_pressed(Key::Character("l".into())) {
123        *error = RenderError::Loop;
124    }
125
126    if input.just_pressed(Key::Character("1".into())) {
127        *handler = RenderErrorHandler(|_, _, _| RenderErrorPolicy::Ignore);
128    }
129    if input.just_pressed(Key::Character("2".into())) {
130        *handler = RenderErrorHandler(|error, _, _| panic!("Rendering error {error:?}"));
131    }
132    if input.just_pressed(Key::Character("3".into())) {
133        *handler = RenderErrorHandler(|_, main_world, _| {
134            main_world.write_message(AppExit::error());
135            RenderErrorPolicy::StopRendering
136        });
137    }
138    if input.just_pressed(Key::Character("4".into())) {
139        *handler = RenderErrorHandler(|_, _, _| RenderErrorPolicy::StopRendering);
140    }
141    if input.just_pressed(Key::Character("5".into())) {
142        *handler = RenderErrorHandler(|_, _, _| RenderErrorPolicy::Recover(default()));
143    }
144}
145
146fn cause_error(error: If<Res<RenderError>>, device: Res<RenderDevice>, queue: Res<RenderQueue>) {
147    match **error {
148        RenderError::None => {}
149        RenderError::OutOfMemory => {
150            let mut textures = Vec::new();
151            for _ in 0..64 {
152                textures.push(device.create_texture(&TextureDescriptor {
153                    label: None,
154                    size: Extent3d {
155                        width: 8192,
156                        height: 8192,
157                        depth_or_array_layers: 1,
158                    },
159                    mip_level_count: 1,
160                    sample_count: 1,
161                    dimension: TextureDimension::D2,
162                    format: TextureFormat::Rgba16Float,
163                    usage: TextureUsages::RENDER_ATTACHMENT,
164                    view_formats: &[],
165                }));
166            }
167        }
168        RenderError::Validation => {
169            device.create_buffer(&BufferDescriptor {
170                label: None,
171                size: 1 << 63,
172                usage: BufferUsages::COPY_SRC,
173                mapped_at_creation: false,
174            });
175        }
176        RenderError::DeviceLost => {
177            device.wgpu_device().destroy();
178            device.poll(PollType::wait_indefinitely()).unwrap();
179        }
180        RenderError::Loop => {
181            let sm = device.create_and_validate_shader_module(ShaderModuleDescriptor {
182                label: Some("shader"),
183                source: ShaderSource::Wgsl(
184                    "@compute @workgroup_size(1, 1, 1) fn main() { loop { workgroupBarrier(); } }"
185                        .into(),
186                ),
187            });
188
189            let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
190                label: Some("pipeline_layout"),
191                bind_group_layouts: &[],
192                immediate_size: 0,
193            });
194
195            let pipeline = device.create_compute_pipeline(&RawComputePipelineDescriptor {
196                label: Some("pipeline"),
197                layout: Some(&pipeline_layout),
198                module: &sm,
199                entry_point: Some("main"),
200                compilation_options: Default::default(),
201                cache: None,
202            });
203
204            let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
205            {
206                let mut cpass = encoder.begin_compute_pass(&ComputePassDescriptor::default());
207                cpass.set_pipeline(&pipeline);
208                cpass.dispatch_workgroups(1, 1, 1);
209            }
210            device.poll(PollType::wait_indefinitely()).unwrap();
211            queue.submit([encoder.finish()]);
212        }
213    }
214}