Skip to content
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

Pipeline keys #10333

Open
wants to merge 56 commits into
base: main
Choose a base branch
from
Open

Pipeline keys #10333

wants to merge 56 commits into from

Conversation

robtfm
Copy link
Contributor

@robtfm robtfm commented Nov 1, 2023

Objective

address the increasing complexity and monolithicness of bitfield-based pipeline keys.

part of a general drive towards pluggability/modularity in the rendering engine, which also requires

  • extensible view group
  • a shader plugin mechanism

both of which i've laid some groundwork for but are not fully implemented yet.

Solution

introduced a PipelineKey macro and related traits. PipelineKey types are either:

  • unit enums
  • tuples or structs containing primitives or other PipelineKeys
  • dynamic keys, for which components can be registered
  • custom implementations on foreign types (wgpu::TextureFormat, etc)

The traits define methods to

  • pack into a KeyPrimitive type (currently a u128, may want to use a u64)
  • unpack from the KeyPrimitive type back to the source type
  • merge parts into composites
  • extract parts from composites

SystemKeys can be registered which set a key value for an entity based on query data. This allows the key value for an entity to be determined by the key itself.

ShaderDefs can be defined for pipeline keys based on the key value.

implemented usage for MeshPipeline and MaterialPipeline (including the prepass pipeline).

the contents of the current MeshPipelineKey bitfield has been broken out into

  • view-specific parts (msaa, hdr, prepass settings, etc) which are built by SystemKeys and attached to the view
  • mesh-specific parts which are calculated in prepare_assets::<Mesh>
  • material-specific parts which are calculated in prepare_materials

the MaterialPipeline now uses a NewMeshPipelineKey struct containing all of the above parts. the shader defs for main pass materials and prepasses are taken from the keys.

todo

[x] document everything properly
[x] collapse some keys onto the types they are based on (no need for a TonemappingKey when Tonemapping itself is essentially the same thing)
[x] at least in debug, validate bitsize vs KeyPrimitive size (added without cfg for now)

todo as follow ups

[x] other pipelines (sprites, ui, deferred etc) still use bitfields, these should be migrated
[x] legacy code to do with the old MeshPipelineKeys bitfield can then be removed
[ ] vertex attributes and defs should also be part of the mesh key, rather than handled separately
[ ] implement KeyTypeConcrete for arbitrary-sized ints (need to choose or build an implementation)
[ ] use a dynamic view key so we can make parts of the architecture optional

performance:

i haven't tested much. i saw a 2% perf regression running many foxes, but i always struggle to get consistent benchmarks. but once all the work is done i would expect performance benefits from making render-feature plugins optional, and eliminating time spent on them when not used.

Migration Guide

  • AsBindGroup::Data types need to derive PipelineKey, and specialize functions now take a PipelineKey<Self::Key> param, which derefs to the raw key type.

@superdump
Copy link
Contributor

Just as a note, I want to move toward a more ‘as needed’ pipeline specialisation if it turns out to perform well enough, so that we can cache a pipeline id as a component on an entity and not update it ever unless a pipeline dependency changes. Perhaps caching the keys as components is a way of managing that.

@alice-i-cecile alice-i-cecile added A-Rendering Drawing game state to the screen C-Usability A targeted quality-of-life change that makes Bevy easier to use X-Controversial There is active debate or serious implications around merging this PR M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide labels Nov 1, 2023
Copy link
Contributor

@nicopap nicopap left a comment

Choose a reason for hiding this comment

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

Guess I'll have to speed up work on my impl, because I think we can do much better.

The current issues I have with this:

  • We are basically reimplementing a bit packing macro, bitbybit should be able to handle the pack/unpack stuff. Maybe it is missing some features, but it's better to contribute those feature to bitbybit that maintain this massive proc macro. Or at worse, maintain a fork of bitbybit with those features we need
  • The naming on PipelineMetaStore is confusing. I don't understand it.
  • This limits every pipeline key to have 128 bits (is that right?)
  • I think https://gist.github.com/nicopap/6c9b351f0df8555f66e30d2ceeda9209 has a better API

use bevy_utils::all_tuples;

