MappedObject library

This library supports creating arrays-of-structs in Java

Main features:
[x] support for MappedObject (structs) and MappedObjectArray (array-of structs)
[x] optional bounds checking for MappedObjectArray
[x] excellent performance (even for query methods like .sizeof(), .stride(), .elements(), etc)
[x] grouped/nested structs: interleaving mapped object arrays using strides and offsets
[x] verifies configurations ahead of time: exceptions, no native crashes
[x] allows both JIT and AOT bytecode transformation (just add JAR to classpath)
[x] no runtime reliance on bytecode transformation, allowing future Dalvik/Android support

General notice:
[x] don’t expect huge performance gains per se: memory access is about as fast as field access, albeit with less cache-misses
[x] the biggest speedup will be caused by no longer having to copy data back and forth between instances and buffers

I might elaborate later on this subject, but for now I expect the code to speak for itself… :slight_smile:

Example of mapped Vec3:


public class Vec3 {
   public float x;
   public float y;
   public float z;
}


ByteBuffer bb = ...;
Vec3 mapped = new Vec3(bb);

mapped.x = 1f;
mapped.y = 0f;
mapped.z = 2f;
mapped.index(2); // slides mapped object to 3rd Vec3
mapped.x = 7f;
mapped.y = 4f;
mapped.z = 5f;

for(int i=0; i<mapped.elements(); i++) {
  mapped.index(i);
  // ...
}


[x][y][z][x][y][z][x][y][z][x][y][z] -->
 ^        ^        ^        ^
 1f,0f,2f          7f,4f,5f


mapped.stride(mapped.sizeof() + 4);


[x][y][z][?][x][y][z][?][x][y][z][?][x][y][z][?] -->
 ^           ^           ^           ^

Example of nested mapped object {Vec3,Vec3,Vec2}:


public static class VNC {
	public Vec3 vertex;
	public Vec3 normal;
	public Vec2 texcoord;
}


VNC vnc = new VNC(bb); // sets the stride of 'vertex', 'normal', 'texcoord'

vnc.normal.z = 0.13f;
vnc.index(13); // sets the index of 'vertex', 'normal', 'texcoord'
vnc.normal.z = 0.14f;

Demo code that actually works: (shipped with JAR)


import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import net.indiespot.mapped.MappedObject;
import net.indiespot.mapped.MappedObjectArray;
import net.indiespot.mapped.MappedObjectBuilder;
import net.indiespot.mapped.MappedObjectFactory;
import net.indiespot.mapped.MappedSet;
import net.indiespot.mapped.ann.MappedField;
import net.indiespot.mapped.impl.MappedObjectEnv;

public class MappedObjectDemo {
	static {
		MappedObjectEnv.BOUNDS_CHECKS = true;
	}
	

	public static void main(String[] args) throws Exception {
		MappedObjectBuilder.fork(MappedObjectDemo.class, args);

		demoVec3();
		demoSet();

		System.out.println("Done.");
	}

	@MappedType(stride=8)
	public static class Vec2 extends MappedObjectArray {
		public Vec2(ByteBuffer buffer) {
			super(buffer);
		}

		@MappedField(offset = 0)		public float x;
		@MappedField(offset = 4)		public float y;
	}

	public static class Vec3 extends MappedObjectArray {
		public Vec3(ByteBuffer buffer) {
			super(buffer);
		}

		@MappedField(offset = 0)		public float x;
		@MappedField(offset = 4)		public float y;
		@MappedField(offset = 8)		public float z;
	}

	public static class VNC extends MappedSet {
		public VNC(ByteBuffer buffer) {
			super(buffer);
		}

		public Vec3 vertex;
		public Vec3 normal;
		public Vec2 texcoord;
	}


