Accidentally reproduced NPE in synchronizeNodes in combination with enterNestedEventLoop

John Hendrikx john.hendrikx at gmail.com
Thu Aug 22 03:24:49 UTC 2024


Hi List,

This is a bit of a long post. I'm mainly wondering if I did something 
wrong that FX should detect early, or if I'm doing nothing unusual and 
FX should handle the case described below correctly.

I encountered the bug where an NPE occurs in 
Scene$ScenePulseListener#synchronizeNodes, and it is reproducable at the 
moment. I'm not sure it is the same one others sometimes see, but the 
version I encountered is prefaced with a failing assert (which may 
easily get lost as synchronizeNodes will spam NPE's in your log, as it 
doesn't recover).

In Parent#validatePG it prints:

     *** pgnodes.size validatePG() [1] != children.size() [2]

And then throws an AssertionError with a long stacktrace.

     java.lang.AssertionError: validation of PGGroup children failed
         (stack trace omitted)

Immediately after this, the NPE in synchronizeNodes starts to get 
spammed, and the application never recovers.

This seems to have something to do with nested event loops, as I 
introduced a new one in the code involved.  When I remove the nested 
event loop, there is no problem (it also initially works fine when the 
application is just starting, only in a different situation, when there 
is some user interaction via a keypress, does the bug trigger).

The nested event loop is entered from a ChangeListener, which may be a 
bit unusual.  The documentation of Platform#enterNestedEventLoop says:

* This method must either be called from an input event handler or

* from the run method of a Runnable passed to

* {@link javafx.application.Platform#runLater Platform.runLater}.

* It must not be called during animation or layout processing.

It is also documented to throw an IllegalStateException if used 
incorrectly.  That is however not happening, so I guess I'm using it 
correctly...?  On the other hand, I'm not in an input event 
handler.... the whole process is triggered by a keypress though, and 
deep down in the AssertionError trace you can see:

     at 
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
     at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
     at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
     at javafx.event.Event.fireEvent(Event.java:198)
     at javafx.scene.Scene$KeyHandler.process(Scene.java:4113)
     at javafx.scene.Scene.processKeyEvent(Scene.java:2159)
     at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2627)
     at 
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218)

So IMHO technically I am in an input event handler...?

Now the code does a LOT of stuff, which is going to make this tough to 
analyze.  In short:

- I'm displaying a Scene
- The user indicates (with a keypress) they want to go to a different Pane
- The previous Pane is created, and added to the scene as a child to a 
TransitionPane that will cross-fade between the current Pane and the new 
Pane
- As soon as the new Pane becomes part of the Scene, lots of things get 
triggered because the Scene property goes from null to the active Scene:

     at javafx.scene.Node.invalidatedScenes(Node.java:1075)
     at javafx.scene.Node.setScenes(Node.java:1142) <-- the new Pane 
will get a Scene assigned to it
     at javafx.scene.Parent$2.onChanged(Parent.java:372) <-- the new 
Pane has its Parent changed to the TransitionPane

- Several parts of the new Pane listen (directly or indirectly) to a 
change in Scene, as they only become "active" when the Node involved is 
displayed (this happens with a Scene listener)
- One of those things is some code that will create and populate a 
ListView; this code uses a relatively fast query, but I had marked it as 
something that should be done in the background as it sometimes does 
take a bit too much time on the FX thread

Now, how I normally do things in the background is to display an 
automatically closing "Please wait" dialog potentially with a progress 
bar on it (this dialog is actually invisible, unless sufficient time 
passes, so in 99% of the cases when things respond fast, the user never 
sees it, even though it was created and is there).  This involves 
starting a nested event loop.  This works marvelously throughout this 
application (and has done so for years), and it is used for almost every 
transition from one part of the application to the next.  In all cases 
so far however I did this directly from an event handler.

So the main difference is that I'm trying to enter a nested event loop 
from a ChangeListener (which deep down was triggered by an Event).  In 
the AssertionError stack trace (which I will include at the end), there 
is no layout or animation code **before** entering the nested loop, 
although there is some layout code **after** it was entered.

I can live with the fact that I may be using enterNestedEventLoop 
incorrectly here, but in that case it should probably also detect this 
incorrect use and throw the IllegalStateException.

Technically, all this code is triggered while "adding" a Child to the 
TransitionPane, so I suspect that is what the AssertionError is about 
(it indicates the child count 1 != 2, which is what TransitionPane has, 
one active pane, and just added to cross fade to).  Still, is this 
really incorrect usage?

I've included an annotated stack trace below.

As it is quite reproducable, I can debug this further by adding 
breakpoints/prints -- I'm just unsure where to start looking.

--John

