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

Add slice access #634

Merged
merged 3 commits into from
Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: rust

rust:
- nightly
- 1.34.0
- 1.36.0
- stable

cache:
Expand All @@ -22,7 +22,7 @@ script:
cargo build --all-features --verbose;
cargo test --all-features --verbose --no-run;
cargo bench --verbose --no-run --all-features;
elif [ "$TRAVIS_RUST_VERSION" == "1.34.0" ]; then
elif [ "$TRAVIS_RUST_VERSION" == "1.36.0" ]; then
cargo check --tests --no-default-features;
cargo check --tests --no-default-features --features "parallel";
cargo check --tests --no-default-features --features "serde";
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Unlike most other ECS libraries out there, it provides
other and you can use barriers to force several stages in system execution
* high performance for real-world applications

Minimum Rust version: 1.34
Minimum Rust version: 1.36

## [Link to the book][book]

Expand Down
56 changes: 49 additions & 7 deletions docs/tutorials/src/05_storages.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,35 @@ has 4 layers).
Here a list of the storages with a short description and a link
to the corresponding heading.

|Storage Type |Description |Optimized for |
|:-------------------:|--------------------------|------------------------------|
| [`BTreeStorage`] | Works with a `BTreeMap` | no particular case |
| [`DenseVecStorage`] | Uses a redirection table | fairly often used components |
| [`HashMapStorage`] | Uses a `HashMap` | rare components |
| [`NullStorage`] | Can flag entities | doesn't depend on rarity |
| [`VecStorage`] | Uses a sparse `Vec` | commonly used components |
|Storage Type |Description |Optimized for |
|:----------------------:|----------------------------------------------------|------------------------------|
| [`BTreeStorage`] | Works with a `BTreeMap` | no particular case |
| [`DenseVecStorage`] | Uses a redirection table | fairly often used components |
| [`HashMapStorage`] | Uses a `HashMap` | rare components |
| [`NullStorage`] | Can flag entities | doesn't depend on rarity |
| [`VecStorage`] | Uses a sparse `Vec`, empty slots are uninitialized | commonly used components |
| [`DefaultVecStorage`] | Uses a sparse `Vec`, empty slots contain `Default` | commonly used components |

[`BTreeStorage`]: #btreestorage
[`DenseVecStorage`]: #densevecstorage
[`HashMapStorage`]: #hashmapstorage
[`NullStorage`]: #nullstorage
[`VecStorage`]: #vecstorage
[`DefaultVecStorage`]: #defaultvecstorage

## Slices

Certain storages provide access to component slices:

|Storage Type | Slice type | Density | Indices |
|:----------------------:|---------------------|---------|---------------|
| [`DenseVecStorage`] | `&[T]` | Dense | Arbitrary |
| [`VecStorage`] | `&[MaybeUninit<T>]` | Sparse | Entity `id()` |
| [`DefaultVecStorage`] | `&[T]` | Sparse | Entity `id()` |

This is intended as an advanced technique. Component slices provide
maximally efficient reads and writes, but they are incompatible with
many of the usual abstractions which makes them more difficult to use.

## `BTreeStorage`