	public static void demoVec3() {
		ByteBuffer bb = ByteBuffer.allocateDirect(64 * 1024);
		bb.order(ByteOrder.nativeOrder());

		Vec3 v3 = MappedObjectFactory.map(Vec3.class, bb);

		// test 1
		v3.x = 13.14f;
		v3.y = 14.15f;
		v3.z = 15.16f;
		if (v3.x != 13.14f || bb.getFloat(0) != 13.14f)			throw new IllegalStateException();
		if (v3.y != 14.15f || bb.getFloat(4) != 14.15f)			throw new IllegalStateException();
		if (v3.z != 15.16f || bb.getFloat(8) != 15.16f)			throw new IllegalStateException();

		// move over data using a sliding window
		v3.index(1337);

		// test 2
		v3.x = -13.14f;
		v3.y = -14.15f;
		v3.z = -15.16f;
		if (v3.x != -13.14f || bb.getFloat(v3.stride() * 1337 + 0) != -13.14f)			throw new IllegalStateException();
		if (v3.y != -14.15f || bb.getFloat(v3.stride() * 1337 + 4) != -14.15f)			throw new IllegalStateException();
		if (v3.z != -15.16f || bb.getFloat(v3.stride() * 1337 + 8) != -15.16f)			throw new IllegalStateException();
	}
	
	public static void demoSet(){
		ByteBuffer bb = ByteBuffer.allocateDirect(64 * 1024);
		bb.order(ByteOrder.nativeOrder());

		VNC vnc = new VNC(bb);

		vnc.index(1337);

		if (vnc.vertex.index() != 1337)			throw new IllegalStateException();
		if (vnc.normal.index() != 1337)			throw new IllegalStateException();
		if (vnc.texcoord.index() != 1337)		 throw new IllegalStateException();

		if(vnc.vertex.stride() != vnc.normal.stride())   throw new IllegalStateException();
		if(vnc.vertex.stride() != vnc.texcoord.stride()) throw new IllegalStateException();
	}
}

Javadoc:
[x] http://indiespot.net/files/projects/mappedobjectlib/mappedobject-0.8.5-doc/index.html

Classpath:
[x] http://indiespot.net/files/projects/mappedobjectlib/mappedobject-0.8.5.jar
[x] http://indiespot.net/files/projects/mappedobjectlib/asm-3.2.jar
[x] http://indiespot.net/files/projects/mappedobjectlib/asm-util-3.2.jar

Main Class:
[x] net.indiespot.mapped.demo.MappedObjectDemo

What is the difference between isAligned and isPageAligned?

You can align a ByteBuffer to any number of bytes, so the ‘page’ version is simply a utility method:


public static boolean isPageAligned(ByteBuffer bb) {
  return isAligned(bb, pageSize());
}

public static ByteBuffer allocatePageAligned(int bytes) {
	return allocateAligned(bytes, pageSize());
}

For those wondering why we need the functionality to allocate page aligned ByteBuffers: ever since Java 1.4 every ByteBuffer you allocated was page-aligned (typically 4K). Since Java7 this is no longer the case, and there doesn’t seem to be any functionality in the Java API to allocate them the old way, except for a commandline parameter, which sets a global flag.

Version 0.8.3:
[x]Added: mappedObject.hasRemaining()

Release:
[x] http://indiespot.net/files/mappedobjectlib/mappedobject-0.8.3-doc/index.html
[x] http://indiespot.net/files/mappedobjectlib/mappedobject-0.8.3.jar

Code:


// old
for(int i=0; i<vec3.elements(); i++){
	vec3.index(i);
	// do stuff
}

// new
for(vec3.reset(); vec3.hasRemaining(); vec3.next()){
	// do stuff
}

This is beyond amazing. Riven, you’re god.

Version 0.8.4:
[x] Renamed: MappedObject to MappedObjectArray
[x] Added: MappedObject class (not positionable, just mapping fields to region in memory)

Release:[x] http://indiespot.net/files/mappedobjectlib/mappedobject-0.8.4-doc/index.html
[x] http://indiespot.net/files/mappedobjectlib/mappedobject-0.8.4.jar

Reason:
[x] Opportunity for encapsulation (OOP)
[x] Create a lightweight API to get familiar with the concept of mapping fields
[x] Closer to the concept of structs (as opposed to struct-pointers)

Most games using this library will use mapped objects as a way to prevent data-copies of VBO data. Game entities by nature are heavily object oriented and therefore are designed to be convenient in use, not necessarily reduced memory footprint. Having 1 game entity holding (mapped) data pointing to 1 area of memory is a likely occurence. The (previous) MappedObject class was way too heavy-weight for this usage, so has been renamed and built on a simplified MappedObject class, that always points to 1 region of memory.


