bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.1k stars 3.56k forks source link

Changing backing image of a `RenderTarget::Image` causes rendering to that image to stop #16159

Open aecsocket opened 1 week ago

aecsocket commented 1 week ago

Bevy version

f005a96dd4a9aa019fecd5ac874cbd2e962e25f2

[Optional] Relevant system information

2024-10-29T20:08:15.195578Z  INFO bevy_diagnostic::system_information_diagnostics_plugin::internal: SystemInfo { os: "Linux 20240728.0.249973 Arch Linux", kernel: "6.11.5-307.bazzite.fc41.x86_64", cpu: "AMD Ryzen 7 7840U w/ Radeon  780M Graphics", core_count: "8", memory: "27.2 GiB" }
2024-10-29T20:08:15.253214Z  INFO bevy_render::renderer: AdapterInfo { name: "AMD Radeon 780M (RADV GFX1103_R1)", vendor: 4098, device: 5567, device_type: IntegratedGpu, driver: "radv", driver_info: "Mesa 24.2.5-arch1.1", backend: Vulkan }

What you did

You can set a Camera's target: RenderTarget to a RenderTarget::Image by giving it a Handle<Image>. However, if you swap out this backing Image on the GPU side (e.g. image_assets.insert(&existing_handle, new_image), then the camera which renders into that image will stop rendering properly, and it will effectively "freeze up", just displaying the last content of what was in that image.

Relevant discussion: https://discord.com/channels/691052431525675048/866787577687310356/1300848109861208084

Here's a modified version of the render_to_texture example which shows this:

```rs //! Shows how to render to a texture. Useful for mirrors, UI, or exporting images. use std::f32::consts::PI; use bevy::{ prelude::*, render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, view::RenderLayers, }, }; use bevy_render::render_resource::TextureDescriptor; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, (cube_rotator_system, rotator_system)) .add_systems( First, |mut imgs: ResMut>, ih: Res, mut foo: Local| { *foo += 1; if (*foo + 15) % 60 != 0 { return; } let size = Extent3d { width: 512, height: 512, ..default() }; // This is the texture that will be rendered to. let mut image = Image { texture_descriptor: TextureDescriptor { label: None, size, dimension: TextureDimension::D2, format: TextureFormat::Bgra8UnormSrgb, mip_level_count: 1, sample_count: 1, usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }, ..default() }; // fill image.data with zeroes image.resize(size); imgs.insert(&ih.0, image); }, ) .run(); } #[derive(Resource)] struct TodoTesting(Handle); // Marks the first pass cube (rendered to a texture.) #[derive(Component)] struct FirstPassCube; // Marks the main pass cube, to which the texture is applied. #[derive(Component)] struct MainPassCube; fn setup( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, mut images: ResMut>, ) { let size = Extent3d { width: 512, height: 512, ..default() }; // This is the texture that will be rendered to. let mut image = Image::new_fill( size, TextureDimension::D2, &[0, 0, 0, 0], TextureFormat::Bgra8UnormSrgb, RenderAssetUsages::default(), ); // You need to set these texture usage flags in order to use the image as a render target image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT; let image_handle = images.add(image); let cube_handle = meshes.add(Cuboid::new(4.0, 4.0, 4.0)); let cube_material_handle = materials.add(StandardMaterial { base_color: Color::srgb(0.8, 0.7, 0.6), reflectance: 0.02, unlit: false, ..default() }); commands.insert_resource(TodoTesting(image_handle.clone())); // This specifies the layer used for the first pass, which will be attached to the first pass camera and cube. let first_pass_layer = RenderLayers::layer(1); // The cube that will be rendered to the texture. commands.spawn(( Mesh3d(cube_handle), MeshMaterial3d(cube_material_handle), Transform::from_translation(Vec3::new(0.0, 0.0, 1.0)), FirstPassCube, first_pass_layer.clone(), )); // Light // NOTE: we add the light to both layers so it affects both the rendered-to-texture cube, and the cube on which we display the texture // Setting the layer to RenderLayers::layer(0) would cause the main view to be lit, but the rendered-to-texture cube to be unlit. // Setting the layer to RenderLayers::layer(1) would cause the rendered-to-texture cube to be lit, but the main view to be unlit. commands.spawn(( PointLight::default(), Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), RenderLayers::layer(0).with(1), )); commands.spawn(( Camera3d::default(), Camera { target: image_handle.clone().into(), clear_color: Color::WHITE.into(), ..default() }, Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)).looking_at(Vec3::ZERO, Vec3::Y), first_pass_layer, )); let cube_size = 4.0; let cube_handle = meshes.add(Cuboid::new(cube_size, cube_size, cube_size)); // This material has the texture that has been rendered. let material_handle = materials.add(StandardMaterial { base_color_texture: Some(image_handle), reflectance: 0.02, unlit: false, ..default() }); // Main pass cube, with material containing the rendered first pass texture. commands.spawn(( Mesh3d(cube_handle), MeshMaterial3d(material_handle), Transform::from_xyz(0.0, 0.0, 1.5).with_rotation(Quat::from_rotation_x(-PI / 5.0)), MainPassCube, )); // The main pass camera. commands.spawn(( Camera3d::default(), Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y), )); } /// Rotates the inner cube (first pass) fn rotator_system(time: Res

What went wrong

It should continue rendering and not freeze.

Additional information

https://github.com/user-attachments/assets/bcdee1ed-e568-41d2-a96b-e0dd75ae4dd4

tychedelia commented 1 week ago

The issue here is that the material asset using the image handle has already been prepared (i.e. bind group created) and so the texture view representing the render target isn't updated. AsBindGroup should track what image handles are in use and invalidate any RenderAsset that uses them in some kind of asset listener.

adidoes commented 5 days ago

I just ran into this issue, is there a workaround for now?

aecsocket commented 5 days ago

Yes, you can manually trigger change detection for the StandardMaterial asset and it should update bind groups IIUC.