louiscad
05/05/2025, 9:55 PMImageComposeScene renders one frame late, or just renders wrong?
I am using this composable where I call render repeatedly on the same ImageComposeScene , but with ever increasing nanoTime values, and I only get the correct render if I call render a second time, which is quite of a dirty workaround.
@Composable
fun JustFrameNumberComposable() {
val frameNumber by produceState(0L) {
while (true) withFrameMillis { frameTimeMillis ->
value = frameTimeMillis * 60L / 1000L
}
}
Text("Frame number: $frameNumber")
}Alexander Maryanovsky
05/06/2025, 8:03 AMAlexander Maryanovsky
05/06/2025, 8:04 AMlouiscad
05/06/2025, 8:04 AMlouiscad
05/06/2025, 8:07 AMAlexander Maryanovsky
05/06/2025, 8:09 AMlouiscad
05/06/2025, 8:10 AMAlexander Maryanovsky
05/06/2025, 8:11 AMlouiscad
05/06/2025, 8:12 AMlouiscad
05/06/2025, 8:12 AMAlexander Maryanovsky
05/06/2025, 8:13 AMlouiscad
05/12/2025, 12:07 AMApplication.desktop.kt?
private object YieldFrameClock : MonotonicFrameClock {
override suspend fun <R> withFrameNanos(
onFrame: (frameTimeNanos: Long) -> R
): R {
// We call `yield` to avoid blocking UI thread. If we don't call this then application
// can be frozen for the user in some cases as it will not receive any input events.
//
// Swing dispatcher will process all pending events and resume after `yield`.
yield()
return onFrame(System.nanoTime())
}
}
To me, the yield() should come after onFrame(…) in a finally block, or after with the result of onFrame being put in a local val.Alexander Maryanovsky
05/12/2025, 6:45 AMAlexander Maryanovsky
05/19/2025, 10:33 AMsetContent call already runs the 1st composition (the one with frameNumber=0).
Then each ImageComposeScene.render call correctly calls the withFrameMillis lambda and then recomposes.
This code seems to work as expected:
fun main() {
val scene = ImageComposeScene(640, 480)
scene.setContent {
val frame by produceState(0L) {
while (true) {
withFrameNanos {
value = it
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
contentAlignment = Alignment.Center
) {
Text("Frame: $frame")
}
}
for (frameNumber in 1..10) {
val image = scene.render(frameNumber.toLong())
image.encodeToData()?.let { data ->
val file = File("frame-$frameNumber.png")
file.writeBytes(data.bytes)
}
}
scene.close()
}
i.e. each frame-N.png file contains a picture with “Frame: N” text.Alexander Maryanovsky
05/19/2025, 11:52 AMwithFrameNanos/Millis is actually called first, before recomposition.Alexander Maryanovsky
05/19/2025, 12:21 PMAlexander Maryanovsky
05/19/2025, 12:24 PMMainUIDispatcher).louiscad
05/19/2025, 6:45 PMproduceState builder (from a parent, visible composition)?Alexander Maryanovsky
05/19/2025, 6:49 PMAlexander Maryanovsky
05/19/2025, 6:53 PMlouiscad
05/19/2025, 7:28 PMrunBlocking(MainUiDispatcher) { ... } or alike?Alexander Maryanovsky
05/19/2025, 7:31 PMlouiscad
05/19/2025, 7:37 PMImageScene and the Compose machinery, as I get the ImageScene.render function to sometimes output another composable that is in the app.louiscad
05/19/2025, 7:41 PMlouiscad
05/19/2025, 7:41 PMlouiscad
05/19/2025, 7:42 PMMainUiDispatcher)Alexander Maryanovsky
05/19/2025, 7:50 PMAlexander Maryanovsky
05/19/2025, 7:50 PMlouiscad
05/19/2025, 7:51 PMAlexander Maryanovsky
05/19/2025, 7:51 PMlouiscad
05/19/2025, 7:51 PMlouiscad
05/19/2025, 7:51 PMlouiscad
05/19/2025, 7:52 PM@RequiresOptIn guard for ImageComposeScene, because I really didn't expect that gotcha, that one discovers far into the project…Alexander Maryanovsky
05/19/2025, 7:55 PMAlexander Maryanovsky
05/19/2025, 7:56 PMlouiscad
05/19/2025, 7:56 PMlouiscad
05/19/2025, 7:57 PMAlexander Maryanovsky
05/19/2025, 7:57 PMAlexander Maryanovsky
05/19/2025, 7:57 PMAlexander Maryanovsky
05/19/2025, 7:58 PMlouiscad
05/19/2025, 7:58 PMAlexander Maryanovsky
05/19/2025, 7:58 PMlouiscad
05/19/2025, 7:58 PMImageComposeScene gets updated to work just like a window, it'd avoid those issues too, wouldn't it?Alexander Maryanovsky
05/19/2025, 7:59 PMlouiscad
05/19/2025, 8:00 PMImageComposeScene was used from MainUiDispatcher blob thinking fastlouiscad
05/19/2025, 8:01 PMcoroutineContext to coroutineContext = coroutineContext[ContinuationInterceptor]!!, which is now MainUiDispatcher too.Alexander Maryanovsky
05/19/2025, 8:01 PMlouiscad
05/19/2025, 8:02 PMrender on Dispatchers.Defaultlouiscad
05/19/2025, 8:12 PMImageComposeScene is instantiated (and closed) on MainUiDispatcher AND render is called there too.
Maybe just calling render there is enough blob thinking fast
Anyway, that's fixing my issue, and performance doesn't seem to suffer significantly, which makes sense since the call to render was the fastest thing in the pipeline (the slowest things being encoding the image to WEBP, and generating a video from those)louiscad
05/19/2025, 8:13 PMImageComposeScene throw when not invoked from the right thread could get a chance of being merged?Alexander Maryanovsky
05/19/2025, 8:56 PMlouiscad
05/19/2025, 10:16 PM@RequiresOptIn that warns about that too?
Otherwise, it's very likely to go unnoticed, and you know that in Kotlin, we're not used to these kinds of gotchas from Kotlin made libraries/primitives, especially first party ones.Alexander Maryanovsky
05/20/2025, 5:06 AMAlexander Maryanovsky
05/20/2025, 5:13 AMImageComposeScene constructor is marked as @ExperimentalComposeUiApi for some reason, but the secondary constructor, and renderComposeScene aren’t.
Not sure why the 2nd constructor is even needed. I added it here.Igor Demin
05/20/2025, 10:43 AMIs that the appropriate annotation for something like that? @Igor Demin What do you think?It can be
DelicateComposeUiApi (similar to DelicateCoroutinesApi).
But I would rather fix the issue of multi-threading. Because adding this annotation isn't easy - we should add to AOSP, as ComposeScene is planned to be upstreamed, and it will cause source-incompatibilityAlexander Maryanovsky
05/20/2025, 10:49 AMAlexander Maryanovsky
05/20/2025, 10:50 AMlouiscad
05/21/2025, 9:18 AMExperimentalComposeUiApi would be more accurate than DelicateComposeUiApi?louiscad
05/21/2025, 9:20 AMAlexander Maryanovsky
05/21/2025, 9:22 AMlouiscad
05/21/2025, 9:27 AMImageComposeScene wasn't available on Android, but only on other platforms that JetBrains supports blob thinking upside downlouiscad
05/21/2025, 9:28 AM@ExperimentalComposeUiApi annotation on the other constructors, no?Alexander Maryanovsky
05/21/2025, 9:28 AMAlexander Maryanovsky
05/21/2025, 9:29 AMAlexander Maryanovsky
05/21/2025, 9:29 AMlouiscad
05/21/2025, 9:29 AMlouiscad
05/21/2025, 9:33 AM@ExperimentalComposeUiApi is already used in this file, why not use it for other constructors, and also for render?
I mean, that's probably the simplest kind of change one can make to Compose, besides improving the KDoc. Just doing it looks faster than debating the possibility of introducing another annotation, and then ask other people who have other priorities than specifics of what is essentially a warning label.Igor Demin
05/21/2025, 11:22 AMlouiscad
05/22/2025, 8:25 PM@DelicateSomething annotation instead?Igor Demin
05/22/2025, 8:59 PMExperimentalComposeUiApi, because it doesn't tell much, users still miss the threading issue if they don't read the doc, and it requires deprecation of the old API
I don't prefer DelicateSomething, because in the end the issue is just bugs we need to fix someday.