Async loading of assets, still stuttering on attach

I’m loading almost all my resources (models and textures) in background thread, creating ready to use models and doing only sceneNode.attach(model) in enqueued action on main jme3 thread. Still, there seems to be considerable amount of stuttering when models are finally attached. From unscientific profiling, it seems that time is spent somewhere in uploading buffers, generating mipmaps, compiling shaders, etc.
Is it supposed to take so much time (I have 2-3 seconds during which just few frames are rendered, when attaching 10 or so models)? Is there any way to do some of these actions from other thread as well, to prepare the buffers, so final attach would be just about updating transforms and final binding of variables?

I’ve never tried it, but it is possible to create another OpenGL context on another thread, load all resources using that, and then our main render thread can use these shared resources without it being blocked

You can try material.preload() for each model. It won’t upload the buffers, but it uploads textures, compile shaders and so on.
I guess you can use it from another thread as it doesn’t modify the scene graph.

Out of curiosity, did either of these suggestions help?

I was unaware of Material.preload() … that’s a good thing to know :wink:

And the OpenGL context on a separate thread is an interesting idea as well. Just to be clear on this, if the resources are loaded to the GPU, both would have access to them? They are not tied to the context in some way? Does this mean that if I have a game running in the background that is using OpenGL, I have access to those resources… say, for instance by texture slot index, etc? Assuming that they are loaded into the slot index still…

Yes, be sure to make the first contesxt a shared one, then this should work. (And would be a great contribution)

Using preload from different thread doesn’t work out of the box