#[allow(unused_macros)]
macro_rules! impl_composite_key_tuples {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you might have disregarded bitbybit a bit too quickly, as this looks very similar to the code it generates.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The big difference is that this supports dynamic keys, where components are registered at runtime.

/// can be independently registered to allow extensibility.

/// The primitive type underlying packed pipeline keys.
pub type KeyPrimitive = u128;
Copy link
Contributor

Choose a reason for hiding this comment

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

So each key will have 128 bits? I recall there were concerns regarding hashing perf.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It could be 64 but with some compromises on current data - specifically the StandardMaterial data uses an i32 for depth bias which could comfortably be condensed. The view and mesh keys are around 30 bits all in

Copy link
Contributor

Choose a reason for hiding this comment

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

Correct me if I'm wrong, but doesn't ahash use AES instructions, which operate on XMM registers, which are 128 bits? So 128 bits should be as fast as 64 bits, in theory.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i don't know all the details, but i'm sure there'll be some hardware where it's as fast and some where it's not.

@nicopap
Copy link
Contributor

nicopap commented Nov 11, 2023

I'm still going through the code, but I've a bunch of question. I'll have more questions later.

API Usage

How would this API be used to build a dynamic keys?

Many keys per entity

I see:

type KeyPrimitive = u128;

#[derive(Component, Default)]
pub struct PipelineKeys {
    packed_keys: HashMap<TypeId, (KeyPrimitive, u8)>,
}

What's going on here? Would what does having multiple keys per entity means? Wouldn't it be more judicious to use a generic component?

SystemKey

I don't understand what is SystemKey. It has nothing to do with systems right? It has more to do with specific plugins.
The PR description doesn't help:

SystemKeys can be registered which set a key value for an entity based on query data. This allows the key value for an entity to be determined by the key itself.

I think you mean "[…] for an entity to be determined by data outside of the key".

@robtfm
Copy link
Contributor Author

robtfm commented Nov 11, 2023

How would this API be used to build a dynamic keys?

with a combination of:

// anywhere
#[derive(PipelineKey)]
[dynamic_key]
pub struct MyDynamicKey(KeyPrimitive);

// in plugin build
render_app
    .register_dynamic_key<MyDynamicKey, F>()
    // here or any other plugin
    .register_dynamic_key_part<MyDynamicKey, SomeOtherKey>();

this would make the dynamic key (which can be used by pipelines etc) equivalent to a struct which contains a member: SomeOtherKey member. the dynamic components can be extracted later if required, and will contribute shader defs (and later bindgroup layouts/data).
the idea here is to make things like EnvironmentMap or shadows entirely standalone and optional, with no knowledge of it required in pbr, render or anywhere else, except for registering the plugin (if we include it by default).

Many keys per entity

I see:

type KeyPrimitive = u128;

#[derive(Component, Default)]
pub struct PipelineKeys {
    packed_keys: HashMap<TypeId, (KeyPrimitive, u8)>,
}

What's going on here? Would what does having multiple keys per entity means? Wouldn't it be more judicious to use a generic component?

the idea of using a per-entity hashmap is to avoid table moves by adding many components. the multiple keys are all the individual key parts (MsaaKey, Tonemapping, etc etc), some of which are used in multiple pipeline keys (Msaa in particular is used in virtually every pipeline).

SystemKey

I don't understand what is SystemKey. It has nothing to do with systems right? It has more to do with specific plugins. The PR description doesn't help:

SystemKeys can be registered which set a key value for an entity based on query data. This allows the key value for an entity to be determined by the key itself.

I think you mean "[…] for an entity to be determined by data outside of the key".

maybe? i mean that the key gets constructed from world/query data using the function (which is almost a system) defined on the key itself, with this trait.
as a side note - i'd love to improve the api so that we could use something closer to raw systems, but i am not sure how, while still preserving the external entity iterator for the Query data. this is the same pattern we used in the GetBatchData trait.

@JMS55 JMS55 added this to the 0.13 milestone Nov 23, 2023
@pcwalton pcwalton self-requested a review November 25, 2023 19:50
Copy link
Contributor

@pcwalton pcwalton left a comment

Choose a reason for hiding this comment

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

This isn't a complete review, but I gave it a quick look over. My main comment is that I wonder whether the pipeline key stuff could be factored out into a separate bevy_bitfields crate or something. bevy_render is already quite large, so this would reduce compile time, and you've created a very generic bitfield crate here that might be useful in other contexts.

/// can be independently registered to allow extensibility.

/// The primitive type underlying packed pipeline keys.
pub type KeyPrimitive = u128;
Copy link
Contributor

Choose a reason for hiding this comment

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

Correct me if I'm wrong, but doesn't ahash use AES instructions, which operate on XMM registers, which are 128 bits? So 128 bits should be as fast as 64 bits, in theory.

let positions = T::positions(store);
let so = positions.get(&TypeId::of::<K>()).unwrap();
self.packed &= !(((1 << so.size) - 1) << so.offset);
self.packed |= value.packed << so.offset;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could maybe add a debug_assert! to make sure the packed value didn't overflow the size

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the only way to construct a PackedPipelineKey is via new(), which does this check (i should probably change that to a debug assert though).

and any type inserted here needs to be part of the composite type already (else the positions lookup will fail) - you can't add new arbitrary types.

#[derive(PipelineKey, Clone, Copy)]
#[repr(u8)]
#[custom_shader_defs]
pub enum ScreenSpaceTransmissionQualityKey {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we just merge this into ScreenSpaceTransmissionQuality?

pub struct MeshPipelineKey {
pub view_key: PbrViewKey,
pub mesh_key: MeshKey,
pub alpha_mode: AlphaKey,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd add comments here documenting what each of these is for.

@@ -89,6 +92,8 @@ pub enum RenderSet {
ManageViews,
/// The copy of [`apply_deferred`] that runs immediately after [`ManageViews`](RenderSet::ManageViews).
ManageViewsFlush,
/// blah
Copy link
Contributor

Choose a reason for hiding this comment

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

Fill in this comment :)

| wgpu::PrimitiveTopology::LineList
| wgpu::PrimitiveTopology::LineStrip
| wgpu::PrimitiveTopology::TriangleList
| wgpu::PrimitiveTopology::TriangleStrip => (),
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above.

3 => wgpu::PrimitiveTopology::TriangleList,
4 => wgpu::PrimitiveTopology::TriangleStrip,
_ => unreachable!(),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be worth macro_rules!ing this. Up to you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

several of these will be much easier once https://doc.rust-lang.org/std/mem/fn.variant_count.html stabilises.

i'm not clear how i could write a macro currently, what sort of thing did you have in mind? something that takes all the variants as args and generates the pack and unpack?

// 2 variants => 1 bits
match value {
wgpu::Face::Front | wgpu::Face::Back => (),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above.

match self.0 {
false => vec!["TONEMAP_IN_SHADER".into()],
true => vec!["HDR".into()],
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could just be if instead of match

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how do you feel about self.0.then_some("TONEMAP_IN_SHADER".into()).into_iter().collect() ?

i did that on GizmoConfigKey for example. it should definitely be consistent whichever one we use though.

#[derive(PipelineKey)]
pub struct Mesh2dPipelineKey {
pub view_key: Mesh2dViewKey,
pub mesh_key: MeshKey,
Copy link
Contributor

Choose a reason for hiding this comment

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

As above, comment these fields.

@robtfm
Copy link
Contributor Author

robtfm commented Nov 27, 2023

My main comment is that I wonder whether the pipeline key stuff could be factored out into a separate bevy_bitfields crate or something. bevy_render is already quite large, so this would reduce compile time, and you've created a very generic bitfield crate here that might be useful in other contexts.

i chose not to as there are a few bits that are very pipeline-key specific - the KeyMeta member for ShaderDefFn, and the derive macro which implements all the base key traits as well as ShaderDefs (if not specified explicitly). i also plan to add more stuff like these in future for bind group management.

i can probably extract the KeyMeta specifics with a generic param. i'm not sure how to handle the macro, i don't think it's possible to call another derive macro from within a derive macro, and i don't really want every instance to have to derive two separate things, but maybe that's preferable.

@alice-i-cecile alice-i-cecile modified the milestones: 0.13, 0.14 Jan 24, 2024
@JMS55 JMS55 removed this from the 0.14 milestone May 14, 2024
@alice-i-cecile alice-i-cecile added X-Contentious There are nontrivial implications that should be thought through and removed X-Controversial There is active debate or serious implications around merging this PR labels Aug 16, 2024
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-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide X-Contentious There are nontrivial implications that should be thought through
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

6 participants