Skip to content

Commit

Permalink
Modular Rendering (#2831)
Browse files Browse the repository at this point in the history
This changes how render logic is composed to make it much more modular. Previously, all extraction logic was centralized for a given "type" of rendered thing. For example, we extracted meshes into a vector of ExtractedMesh, which contained the mesh and material asset handles, the transform, etc. We looked up bindings for "drawn things" using their index in the `Vec<ExtractedMesh>`. This worked fine for built in rendering, but made it hard to reuse logic for "custom" rendering. It also prevented us from reusing things like "extracted transforms" across contexts.

To make rendering more modular, I made a number of changes:

* Entities now drive rendering:
  * We extract "render components" from "app components" and store them _on_ entities. No more centralized uber lists! We now have true "ECS-driven rendering"
  * To make this perform well, I implemented #2673 in upstream Bevy for fast batch insertions into specific entities. This was merged into the `pipelined-rendering` branch here: #2815
* Reworked the `Draw` abstraction:
  * Generic `PhaseItems`: each draw phase can define its own type of "rendered thing", which can define its own "sort key"
  * Ported the 2d, 3d, and shadow phases to the new PhaseItem impl (currently Transparent2d, Transparent3d, and Shadow PhaseItems)
  * `Draw` trait and and `DrawFunctions` are now generic on PhaseItem
  * Modular / Ergonomic `DrawFunctions` via `RenderCommands`
    * RenderCommand is a trait that runs an ECS query and produces one or more RenderPass calls. Types implementing this trait can be composed to create a final DrawFunction. For example the DrawPbr DrawFunction is created from the following DrawCommand tuple. Const generics are used to set specific bind group locations:
        ```rust
         pub type DrawPbr = (
            SetPbrPipeline,
            SetMeshViewBindGroup<0>,
            SetStandardMaterialBindGroup<1>,
            SetTransformBindGroup<2>,
            DrawMesh,
        );
        ```
    * The new `custom_shader_pipelined` example illustrates how the commands above can be reused to create a custom draw function:
       ```rust
       type DrawCustom = (
           SetCustomMaterialPipeline,
           SetMeshViewBindGroup<0>,
           SetTransformBindGroup<2>,
           DrawMesh,
       );
       ``` 
* ExtractComponentPlugin and UniformComponentPlugin:
  * Simple, standardized ways to easily extract individual components and write them to GPU buffers
* Ported PBR and Sprite rendering to the new primitives above.
* Removed staging buffer from UniformVec in favor of direct Queue usage
  * Makes UniformVec much easier to use and more ergonomic. Completely removes the need for custom render graph nodes in these contexts (see the PbrNode and view Node removals and the much simpler call patterns in the relevant Prepare systems).
* Added a many_cubes_pipelined example to benchmark baseline 3d rendering performance and ensure there were no major regressions during this port. Avoiding regressions was challenging given that the old approach of extracting into centralized vectors is basically the "optimal" approach. However thanks to a various ECS optimizations and render logic rephrasing, we pretty much break even on this benchmark!
* Lifetimeless SystemParams: this will be a bit divisive, but as we continue to embrace "trait driven systems" (ex: ExtractComponentPlugin, UniformComponentPlugin, DrawCommand), the ergonomics of `(Query<'static, 'static, (&'static A, &'static B, &'static)>, Res<'static, C>)` were getting very hard to bear. As a compromise, I added "static type aliases" for the relevant SystemParams. The previous example can now be expressed like this: `(SQuery<(Read<A>, Read<B>)>, SRes<C>)`. If anyone has better ideas / conflicting opinions, please let me know!
* RunSystem trait: a way to define Systems via a trait with a SystemParam associated type. This is used to implement the various plugins mentioned above. I also added SystemParamItem and QueryItem type aliases to make "trait stye" ecs interactions nicer on the eyes (and fingers).
* RenderAsset retrying: ensures that render assets are only created when they are "ready" and allows us to create bind groups directly inside render assets (which significantly simplified the StandardMaterial code). I think ultimately we should swap this out on "asset dependency" events to wait for dependencies to load, but this will require significant asset system changes.
* Updated some built in shaders to account for missing MeshUniform fields
  • Loading branch information
cart committed Sep 23, 2021
1 parent 1e03a97 commit 08969a2
Show file tree
Hide file tree
Showing 36 changed files with 1,653 additions and 927 deletions.
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ ron = "0.6.2"
serde = { version = "1", features = ["derive"] }
# Needed to poll Task examples
futures-lite = "1.11.3"
crevice = {path = "crates/crevice"}

[[example]]
name = "hello_world"
Expand Down Expand Up @@ -159,6 +160,10 @@ path = "examples/3d/3d_scene.rs"
name = "3d_scene_pipelined"
path = "examples/3d/3d_scene_pipelined.rs"

[[example]]
name = "many_cubes_pipelined"
path = "examples/3d/many_cubes_pipelined.rs"

[[example]]
name = "cornell_box_pipelined"
path = "examples/3d/cornell_box_pipelined.rs"
Expand Down Expand Up @@ -462,6 +467,10 @@ path = "examples/shader/shader_custom_material.rs"
name = "shader_defs"
path = "examples/shader/shader_defs.rs"

[[example]]
name = "custom_shader_pipelined"
path = "examples/shader/custom_shader_pipelined.rs"

# Tools
[[example]]
name = "bevymark"
Expand Down
46 changes: 46 additions & 0 deletions assets/shaders/custom.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[[block]]
struct View {
view_proj: mat4x4<f32>;
projection: mat4x4<f32>;
world_position: vec3<f32>;
};
[[group(0), binding(0)]]
var view: View;

[[block]]
struct Mesh {
transform: mat4x4<f32>;
};
[[group(2), binding(0)]]
var mesh: Mesh;

struct Vertex {
[[location(0)]] position: vec3<f32>;
[[location(1)]] normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
};

struct VertexOutput {
[[builtin(position)]] clip_position: vec4<f32>;
};

[[stage(vertex)]]
fn vertex(vertex: Vertex) -> VertexOutput {
let world_position = mesh.transform * vec4<f32>(vertex.position, 1.0);

var out: VertexOutput;
out.clip_position = view.view_proj * world_position;
return out;
}

[[block]]
struct CustomMaterial {
color: vec4<f32>;
};
[[group(1), binding(0)]]
var material: CustomMaterial;

[[stage(fragment)]]
fn fragment() -> [[location(0)]] vec4<f32> {
return material.color;
}
2 changes: 2 additions & 0 deletions crates/bevy_asset/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ impl<T: Asset> Handle<T> {
}
}