Exception in thread “pool-3-thread-19” java.lang.RuntimeException: No OpenGL context found in the current thread.
at org.lwjgl.opengl.GLContext.getCapabilities(
at org.lwjgl.opengl.GL11.glGenTextures(
at com.jme3.renderer.lwjgl.LwjglRenderer.updateTexImageData(
at com.jme3.renderer.lwjgl.LwjglRenderer.setTexture(
at com.jme3.material.Material.preload(
at net.virtualmat.locator.async.ModelLoader$4.visit(
at com.jme3.scene.Spatial.breadthFirstTraversal(
at net.virtualmat.locator.async.ModelLoader.tryLoadingModel(
at net.virtualmat.locator.async.ModelLoader.access$0(
at net.virtualmat.locator.async.ModelLoader$
at java.util.concurrent.ThreadPoolExecutor.runWorker(
at java.util.concurrent.ThreadPoolExecutor$

I tried to created shared context with

val sd = new SharedDrawable(Display.getDrawable());
(after main windows is already rendering for some time) - but it fails with

org.lwjgl.LWJGLException: Could not create context (WGL_ARB_create_context)
at org.lwjgl.opengl.WindowsContextImplementation.nCreate(Native Method)
at org.lwjgl.opengl.WindowsContextImplementation.create(
at org.lwjgl.opengl.ContextGL.<init>(
at org.lwjgl.opengl.DrawableGL.createSharedContext(
at org.lwjgl.opengl.DrawableGL.createSharedContext(
at org.lwjgl.opengl.SharedDrawable.<init>(
at net.virtualmat.locator.async.ModelLoader.tryLoadingModel(
at net.virtualmat.locator.async.ModelLoader.access$0(
at net.virtualmat.locator.async.ModelLoader$
at java.util.concurrent.ThreadPoolExecutor.runWorker(
… 2 more

Seems to be a common problem:;wap2

I’ll try that on my ATI machine in few days to see if it is nvidia specific, as some people suggest in the threads.

One of the suggestions was saying something about specifying at least 3.2 context. Unfortunately, shared drawable has no way to pass attributes…

Well I can at lest confirm that that code part works perfectly fine under linux. (I just dumped it into the simpleInit)


@Empire Phoenix said: Well I can at lest confirm that that code part works perfectly fine under linux. (I just dumped it into the simpleInit)

That is interesting. It also works on my PC. Of course, for it to make sense, you need to do it in different thread - and then it fails. But you gave me an idea - I have created SharedContext in jme3 render thread and only called makeCurrent in worker thread. This seems to not crash immediately, but rather with more interesting exception like
Exception in thread “pool-3-thread-1” java.lang.IndexOutOfBoundsException
at java.nio.Buffer.checkIndex(
at java.nio.DirectByteBuffer.get(
at org.lwjgl.BufferChecks.checkNullTerminated(
at org.lwjgl.opengl.GL20.glGetUniformLocation(
at com.jme3.renderer.lwjgl.LwjglRenderer.updateUniformLocation(
at com.jme3.renderer.lwjgl.LwjglRenderer.updateUniform(
at com.jme3.renderer.lwjgl.LwjglRenderer.updateShaderUniforms(
at com.jme3.renderer.lwjgl.LwjglRenderer.setShader(
at com.jme3.material.Material.preload(
at net.virtualmat.locator.async.ModelLoader$4.visit(
at com.jme3.scene.Spatial.breadthFirstTraversal(
at net.virtualmat.locator.async.ModelLoader.tryLoadingModel(
at net.virtualmat.locator.async.ModelLoader.access$0(
at net.virtualmat.locator.async.ModelLoader$
at java.util.concurrent.ThreadPoolExecutor.runWorker(
at java.util.concurrent.ThreadPoolExecutor$

I’ll investigate more, but it seems that way to go is to call new SharedDrawable(Display.getDrawable()); in main render thread and only sd.makeCurrent(); in worked thread.

Sometimes it works, sometimes fails with random exceptions like
java.lang.IllegalStateException: Framebuffer has erronous attachment.
at com.jme3.renderer.lwjgl.LwjglRenderer.checkFrameBufferError( ~[bin/:na]
at com.jme3.renderer.lwjgl.LwjglRenderer.setFrameBuffer( ~[bin/:na]
at com.jme3.shadow.AbstractShadowRenderer.renderShadowMap( ~[bin/:na]
at com.jme3.shadow.AbstractShadowRenderer.postQueue( ~[bin/:na]
at com.jme3.shadow.AbstractShadowFilter.postQueue( ~[bin/:na]
at ~[bin/:na]
at com.jme3.renderer.RenderManager.renderViewPort( ~[bin/:na]
at com.jme3.renderer.RenderManager.render( ~[bin/:na]
at net.virtualmat.gui.VmatGui.update( ~[bin/:na]
at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop( ~[bin/:na]
at com.jme3.system.lwjgl.LwjglDisplay.runLoop( ~[bin/:na]
at ~[bin/:na]
at ~[na:1.8.0-ea]

There might be a lot of issues around that - probably more than I’m able to solve atm with trial and error approach :wink:

Well but it is supposed to work at least, many games that use opengl use shared contexts.

What might be a reason, is the main context of jme shared?

Here maybeet his helps, For a test I would suggest to remove jme from the tests (because "You should call wglShareLists as soon as possible, meaning before you create any resources. "), and just try to start two empty contexts that are shared. And only after that works implement it baack to jme.

@abies said: I'll investigate more, but it seems that way to go is to call new SharedDrawable(Display.getDrawable()); in main render thread and only sd.makeCurrent(); in worked thread.

Yes that looks like it is the correct way to do it.

There is a LWJGL test here, that loads Textures on a background context.


Below is a modified You need this file “” to run the program. What the modification does:

this.terrain = new TerrainGrid(“terrain”, 33, 257, new ImageTileLoader(assetManager, new Namer() {

        public String getName(int x, int y) {
            return "TerrainAlphaTest/terrain_" + x + "_" + y + ".png";


Is to load .png as opposed to using a fractal. When the required .png is not found then it uses a plain height map with no elevations.

What I’m trying to say is that you should look at all the classes involved in this sample. Because it runs smoothly, which means that the stuttering you experience from dynamically loading models in not a JMonkey limitation as it has been solved before, like to make this sample.

package co.pixelapp.terrain;

import com.jme3.asset.plugins.HttpZipLocator;
import com.jme3.asset.plugins.ZipLocator;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.HeightfieldCollisionShape;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.Arrow;
import com.jme3.terrain.geomipmap.TerrainGrid;
import com.jme3.terrain.geomipmap.TerrainGridListener;
import com.jme3.terrain.geomipmap.TerrainGridLodControl;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.geomipmap.grid.FractalTileLoader;
import com.jme3.terrain.geomipmap.grid.ImageTileLoader;
import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator;
import com.jme3.terrain.heightmap.Namer;
import com.jme3.terrain.noise.ShaderUtils;
import com.jme3.terrain.noise.basis.FilteredBasis;
import com.jme3.terrain.noise.filter.IterativeFilter;
import com.jme3.terrain.noise.filter.OptimizedErode;
import com.jme3.terrain.noise.filter.PerturbFilter;
import com.jme3.terrain.noise.filter.SmoothFilter;
import com.jme3.terrain.noise.fractal.FractalSum;
import com.jme3.terrain.noise.modulator.NoiseModulator;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;

public class TerrainGridAlphaMapTest extends SimpleApplication {

private TerrainGrid terrain;
private float grassScale = 64;
private float dirtScale = 16;
private float rockScale = 128;
private boolean usePhysics = true;

public static void main(final String[] args) {
    TerrainGridAlphaMapTest app = new TerrainGridAlphaMapTest();
private CharacterControl player3;
private FractalSum base;
private PerturbFilter perturb;
private OptimizedErode therm;
private SmoothFilter smooth;
private IterativeFilter iterate;
private Material material;
private Material matWire;

public void simpleInitApp() {
    DirectionalLight sun = new DirectionalLight();
    sun.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());

    AmbientLight al = new AmbientLight();

    File file = new File("");
    if (!file.exists()) {
        assetManager.registerLocator("", HttpZipLocator.class);
    } else {
        assetManager.registerLocator("", ZipLocator.class);

    ScreenshotAppState state = new ScreenshotAppState();

    // TERRAIN TEXTURE material
    material = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md");
    material.setBoolean("useTriPlanarMapping", false);
    //material.setBoolean("isTerrainGrid", true);
    material.setFloat("Shininess", 0.0f);

    // GRASS texture
    Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
    material.setTexture("DiffuseMap", grass);
    material.setFloat("DiffuseMap_0_scale", grassScale);

    // DIRT texture
    Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
    material.setTexture("DiffuseMap_1", dirt);
    material.setFloat("DiffuseMap_1_scale", dirtScale);

    // ROCK texture
    Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
    material.setTexture("DiffuseMap_2", rock);
    material.setFloat("DiffuseMap_2_scale", rockScale);

    // WIREFRAME material
    matWire = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    matWire.setColor("Color", ColorRGBA.Green);

    this.base = new FractalSum();
    this.base.addModulator(new NoiseModulator() {

        public float value(float... in) {
            return ShaderUtils.clamp(in[0] * 0.5f + 0.5f, 0, 1);

    FilteredBasis ground = new FilteredBasis(this.base);

    this.perturb = new PerturbFilter();

    this.therm = new OptimizedErode();

    this.smooth = new SmoothFilter();

    this.iterate = new IterativeFilter();


    this.terrain = new TerrainGrid("terrain", 33, 257, new ImageTileLoader(assetManager, new Namer() {

        public String getName(int x, int y) {
            return "TerrainAlphaTest/terrain_" + x + "_" + y + ".png";

    this.terrain.setLocalTranslation(0, 0, 0);
    this.terrain.setLocalScale(2f, 1f, 2f);

    TerrainLodControl control = new TerrainGridLodControl(this.terrain, this.getCamera());
    control.setLodCalculator( new DistanceLodCalculator(33, 2.7f) ); // patch size, and a multiplier

    final BulletAppState bulletAppState = new BulletAppState();

    this.getCamera().setLocation(new Vector3f(0, 256, 0));

    this.viewPort.setBackgroundColor(new ColorRGBA(0.7f, 0.8f, 1f, 1f));

    if (usePhysics) {
        CapsuleCollisionShape capsuleShape = new CapsuleCollisionShape(0.5f, 1.8f, 1);
        player3 = new CharacterControl(capsuleShape, 0.5f);

        player3.setPhysicsLocation(new Vector3f(cam.getLocation().x, 256, cam.getLocation().z));


    terrain.addListener(new TerrainGridListener() {

        public void gridMoved(Vector3f newCenter) {

        public void tileAttached(Vector3f cell, TerrainQuad quad) {
            Texture alpha = null;
            try {
                alpha = assetManager.loadTexture("TerrainAlphaTest/alpha_" + (int)cell.x+ "_" + (int)cell.z + ".png");
            } catch (Exception e) {
                alpha = assetManager.loadTexture("TerrainAlphaTest/alpha_default.png");
            quad.getMaterial().setTexture("AlphaMap", alpha);
            if (usePhysics) {
                quad.addControl(new RigidBodyControl(new HeightfieldCollisionShape(quad.getHeightMap(), terrain.getLocalScale()), 0));

        public void tileDetached(Vector3f cell, TerrainQuad quad) {
            if (usePhysics) {
                if (quad.getControl(RigidBodyControl.class) != null) {

    markers = new Node();

Node markers;

private void createMarkerPoints(float count) {
    Node center = createAxisMarker(10);
    float xS = (count-1)*terrain.getTerrainSize() - (terrain.getTerrainSize()/2);
    float zS = (count-1)*terrain.getTerrainSize() - (terrain.getTerrainSize()/2);
    float xSi = xS;
    float zSi = zS;
    for (int x=0; x&lt;count*2; x++) {
        for (int z=0; z&lt;count*2; z++) {
            Node m = createAxisMarker(5);
            m.setLocalTranslation(xSi, 0, zSi);
            zSi += terrain.getTerrainSize();
        zSi = zS;
        xSi += terrain.getTerrainSize();

private void updateMarkerElevations() {
    for (Spatial s : markers.getChildren()) {
        float h = terrain.getHeight(new Vector2f(s.getLocalTranslation().x, s.getLocalTranslation().z));
        s.setLocalTranslation(s.getLocalTranslation().x, h+1, s.getLocalTranslation().z);

private void initKeys() {
    // You can map one or several inputs to one named action
    this.inputManager.addMapping("Lefts", new KeyTrigger(KeyInput.KEY_A));
    this.inputManager.addMapping("Rights", new KeyTrigger(KeyInput.KEY_D));
    this.inputManager.addMapping("Ups", new KeyTrigger(KeyInput.KEY_W));
    this.inputManager.addMapping("Downs", new KeyTrigger(KeyInput.KEY_S));
    this.inputManager.addMapping("Jumps", new KeyTrigger(KeyInput.KEY_SPACE));
    this.inputManager.addListener(this.actionListener, "Lefts");
    this.inputManager.addListener(this.actionListener, "Rights");
    this.inputManager.addListener(this.actionListener, "Ups");
    this.inputManager.addListener(this.actionListener, "Downs");
    this.inputManager.addListener(this.actionListener, "Jumps");
private boolean left;
private boolean right;
private boolean up;
private boolean down;
private final ActionListener actionListener = new ActionListener() {

    public void onAction(final String name, final boolean keyPressed, final float tpf) {
        if (name.equals("Lefts")) {
            if (keyPressed) {
                TerrainGridAlphaMapTest.this.left = true;
            } else {
                TerrainGridAlphaMapTest.this.left = false;
        } else if (name.equals("Rights")) {
            if (keyPressed) {
                TerrainGridAlphaMapTest.this.right = true;
            } else {
                TerrainGridAlphaMapTest.this.right = false;
        } else if (name.equals("Ups")) {
            if (keyPressed) {
                TerrainGridAlphaMapTest.this.up = true;
            } else {
                TerrainGridAlphaMapTest.this.up = false;
        } else if (name.equals("Downs")) {
            if (keyPressed) {
                TerrainGridAlphaMapTest.this.down = true;
            } else {
                TerrainGridAlphaMapTest.this.down = false;
        } else if (name.equals("Jumps")) {
private final Vector3f walkDirection = new Vector3f();

public void simpleUpdate(final float tpf) {
    Vector3f camDir =;
    Vector3f camLeft =;
    this.walkDirection.set(0, 0, 0);
    if (this.left) {
    if (this.right) {
    if (this.up) {
    if (this.down) {

    if (usePhysics) {

protected Node createAxisMarker(float arrowSize) {

    Material redMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    redMat.setColor("Color", ColorRGBA.Red);
    Material greenMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    greenMat.setColor("Color", ColorRGBA.Green);
    Material blueMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    blueMat.setColor("Color", ColorRGBA.Blue);

    Node axis = new Node();

    // create arrows
    Geometry arrowX = new Geometry("arrowX", new Arrow(new Vector3f(arrowSize, 0, 0)));
    Geometry arrowY = new Geometry("arrowY", new Arrow(new Vector3f(0, arrowSize, 0)));
    Geometry arrowZ = new Geometry("arrowZ", new Arrow(new Vector3f(0, 0, arrowSize)));

    //axis.setModelBound(new BoundingBox());
    return axis;