mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
f1f41547fc
# render-graph-as-systems > [!NOTE] > Remember to check hide whitespace in diff view options when reviewing this PR This PR removes the `RenderGraph` in favor of using systems. ## Motivation The `RenderGraph` API was originally created when the ECS was significantly more immature. It was also created with the intention of supporting an input/output based slot system for managing resources that has never been used. While resource management is an important potential use of a render graph, current rendering code doesn't make use of any patterns relating to it. Since the ECS has improved, the functionality of `Schedule` has basically become co-extensive with what the `RenderGraph` API is doing, i.e. ordering bits of system-like logic relative to one another and executing them in a big chunk. Additionally, while there's still desire for more advanced techniques like resource management in the graph, it's desirable to implement those in ECS terms rather than creating more `RenderGraph` specific abstraction. In short, this sets us up to iterate on a more ECS based approach, while deleting ~3k lines of mostly unused code. ## Implementation At a high level: We use `Schedule` as our "sub-graph." Rather than running the graph, we run a schedule. Systems can be ordered relative to one another. The render system uses a `RenderGraph` schedule to define the "root" of the graph. `core_pipeline` adds a `camera_driver` system that runs the per-camera schedules. This top level schedule provides an extension point for apps that may want to do custom rendering, or non-camera rendering. ### `CurrentView` / `ViewQuery` When running schedules per-camera in the `camera_driver` system, we insert a `CurrentView` resource that's used to mark the currently iterating view. We also add a new param `ViewQuery` that internally uses this resource to execute the query and skip the system if it doesn't match as a convenience. ### `RenderContext` The `RenderContext` is now a system param that wraps a `Deferred` for tracking the state of the current command encoder and queued buffers. ### `SystemBuffer` We use an system buffer impl to track command encoders in the render context and rely on apply deferred in order to encode them all. Currently, this encodes them in series. There are likely opportunities here to make this more efficient. ## Benchmarks ### Bistro <img width="1635" height="825" alt="Screenshot 2026-01-15 at 7 57 40 PM" src="https://github.com/user-attachments/assets/8e55a959-89a3-4947-bfc5-c04780f82e7b" /> ### Caldera <img width="1631" height="828" alt="Screenshot 2026-01-15 at 8 13 06 PM" src="https://github.com/user-attachments/assets/e7e8ae0d-41c3-430f-8b4d-9099b3d922a0" /> ## Future steps There are a number of exciting potential changes that could follow here: - We can explore adding something like a read-only schedule to pick up some more potential parallelism in graph execution. - We can use more things like run conditions in order to prevent systems from running at all in the first place. - We can explore things like automating resource creation via system params. ## TODO: - [x] Make sure 100% of everything still works. - [x] Benchmark to make sure we don't regress performance - [x] Re-add docs --------- Co-authored-by: atlas dostal <rodol@rivalrebels.com>
216 lines
7.6 KiB
Rust
216 lines
7.6 KiB
Rust
//! Simple example demonstrating the use of the [`Readback`] component to read back data from the GPU
|
|
//! using both a storage buffer and texture.
|
|
|
|
use bevy::{
|
|
asset::RenderAssetUsages,
|
|
prelude::*,
|
|
render::{
|
|
extract_resource::{ExtractResource, ExtractResourcePlugin},
|
|
gpu_readback::{Readback, ReadbackComplete},
|
|
render_asset::RenderAssets,
|
|
render_resource::{
|
|
binding_types::{storage_buffer, texture_storage_2d},
|
|
*,
|
|
},
|
|
renderer::{RenderContext, RenderDevice, RenderGraph},
|
|
storage::{GpuShaderBuffer, ShaderBuffer},
|
|
texture::GpuImage,
|
|
Render, RenderApp, RenderStartup, RenderSystems,
|
|
},
|
|
};
|
|
|
|
/// This example uses a shader source file from the assets subdirectory
|
|
const SHADER_ASSET_PATH: &str = "shaders/gpu_readback.wgsl";
|
|
|
|
// The length of the buffer sent to the gpu
|
|
const BUFFER_LEN: usize = 16;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins((
|
|
DefaultPlugins,
|
|
GpuReadbackPlugin,
|
|
ExtractResourcePlugin::<ReadbackBuffer>::default(),
|
|
ExtractResourcePlugin::<ReadbackImage>::default(),
|
|
))
|
|
.insert_resource(ClearColor(Color::BLACK))
|
|
.add_systems(Startup, setup)
|
|
.run();
|
|
}
|
|
|
|
// We need a plugin to organize all the systems and render node required for this example
|
|
struct GpuReadbackPlugin;
|
|
impl Plugin for GpuReadbackPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
render_app
|
|
.add_systems(RenderStartup, init_compute_pipeline)
|
|
.add_systems(
|
|
Render,
|
|
prepare_bind_group
|
|
.in_set(RenderSystems::PrepareBindGroups)
|
|
// We don't need to recreate the bind group every frame
|
|
.run_if(not(resource_exists::<GpuBufferBindGroup>)),
|
|
)
|
|
.add_systems(RenderGraph, compute);
|
|
}
|
|
}
|
|
|
|
#[derive(Resource, ExtractResource, Clone)]
|
|
struct ReadbackBuffer(Handle<ShaderBuffer>);
|
|
|
|
#[derive(Resource, ExtractResource, Clone)]
|
|
struct ReadbackImage(Handle<Image>);
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut images: ResMut<Assets<Image>>,
|
|
mut buffers: ResMut<Assets<ShaderBuffer>>,
|
|
) {
|
|
// Create a storage buffer with some data
|
|
let buffer: Vec<u32> = (0..BUFFER_LEN as u32).collect();
|
|
let mut buffer = ShaderBuffer::from(buffer);
|
|
// We need to enable the COPY_SRC usage so we can copy the buffer to the cpu
|
|
buffer.buffer_description.usage |= BufferUsages::COPY_SRC;
|
|
let buffer = buffers.add(buffer);
|
|
|
|
// Create a storage texture with some data
|
|
let size = Extent3d {
|
|
width: BUFFER_LEN as u32,
|
|
height: 1,
|
|
..default()
|
|
};
|
|
// We create an uninitialized image since this texture will only be used for getting data out
|
|
// of the compute shader, not getting data in, so there's no reason for it to exist on the CPU
|
|
let mut image = Image::new_uninit(
|
|
size,
|
|
TextureDimension::D2,
|
|
TextureFormat::R32Uint,
|
|
RenderAssetUsages::RENDER_WORLD,
|
|
);
|
|
// We also need to enable the COPY_SRC, as well as STORAGE_BINDING so we can use it in the
|
|
// compute shader
|
|
image.texture_descriptor.usage |= TextureUsages::COPY_SRC | TextureUsages::STORAGE_BINDING;
|
|
let image = images.add(image);
|
|
|
|
// Spawn the readback components. For each frame, the data will be read back from the GPU
|
|
// asynchronously and trigger the `ReadbackComplete` event on this entity. Despawn the entity
|
|
// to stop reading back the data.
|
|
commands
|
|
.spawn(Readback::buffer(buffer.clone()))
|
|
.observe(|event: On<ReadbackComplete>| {
|
|
// This matches the type which was used to create the `ShaderBuffer` above,
|
|
// and is a convenient way to interpret the data.
|
|
let data: Vec<u32> = event.to_shader_type();
|
|
info!("Buffer {:?}", data);
|
|
});
|
|
|
|
// It is also possible to read only a range of the buffer.
|
|
commands
|
|
.spawn(Readback::buffer_range(
|
|
buffer.clone(),
|
|
4 * u32::SHADER_SIZE.get(), // skip the first four elements
|
|
8 * u32::SHADER_SIZE.get(), // read eight elements
|
|
))
|
|
.observe(|event: On<ReadbackComplete>| {
|
|
let data: Vec<u32> = event.to_shader_type();
|
|
info!("Buffer range {:?}", data);
|
|
});
|
|
|
|
// This is just a simple way to pass the buffer handle to the render app for our compute node
|
|
commands.insert_resource(ReadbackBuffer(buffer));
|
|
|
|
// Textures can also be read back from the GPU. Pay careful attention to the format of the
|
|
// texture, as it will affect how the data is interpreted.
|
|
commands
|
|
.spawn(Readback::texture(image.clone()))
|
|
.observe(|event: On<ReadbackComplete>| {
|
|
// You probably want to interpret the data as a color rather than a `ShaderType`,
|
|
// but in this case we know the data is a single channel storage texture, so we can
|
|
// interpret it as a `Vec<u32>`
|
|
let data: Vec<u32> = event.to_shader_type();
|
|
info!("Image {:?}", data);
|
|
});
|
|
commands.insert_resource(ReadbackImage(image));
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct GpuBufferBindGroup(BindGroup);
|
|
|
|
fn prepare_bind_group(
|
|
mut commands: Commands,
|
|
pipeline: Res<ComputePipeline>,
|
|
render_device: Res<RenderDevice>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
buffer: Res<ReadbackBuffer>,
|
|
image: Res<ReadbackImage>,
|
|
buffers: Res<RenderAssets<GpuShaderBuffer>>,
|
|
images: Res<RenderAssets<GpuImage>>,
|
|
) {
|
|
let buffer = buffers.get(&buffer.0).unwrap();
|
|
let image = images.get(&image.0).unwrap();
|
|
let bind_group = render_device.create_bind_group(
|
|
None,
|
|
&pipeline_cache.get_bind_group_layout(&pipeline.layout),
|
|
&BindGroupEntries::sequential((
|
|
buffer.buffer.as_entire_buffer_binding(),
|
|
image.texture_view.into_binding(),
|
|
)),
|
|
);
|
|
commands.insert_resource(GpuBufferBindGroup(bind_group));
|
|
}
|
|
|
|
#[derive(Resource)]
|
|
struct ComputePipeline {
|
|
layout: BindGroupLayoutDescriptor,
|
|
pipeline: CachedComputePipelineId,
|
|
}
|
|
|
|
fn init_compute_pipeline(
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
) {
|
|
let layout = BindGroupLayoutDescriptor::new(
|
|
"",
|
|
&BindGroupLayoutEntries::sequential(
|
|
ShaderStages::COMPUTE,
|
|
(
|
|
storage_buffer::<Vec<u32>>(false),
|
|
texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly),
|
|
),
|
|
),
|
|
);
|
|
let shader = asset_server.load(SHADER_ASSET_PATH);
|
|
let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
|
|
label: Some("GPU readback compute shader".into()),
|
|
layout: vec![layout.clone()],
|
|
shader: shader.clone(),
|
|
..default()
|
|
});
|
|
commands.insert_resource(ComputePipeline { layout, pipeline });
|
|
}
|
|
|
|
fn compute(
|
|
mut render_context: RenderContext,
|
|
pipeline_cache: Res<PipelineCache>,
|
|
pipeline: Res<ComputePipeline>,
|
|
bind_group: Res<GpuBufferBindGroup>,
|
|
) {
|
|
if let Some(init_pipeline) = pipeline_cache.get_compute_pipeline(pipeline.pipeline) {
|
|
let mut pass =
|
|
render_context
|
|
.command_encoder()
|
|
.begin_compute_pass(&ComputePassDescriptor {
|
|
label: Some("GPU readback compute pass"),
|
|
..default()
|
|
});
|
|
|
|
pass.set_bind_group(0, &bind_group.0, &[]);
|
|
pass.set_pipeline(init_pipeline);
|
|
pass.dispatch_workgroups(BUFFER_LEN as u32, 1, 1);
|
|
}
|
|
}
|