Vector caching in Ferox with Entreri
The ferox-scene API uses the Entreri component framework to define a number of components that can be combined to describe entities within a scene. Many properties of these components are vector or matrix types, such as direction, position, transform, color, etc. I needed a solution that had the following features:
- Packed the data internally into a primitive array
- Exposed the data as a Vector3 or Matrix3
- Did not require the allocation of a result object to access the data
To solve problem #1, I implemented custom Properties that wrap a DoubleProperty but provide getters and setters using the appropriate math types. At the Property level, they getters require a result type but I was on the right path to solving #2 and #3.
The math Properties were implemented very similarly and fit this general pattern:
@Factory(FooProperty.Factory.class) public class FooProperty implements Property { private final DoubleProperty realData; public Foo get(int index, Foo result) { } public void set(int index, Foo value) { } @Attribute public static @interface DefaultFoo { // attributes for default Foo's } // Factory implementation to create FooProperties public static class Factory implements PropertyFactory<FooProperty> { } }
Here is an example of how a Vector3 could be stored as a property:
@Factory(Vector3Property.Factory.class); public class Vector3Property { // 3 elements for x, y, z private final DoubleProperty values = new DoubleProperty(3); public void get(int index, Vector3 v) { // sets the state of v to match the indexed data v.set(values.getIndexedData(), index * 3); } public void set(int index, Vector3 v) { // gets the state of v into the indexed data v.get(values.getIndexedData(), index * 3); } @Attribute public static @interface DefaultVector3 { double x(); double y(); double z(); } public static class Factory implements PropertyFactory<Vector3Property> { // implementation checks for DefaultVector3 attribute for // initial value assignment } }
This provides a convenient wrapper around a DoubleProperty and provides us with an annotation to set the default value of each vector to separate x, y, and z values. However, it does not help with reduced instantiation because it requires an input Vector3 to get or set values in the property.
By using the protected onSet(int) method in ComponentData implementations, we can add an unmanaged Vector3 field to a component type that is cached, and is kept in-sync with the property with the set listener method.
public class MyComponent extends ComponentData<MyComponent> { @DefaultVector3(x=1.0, y=0.0, z=1.0); private Vector3Property vector; @Unmanaged private final Vector3 cache = new Vector3(); // Use @Const to tell programmers not to mutate data. // Alternatively expose a read-only interface. public @Const Vector3 getVector3() { return cache; } public void setVector3(Vector3 v) { cache.set(v); vector.set(getIndex(), v); } @Override protected void onSet(int index) { vector.get(index, v); } }
With all of this together, you can create many Component<MyComponent> and the vectors will be automatically packed into a hidden double array. This provides good cache locality. Using the wrapper Vector3Property and the onSet() event listener in MyComponent, the final exposed interface is a convenient Vector3 instance. Even with all of this, only a single instance of Vector3 is required for each instance of MyComponent.
Because ComponentData instances are used as fly-weight objects over the identifying Components, this makes iterating over all MyComponent’s require minimal object creation overhead.