public class Vec3 extends MappedObject
{
	public Vec3(ByteBuffer buffer) {
		super(buffer);
	}

	@MappedField(offset = 0)		public float x;
	@MappedField(offset = 4)		public float y;
	@MappedField(offset = 8)		public float z;
}

At runtime, the entire class would look like:


public class Vec3 extends MappedObject
{
	public LightVec3(ByteBuffer buffer) {
		super(buffer);
	}

	public long address; // inherited from super-class
}

No positioning, no bounds-checks.


// Usage 1:
Vec3 vec3 = new Vec3(buffer);

// Usage 2:
int stride = 12;
int elements = 33;
List<Vec3> instances = MappedObjectFactory.mapInstances(buffer, stride, Vec3.class, elements);

All elements of List are individual Vec3 instances that will point to their own chunk of memory.

Usage in a game entity class:


public class GameEntity
{
   // ...
   public Vec3 position;
   // ...
}

That sounds like a nifty tweak.

Cas :slight_smile:

This is a lot more convenient! Questions:

  1. Why is the class named Vec3 but the constructor LightVec3? >_>
  2. Shouldn’t the stride variable be contained in Vec3, or is it there to allow you to store multiple interleaved structs in the array (I completely approve of the idea). In that case you also need an offset variable to allow positioning of the second MappedObject into the buffer. Whichever the case is, the byte size should still be retrievable from the Vec3 class and a mapInstances() function without stride should be added.
  3. What’s the performance compared to the old MappedObject implementation?
  4. Is it possible to have a struct inside a struct like this?
public class Particle extends MappedObject{
    @MappedField(offset = 0)    private Vec3 position;
    @MappedField(offset = 12)    private Color color; //4 byte RGBA color
    @MappedField(offset = 16)    private short life;
    ...
}
  1. This one is a bit unrelated, but I was wondering if you have any suggestions regarding this problem: In my CPU particle engine I had particles which contained both a bunch of data. Some of it was needed for rendering and belonged in a MappedObject (position, color) and some of it did not (velocity, life). I had 12 bytes of needed data and 12 bytes of data that wasn’t needed on the GPU. All in all I had to create both a Particle class and a MappedParticle class to split up the data, but everything got a lot more complicated, and it kind of seemed like it defeated the purpose of MappedObjects. This update would simplify a it a bit since I can just pass in an instance from the List gotten by mapInstances() to my (unmapped) Particle class.

I’d love to test it out but I have a slight problem…

No way is it going to stay stable for a MEGABYTE download! I assume it isn’t even ready yet in any LWJGL release?

For years people told me it could not be done, and now you have created the proof that supports my argument.

Good Job.

That is nice, I will have to check it out later though. Damn time zone!

My refactoring support of forum-post is kinda flaky ::slight_smile:

Only .sizeof() is tied to a type. The .stride() is tied to an instance and can be altered at runtime. With the new MappedObject (not the MappedObjectArray) it makes no sense to add a stride, as it won’t slide over memory. I could move .sizeof() from MappedObjectArray to MappedObject, and set the stride to sizeof, if the specified stride is zero.

There is no need to provide an offset parameter: the constructor respects the buffer.position(). Even if it wouldn’t you could .buffer.slice() it before passing it in the constructor, but I hope support for buffer.position() is enough. (feedback?)

The bytecode it generates is (supposed to be) a lot friendlier to HotSpot. But really, please enlighten me with some real world particle engine benchmarks. My synthetic benchmarks show it’s just as fast in small methods, and ‘a bunch’ faster in bulky methods, for varying levels of ‘a bunch’.

Not yet, but that was because that would mean all kind of wizardry for MappedObject, as it would have to re-calculate the offsets of all (nested) children, every time the position was changed… I can’t do it like C/C++ where the nested structs are simply ‘burnt in’.

You can see how I solved it in MappedSet: simply generating an set.index(i) method that calls child[n].index(i). You can see the limitations of this approach.

Even better: this implementation supports mapped and unmapped fields in the same type.


public class Particle
{
   @MappedField(offset=0) public Vec3 position;
   @MappedField(offset=12) public Vec3 color;

   public Vec3 velocity;
   public float life;
}

