Skip to content

Conversation

@tychedelia
Copy link
Member

@tychedelia tychedelia commented Dec 15, 2025

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

Screenshot 2026-01-15 at 7 57 40 PM

Caldera

Screenshot 2026-01-15 at 8 13 06 PM

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:

  • Make sure 100% of everything still works.
  • Benchmark to make sure we don't regress performance
  • Re-add docs

@tychedelia tychedelia added C-Feature A new feature, making something new possible A-Rendering Drawing game state to the screen X-Contentious There are nontrivial implications that should be thought through D-Domain-Expert Requires deep knowledge in a given domain S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged S-Needs-SME Decision or review from an SME is required labels Dec 15, 2025
@alice-i-cecile alice-i-cecile added X-Controversial There is active debate or serious implications around merging this PR M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Release-Note Work that should be called out in the blog due to impact and removed X-Contentious There are nontrivial implications that should be thought through labels Dec 15, 2025
@github-actions
Copy link
Contributor

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Please review the instructions for writing migration guides, then expand or revise the content in the migration guides directory to reflect your changes.

@github-actions
Copy link
Contributor

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

)
.entered();

world.run_schedule(schedule);
Copy link
Member

Choose a reason for hiding this comment

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

One of the "big" reasons I've pushed back against RenderGraph -> Systems in the past is that it prevents us from parallelizing command encoding in cases like this where we need to run specific, potentially very expensive "subgraphs" in a loop.

The ownership and access patterns in the current system allow for that sort of thing (and were explicitly chosen to enable it).

Copy link
Member Author

@tychedelia tychedelia Dec 16, 2025

Choose a reason for hiding this comment

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

A few things that I'd like to note:

  • With the exception of one caveat I'll note in the PR description, this isn't worse than the existing solution in terms of overall parallelism.
  • In the context of cameras, we effectively currently need to be serial because we re-use ViewTarget textures. Without a higher level resource tracking system, we have to pessimistically assume that cameras need to run in terms of the order they are given due to implicit dependencies.
  • Something like a "read only" schedule marker has been proposed before to allow running schedules on &World. ECS devs have suggested to me this is feasible.
  • We do do mutation in the current graph in many places, but have to resort to hack-y atomics usage. It's not crazy that you want to mutate world sometimes (i.e. to insert some gpu resource to be used by the next frame).
  • I'd like to solve issues around parallelism in the context of more generalized ECS patterns. I have a number of more speculative ideas here, but I think there are a few different directions we could go.

But yeah, we obviously need to make sure we don't regress current state performance.

Copy link
Contributor

@IceSentry IceSentry Dec 16, 2025

Choose a reason for hiding this comment

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

I think we also need to keep in mind maintenance. This PR manages to remove a lot of code and makes the render graph much more approachable for people that already know bevy's ECS. It's 3.8k less lines of code to maintain. Of course, line of code isn't a super useful metric for many things but I think it's significant enough that it can't just be ignored. With the current render graph, we almost never see PRs to it and I wouldn't be surprised if a big reason for that is that it looks so foreign in bevy and because it's also a lot of very complex code.

Copy link

@dcvz dcvz Jan 16, 2026

Choose a reason for hiding this comment

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

Besides the points made above about approachability, this also means dogfooding systems for a very performance critical part of Bevy. Working towards meeting the demands of that via systems and working on optimizations will then turn into benefits for everything else

