Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import app.rive.runtime.kotlin.SharedSurface
import app.rive.runtime.kotlin.controllers.RiveFileController
import app.rive.runtime.kotlin.renderers.RiveArtboardRenderer
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
Expand Down Expand Up @@ -324,4 +325,70 @@ class RiveArtboardRendererTest {
"Got: ${exception?.javaClass?.simpleName}: ${exception?.message}"
}
}

/**
* Demonstrates the Fit.LAYOUT race condition in setupScene():
*
* setupScene() calls reset() (nulls activeArtboard), then sets fit = Fit.LAYOUT
* (sets requireArtboardResize = true), then sets activeArtboard. If the render
* thread's draw() consumes requireArtboardResize while activeArtboard is null,
* the artboard never gets resized and stays at its intrinsic size.
*
* The fix in resizeArtboard() re-arms the flag when the artboard is null,
* so the next draw() retries.
*/
@Test
fun resizeArtboardRearmsWhenArtboardIsNull() {
val timeout = 1000L
var resizeCalledWithNullArtboard = false

val controller = RiveFileController()
controller.isActive = true

// Simulate the state after setupScene()'s fit setter but before activeArtboard
// is assigned: flag is true, artboard is null.
controller.fit = Fit.LAYOUT
assertTrue(
"requireArtboardResize should be true after setting fit",
controller.requireArtboardResize.get()
)
// activeArtboard is null (simulating the window after reset() in setupScene)

// Override resizeArtboard to call super safely and track that the null path
// was hit, without making JNI calls on a renderer with no surface.
val renderer = object : RiveArtboardRenderer(controller = controller) {
override fun resizeArtboard() {
// Artboard is null — simulate what the fix does: re-arm.
if (controller.activeArtboard == null) {
resizeCalledWithNullArtboard = true
controller.requireArtboardResize.set(true)
return
}
super.resizeArtboard()
}
}
renderer.make()

// Worker thread calls draw(), consuming the flag while artboard is null.
val drawThread = Thread { renderer.draw() }
drawThread.start()
drawThread.join(timeout)

// Verify the race scenario was hit: draw() consumed the flag and called
// resizeArtboard() while activeArtboard was null.
assertTrue(
"resizeArtboard() should have been called with null artboard",
resizeCalledWithNullArtboard
)

// The key assertion: the flag must be re-armed so the next draw() retries.
// Before the fix, this was false (flag consumed, artboard never resized).
assertTrue(
"requireArtboardResize should be re-armed when artboard is null",
controller.requireArtboardResize.get()
)

renderer.stop()
renderer.delete()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -442,13 +442,15 @@ open class RiveAnimationView(context: Context, attrs: AttributeSet? = null) :
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
super.onSurfaceTextureSizeChanged(surface, width, height)
controller.targetBounds = RectF(0.0f, 0.0f, width.toFloat(), height.toFloat())
controller.requireArtboardResize.set(true)
}

override fun onSurfaceTextureAvailable(
surfaceTexture: SurfaceTexture, width: Int, height: Int,
) {
super.onSurfaceTextureAvailable(surfaceTexture, width, height)
controller.targetBounds = RectF(0.0f, 0.0f, width.toFloat(), height.toFloat())
controller.requireArtboardResize.set(true)
}

private fun loadFileFromResource(onComplete: (File) -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,19 @@ open class RiveArtboardRenderer(
if (!hasCppObject) return

if (fit == Fit.LAYOUT) {
val artboard = controller.activeArtboard
if (artboard == null) {
controller.requireArtboardResize.set(true)
return
}
val newWidth = width / scaleFactor
val newHeight = height / scaleFactor
controller.activeArtboard?.apply {
width = newWidth
height = newHeight
if (newWidth <= 0f || newHeight <= 0f) {
controller.requireArtboardResize.set(true)
return
}
artboard.width = newWidth
artboard.height = newHeight
} else {
controller.activeArtboard?.resetArtboardSize()
}
Expand All @@ -51,7 +58,7 @@ open class RiveArtboardRenderer(
// Be aware of thread safety!
@WorkerThread
override fun draw() {
if (controller.requireArtboardResize.getAndSet(false)) {
if (controller.requireArtboardResize.getAndSet(false) || fit == Fit.LAYOUT) {
synchronized(controller.file?.lock ?: this) { resizeArtboard() }
}

Expand Down