Multithreaded loading of assets

I want to load the assets on a separate thread, so that the main thread can run a simple animation. Sometimes it works, but sometimes I get this:

ago 04, 2020 3:41:01 PM com.jme3.app.LegacyApplication handleError
GRAVE: Uncaught exception thrown in Thread[jME3 Main,5,main]
java.lang.IllegalStateException: Scene graph is not properly updated for rendering.
State was changed after rootNode.updateGeometricState() call. 
Make sure you do not modify the scene from another thread!
Problem spatial name: Gui Node
	at com.jme3.scene.Spatial.checkCulling(Spatial.java:361)
	at com.jme3.renderer.RenderManager.renderSubScene(RenderManager.java:717)
	at com.jme3.renderer.RenderManager.renderScene(RenderManager.java:710)
	at com.jme3.renderer.RenderManager.renderViewPort(RenderManager.java:1096)
	at com.jme3.renderer.RenderManager.render(RenderManager.java:1166)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:272)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:153)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:193)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:234)
	at java.lang.Thread.run(Thread.java:745)

My code is:

public class Loading3AppState extends BaseAppState {

    Node stateGuiNode = new Node();
    Geometry bg;
    Material bgM;
    Geometry eve;
    Application app;

    @Override
    protected void initialize(Application app) {

//some spatials for the loading screen
        bg = new Geometry("monkeybg", new Quad(MS_WIDTH, MS_HEIGHT));
        bgM = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        bgM.setColor("Color", new ColorRGBA(0.21875f, 0.21875f, 0.21875f, 1f));
        bg.setMaterial(bgM);

        eve = new Geometry("monkey", new Quad(540, 640));
        Material logoM = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        logoM.setTexture("ColorMap", app.getAssetManager().loadTexture("Comics/eve.png"));
        logoM.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);  // !
        logoM.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        eve.setMaterial(logoM);
        eve.setLocalTranslation(500, 0, 0);
        this.app = app;
        stateGuiNode.attachChild(bg);
        stateGuiNode.attachChild(eve);
        setEnabled(false);

    }

    SharedDrawable drawable;
    float totTPF = 0;

    @Override
    public void update(float tpf) {

//some animation...
            bgM.setColor("Color", new ColorRGBA(1f, interp(0, step_dur,0.3f,0.5f,totTPF), interp(0, step_dur,1f,0.9f,totTPF), 1f));
//other animations...
    }

    private float interp(float Tstart, float Tend, float Cstart, float Cend, float Tval){
        return ((Tval-Tstart)*(Cend-Cstart)/(Tend-Tstart))+Cstart;
    }

    private void cacheTexture(String texture) {
        System.out.println(new Date() + " cache load " + texture);
        Geometry cache = new Geometry("????", new Quad(1, 1));
        Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setTexture("ColorMap", app.getAssetManager().loadTexture(texture));
        cache.setMaterial(mat);
        mat.preload(app.getRenderManager(), cache);
    }

    @Override
    protected void cleanup(Application app) {
    }

    @Override
    protected void onEnable() {
        System.out.println(new Date() + " onEnable");
        ((Main) getApplication()).scaledGuiNode.attachChild(stateGuiNode);
        try {
            drawable = new SharedDrawable(Display.getDrawable());
        } catch (LWJGLException e) {
            e.printStackTrace();
        }
        System.out.println(new Date() + " created drawable");
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(new Date() + " BG thread");
                try {
                    drawable.makeCurrent();
                } catch (LWJGLException e) {
                    e.printStackTrace();
                }
                cacheTexture("Textures/deepsky01p.png");
                cacheTexture("Textures/moon-blue.png");
                for (int i = 0; i < nebulas.length; i++) {
                    if (i < 9) {
                        cacheTexture("Textures/aurora0" + (i + 1) + ".png");
                    } else {
                        cacheTexture("Textures/aurora" + (i + 1) + ".png");
                    }
                }

                cacheTexture("Textures/deepsky01p.png");
                cacheTexture("Textures/aurora01.png");
                cacheTexture("Textures/aurora02.png");
                cacheTexture("Textures/aurora03.png");
                cacheTexture("Textures/aurora04.png");
                cacheTexture("Textures/aurora05.png");
                cacheTexture("Textures/aurora06.png");
                cacheTexture("Textures/aurora07.png");
                cacheTexture("Textures/aurora08.png");
                cacheTexture("Textures/aurora09.png");
                cacheTexture("Textures/aurora10.png");
                cacheTexture("Textures/moon-blue.png");
                cacheTexture("Textures/black-hole4.png");
                cacheTexture("Textures/MonkeySheet/boss-armor-break.dds");
                cacheTexture("Textures/MonkeySheet/pppp.dds");
                cacheTexture("Textures/MonkeySheet/shell.dds");
                cacheTexture("Textures/MonkeySheet/simonetta2.dds");
                cacheTexture("Textures/MonkeySheet/size64.dds");
                cacheTexture("Textures/MonkeySheet/size128.dds");
                cacheTexture("Textures/MonkeySheet/size256.dds");
                cacheTexture("Textures/MonkeySheet/slugdeath.dds");
                cacheTexture("Textures/MonkeySheet/slug-pinata-death.dds");
                cacheTexture("Textures/MonkeySheet/slugs128.dds");
                cacheTexture("Textures/MonkeySheet/slugs512.dds");
                cacheTexture("Textures/MonkeySheet/slugs1024.dds");
                cacheTexture("Textures/MonkeySheet/ssss.dds");
                cacheTexture("Textures/boot.png");


                drawable.destroy();
//loading is finished, so start the game
                getState(UberState.class).setState(INGAME);
            }
        }).start();
    }

    @Override
    protected void onDisable() {
        stateGuiNode.removeFromParent();
    }

}

What’s that doing?

You can’t modify the scene graph from a separate thread. You must app.enqueue() something to do it on the render thread. That line is the only line in your thread.run() that looks suspicious.

This may be helpful: Multithreading Optimization :: jMonkeyEngine Docs

5 Likes

That is to say you can load the assets from another thread, but you can’t add/remove/modify the scene from another thread. Once it’s loaded, use app.enqueue to add it to the scene.

3 Likes
2 Likes

Thanks, looks like this solved the issue!

2 Likes

With LWJGL3, there’s no SharedDrawable and therefore no way to load assets on background?
I hope I’m wrong; any advice?
Thanks!

You can use Java 8 CompletableFuture

        CompletableFuture.runAsync(() -> {
            Spatial model = getApplication().getAssetManager().loadModel("path to model");
            getApplication().enqueue(() -> rootNode.attachChild(model));
        });

Edit:

By the way you can take use of AsyncAssetManager in

or the JobState in

4 Likes

How do I include the dependency with gradle?

I think there is no release yet so I suppose you should build it from the source code.

You can fork, create a release at git & use jitpack io direclty…