#[inline]
pub fn weak(id: HandleId) -> Self {
Self {
id,
Expand Down Expand Up @@ -129,6 +130,7 @@ impl<T: Asset> Handle<T> {
self.handle_type = HandleType::Strong(sender);
}

#[inline]
pub fn clone_weak(&self) -> Self {
Handle::weak(self.id)
}
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub mod prelude {
};
}

pub use bevy_ecs_macros::all_tuples;

#[cfg(test)]
mod tests {
use crate as bevy_ecs;
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/query/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub trait WorldQuery {
type State: FetchState;
}

pub type QueryItem<'w, 's, Q> = <<Q as WorldQuery>::Fetch as Fetch<'w, 's>>::Item;

pub trait Fetch<'world, 'state>: Sized {
type Item;
type State: FetchState;
Expand Down
74 changes: 73 additions & 1 deletion crates/bevy_ecs/src/system/function_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
query::{Access, FilteredAccessSet},
system::{
check_system_change_tick, ReadOnlySystemParamFetch, System, SystemId, SystemParam,
SystemParamFetch, SystemParamState,
SystemParamFetch, SystemParamItem, SystemParamState,
},
world::{World, WorldId},
};
Expand Down Expand Up @@ -48,6 +48,11 @@ impl SystemMeta {
pub fn set_non_send(&mut self) {
self.is_send = false;
}

#[inline]
pub(crate) fn check_change_tick(&mut self, change_tick: u32) {
check_system_change_tick(&mut self.last_change_tick, change_tick, self.name.as_ref());
}
}

// TODO: Actually use this in FunctionSystem. We should probably only do this once Systems are constructed using a World reference
Expand Down Expand Up @@ -123,6 +128,10 @@ impl<Param: SystemParam> SystemState<Param> {
self.world_id == world.id()
}

pub(crate) fn new_archetype(&mut self, archetype: &Archetype) {
self.param_state.new_archetype(archetype, &mut self.meta);
}

fn validate_world_and_update_archetypes(&mut self, world: &World) {
assert!(self.matches_world(world), "Encountered a mismatched World. A SystemState cannot be used with Worlds other than the one it was created with.");
let archetypes = world.archetypes();
Expand Down Expand Up @@ -161,6 +170,69 @@ impl<Param: SystemParam> SystemState<Param> {
}
}

pub trait RunSystem: Send + Sync + 'static {
type Param: SystemParam;
fn run(param: SystemParamItem<Self::Param>);
fn system(world: &mut World) -> ParamSystem<Self::Param> {
ParamSystem {
run: Self::run,
state: SystemState::new(world),
}
}
}

pub struct ParamSystem<P: SystemParam> {
state: SystemState<P>,
run: fn(SystemParamItem<P>),
}

impl<P: SystemParam + 'static> System for ParamSystem<P> {
type In = ();

type Out = ();

fn name(&self) -> Cow<'static, str> {
self.state.meta().name.clone()
}

fn id(&self) -> SystemId {
self.state.meta().id
}

fn new_archetype(&mut self, archetype: &Archetype) {
self.state.new_archetype(archetype);
}

fn component_access(&self) -> &Access<ComponentId> {
self.state.meta().component_access_set.combined_access()
}

fn archetype_component_access(&self) -> &Access<ArchetypeComponentId> {
&self.state.meta().archetype_component_access
}

fn is_send(&self) -> bool {
self.state.meta().is_send()
}