Expand All @@ -57,6 +73,11 @@ one which provides a mapping from the entity id to the index for the data vec
(it's a redirection table). This is useful when your component is bigger
than a `usize` because it consumes less RAM.

`DefaultVecStorage<T>` provides `as_slice()` and `as_mut_slice()` accessors
which return `&[T]`. The indices in this slice do not correspond to entity
IDs, nor do they correspond to indices in any other storage, nor do they
correspond to indices in this storage at a different point in time.

## `HashMapStorage`

This should be used for components which are associated with very few entities,
Expand All @@ -81,3 +102,24 @@ just leaves uninitialized gaps where we don't have any component.
Therefore it would be a waste of memory to use this storage for
rare components, but it's best suited for commonly used components
(like transform values).

`VecStorage<T>` provides `as_slice()` and `as_mut_slice()` accessors which
return `&[MaybeUninit<T>]`. Consult the `Storage::mask()` to determine
which indices are populated. Slice indices cannot be converted to `Entity`
values because they lack a generation counter, but they do correspond to
`Entity::id()`s, so indices can be used to collate between multiple
`VecStorage`s.

## `DefaultVecStorage`

This storage works exactly like `VecStorage`, but instead of leaving gaps
uninitialized, it fills them with the component's default value. This
requires the component to `impl Default`, and it results in more memory
writes than `VecStorage`.

`DefaultVecStorage` provides `as_slice()` and `as_mut_slice()` accessors
which return `&[T]`. `Storage::mask()` can be used to determine which
indices are in active use, but all indices are fully initialized, so the
`mask()` is not necessary for safety. `DefaultVecStorage` indices all
correspond with each other, with `VecStorage` indices, and with
`Entity::id()`s.
107 changes: 107 additions & 0 deletions examples/slices.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
extern crate specs;

use specs::prelude::*;

// A component contains data which is associated with an entity.

#[derive(Debug)]
struct Vel(f32);

impl Component for Vel {
type Storage = DefaultVecStorage<Self>;
}

impl Default for Vel {
fn default() -> Self {
Self(0.0)
}
}

#[derive(Debug)]
struct Pos(f32);

impl Component for Pos {
type Storage = DefaultVecStorage<Self>;
}

impl Default for Pos {
fn default() -> Self {
Self(0.0)
}
}

struct SysA;

impl<'a> System<'a> for SysA {
// These are the resources required for execution.
// You can also define a struct and `#[derive(SystemData)]`,
// see the `full` example.
type SystemData = (WriteStorage<'a, Pos>, ReadStorage<'a, Vel>);

fn run(&mut self, (mut pos, vel): Self::SystemData) {
// Both the `Pos` and `Vel` components use `DefaultVecStorage`, which supports
// `as_slice()` and `as_mut_slice()`. This lets us access components without
// indirection.
let mut pos_slice = pos.as_mut_slice();
let vel_slice = vel.as_slice();

// Note that an entity which has position but not velocity will still have
// an entry in both slices. `DefaultVecStorage` is sparse, and here is where
// that matters: the storage has space for many entities, but not all of them
// contain meaningful values. These slices may be a mix of present and absent
// (`Default`) data.
//
// We could check the `mask()` before reading the velocity and updating the
// position, and this is what `.join()` normally does. However, because:
//
// 1. `Vel` uses `DefaultVecStorage`,
// 2. `Vel`'s default is 0.0, and
// 3. `Pos` += 0.0 is a no-op,
//
// we can unconditionally add `Vel` to `Pos` without reading the `mask()`!
// This results in a tight inner loop of known size, which is especially
// suitable for SIMD, OpenCL, CUDA, and other accelerator technologies.
//
// Finally, note that `DefaultVecStorage` and `VecStorage` slice indices
// always agree. If an entity is at location `i` in one `VecStorage`, it
// will be at location `i` in every other `VecStorage`. (By contrast,
// `DenseVecStorage` uses unpredictable indices and cannot be used in
// this way.) We need only worry about handling slices of different
// lengths.
let len = pos_slice.len().min(vel_slice.len());
for i in 0..len {
pos_slice[i].0 += vel_slice[i].0;
}
}
}

fn main() {
// The `World` is our
// container for components
// and other resources.

let mut world = World::new();

// This builds a dispatcher.
// The third parameter of `add` specifies
// logical dependencies on other systems.
// Since we only have one, we don't depend on anything.
// See the `full` example for dependencies.
let mut dispatcher = DispatcherBuilder::new().with(SysA, "sys_a", &[]).build();

// setup() must be called before creating any entity, it will register
// all Components and Resources that Systems depend on
dispatcher.setup(&mut world);

// An entity may or may not contain some component.

world.create_entity().with(Vel(2.0)).with(Pos(0.0)).build();
world.create_entity().with(Vel(4.0)).with(Pos(1.6)).build();
world.create_entity().with(Vel(1.5)).with(Pos(5.4)).build();

// This entity does not have `Vel`, so it won't be dispatched.
world.create_entity().with(Pos(2.0)).build();

// This dispatches all the systems in parallel (but blocking).
dispatcher.dispatch(&world);
}
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ pub use crate::{
changeset::ChangeSet,
join::Join,
storage::{
DenseVecStorage, FlaggedStorage, HashMapStorage, NullStorage, ReadStorage, Storage,
Tracked, VecStorage, WriteStorage,
DenseVecStorage, DefaultVecStorage, FlaggedStorage, HashMapStorage, NullStorage,
ReadStorage, Storage, Tracked, VecStorage, WriteStorage,
},
world::{Builder, Component, Entities, Entity, EntityBuilder, LazyUpdate, WorldExt},
};
4 changes: 2 additions & 2 deletions src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ pub use shred::AsyncDispatcher;
pub use crate::{
changeset::ChangeSet,
storage::{
ComponentEvent, DenseVecStorage, FlaggedStorage, HashMapStorage, NullStorage, ReadStorage,
Storage, Tracked, VecStorage, WriteStorage,
ComponentEvent, DenseVecStorage, DefaultVecStorage, FlaggedStorage, HashMapStorage,
NullStorage, ReadStorage, Storage, Tracked, VecStorage, WriteStorage,
},
world::{Builder, Component, Entities, Entity, EntityBuilder, LazyUpdate, WorldExt},
};
34 changes: 33 additions & 1 deletion src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ pub use self::{
ImmutableParallelRestriction, MutableParallelRestriction, RestrictedStorage,
SequentialRestriction,
},
storages::{BTreeStorage, DenseVecStorage, HashMapStorage, NullStorage, VecStorage},
storages::{BTreeStorage, DenseVecStorage, DefaultVecStorage, HashMapStorage, NullStorage, VecStorage},
track::{ComponentEvent, Tracked},
};

use self::storages::SliceAccess;

use std::{
self,
marker::PhantomData,
Expand Down Expand Up @@ -259,6 +261,36 @@ where
}
}

impl<'e, T, D> Storage<'e, T, D>
where
T: Component,
D: Deref<Target = MaskedStorage<T>>,
T::Storage: SliceAccess<T>
{
/// Returns the component data as a slice.
///
/// The indices of this slice may not correspond to anything in particular.
/// Check the underlying storage documentation for details.
pub fn as_slice(&self) -> &[<T::Storage as SliceAccess<T>>::Element] {
self.data.inner.as_slice()
}
}

impl<'e, T, D> Storage<'e, T, D>
where
T: Component,
D: DerefMut<Target = MaskedStorage<T>>,
T::Storage: SliceAccess<T>
{
/// Returns the component data as a slice.
///
/// The indices of this slice may not correspond to anything in particular.
/// Check the underlying storage documentation for details.
pub fn as_mut_slice(&mut self) -> &mut [<T::Storage as SliceAccess<T>>::Element] {
self.data.inner.as_mut_slice()
}
}

impl<'e, T, D> Storage<'e, T, D>
where
T: Component,
Expand Down
Loading