Search code examples
libgdxlibktx

Async fields generation and TextureRegion rendering problem


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)
        }
    }

And it looks like this: enter image description here

But when I move addEmptyFields() before async section it is rendering correctly enter image description here

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?


Solution

  • 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:

    • Needs access to OpenGL context (e.g. texture loading).
    • Accesses or modifies data from the rendering thread.
    • Performs any kind of rendering or updates relying on the rendering delta time.

    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.