-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
base: main
Are you sure you want to change the base?
Pipeline keys #10333
Conversation
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. |
There was a problem hiding this 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 thepack/unpack
stuff. Maybe it is missing some features, but it's better to contribute those feature tobitbybit
that maintain this massive proc macro. Or at worse, maintain a fork ofbitbybit
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
I'm still going through the code, but I've a bunch of question. I'll have more questions later. API UsageHow would this API be used to build a dynamic keys? Many keys per entityI 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?
|
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
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).
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. |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
crates/bevy_render/src/lib.rs
Outdated
@@ -89,6 +92,8 @@ pub enum RenderSet { | |||
ManageViews, | |||
/// The copy of [`apply_deferred`] that runs immediately after [`ManageViews`](RenderSet::ManageViews). | |||
ManageViewsFlush, | |||
/// blah |
There was a problem hiding this comment.
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 => (), |
There was a problem hiding this comment.
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!(), | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 => (), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
crates/bevy_render/src/view/mod.rs
Outdated
match self.0 { | ||
false => vec!["TONEMAP_IN_SHADER".into()], | ||
true => vec!["HDR".into()], | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
i chose not to as there are a few bits that are very pipeline-key specific - the 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. |
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
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:The traits define methods to
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
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
PipelineKey
, and specialize functions now take aPipelineKey<Self::Key>
param, which derefs to the raw key type.