Skip to content

Render events #18517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2866,6 +2866,17 @@ description = "A very simple compute shader that writes to a buffer that is read
category = "Shaders"
wasm = false

[[example]]
name = "render_event"
path = "examples/shader/render_event.rs"
doc-scrape-examples = true

[package.metadata.example.render_event]
name = "Render Event"
description = "A simple demonstration of sending events from the render world to the main world."
category = "Shaders"
wasm = false

[[example]]
name = "array_texture"
path = "examples/shader/array_texture.rs"
Expand Down
5 changes: 4 additions & 1 deletion crates/bevy_ecs/src/event/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ impl<E: Event> Events<E> {
self.send_with_caller(event, MaybeLocation::caller())
}

pub(crate) fn send_with_caller(&mut self, event: E, caller: MaybeLocation) -> EventId<E> {
/// "Sends" an `event` with a user-specified calling location
///
/// [`Events::send`] should be strongly preferred over this method
pub fn send_with_caller(&mut self, event: E, caller: MaybeLocation) -> EventId<E> {
let event_id = EventId {
id: self.event_count,
caller,
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub mod mesh;
pub mod pipelined_rendering;
pub mod primitives;
pub mod render_asset;
pub mod render_event;
pub mod render_graph;
pub mod render_phase;
pub mod render_resource;
Expand Down
121 changes: 121 additions & 0 deletions crates/bevy_render/src/render_event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use core::marker::PhantomData;

use async_channel::{Receiver, Sender};
use bevy_app::{App, Plugin, PreUpdate};
use bevy_ecs::{
change_detection::MaybeLocation,
event::{Event, Events},
resource::Resource,
system::{Res, ResMut, SystemParam},
};

use crate::RenderApp;

pub trait RenderEventApp {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what our convention is here -- should this be in the prelude so it doesn't have to be imported?

fn add_render_event<E: Event>(&mut self) -> &mut Self;
}

impl RenderEventApp for App {
fn add_render_event<E: Event>(&mut self) -> &mut Self {
self.add_plugins(RenderEventPlugin::<E>::default())
}
}

struct RenderEventPlugin<E: Event>(PhantomData<E>);

impl<E: Event> Default for RenderEventPlugin<E> {
fn default() -> Self {
Self(PhantomData)
}
}

impl<E: Event> Plugin for RenderEventPlugin<E> {
fn build(&self, app: &mut App) {
app.add_event::<E>()
.add_systems(PreUpdate, relay_render_events::<E>);
}

fn finish(&self, app: &mut App) {
let (sender, receiver) = async_channel::unbounded::<(E, MaybeLocation)>();

app.insert_resource(RenderEventReceiver(receiver));

let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};

render_app.insert_resource(RenderEventSender(sender));
}
}

#[derive(Resource)]
struct RenderEventReceiver<E: Event>(Receiver<(E, MaybeLocation)>);
#[derive(Resource)]
struct RenderEventSender<E: Event>(Sender<(E, MaybeLocation)>);

fn relay_render_events<E: Event>(
mut events: ResMut<Events<E>>,
receiver: Res<RenderEventReceiver<E>>,
) {
while let Ok((event, caller)) = receiver.0.try_recv() {
events.send_with_caller(event, caller);
}
}

/// An event writer for sending events from the render world to the main world.
///
/// Internally, this struct writes to a channel, the contents of which are relayed
/// to the main world's [`Events`] stream during [`PreUpdate`]. Note that because
/// the render world is pipelined, the events may not arrive before the next frame begins.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They almost certainly will not, but they also could. I think we might want to be clearer about how this works. In practice, you'll receive events from rendering frame N-2 with respect the the main world, but that isn't guaranteed. Users should supply their own temporal correlation id if necessary.

#[derive(SystemParam)]
pub struct MainEventWriter<'w, E: Event> {
sender: ResMut<'w, RenderEventSender<E>>,
}

const ERR_MSG: &str = "A render events channel has been closed. This is illegal";

impl<'w, E: Event> MainEventWriter<'w, E> {
/// Writes an `event`, which can later be read by [`EventReader`](super::EventReader)s
/// in the main world.
///
/// See [`Events`] for details.
#[doc(alias = "send")]
#[track_caller]
pub fn write(&mut self, event: E) {
self.sender
.0
.try_send((event, MaybeLocation::caller()))
.expect(ERR_MSG);
}

/// Sends a list of `events` all at once, which can later be read
/// by [`EventReader`](super::EventReader)s in the main world.
/// This is more efficient than sending each event individually.
///
/// See [`Events`] for details.
#[doc(alias = "send_batch")]
#[track_caller]
pub fn write_batch(&mut self, events: impl IntoIterator<Item = E>) {
events.into_iter().for_each(|event| {
self.sender
.0
.try_send((event, MaybeLocation::caller()))
.expect(ERR_MSG);
});
}

/// Writes the default value of the event. Useful when the event is an empty struct.
///
/// See [`Events`] for details.
#[doc(alias = "send_default")]
#[track_caller]
pub fn write_default(&mut self)
where
E: Default,
{
self.sender
.0
.try_send((Default::default(), MaybeLocation::caller()))
.expect(ERR_MSG);
}
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ Example | Description
[Material - WESL](../examples/shader/shader_material_wesl.rs) | A shader that uses WESL
[Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the various textures generated by the prepass
[Post Processing - Custom Render Pass](../examples/shader/custom_post_processing.rs) | A custom post processing effect, using a custom render pass that runs after the main pass
[Render Event](../examples/shader/render_event.rs) | A simple demonstration of sending events from the render world to the main world.
[Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader)
[Specialized Mesh Pipeline](../examples/shader/specialized_mesh_pipeline.rs) | Demonstrates how to write a specialized mesh pipeline
[Storage Buffer](../examples/shader/storage_buffer.rs) | A shader that shows how to bind a storage buffer using a custom material.
Expand Down
90 changes: 90 additions & 0 deletions examples/shader/render_event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Simple example demonstrating the use of [`App::init_render_event`] to send events from the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be expanded to say, in effect, "you probably don't want to do this."

//! render world to the main world

use bevy::{
diagnostic::FrameCount,
ecs::system::{LocalBuilder, ParamBuilder},
prelude::*,
render::{
render_event::{MainEventWriter, RenderEventApp},
Extract, Render, RenderApp,
},
};

fn main() -> AppExit {
App::new()
.add_plugins((DefaultPlugins, RenderEventDemoPlugin))
.run()
}

// We need a plugin to organize all the systems and render node required for this example
struct RenderEventDemoPlugin;
impl Plugin for RenderEventDemoPlugin {
fn build(&self, app: &mut App) {
app.add_render_event::<FrameRenderedEvent>()
.add_systems(Update, read_render_event);
}

fn finish(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};

// Since `FrameCount` is not present in the render world by default,
// we need to initialize it here. This is specific to this example,
// and should be unnecessary for most users.
render_app.init_resource::<FrameCount>();

let send_render_event = (
ParamBuilder,
ParamBuilder,
ParamBuilder,
LocalBuilder(Timer::from_seconds(2.0, TimerMode::Repeating)),
)
.build_state(render_app.world_mut())
.build_system(send_render_event);
Comment on lines +38 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit unnecessarily complex. I'd suggest using a resource instead just because that's a more common ECS api.


render_app
.add_systems(ExtractSchedule, extract_frame_count)
.add_systems(Render, send_render_event);
}
}

// Since `FrameCount` is not present in the render world by default,
// we need to extract it here. This is specific to this example, and should
// be unnecessary for most users.
fn extract_frame_count(
main_frame_count: Extract<Res<FrameCount>>,
mut render_frame_count: ResMut<FrameCount>,
) {
//since the frame count gets updated before extraction, it'll appear
//as one greater than it should be.
render_frame_count.0 = main_frame_count.0 - 1;
}

#[derive(Event)]
struct FrameRenderedEvent(u32);

fn send_render_event(
frame_count: Res<FrameCount>,
mut render_events: MainEventWriter<FrameRenderedEvent>,
time: Res<Time>,
mut timer: Local<Timer>,
) {
timer.tick(time.delta());
if timer.finished() {
render_events.write(FrameRenderedEvent(frame_count.0));
}
}

fn read_render_event(
mut render_events: EventReader<FrameRenderedEvent>,
frame_count: Res<FrameCount>,
) {
for render_event in render_events.read() {
println!(
"Render event from frame {} received in frame {}",
render_event.0, frame_count.0
);
}
}