Client crashes when rendering is concurrent to level ticking
MartijnMuijsers opened this issue ยท 0 comments
When LiquidBlockRenderer.renderFluid
is called on a different thread than LevelHandler.levelTick
, the following occasionally happens leading to random crashes from time to time (more likely when many flowing water blocks are nearby due to the longer time that they are being processed):
- On the tick thread,
WaterfallCloudGenerators:78
begins computing, and ends up in the internal implementation of the stream operations, which are not thread-safe, and as a first step initializes the immutable non-resizable list to collect into, for which it usesGENERATORS.size
as size - On the render thread,
LiquidBlockRendererMixin:34
callsWaterfallCloudGenerators.addGenerator
and completes, changing the size ofWaterfallCloudGenerators.addGenerator.GENERATORS
- On the tick thread, The internal implementation of the stream operations processes elements of
GENERATORS
of which there is now 1 more than the previously obtained size, as such not fitting in the initialized list
This leads to the following crash:
java.lang.ArrayIndexOutOfBoundsException: Index 966 out of bounds for length 966
at java.util.stream.SortedOps$SizedRefSortingSink.accept(SortedOps.java:369) ~[?:?] {}
at java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1707) ~[?:?] {}
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[?:?] {}
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[?:?] {}
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[?:?] {}
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[?:?] {}
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) ~[?:?] {}
at bottomtextdanny.effective_fg.level.WaterfallCloudGenerators.tick(WaterfallCloudGenerators.java:78) ~[effective_fg-1.3.4.jar%23126!/:1.3] {re:mixin,re:classloading}
at bottomtextdanny.effective_fg.level.LevelHandler.levelTick(LevelHandler.java:10) ~[effective_fg-1.3.4.jar%23126!/:1.3] {re:classloading}
at net.minecraftforge.eventbus.EventBus.doCastFilter(EventBus.java:260) ~[eventbus-6.0.3.jar%23106!/:?] {}
at net.minecraftforge.eventbus.EventBus.lambda$addListener$11(EventBus.java:252) ~[eventbus-6.0.3.jar%23106!/:?] {}
at net.minecraftforge.eventbus.EventBus.post(EventBus.java:315) ~[eventbus-6.0.3.jar%23106!/:?] {}
at net.minecraftforge.eventbus.EventBus.post(EventBus.java:296) ~[eventbus-6.0.3.jar%23106!/:?] {}
at net.minecraftforge.event.ForgeEventFactory.onPostClientTick(ForgeEventFactory.java:803) ~[forge-1.19.2-43.1.1-universal.jar%23138!/:?] {re:classloading}
...
Solution:
Around each usage of GENERATORS
, use synchronized (GENERATORS)
- this fixes the issue.
But implementation can be made slightly more optimal:
- Instead of synchronizing around the entire
removeIf
block, synchronize overGENERATORS
and clone it. Then, outside of synchronization, filter is to get the elements to remove. Then synchronize overGENERATORS
and remove all elements to remove. - Instead of synchronizing around the entire sorting block, synchronize over
GENERATORS
and clone it. Then, outside of synchronization, sort it.