Having recently been hit with my least favourite exception 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
I’ve created a utility to give a bit more detail on what’s causing the problem (and where) and I though it may be useful to other people so I’ve released it as Thread Warden
The problem
When you accidentally modify the scene graph from the wrong thread JME may or may not notice (depending on how your threads interleave). If it does notice it will only tell you that you made a mistake (somewhere) and what the name of the spatial is (if you named your spatials; which I now do obsessively!). It doesn’t tell you where you did this, just that you did. These bugs kill my motivation and I usually take a day out after they hit because I know it will be such a slog to find the cause.
It is non trivial to avoid because this is a perfectly legal thing you might want to do
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());
Whereas this is illegal and may (or may not) cause JME to throw an assertion at some later point
Future<Void> illegalThreading = executorService.submit(() -> {
Node child4= new Node("child4");
rootNode.attachChild(child4);
return null;
});
My Solution
I’ve used 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 off 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.
This isn’t as bad for performance as I was expecting (I didn’t get a noticable FPS drop) but I’m sure there is some penalty. It goes without saying this is a debugging utility not intended to be included in releases
Examples of bugs this can find the source of
- “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
- State was changed after rootNode.updateGeometricState() call. Make sure you do not modify the scene from another thread!
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.1'
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 in your code you made the mistake. And it will detect it the very first time it happens, not just if you get unlucky with how your threads interleave (which might not happen for a while during your testing)
(Depending on your java version you may need to run with -XX:+EnableDynamicAgentLoading
)