-
Notifications
You must be signed in to change notification settings - Fork 48
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
Store new type components as SIMD vectors #136
Comments
I've kinda been thinking about this a bit more, and since the component can just be stored as normal in the data array, there could potentially be a #[derive(Component)]
#[simd(u32x8)]
struct Health(pub u32);
fn system(healths: ViewMut<Health>) {
for health in healths {
// Health is just a normal component.
}
for health in healths.simd() {
// Health is a u8x4
}
} If the user does not call the However, there is another issue I didn't mention. Currently, iterators return a reference to the object, but if we were to pack it into a SIMD vector, it would have to be the actual object, not just a reference. |
Hi, I'm no expert in SIMD but I was also tempted to use it in the past. So shipyard can already work with SIMD to some extent. Before diving into the code, note that there is another way to store So as you noticed, trying to use SIMD + ECS is not that easy. What shipyard can do: single storageWhen you iterate a single storage it's guaranteed that the components are next to each other. What shipyard cannot do (anymore/yet): multiple storagesAs you know with multiple To me the "best" solution here is what I call "packing". If you read Skypjack's blog it's called "grouping". There was also "tight"/"loose"/"free" packing ("free" was never implemented). "tight" and "loose" were removed in version 0.5 (I think). But the issue was not the feature but the implementation. Later Skypjack talked about a better way, nested grouping. You could tightly pack as many storages as you want as long as the later pack contains the types of the pack before. Example: [A, B] and [A, B, C] and [A, B, C, D, E]. So what's the future for shipyard? Implementing nested groups? This new packing implementation should have very low cost on non packed storages, no limit on what can be packed and better add/remove scaling. So back to SIMD, if you have this packing in place then you can do the same as with a single storage. You'll be able to create a chunk iterator and use that. With all that said the tiny example and probably the extension trait should be part of a recipe (one day ^^). I hope this was at least a bit helpful. |
Thanks, that was very helpful. I somehow completely forgot that if you iterate over multiple |
Edit: I've made a few edits to this issue that I will append to the end. They negate some of what I say, so it might be better to read them first.
Hi, I'd like to preface this issue by saying that this suggestion is pretty out there and probably won't be implemented. I'm writing this more just to put the idea out there. I'm not particularly familiar with SIMD, but I've been reading about it, and it kinda seems like "free" speed.
The idea is to give the option to store new types (i.e. structs with one field, e.g.
Health(u32)
) asSimd
objects in theSparseMap
. ForSparseMaps
with this option enabled, thedata
array would have the type signature:Vec<Simd<[T; N]>>
whereT
is the type of the inner field of the new type (e.g.u32
) andN
is configurable. Alternatively, the data could be stored as aVec<T>
and then, when needed, we could transmute it into aVec<Simd<[T; N]>>
(I think). With this approach, there should be some new type aroundVec
that ensures that its length is always a multiple ofN
. Although this requires unsafe transmutes, I think it is the better option.The option could be made an attribute of the
Component
derive macro. For example,Adding the attribute would lead to some additional trait being derived. The additional trait would look something like this:
Health
would be stored as aVec<u32>
(orVec<Simd<[u32; 8]>>
) and iterating overView<Health>
would yieldSimd<[u32; 8]>
. The derivedNewType
trait would look like:Good Stuff
Fast.
Bad Stuff
This would obviously be a complete nightmare to implement.
Iterating over SIMD and non-SIMD components.
I think it would be best not to allow (if that is even possible. You would need some way to differentiate between SIMD and non-SIMD components by the View). There could be a method that you can call on a
View
that converts all the SIMDs into scalars. For example:From<Self::Inner>
would need to be implemented for the component, e.g.From<u32> for Health(u32)
to do this.The
SparseMap
stores a different type to the component it represents. This might not be that big an issue becauseAllStorages.storages
is a hashmap where the key is the component type, so I don't think theSparseMap
's type is that important.Data alignment. If there are nine components that we are storing in a
Simd<[u32; 8]>
, then we would need two of them and just pad the second one with 7 bits. This could get quite confusing, especially when iterating over components, as the user wouldn't know if there is actually a zero in that location or if it's padding (Although I don't think the user should really care, as the changes to the padding would just be discarded). Also, we might not be able to just pad it with 0s, as what if the user wants to divide? That would cause a panic.Methods on the new types. I guess the burden could be on the user to manually convert it to the new type, call the method and turn it back into a
Simd
.Removing components. If the components are stored as
Vec<Simd<[T; N]>>
, this would probably be very expensive, but if they are stored as aVec<T>
, it should be fairly simple.If this ever gets implemented, it could potentially be used to store new types with more than one field. The
SparseMap
would contain multipledata
arrays. For example, the component:could be stored as:
The longer I write, the more I realise this is a terrible idea, so imma stop. There are probably a ton of things that I've missed that would further complicate the implementation.
I'd be happy to help in any way I can; however, this seems like the kind of thing which requires extensive knowledge of Rust generics, traits, and macros. I'm reasonably comfortable with these concepts, but I've only been using Rust for about seven months, so take that as you will.
Something to note is this won't affect the performance or ergonomics of the library if the user doesn't opt-in.
Edit:
According to this post (re 5) it could be possible to just store the new types themselves as long as they are
#repr[(transparent)]
and properly aligned.Edit 2:
The following code works:
but from the slice docs:
Potentially after aligning we could take the first and last slices and feed them into a slower function that manually creates the
u8x4
(or whatever vector is being used). Order doesn't matter, (unlessEntityIds
are involved?).The text was updated successfully, but these errors were encountered: