mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
71dd9ea7db
# Objective - Recover from rendering errors. - Another step towards render recovery after #22714 #22759 and #16481 ## Solution - Use `wgpu::Device::set_device_lost_callback` and `wgpu::Device::on_uncaptured_error` to listen for errors. - Add a state machine for the renderer - Update it on error - Add a `RenderErrorHandler` to let users specify behavior on error by returning a specific `RenderErrorPolicy` - This lets us for example ignore validation errors, delete responsible entities, or reload the renderer if the device was lost. ## Testing - #22757 with any of ```rs .insert_resource(bevy_render::error_handler::RenderErrorHandler(|_, _, _| { bevy_render::error_handler::RenderErrorPolicy::StopRendering })) ``` ```rs .insert_resource(bevy_render::error_handler::RenderErrorHandler(|_, _, _| { bevy_render::error_handler::RenderErrorPolicy::Recover(default()) })) ``` Note: no release note yet, as recovery does not exactly work well: this PR gets us to the point of being able to care about it, but we currently instantly crash on recover due to gpu resources not existing anymore. We need to build more resilience before publicizing imo. --------- Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
214 lines
7.3 KiB
Rust
214 lines
7.3 KiB
Rust
//! Demonstrates how to trigger various rendering errors, and how bevy can recover from them.
|
|
|
|
use bevy::{
|
|
input::keyboard::Key,
|
|
prelude::*,
|
|
render::{
|
|
error_handler::{RenderErrorHandler, RenderErrorPolicy},
|
|
extract_resource::{ExtractResource, ExtractResourcePlugin},
|
|
render_resource::{
|
|
BufferDescriptor, BufferUsages, CommandEncoderDescriptor, ComputePassDescriptor,
|
|
Extent3d, PipelineLayoutDescriptor, PollType, RawComputePipelineDescriptor,
|
|
ShaderModuleDescriptor, ShaderSource, TextureDescriptor, TextureDimension,
|
|
TextureFormat, TextureUsages,
|
|
},
|
|
renderer::{RenderDevice, RenderQueue},
|
|
Render, RenderApp,
|
|
},
|
|
};
|
|
|
|
fn main() {
|
|
let mut app = App::new();
|
|
app.add_plugins((
|
|
DefaultPlugins,
|
|
ExtractResourcePlugin::<RenderError>::default(),
|
|
))
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, (update_camera, input))
|
|
.init_resource::<RenderError>()
|
|
.sub_app_mut(RenderApp)
|
|
.add_systems(Render, cause_error);
|
|
app.run();
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
// circular base
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Circle::new(4.0))),
|
|
MeshMaterial3d(materials.add(Color::WHITE)),
|
|
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
|
|
));
|
|
// cube
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))),
|
|
MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
|
|
Transform::from_xyz(0.0, 1.0, 0.0),
|
|
));
|
|
// light
|
|
commands.spawn((
|
|
PointLight {
|
|
shadow_maps_enabled: true,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(4.0, 8.0, 4.0),
|
|
));
|
|
// camera
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
));
|
|
// help text
|
|
commands.spawn((
|
|
Text::new(
|
|
"Test at your own risk: you may need to restart your computer to fully recover\n\
|
|
Press O to trigger an OutOfMemory error\n\
|
|
Press V to trigger a Validation error\n\
|
|
Press D to Destroy the render device (causes device lost error)\n\
|
|
Press L to Loop infinitely in a compute shader (causes device lost error)\n\
|
|
\n\
|
|
Press 1 to ignore errors, pretending nothing happened and continue rendering.\n\
|
|
Press 2 to panic on error.\n\
|
|
Press 3 to signals app exit on error.\n\
|
|
Press 4 to keeps the app alive, but stops rendering further on error.\n\
|
|
Press 5 to attempt renderer recovery.\n\
|
|
",
|
|
),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
top: px(12),
|
|
left: px(12),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
fn update_camera(mut camera: Query<&mut Transform, With<Camera>>, time: Res<Time>) {
|
|
for mut t in camera.iter_mut() {
|
|
let (s, c) = ops::sin_cos(time.elapsed_secs() * 0.3);
|
|
*t = Transform::from_xyz(s * 10.0, 4.5, c * 10.0).looking_at(Vec3::ZERO, Vec3::Y);
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, ExtractResource, Clone, Default)]
|
|
enum RenderError {
|
|
#[default]
|
|
None,
|
|
OutOfMemory,
|
|
Validation,
|
|
DeviceLost,
|
|
Loop,
|
|
}
|
|
|
|
fn input(
|
|
input: Res<ButtonInput<Key>>,
|
|
mut error: ResMut<RenderError>,
|
|
mut handler: ResMut<RenderErrorHandler>,
|
|
) {
|
|
*error = RenderError::None;
|
|
if input.just_pressed(Key::Character("o".into())) {
|
|
*error = RenderError::OutOfMemory;
|
|
}
|
|
if input.just_pressed(Key::Character("v".into())) {
|
|
*error = RenderError::Validation;
|
|
}
|
|
if input.just_pressed(Key::Character("d".into())) {
|
|
*error = RenderError::DeviceLost;
|
|
}
|
|
if input.just_pressed(Key::Character("l".into())) {
|
|
*error = RenderError::Loop;
|
|
}
|
|
|
|
if input.just_pressed(Key::Character("1".into())) {
|
|
*handler = RenderErrorHandler(|_, _, _| RenderErrorPolicy::Ignore);
|
|
}
|
|
if input.just_pressed(Key::Character("2".into())) {
|
|
*handler = RenderErrorHandler(|error, _, _| panic!("Rendering error {error:?}"));
|
|
}
|
|
if input.just_pressed(Key::Character("3".into())) {
|
|
*handler = RenderErrorHandler(|_, main_world, _| {
|
|
main_world.write_message(AppExit::error());
|
|
RenderErrorPolicy::StopRendering
|
|
});
|
|
}
|
|
if input.just_pressed(Key::Character("4".into())) {
|
|
*handler = RenderErrorHandler(|_, _, _| RenderErrorPolicy::StopRendering);
|
|
}
|
|
if input.just_pressed(Key::Character("5".into())) {
|
|
*handler = RenderErrorHandler(|_, _, _| RenderErrorPolicy::Recover(default()));
|
|
}
|
|
}
|
|
|
|
fn cause_error(error: If<Res<RenderError>>, device: Res<RenderDevice>, queue: Res<RenderQueue>) {
|
|
match **error {
|
|
RenderError::None => {}
|
|
RenderError::OutOfMemory => {
|
|
let mut textures = Vec::new();
|
|
for _ in 0..64 {
|
|
textures.push(device.create_texture(&TextureDescriptor {
|
|
label: None,
|
|
size: Extent3d {
|
|
width: 8192,
|
|
height: 8192,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: TextureDimension::D2,
|
|
format: TextureFormat::Rgba16Float,
|
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
view_formats: &[],
|
|
}));
|
|
}
|
|
}
|
|
RenderError::Validation => {
|
|
device.create_buffer(&BufferDescriptor {
|
|
label: None,
|
|
size: 1 << 63,
|
|
usage: BufferUsages::COPY_SRC,
|
|
mapped_at_creation: false,
|
|
});
|
|
}
|
|
RenderError::DeviceLost => {
|
|
device.wgpu_device().destroy();
|
|
device.poll(PollType::wait_indefinitely()).unwrap();
|
|
}
|
|
RenderError::Loop => {
|
|
let sm = device.create_and_validate_shader_module(ShaderModuleDescriptor {
|
|
label: Some("shader"),
|
|
source: ShaderSource::Wgsl(
|
|
"@compute @workgroup_size(1, 1, 1) fn main() { loop { workgroupBarrier(); } }"
|
|
.into(),
|
|
),
|
|
});
|
|
|
|
let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
|
label: Some("pipeline_layout"),
|
|
bind_group_layouts: &[],
|
|
immediate_size: 0,
|
|
});
|
|
|
|
let pipeline = device.create_compute_pipeline(&RawComputePipelineDescriptor {
|
|
label: Some("pipeline"),
|
|
layout: Some(&pipeline_layout),
|
|
module: &sm,
|
|
entry_point: Some("main"),
|
|
compilation_options: Default::default(),
|
|
cache: None,
|
|
});
|
|
|
|
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
|
|
{
|
|
let mut cpass = encoder.begin_compute_pass(&ComputePassDescriptor::default());
|
|
cpass.set_pipeline(&pipeline);
|
|
cpass.dispatch_workgroups(1, 1, 1);
|
|
}
|
|
device.poll(PollType::wait_indefinitely()).unwrap();
|
|
queue.submit([encoder.finish()]);
|
|
}
|
|
}
|
|
}
|