unsafe fn run_unsafe(&mut self, _input: Self::In, world: &World) -> Self::Out {
let param = self.state.get_unchecked_manual(world);
(self.run)(param);
}

fn apply_buffers(&mut self, world: &mut World) {
self.state.apply(world);
}

fn initialize(&mut self, _world: &mut World) {
// already initialized by nature of the SystemState being constructed
}

fn check_change_tick(&mut self, change_tick: u32) {
self.state.meta.check_change_tick(change_tick);
}
}

/// Conversion trait to turn something into a [`System`].
///
/// Use this to get a system from a function. Also note that every system implements this trait as
Expand Down
11 changes: 11 additions & 0 deletions crates/bevy_ecs/src/system/system_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub trait SystemParam: Sized {
type Fetch: for<'w, 's> SystemParamFetch<'w, 's>;
}

pub type SystemParamItem<'w, 's, P> = <<P as SystemParam>::Fetch as SystemParamFetch<'w, 's>>::Item;

/// The state of a [`SystemParam`].
///
/// # Safety
Expand Down Expand Up @@ -1220,3 +1222,12 @@ macro_rules! impl_system_param_tuple {
}

all_tuples!(impl_system_param_tuple, 0, 16, P);

pub mod lifetimeless {
pub type SQuery<Q, F = ()> = super::Query<'static, 'static, Q, F>;
pub type Read<T> = &'static T;
pub type Write<T> = &'static mut T;
pub type SRes<T> = super::Res<'static, T>;
pub type SResMut<T> = super::ResMut<'static, T>;
pub type SCommands = crate::system::Commands<'static, 'static>;
}
50 changes: 50 additions & 0 deletions examples/3d/many_cubes_pipelined.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
ecs::prelude::*,
pbr2::{PbrBundle, StandardMaterial},
prelude::{App, Assets, Transform},
render2::{
camera::PerspectiveCameraBundle,
color::Color,
mesh::{shape, Mesh},
},
PipelinedDefaultPlugins,
};

fn main() {
App::new()
.add_plugins(PipelinedDefaultPlugins)
.add_plugin(FrameTimeDiagnosticsPlugin::default())
.add_plugin(LogDiagnosticsPlugin::default())
.add_startup_system(setup.system())
.run();
}

fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
const WIDTH: usize = 100;
const HEIGHT: usize = 100;
for x in 0..WIDTH {
for y in 0..HEIGHT {
// cube
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(StandardMaterial {
base_color: Color::PINK,
..Default::default()
}),
transform: Transform::from_xyz((x as f32) * 2.0, (y as f32) * 2.0, 0.0),
..Default::default()
});
}
}

// camera
commands.spawn_bundle(PerspectiveCameraBundle {
transform: Transform::from_xyz(80.0, 80.0, 300.0),
..Default::default()
});
}
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Example | File | Description
`cornell_box_pipelined` | [`3d/cornell_box_pipelined.rs`](./3d/cornell_box_pipelined.rs) | Re-production of the cornell box
`load_gltf` | [`3d/load_gltf.rs`](./3d/load_gltf.rs) | Loads and renders a gltf file as a scene
`load_gltf_pipelined` | [`3d/load_gltf_pipelined.rs`](./3d/load_gltf_pipelined.rs) | Loads and renders a gltf file as a scene
`many_cubes_pipelined` | [`3d/many_cubes_pipelined.rs`](./3d/many_cubes_pipelined.rs) | Simple benchmark to test per-entity draw overhead
`msaa` | [`3d/msaa.rs`](./3d/msaa.rs) | Configures MSAA (Multi-Sample Anti-Aliasing) for smoother edges
`orthographic` | [`3d/orthographic.rs`](./3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look games or CAD applications)
`orthographic_pipelined` | [`3d/orthographic_pipelined.rs`](./3d/orthographic_pipelined.rs) | Shows how to create a 3D orthographic view (for isometric-look games or CAD applications)
Expand Down Expand Up @@ -222,6 +223,7 @@ Example | File | Description
--- | --- | ---
`animate_shader` | [`shader/animate_shader.rs`](./shader/animate_shader.rs) | Shows how to animate a shader by accessing a time uniform variable
`array_texture` | [`shader/array_texture.rs`](./shader/array_texture.rs) | Illustrates how to create a texture for use with a texture2DArray shader uniform variable
`custom_shader_pipelined` | [`shader/custom_shader_pipelined.rs`](./shader/custom_shader_pipelined.rs) | Illustrates how to create custom shaders
`hot_shader_reloading` | [`shader/hot_shader_reloading.rs`](./shader/hot_shader_reloading.rs) | Illustrates how to load shaders such that they can be edited while the example is still running
`mesh_custom_attribute` | [`shader/mesh_custom_attribute.rs`](./shader/mesh_custom_attribute.rs) | Illustrates how to add a custom attribute to a mesh and use it in a custom shader
`shader_custom_material` | [`shader/shader_custom_material.rs`](./shader/shader_custom_material.rs) | Illustrates creating a custom material and a shader that uses it
Expand Down
Loading

0 comments on commit 08969a2

Please sign in to comment.