How to add Jolt JNI to an existing project

Adding Jolt JNI to an existing project is an 10-step process:

  1. Add libraries to the classpath.

  2. Load the native library.

  3. Register memory allocators and install callbacks.

  4. Create a factory and register the standard types.

  5. Create a temporary allocator and a job system.

  6. Define 2 kinds of collision layers (object and broadphase), the mapping from object layers to broadphase layers, and the collision filters for layers.

  7. Create and configure a physics system.

  8. Create and configure physics objects and add them to the system.

  9. Simulate the system.

  10. Test and tune as necessary.

Add libraries to classpath

Jolt JNI requires both a JVM library and a native library. Both kinds of libraries are available pre-built from Maven Central.

Build types: use "Debug" native libraries for development and troubleshooting, then switch to "Release" libraries for performance testing and production.

Flavors: use "Dp" native libraries to simulate large worlds (>1000 meters in diameter) otherwise use "Sp".

Gradle-built Android projects

Jolt JNI comes pre-built for Android as a pair of AARs (one for each build type). Each AAR includes both a JVM library and all necessary native libraries.

I suggest starting out with the Debug-type AAR. Add to the relevant "build.gradle" or "build.gradle.kts" file:

repositories {
    mavenCentral()
}
dependencies {
    implementation(
            group: "com.github.stephengold",
            name: "jolt-jni-Android",
            version: "1.0.0",
            classifier: "SpDebug",
            ext: "aar"
    )
}

Gradle-built desktop projects

Jolt JNI comes pre-built for desktops as a platform-independent JVM library plus a set of (separately-packaged) native libraries.

Because of how releases are built, the desktop JVM library is released under 8 distinct names (artifact IDs). In contrast, each desktop native library is specific to a particular platform, build type, and flavor.

I suggest starting with a JVM library plus the "DebugSp" native library for your development environment. For a "Linux on x86_64" environment, add to the relevant "build.gradle" or "build.gradle.kts" file:

repositories {
    mavenCentral()
}
dependencies {
    // JVM library:
    implementation("com.github.stephengold:jolt-jni-Windows64:1.0.0")

    // native libraries:
    runtimeOnly("com.github.stephengold:jolt-jni-Linux64:1.0.0:DebugSp")
        // Native libraries for other platforms also could be added.
}
  • For a 64-bit MS-Windows environment, replace "Linux64" with "Windows64".

  • For an "Apple silicon" MacOS environment, replace "Linux64" with "MacOSX_ARM64".

Load the native library

You must load Jolt JNI’s native library before instantiating any physics objects.

Android projects

Add to your physics initialization:

System.loadLibrary("joltjni");

Desktop projects

The JSnapLoader library may be used for this purpose. Add to the relevant "build.gradle" or "build.gradle.kts" file:

dependencies {
    implementation("io.github.electrostat-lab:snaploader:1.1.1-stable")
    runtimeOnly("com.github.oshi:oshi-core:6.8.1")
}

Add to your physics initialization:

import electrostatic4j.snaploader.LibraryInfo;
import electrostatic4j.snaploader.LoadingCriterion;
import electrostatic4j.snaploader.NativeBinaryLoader;
import electrostatic4j.snaploader.filesystem.DirectoryPath;
import electrostatic4j.snaploader.platform.NativeDynamicLibrary;
import electrostatic4j.snaploader.platform.util.PlatformPredicate;

// ...

LibraryInfo info = new LibraryInfo(null, "joltjni", DirectoryPath.USER_DIR);
NativeBinaryLoader loader = new NativeBinaryLoader(info);

NativeDynamicLibrary[] libraries = {
        new NativeDynamicLibrary("linux/aarch64/com/github/stephengold", PlatformPredicate.LINUX_ARM_64),
        new NativeDynamicLibrary("linux/armhf/com/github/stephengold", PlatformPredicate.LINUX_ARM_32),
        new NativeDynamicLibrary("linux/x86-64/com/github/stephengold", PlatformPredicate.LINUX_X86_64),
        new NativeDynamicLibrary("osx/aarch64/com/github/stephengold", PlatformPredicate.MACOS_ARM_64),
        new NativeDynamicLibrary("osx/x86-64/com/github/stephengold", PlatformPredicate.MACOS_X86_64),
        new NativeDynamicLibrary("windows/x86-64/com/github/stephengold", PlatformPredicate.WIN_X86_64)
};
loader.registerNativeLibraries(libraries).initPlatformLibrary();
loader.loadLibrary(LoadingCriterion.CLEAN_EXTRACTION);

Memory allocators and callbacks

To avoid the need to explicitly free Jolt-Physics objects, I recommend starting Jolt JNI’s built-in cleaner thread, which frees objects automatically during JVM garbage collection.

Jolt Physics allows custom allocators for native heap memory, but Jolt JNI exposes only the default allocator.

import com.github.stephengold.joltjni.Jolt;
import com.github.stephengold.joltjni.JoltPhysicsObject;

// ...

JoltPhysicsObject.startCleaner(); // to free Jolt-Physics objects automatically
Jolt.registerDefaultAllocator(); // tell Jolt Physics to use malloc/free

Similarly, Jolt Physics allows custom callbacks for native assertions and trace output, but Jolt JNI exposes only the default callbacks, which print to cout, the standard output.

Jolt.installDefaultAssertCallback();
Jolt.installDefaultTraceCallback();

In Release-type native libraries, assertions are disabled, so installDefaultAssertCallback() is a no-op.

Create factory and register types

Jolt Physics uses a factory object to create instances of classes based on their name or hash, for instance when deserializing saved data.

Once the factory is created, you should register all the standard types and install their collision handlers.

