Skip to content

Commit 757b2aa

Browse files
committed
Merge remote-tracking branch 'richardTingle/multi-scenario-screenshot-test' into fullscreen_triangle
2 parents 7570143 + dc53005 commit 757b2aa

7 files changed

Lines changed: 299 additions & 114 deletions

File tree

jme3-core/src/main/java/com/jme3/app/SimpleApplication.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -307,13 +307,6 @@ public void initialize() {
307307
simpleInitApp();
308308
}
309309

310-
@Override
311-
public void stop(boolean waitFor) {
312-
//noinspection AssertWithSideEffects
313-
assert SceneGraphThreadWarden.reset();
314-
super.stop(waitFor);
315-
}
316-
317310
@Override
318311
public void update() {
319312
if (prof != null) {

jme3-core/src/main/java/com/jme3/scene/threadwarden/SceneGraphThreadWarden.java

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import com.jme3.scene.Spatial;
55

66
import java.util.Collections;
7-
import java.util.Set;
7+
import java.util.Map;
88
import java.util.WeakHashMap;
99
import java.util.logging.Level;
1010
import java.util.logging.Logger;
@@ -17,6 +17,7 @@
1717
* Only has an effect if asserts are on
1818
* </p>
1919
*/
20+
@SuppressWarnings("SameReturnValue")
2021
public class SceneGraphThreadWarden {
2122

2223
private static final Logger logger = Logger.getLogger(SceneGraphThreadWarden.class.getName());
@@ -34,8 +35,7 @@ public class SceneGraphThreadWarden {
3435
assert ASSERTS_ENABLED = true;
3536
}
3637

37-
public static Thread mainThread;
38-
public static final Set<Spatial> spatialsThatAreMainThreadReserved = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
38+
public static final Map<Spatial, Thread> spatialsThatAreMainThreadReserved = Collections.synchronizedMap(new WeakHashMap<>());
3939

4040
/**
4141
* Marks the given node as being reserved for the main thread.
@@ -48,12 +48,11 @@ public static boolean setup(Node rootNode){
4848
return true;
4949
}
5050
Thread thisThread = Thread.currentThread();
51-
if(mainThread != null && mainThread != thisThread ){
52-
throw new IllegalStateException("The main thread has already been set to " + mainThread.getName() + " but now it's being set to " + Thread.currentThread().getName());
51+
Thread existingRestriction = spatialsThatAreMainThreadReserved.get(rootNode);
52+
if(existingRestriction != null && existingRestriction != thisThread ){
53+
throw new IllegalStateException("The node is already restricted to " + existingRestriction.getName() + " but now it's being restricted to " + Thread.currentThread().getName());
5354
}
54-
mainThread = thisThread;
55-
setTreeRestricted(rootNode);
56-
55+
setTreeRestricted(rootNode, thisThread);
5756
return true; // return true so can be a "side effect" of an assert
5857
}
5958

@@ -71,11 +70,11 @@ public static void disableChecks(){
7170
* Runs through the entire tree and sets the restriction state of all nodes below the given node
7271
* @param spatial the node (and children) to set the restriction state of
7372
*/
74-
private static void setTreeRestricted(Spatial spatial){
75-
spatialsThatAreMainThreadReserved.add(spatial);
73+
private static void setTreeRestricted(Spatial spatial, Thread threadRestriction){
74+
spatialsThatAreMainThreadReserved.put(spatial, threadRestriction);
7675
if(spatial instanceof Node){
7776
for(Spatial child : ((Node) spatial).getChildren()){
78-
setTreeRestricted(child);
77+
setTreeRestricted(child, threadRestriction);
7978
}
8079
}
8180
}
@@ -99,8 +98,8 @@ public static boolean updateRequirement(Spatial spatial, Node newParent){
9998
return true;
10099
}
101100

102-
boolean shouldNowBeRestricted = newParent !=null && spatialsThatAreMainThreadReserved.contains(newParent);
103-
boolean wasPreviouslyRestricted = spatialsThatAreMainThreadReserved.contains(spatial);
101+
boolean shouldNowBeRestricted = newParent !=null && spatialsThatAreMainThreadReserved.containsKey(newParent);
102+
boolean wasPreviouslyRestricted = spatialsThatAreMainThreadReserved.containsKey(spatial);
104103

105104
if(shouldNowBeRestricted || wasPreviouslyRestricted ){
106105
assertOnCorrectThread(spatial);
@@ -110,17 +109,19 @@ public static boolean updateRequirement(Spatial spatial, Node newParent){
110109
return true;
111110
}
112111
if(shouldNowBeRestricted){
113-
setTreeRestricted(spatial);
112+
setTreeRestricted(spatial, Thread.currentThread());
114113
}else{
115114
setTreeNotRestricted(spatial);
116115
}
117116

118117
return true; // return true so can be a "side effect" of an assert
119118
}
120119

120+
/**
121+
* This is a full reset over all options and all threads
122+
*/
121123
public static boolean reset(){
122124
spatialsThatAreMainThreadReserved.clear();
123-
mainThread = null;
124125
THREAD_WARDEN_ENABLED = !Boolean.getBoolean("nothreadwarden");
125126
return true; // return true so can be a "side effect" of an assert
126127
}
@@ -134,8 +135,9 @@ public static boolean assertOnCorrectThread(Spatial spatial){
134135
if(checksDisabled()){
135136
return true;
136137
}
137-
if(spatialsThatAreMainThreadReserved.contains(spatial)){
138-
if(Thread.currentThread() != mainThread){
138+
Thread restrictingThread = spatialsThatAreMainThreadReserved.get(spatial);
139+
if(restrictingThread!=null){
140+
if(Thread.currentThread() != restrictingThread){
139141
// log as well as throw an exception because we are running in a thread, if we are in an executor service the exception
140142
// might not make itself known until `get` is called on the future (and JME might crash before that happens).
141143
String message = "The spatial " + spatial + " was mutated on a thread other than the main thread, was mutated on " + Thread.currentThread().getName();

jme3-core/src/test/java/com/jme3/scene/threadwarden/SceneGraphThreadWardenTest.java

Lines changed: 136 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
public class SceneGraphThreadWardenTest {
2727

2828
private static ExecutorService executorService;
29+
private static ExecutorService executorService2;
2930

3031
@SuppressWarnings({"ReassignedVariable", "AssertWithSideEffects"})
3132
@BeforeClass
@@ -42,11 +43,13 @@ public static void setupClass() {
4243
@Before
4344
public void setup() {
4445
executorService = newSingleThreadDaemonExecutor();
46+
executorService2 = newSingleThreadDaemonExecutor();
4547
}
4648

4749
@After
4850
public void tearDown() {
4951
executorService.shutdown();
52+
executorService2.shutdown();
5053
SceneGraphThreadWarden.reset();
5154
}
5255

@@ -71,6 +74,31 @@ public void testNormalNodeMutationOnMainThread() {
7174
rootNode.detachChild(child);
7275
}
7376

77+
@Test
78+
public void restrictingTheSameRootNodeToTwoThreadsIsIllegal() throws ExecutionException, InterruptedException {
79+
Node rootNode = new Node("root");
80+
81+
executorService.submit(() -> SceneGraphThreadWarden.setup(rootNode)).get();
82+
83+
try {
84+
executorService2.submit(() -> SceneGraphThreadWarden.setup(rootNode)).get();
85+
fail("Expected an IllegalThreadSceneGraphMutation exception");
86+
} catch (ExecutionException e) {
87+
// This is expected - verify it's the right exception type
88+
assertTrue("Expected IllegalStateException, got: " + e.getCause().getClass().getName(),
89+
e.getCause() instanceof IllegalStateException);
90+
}
91+
}
92+
93+
@Test
94+
public void restrictingTwoRootNodesIsFine(){
95+
Node rootNode = new Node("root");
96+
Node rootNode2 = new Node("root2");
97+
98+
SceneGraphThreadWarden.setup(rootNode);
99+
SceneGraphThreadWarden.setup(rootNode2);
100+
}
101+
74102
/**
75103
* Test that node mutation on nodes not connected to the root node is fine even on a non main thread.
76104
* <p>
@@ -107,7 +135,7 @@ public void testNodeMutationOnNonConnectedNodesOnNonMainThread() throws Executio
107135
* exception.
108136
*/
109137
@Test
110-
public void testAddingNodeToSceneGraphOnNonMainThread() throws InterruptedException {
138+
public void testAddingNodeToSceneGraphOnNonMainThread() {
111139
Node rootNode = new Node("root");
112140
SceneGraphThreadWarden.setup(rootNode);
113141

@@ -122,14 +150,7 @@ public void testAddingNodeToSceneGraphOnNonMainThread() throws InterruptedExcept
122150
return null;
123151
});
124152

125-
try {
126-
illegalMutationFuture.get();
127-
fail("Expected an IllegalThreadSceneGraphMutation exception");
128-
} catch (ExecutionException e) {
129-
// This is expected - verify it's the right exception type
130-
assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
131-
e.getCause() instanceof IllegalThreadSceneGraphMutation);
132-
}
153+
expectSceneGraphException(illegalMutationFuture);
133154
}
134155

135156
/**
@@ -141,7 +162,7 @@ public void testAddingNodeToSceneGraphOnNonMainThread() throws InterruptedExcept
141162
* </p>
142163
*/
143164
@Test
144-
public void testMovingNodeAttachedToRootOnNonMainThread() throws InterruptedException {
165+
public void testMovingNodeAttachedToRootOnNonMainThread() {
145166
Node rootNode = new Node("root");
146167
SceneGraphThreadWarden.setup(rootNode);
147168

@@ -157,14 +178,7 @@ public void testMovingNodeAttachedToRootOnNonMainThread() throws InterruptedExce
157178
return null;
158179
});
159180

160-
try {
161-
illegalMutationFuture.get();
162-
fail("Expected an IllegalThreadSceneGraphMutation exception");
163-
} catch (ExecutionException e) {
164-
// This is expected - verify it's the right exception type
165-
assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
166-
e.getCause() instanceof IllegalThreadSceneGraphMutation);
167-
}
181+
expectSceneGraphException(illegalMutationFuture);
168182
}
169183

170184
/**
@@ -199,7 +213,7 @@ public void testDetachmentReleasesProtection() throws ExecutionException, Interr
199213
* then try (and fail) to make an illegal on-thread change to the grandchild.
200214
*/
201215
@Test
202-
public void testAddingAChildToTheRootNodeAlsoRestrictsTheGrandChild() throws InterruptedException {
216+
public void testAddingAChildToTheRootNodeAlsoRestrictsTheGrandChild() {
203217
Node rootNode = new Node("root");
204218
SceneGraphThreadWarden.setup(rootNode);
205219

@@ -221,14 +235,7 @@ public void testAddingAChildToTheRootNodeAlsoRestrictsTheGrandChild() throws Int
221235
return null;
222236
});
223237

224-
try {
225-
illegalMutationFuture.get();
226-
fail("Expected an IllegalThreadSceneGraphMutation exception");
227-
} catch (ExecutionException e) {
228-
// This is expected - verify it's the right exception type
229-
assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
230-
e.getCause() instanceof IllegalThreadSceneGraphMutation);
231-
}
238+
expectSceneGraphException(illegalMutationFuture);
232239
}
233240

234241
/**
@@ -294,7 +301,96 @@ public void testDisableChecksAllowsIllegalMutation() throws ExecutionException,
294301
mutationFuture.get();
295302
}
296303

304+
/**
305+
* This tests that scenarios where multiple JME applications are running simultaneously within the same
306+
* JMV. This is a weird case but does happen sometimes (e.g. in TestChooser).
307+
* <p>
308+
* This test tests that using two seperate executor services with their own seperate threads
309+
* </p>
310+
*/
311+
@Test
312+
public void testCanCopeWithMultipleSimultaneousRootThreads() throws ExecutionException, InterruptedException {
313+
Node rootNodeThread1 = new Node("root1");
314+
Node rootNodeThread2 = new Node("root2");
297315

316+
// Create a child node and attach it to the root node
317+
Node childThread1 = new Node("child1");
318+
Node childThread2 = new Node("child2");
319+
320+
// on two threads set up two root nodes (for two JME applications on the same VM)
321+
Future<Void> mutationFuture1 = executorService.submit(() -> {
322+
SceneGraphThreadWarden.setup(rootNodeThread1);
323+
rootNodeThread1.attachChild(childThread1);
324+
return null;
325+
});
326+
327+
Future<Void> mutationFuture2 = executorService2.submit(() -> {
328+
SceneGraphThreadWarden.setup(rootNodeThread2);
329+
rootNodeThread2.attachChild(childThread2);
330+
return null;
331+
});
332+
333+
// These should complete without exceptions, these are independent scene graphs
334+
mutationFuture1.get();
335+
mutationFuture2.get();
336+
337+
// try to use a child from one thread on the other, this should exception as the child is reserved
338+
expectSceneGraphException(executorService.submit(() -> {
339+
SceneGraphThreadWarden.setup(rootNodeThread1);
340+
rootNodeThread1.attachChild(childThread2);
341+
return null;
342+
}));
343+
}
344+
345+
/**
346+
* Where there are multiple JME roots this tests that is *is* allowed to remove a child from one root and attach it
347+
* to another as long as the detachments and reattachments are done in the right threads.
348+
*
349+
* <p>
350+
* This is a weird thing to do and unlikely to come up in practice but is allowed
351+
* </p>
352+
*/
353+
@Test
354+
public void testCanTransferNodeBetweenMultipleSimultaneousRootThreads() throws ExecutionException, InterruptedException {
355+
Node rootNodeThread1 = new Node("root1");
356+
Node rootNodeThread2 = new Node("root2");
357+
358+
// Create a child node and attach it to the root node
359+
Node childThread1 = new Node("child1");
360+
Node childThread2 = new Node("child2");
361+
Node childTransferred = new Node("childTransferred");
362+
363+
// on two threads set up two root nodes (for two JME applications on the same VM)
364+
Future<Void> mutationFuture1 = executorService.submit(() -> {
365+
SceneGraphThreadWarden.setup(rootNodeThread1);
366+
rootNodeThread1.attachChild(childThread1);
367+
rootNodeThread1.attachChild(childTransferred);
368+
return null;
369+
});
370+
371+
Future<Void> mutationFuture2 = executorService2.submit(() -> {
372+
SceneGraphThreadWarden.setup(rootNodeThread2);
373+
rootNodeThread2.attachChild(childThread2);
374+
return null;
375+
});
376+
377+
// These should complete without exceptions, these are independent scene graphs
378+
mutationFuture1.get();
379+
mutationFuture2.get();
380+
381+
Future<Void> legalRemovalFuture = executorService.submit(() -> {
382+
childTransferred.removeFromParent();
383+
return null;
384+
});
385+
386+
legalRemovalFuture.get();
387+
388+
Future<Void> legalAttachmentFuture = executorService2.submit(() -> {
389+
rootNodeThread2.attachChild(childTransferred);
390+
return null;
391+
});
392+
legalAttachmentFuture.get();
393+
}
298394

299395
/**
300396
* Creates a single-threaded executor service with daemon threads.
@@ -313,4 +409,17 @@ private static ThreadFactory daemonThreadFactory() {
313409
return t;
314410
};
315411
}
412+
413+
private void expectSceneGraphException(Future<Void> future){
414+
try {
415+
future.get();
416+
fail("Expected an IllegalThreadSceneGraphMutation exception");
417+
} catch (ExecutionException e) {
418+
// This is expected - verify it's the right exception type
419+
assertTrue("Expected IllegalThreadSceneGraphMutation, got: " + e.getCause().getClass().getName(),
420+
e.getCause() instanceof IllegalThreadSceneGraphMutation);
421+
} catch (InterruptedException e){
422+
fail("Unexpected InterruptedException");
423+
}
424+
}
316425
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.jmonkeyengine.screenshottests.testframework;
2+
3+
import com.jme3.app.state.AppState;
4+
5+
public class Scenario {
6+
String scenarioName;
7+
AppState[] states;
8+
9+
public Scenario(String scenarioName, AppState... states) {
10+
this.scenarioName = scenarioName;
11+
this.states = states;
12+
}
13+
}

0 commit comments

Comments
 (0)