I have a piece of code to render a grid from which I will render a maze. My build function:
fun buildAsync(): Deferred<RegularMaze> {
return KtxAsync.async(newSingleThreadAsyncContext()) {
addEmptyFields()
enableLeftBordersRender()
enableBottomBordersRender()
regularMazeService.convertFieldsToMaze(fields, colsNo, rowsNo)
}
}
But when I move addEmptyFields()
before async
section it is rendering correctly
And my Wall
class
class Wall (width: Float, height: Float, x: Float = 0F, y: Float = 0F, rotation: Float = 0F) : BasePart() {
private val textureRegion: TextureRegion
private val size: Size = Size(width, height)
private val position: Position = Position(x, y, rotation)
var relatedFieldIndex: Int? = null
var shouldBeDraw = true
init {
inject()
val wallTexture = assetsHelper.getTextureFromAsset(TextureAsset.WALL)
wallTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat)
textureRegion = TextureRegion(wallTexture, 0, 0, size.widthInt, size.heightInt)
setBounds(
position.x,
position.y,
width,
height
)
rotateBy(position.rotation)
}
override fun draw(batch: Batch, parentAlpha: Float) {
super.draw(batch, parentAlpha)
if (shouldBeDraw) {
batch.draw(textureRegion, position.x, position.y, 0F, 0F, size.width, size.height, 1F, 1F, position.rotation)
}
}
class Size (val width: Float, val height: Float) {
val widthInt: Int
get() = ceil(width).toInt()
val heightInt: Int
get() = ceil(height).toInt()
}
data class Position(val x: Float, val y: Float, val rotation: Float)
}
[EDIT/UPDATE]
I discover something strange, when I create a single Wall
without dimensions before async
it starts working. "Working" code:
fun buildAsync(): Deferred<RegularMaze> {
Wall(0f,0f) // <---- new line
return KtxAsync.async(newSingleThreadAsyncContext()) {
addEmptyFields()
enableLeftBordersRender()
enableBottomBordersRender()
regularMazeService.convertFieldsToMaze(fields, colsNo, rowsNo)
}
}
Why?
You are running into concurrency issues. The "fixed" code probably works by accident due to a slight delay in executing the coroutine or performing some crucial operation on the main thread, not because it addresses the actual problem.
As a rule of thumb, you should never modify anything that is accessed by the rendering thread on different thread pools. My guess is that something modifies or accesses the regularMazeService
while the coroutine is being executed, or the assetsHelper.getTextureFromAsset(TextureAsset.WALL)
fails to load the texture in a background thread as it lacks the OpenGL context.
If there is some expensive part of the operation that you want to do on a separate thread - that's OK, but anything that modifies the state of the rendering thread should be done on the rendering thread. For example:
fun buildAsync(): Deferred<RegularMaze> {
return KtxAsync.async(asyncContext) {
onRenderingThread {
addEmptyFields()
}
enableLeftBordersRender()
enableBottomBordersRender()
regularMazeService.convertFieldsToMaze(fields, colsNo, rowsNo)
}
}
If any part of your code executed via coroutine launched with a custom thread:
You should move it into the onRenderingThread
block.
Also, you should reuse coroutine contexts. Do not start a new thread for each coroutine with newSingleThreadAsyncContext()
- assign the AsyncContext
and reuse its instance.
If you are loading textures manually, I encourage you to use the AssetStorage
available in ktx-assets-async
module. It leverages coroutines for true parallel asset loading.