java.lang.AssertionError: validation of PGGroup children failed
   at javafx.scene.Parent.validatePG(Parent.java:243)
   at javafx.scene.Parent.doUpdatePeer(Parent.java:201)
   at javafx.scene.Parent$1.doUpdatePeer(Parent.java:109)
   at com.sun.javafx.scene.ParentHelper.updatePeerImpl(ParentHelper.java:78)
   at 
com.sun.javafx.scene.layout.RegionHelper.updatePeerImpl(RegionHelper.java:72)
   at com.sun.javafx.scene.NodeHelper.updatePeer(NodeHelper.java:104)
   at javafx.scene.Node.syncPeer(Node.java:721)
   at 
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2396)
   at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)
   at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)
   at 
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
   at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)
   at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:436)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)
   at 
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
   at 
com.sun.glass.ui.win.WinApplication._enterNestedEventLoopImpl(Native Method)
   at 
com.sun.glass.ui.win.WinApplication._enterNestedEventLoop(WinApplication.java:211)
   at 
com.sun.glass.ui.Application.enterNestedEventLoop(Application.java:515)
   at com.sun.glass.ui.EventLoop.enter(EventLoop.java:107)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.enterNestedEventLoop(QuantumToolkit.java:631)
   at javafx.application.Platform.enterNestedEventLoop(Platform.java:301)
   at 
hs.mediasystem.util.javafx.SceneUtil.enterNestedEventLoop(SceneUtil.java:75) 
<-- just my wrapper to trace slow calls, it delegates to Platform
   at hs.mediasystem.runner.dialog.DialogPane.showDialog(DialogPane.java:78)
   at 
hs.mediasystem.runner.dialog.Dialogs.showProgressDialog(Dialogs.java:163)
   at hs.mediasystem.runner.dialog.Dialogs.runNested(Dialogs.java:103)
   at 
hs.mediasystem.plugin.home.HomeScreenNodeFactory.lambda$1(HomeScreenNodeFactory.java:123) 


The above line is where the nested event loop is entered.  It starts to 
display a "busy" dialog. A background task (on a new thread) will create 
a ListView (this never actually happens, the AssertionError is thrown 
immediately even with an empty task that just sleeps 10 seconds).

   at 
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:360)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at 
javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:80)
   at 
javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
   at 
javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:118)
   at 
javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:172)
   at 
javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:105)
   at 
javafx.scene.control.MultipleSelectionModelBase.lambda$new$0(MultipleSelectionModelBase.java:67)
   at 
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at 
javafx.beans.property.ReadOnlyIntegerPropertyBase.fireValueChangedEvent(ReadOnlyIntegerPropertyBase.java:78)
   at 
javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:102)
   at 
javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:114)
   at 
javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:148)
   at 
javafx.scene.control.SelectionModel.setSelectedIndex(SelectionModel.java:69)
   at 
javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:424)
   at 
javafx.scene.control.MultipleSelectionModelBase.select(MultipleSelectionModelBase.java:456)
   at 
hs.mediasystem.plugin.home.HomeScreenNodeFactory.lambda$3(HomeScreenNodeFactory.java:176) 
<-- triggers the creation of a ListView
   at javafx.beans.value.ObservableValue.lambda$0(ObservableValue.java:364)
   at 
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
   at 
com.sun.javafx.binding.ConditionalBinding.conditionChanged(ConditionalBinding.java:53)
   at com.sun.javafx.binding.Subscription.lambda$2(Subscription.java:63)
   at 
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
   at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
   at 
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
   at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
   at 
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
   at 
com.sun.javafx.binding.FlatMappedBinding.invalidateAll(FlatMappedBinding.java:102)
   at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
   at 
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
   at 
com.sun.javafx.binding.FlatMappedBinding.invalidateAll(FlatMappedBinding.java:102)
   at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
   at 
com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:348)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at 
javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:80)
   at 
javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
   at 
javafx.scene.Node$ReadOnlyObjectWrapperManualFire.fireSuperValueChangedEvent(Node.java:1053) 
<-- there is a listener on a Scene property
   at javafx.scene.Node.invalidatedScenes(Node.java:1104)
   at javafx.scene.Node.setScenes(Node.java:1142)
   at javafx.scene.Parent.scenesChanged(Parent.java:772)    <-- the new 
Pane has a few nested Panes, so you see multiple Parent assignments
   at javafx.scene.Node.invalidatedScenes(Node.java:1075)
   at javafx.scene.Node.setScenes(Node.java:1142)
   at javafx.scene.Parent.scenesChanged(Parent.java:772)
   at javafx.scene.Node.invalidatedScenes(Node.java:1075)
   at javafx.scene.Node.setScenes(Node.java:1142)            <-- the 
Parent is part of a Scene, so this new Pane also gets the same Scene 
assigned
   at javafx.scene.Parent$2.onChanged(Parent.java:372)    <-- the new 