boolean success = Jolt.newFactory();
assert success;
Jolt.registerTypes();

Temporary allocator and job system

Jolt Physics needs a TempAllocator to allocate temporary memory during simulation. Since it’s difficult to predict how much temporary memory will be needed, I recommend using TempAllocatorMalloc, the most flexible implementation.

import com.github.stephengold.joltjni.TempAllocator;
import com.github.stephengold.joltjni.TempAllocatorMalloc;

// ...

TempAllocator tempAllocator = new TempAllocatorMalloc();

Similarly, Jolt Physics needs a JobSystem to assign simulation tasks to CPUs. Since it’s difficult to predict how many jobs and barriers will be needed, I suggest using the default limits, even though they may be larger than needed. For now, I assume you’ll want to use all available CPUs.

import com.github.stephengold.joltjni.JobSystem;
import com.github.stephengold.joltjni.JobSystemThreadPool;

// ...

int numWorkerThreads = Runtime.getRuntime().availableProcessors();
JobSystem jobSystem = new JobSystemThreadPool(
        Jolt.cMaxPhysicsJobs, Jolt.cMaxPhysicsBarriers, numWorkerThreads);

Collision layers

Jolt Physics allows you to organize bodies into collision layers and specify that some layers never collide. For instance, it’s typical to put moving and non-moving bodies into separate layers and specify that non-moving bodies collide only with moving bodies, not with other non-moving bodies.

Each body belongs to an object layer, defaulting to layer 0. You can define up to 65_536 object layers.

Collision layers can be defined both for objects and broadphase:

  • broadphase layers are used during broad-phase detection, and

  • object layers are used during narrow-phase detection.

The sole limitation is that there must be a consistent mapping from object layers to broadphase layers.

For now, I suggest putting moving and non-moving bodies into separate object layers and mapping both object layers to broadphase layer 0.

int numObjLayers = 2;
int numBpLayers = 1;

ObjectLayerPairFilterTable ovoFilter
        = new ObjectLayerPairFilterTable(numObjLayers);
// Enable collisions between 2 moving bodies:
ovoFilter.enableCollision(objLayerMoving, objLayerMoving);
// Enable collisions between a moving body and a non-moving one:
ovoFilter.enableCollision(objLayerMoving, objLayerNonMoving);
// Disable collisions between 2 non-moving bodies:
ovoFilter.disableCollision(objLayerNonMoving, objLayerNonMoving);

// Map both object layers to broadphase layer 0:
BroadPhaseLayerInterfaceTable layerMap
        = new BroadPhaseLayerInterfaceTable(numObjLayers, numBpLayers);
layerMap.mapObjectToBroadPhaseLayer(objLayerMoving, 0);
layerMap.mapObjectToBroadPhaseLayer(objLayerNonMoving, 0);

// Pre-compute the rules for colliding object layers with broadphase layers:
ObjectVsBroadPhaseLayerFilter ovbFilter
        = new ObjectVsBroadPhaseLayerFilterTable(
                layerMap, numBpLayers, ovoFilter, numObjLayers);

Create a physics system

As soon as you create a PhysicsSystem, you should configure it to the expected number of number of bodies, mutexes, body pairs, and contacts. In many cases, it’s difficult to predict how many resources will be needed, so I suggest setting these limits fairly high.

PhysicsSystem system = new PhysicsSystem();

int maxBodies = 5_000;
int numBodyMutexes = 0; // 0 means "use the default number"
int maxBodyPairs = 65_536;
int maxContacts = 20_480;
system.init(maxBodies, numBodyMutexes, maxBodyPairs, maxContacts,
        layerMap, ovbFilter, ovoFilter);

Add physics objects

Physics objects include:

  • bodies (Body)

    • soft bodies

    • rigid bodies

  • constraints (Constraint)

    • vehicles (VehicleConstraint)

  • characters (CharacterBase)

Body creation starts with a BodyCreationSettings object, which can be reused. Here’s a code fragment that creates a spherical rigid body:

// Create a collision shape:
float ballRadius = 1f;
ConstShape ballShape = new SphereShape(ballRadius);

// Create and configure body-creation settings:
BodyCreationSettings bcs = new BodyCreationSettings();
bcs.setShape(ballShape);

// Create a rigid body for a specific PhysicsSystem:
BodyInterface bi = physicsSystem.getBodyInterface();
Body ball = bi.createBody(bcs);

Bodies aren’t simulated unless they’re added to a physics system. The best way is to use BodyInterface.addBody():

bi.addBody(ball, EActivation.Activate);

Simulate the system

To simulate a single 20-millisecond step:

float timePerStep = 0.02f; // in seconds
int numCollisionSteps = 1;
physicsSystem.update(
        timePerStep, numCollisionSteps, tempAllocator, jobSystem);

HelloJoltJni

HelloJoltJni is a complete console app (no graphics) that serves as a starting point for using Jolt JNI.

It illustrates:

  1. loading a native library

  2. creating a PhysicsSystem

  3. creating 2 bodies and adding them to the system

  4. simulating 50 steps

HelloJoltJni is the first in a series of tutorial apps designed for hands-on learning. I expect you to not only study the source code, but to actually run the app as well. Take time now to set up a software development environment for this purpose!

For instance, if you install Git and a Java Development Kit, you should be able to launch tutorial apps from a command shell, like so:

  1. git clone https://github.com/stephengold/jolt-jni-docs.git

  2. cd jolt-jni-docs

  3. ./gradlew :java-apps:HelloJoltJni

Summary

  • Two libraries are required: a JVM library and a native library.

  • You can organize bodies into object layers and specify that some layers never collide.

  • Physics objects aren’t simulated unless they’re added to a system.