Skip to content
oneMillionWorlds edited this page May 14, 2024 · 2 revisions

Thread warden

Thread warden is for diagnosing bugs associated with mutating the JMonkeyEngine scene graph from a thread other than the main thread. This can cause strange assertion or IllegalStateExceptions.

It is a debugging tool, not for inclusion in releases. The instrumentation it does it not that expensive but it isn't free either

Examples of bugs this can solve

  • "Illegal Refresh FlagState: 1 for spatial null"
  • "Illegal refreshFlags RF_BOUND state for spatial null"
  • "Illegal light list update. Problem spatial name: null"
  • "Illegal mat param update. Problem spatial name: null"
  • "Illegal rf transform update. Problem spatial name: null"
  • 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: null

How to use it

Include ThreadWarden as a dependency, in gradle this would be like this

implementation group: 'com.onemillionworlds', name: 'thread-warden', version: '1.0.0'

Then inside your applications register the root node as protected

public class MyApplication extends SimpleApplication{

    @Override
    public void simpleInitApp(){
        ThreadWarden.setup(rootNode);
    }
}

Then if an illegal scene graph mutation occurs then you will get a ThreadWardenException at the point the illegal mutation happens with a full stack trace to where it happens

How to works

What is considered illegal

It is perfectly fine to prepare geometry off the main thread and then attach that prepared state later (on the main thread). This is legal:

Future<Node> legalThreadingFuture = executorService.submit(() -> {
    //this is fine
    Node child2 = new Node("child2");
    Node child3 = new Node("child3");
    child2.attachChild(child3);
    return child2;
});

rootNode.attachChild(legalThreadingFuture.get());

This on the other hand is illegal

Future<Void> illegalThreading = executorService.submit(() -> {
    Node child4= new Node("child4");
    rootNode.attachChild(child4);
    return null;
});

Thread warden understands the difference and only throws an exception on the second

How it works (under the hood)

It uses ByteBuddy to do bytecode rewriting and add instrumentation to the Node and Geometry classes. When a node (or geometry) gets a parent added it detects if that is part of the main scene. If it is it marks this spatial as being only allowed to be interacted with on the main thread (it does the same to all children). When the node is then mutated in any way later ThreadWarden checks the thread and raises an exception if the node should not be interacted with on the current thread