Have you tried turning it off and on again?

200KB, you silly! (of which 100KB can be discarded)

This new impl. will not be in the LWJGL repository for historical reasons. :slight_smile:

Version 0.8.5:
[x] Moved: MappedObjectArray.sizeof() to MappedObject.sizeof()
[x] Modified: MappedObjectMemory.sizeof(Class --> Class)
[x] Modified: MappedObjectFactory.mapInstances(buffer, stride = 0, type, elements) => stride=sizeof(type)

Release:
[x] http://indiespot.net/files/mappedobjectlib/mappedobject-0.8.5-doc/index.html
[x] http://indiespot.net/files/mappedobjectlib/mappedobject-0.8.5.jar


public class Particle
{
   @MappedField(offset=0) public Vec3 position;
   @MappedField(offset=12) public Vec3 color;

   public Vec3 velocity;
   public float life;
}

int elements = 33;
int sizeof = MappedObjectMemory.sizeof(Vec3.class);
int stride = sizeof + sizeof;
ByteBuffer bb = ByteBuffer.allocateDirect(elements * stride);

bb.position(0);
List<Vec3> positions = MappedObjectFactory.mapInstances(bb, stride, Vec3.class, elements);

bb.position(sizeof);
List<Vec3> colors = MappedObjectFactory.mapInstances(bb, stride, Vec3.class, elements);

//MappedObjectFactory.mapInstances((ByteBuffer)bb.position(0 * sizeof), stride, Vec3.class, elements);
//MappedObjectFactory.mapInstances((ByteBuffer)bb.position(1 * sizeof), stride, Vec3.class, elements);



Particle[] particles = new Particle[elements];
for(int i=0; i<elements; i++) {
   particles[i] = new Particle();
   particles[i].position = positions.get(i);
   particles[i].color = colors.get(i);
}

I don’t know if you already have, but could you deploy it to maven central?

Also a little followup question, I tried your lib once, just to check it out and my question is, if because of the needed application “restart” this lib works only on the desktop? Or can it be used as an applet without natives or other security restrictions. What is about android?

As specified in the opening post: allows both JIT and AOT bytecode transformation (just add JAR to classpath) :point:

[quote=“Danny02,post:13,topic:39299”]
no natives involved

[quote=“Danny02,post:13,topic:39299”]
I need runtime access to sun.misc.Unsafe, which means I need a reflection permission.

[quote=“Danny02,post:13,topic:39299”]
Android does have Unsafe, but doesn’t have Unsafe.getFloat(addr) and Unsafe.putFloat(addr, value). Further, FloatBuffer.get(index) and FloatBuffer.put(index, value) are about 50 times (!!) slower than access to a float[]. For Android we probably need an implementation that is backed by a specific primitive array per type, which means 1 type can only have float fields, or only int fields, etc etc.

Just having installed Maven… :persecutioncomplex: I’m now able to compile it, package it with dependencies, make it spit out a JAR and even deploy it locally. Now what’s next?

Judging from the comments, this is really nice. I’d enjoy to hop in on the cake, but I don’t know how, where or when.
I can reason that values in structs can now be stored in a single array, with the fields on indexes instead of the objects themselves. Is that accurate? I can’t imagine a scenario where this is useful. :frowning:

How is this better than me, hacking up an array of Object children, and defining the protocol for whatever I need the structs to contain?

Sorry for being ignorant. I’d really like to get more enlightened on this. :slight_smile:

[x] the biggest speedup will be caused by no longer having to copy data back and forth between instances and buffers :point:

Just follow this official guide https://docs.sonatype.org/display/Repository/Sonatype+OSS+Maven+Repository+Usage+Guide

it is especially usefull when communicating with OpenGL. When you need to send to OpenGL vertex data it exspects a Bytebuffer, so without this lib you would have to copy all the data you have from your objects to such a buffer. Espsecial usefull if you not only want to push the data once, like for 3d models, but want to operate on it often, like for a particle engine.

Thanks, but I’m not going to jump through all the hoops, let alone creating some account on some website I never heard of on which my library will be dependent. Well, maybe later.

It’s simply not worth the effort: just add 3 JARs to your classpath and be done with it, mkay? :slight_smile:

sonatype is the company who runs maven central btw