Pane gets its parent assigned (TransitionPane)
   at 
com.sun.javafx.collections.TrackableObservableList.lambda$new$0(TrackableObservableList.java:45)
   at 
com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)
   at 
com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
   at 
javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:239)
   at 
javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)
   at 
javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
   at 
javafx.collections.ObservableListBase.endChange(ObservableListBase.java:211)
   at 
javafx.collections.ModifiableObservableListBase.add(ModifiableObservableListBase.java:162)
   at 
com.sun.javafx.collections.VetoableListDecorator.add(VetoableListDecorator.java:319)
   at 
hs.mediasystem.util.javafx.ui.transition.TransitionPane.add(TransitionPane.java:94) 
<-- new Pane is added to a TransitionPane that handles cross fade 
between old and new Pane
   at 
hs.mediasystem.util.javafx.ui.transition.TransitionPane.addAtStart(TransitionPane.java:105)
   at 
hs.mediasystem.util.javafx.ui.transition.TransitionPane.add(TransitionPane.java:113)
   at 
hs.mediasystem.runner.presentation.ViewPort.updateChildNode(ViewPort.java:68) 
<-- here it installs the new Pane to display
   at hs.mediasystem.runner.presentation.ViewPort.lambda$0(ViewPort.java:39)
   at 
com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at javafx.beans.binding.ObjectBinding.invalidate(ObjectBinding.java:192)
   at com.sun.javafx.binding.Subscription.lambda$4(Subscription.java:83)
   at 
com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:136)
   at 
com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
   at 
javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:111)
   at 
javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:118)
   at 
javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:172)
   at 
hs.mediasystem.presentation.ParentPresentation.navigateBack(ParentPresentation.java:39) 
<-- this handles the keyPress (it was a "back" press)
   at hs.mediasystem.util.expose.Expose.lambda$3(Expose.java:55)
   at hs.mediasystem.util.expose.Trigger$1.run(Trigger.java:58)
   at 
hs.mediasystem.runner.RootPresentationHandler.tryRunAction(RootPresentationHandler.java:106)
   at 
hs.mediasystem.runner.RootPresentationHandler.handleActionEvent(RootPresentationHandler.java:81)
   at 
com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247)
   at 
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
   at 
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)
   at 
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
   at 
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
   at 
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
   at hs.mediasystem.util.javafx.SceneUtil.lambda$1(SceneUtil.java:101) 
<-- this is just my Slow Event detection, and just delegates the event
   at 
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
   at 
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
   at 
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
   at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
   at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
   at hs.mediasystem.util.javafx.base.Events.dispatchEvent(Events.java:35)
   at 
hs.mediasystem.runner.action.InputActionHandler.handleKeyEvent(InputActionHandler.java:153) 
<-- just code that handles a keypress that bubbled all the way to the top
   at 
hs.mediasystem.runner.action.InputActionHandler.onKeyPressed(InputActionHandler.java:138)
   at 
com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
   at 
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:234)
   at 
com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
   at 
com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
   at 
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
   at hs.mediasystem.util.javafx.SceneUtil.lambda$1(SceneUtil.java:101) 
<-- this is just my Slow Event detection, and just delegates the event
   at 
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
   at 
com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
   at 
com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
   at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
   at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
   at javafx.event.Event.fireEvent(Event.java:198)
   at javafx.scene.Scene$KeyHandler.process(Scene.java:4113)
   at javafx.scene.Scene.processKeyEvent(Scene.java:2159)
   at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2627)
   at 
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218)
   at 
com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:150)
   at 
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
   at 
com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$1(GlassViewEventHandler.java:250)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424)
   at 
com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:249)
   at com.sun.glass.ui.View.handleKeyEvent(View.java:542)
   at com.sun.glass.ui.View.notifyKey(View.java:966)
   at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
   at 
com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
   at java.base/java.lang.Thread.run(Thread.java:1583)

And the NPE exception:

java.lang.NullPointerException: Cannot invoke 
"javafx.scene.Node.getScene()" because "<local2>" is null
     at 
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2395)
     at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)
     at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)
     at 
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
     at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)
     (rest of trace identical to the AssertionError one)

And another variant of the NPE exception:

java.lang.NullPointerException: Cannot invoke 
"javafx.scene.Node.getScene()" because "<local2>" is null
   at 
javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2395)
   at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2542)
   at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:407)
   at 
java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
   at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:406)
   at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:436)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548)
   at 
com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352)
   at 
com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
   at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
   at 
com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
   at java.base/java.lang.Thread.run(Thread.java:1583)




-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20240822/0b2d3a8b/attachment-0001.htm>


More information about the openjfx-dev mailing list