@IceSentry IceSentry self-requested a review December 16, 2025 00:36
.add_systems(Render, prepare_cas_pipelines.in_set(RenderSystems::Prepare))
.add_systems(
Core3d,
cas.after(fxaa)
Copy link
Contributor

Choose a reason for hiding this comment

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

reviewer note: fxaa is ordered after tonemapping, so the .after tonemapping here is redundant thus removed

Core3d,
temporal_anti_alias
.after(Core3dSystems::StartMainPassPostProcessing)
.before(Core3dSystems::PostProcessing),
Copy link
Contributor

Choose a reason for hiding this comment

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

some ordering constraints were lost here

Copy link
Contributor

@atlv24 atlv24 left a comment

Choose a reason for hiding this comment

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

Remember to check Hide whitespace in diff view options!

Some notes:

  • should we be using fallible systems to more closely match the Result behavior of the original nodes?
  • some ordering constraints were removed but i think many were redundant. i noted these
  • tracing infra probably is redundant with system traces now (followup)
  • can probably use run conditions in a bunch of places too now (followup)
  • render_context -> ctx could have been a different pr (and is a matter of taste but i agree with it anyways) its just noise here
  • one of the solari files is jumbled and unreviewable

This is an absolutely colossal amount of work with incredible attention to detail
fantastic job, excited to see this merge. Lots of small fixes bundled here and its pretty much all good. There's some comments that were removed but i dont feel very strongly about them.

Very in favor of this merging, approval will come after comments addressed and pr exits draft status.

I'm going to dig into the CI failures soon

@alice-i-cecile alice-i-cecile added X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers and removed S-Needs-SME Decision or review from an SME is required labels Jan 25, 2026
@alice-i-cecile
Copy link
Member

At least three SME-Rendering are in favor of this (@superdump, @IceSentry, @tychedelia), so I'm marking this as blessed. There's still appetite for a quick decision from @cart on this, so I'm going to hold off on merging this until then.

I'm also going to do my own review today, from an ECS / maintenance perspective. @tychedelia, hold off on the merge conflicts until after we've made the final call IMO, and then we'll merge it immediately after you fix those if there's a yes. I expect some rendering regressions, but that's what automated tooling and the release candidate is for. It's never going to be an easy migration 😅

@alice-i-cecile alice-i-cecile added S-Needs-SME Decision or review from an SME is required and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 25, 2026
@cart
Copy link
Member

cart commented Jan 26, 2026

I think I'm on board for this. I'd like to do a pass on Monday. I agree that if we're going to do it, we should do it asap.

@alice-i-cecile
Copy link
Member

High-level review for you.

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.

This is the intended pattern from an ECS perspective. Great to see it being useful here.

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.

Good, this is clear, and makes sense based on what I know of how Bevy's camera-driven rendering works. Each camera renders its own view, and may have different effects applied.

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.

Sensible. General work on command batching may pay dividends here, although careful design work will be required.

Docs

I know documentation standards are lower over in the rendering team currently, but I think we can try to leave this better than we left it. I've left suggestions for a few high-impact spots where we could record these design choices.

Release content

100% needs a release note and migration guide before we merge this.

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

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

I quite like it! I love how terse it is, that we're removing abstractions, and that it all feels more "Bevy-ey".

I'm convinced that we can make this parallel if/when we need to (via readonly schedules).

/// specify any edges you want but make sure to include your own label.
fn node_edges() -> Vec<InternedRenderLabel>;
/// Defaults to [`Core3d`] for 3D post-processing effects.
fn schedule() -> impl ScheduleLabel + Clone {
Copy link
Member

Choose a reason for hiding this comment

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

Making the author decide if the fullscreen material is 3D or 2D (or something else) in the trait doesn't feel great to me. Nor does silently defaulting to 3D-only. Many of our other "camera effects" work in both contexts, so locking it in at the trait level feels overly limiting. I imagine a large set of use cases here should be generic.

The "detect subgraph based on camera" behavior we had previously felt like it was closer to the mark. What we really need is an opinionated "camera effect stack" that is cross-compatible.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree, but I think we should fix this up in the 2d/3d collapse that I've committed to completing in this cycle, as that's ultimately the cleanest solution here.

/// Additional systems can be added to these sets to customize the rendering pipeline, or additional
/// sets can be created relative to these core sets.
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub enum Core3dSystems {
Copy link
Member

Choose a reason for hiding this comment

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

Something to consider here: if we split this out into individual SystemSet types (or just rename it to CoreSystems and share it), we can share them between pipelines (ex: Core2d and Core3d could both use the PostProcess system set). This could help enable cross-compatibility of logic in cases like FullscreenMaterial, which (currently) asks that people define before/after system set orderings.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, am also planning on tackling that with the 2d/3d refactor.


/// A system parameter that provides access to the entity corresponding to the current view being rendered.
#[derive(SystemParam)]
pub struct CurrentView<'w> {
Copy link
Member

Choose a reason for hiding this comment

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

In Bevy, nested/high level SystemParams are generally a "last resort" thing, as they are an additional "meta" layer on top of the core data types. CurrentView isn't currently being used by anything, and it provides no value over CurrentViewEntity. I think we should remove this (or merge it with CurrentViewEntity) unless a real need arises.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah for sure, this was a vestige from a different pattern I was trying at one point, removed!

/// Updates the [`RenderGraph`] with all of its nodes and then runs it to render the entire frame.
/// Schedule label for the root render graph schedule. This schedule runs once per frame
/// in the [`render_system`] system and is responsible for driving the entire rendering process.
#[derive(ScheduleLabel, Debug, Clone, PartialEq, Eq, Hash, Default)]
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure calling this the RenderGraph is helpful at this stage, given that everything else "rendering" is just as "render graph-like" now: they are also members of the Render schedule and they use the ECS schedule "graph".

Good naming and documentation is pretty critical now, as execution is very hard to track:

RenderApp (App)
    Render (Schedule)
        RenderSystems::Render (SystemSet)
            render_system (System)
                RenderGraph (Schedule)
                    camera_driver (System)
                        Core2d (Schedule)
                        Core3d (Schedule)
                        CUSTOM_PIPELINE (Schedule)

Copy link
Member

Choose a reason for hiding this comment

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

I would love a revised version of that diagram front-and-center in the docs for bevy_render.

let command_encoder = ctx.command_encoder();
let mut dlss_context = dlss_context.context.lock().unwrap();

command_encoder.push_debug_group("dlss_super_resolution");
Copy link
Contributor

Choose a reason for hiding this comment

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

DLSS node was changed recently, and it looks like you clobbered those changes during a recent merge.

&'static ViewUniformOffset,
&'static PreviousViewUniformOffset,
/// Initializes the Solari lighting pipelines at render startup.
pub fn init_solari_lighting_pipelines(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you move this to the end of the file? I'd rather the node itself first.

@JMS55
Copy link
Contributor

JMS55 commented Jan 27, 2026

I would also ask that you test DLSS out. It relies on some tricky semantics starting with wgpu 28. Specifically:

  1. Doing some work and putting the start timestamp on the existing command encoder
  2. Finishing that encoder
  3. Making a new command encoder, doing DLSS on it
  4. Finishing that encoder
  5. Starting a new encoder
  6. Putting the end timestamp on the encoder

And we need to make sure the encoders still get flushed correctly at the various points.

@github-actions
Copy link
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-22144

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

@github-actions
Copy link
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-22144

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Rendering Drawing game state to the screen C-Feature A new feature, making something new possible D-Domain-Expert Requires deep knowledge in a given domain M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Release-Note Work that should be called out in the blog due to impact S-Needs-SME Decision or review from an SME is required X-Blessed Has a large architectural impact or tradeoffs, but the design has been endorsed by decision makers

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

10 participants