Skip to content

Commit

Permalink
[citool] run MIDI input callback on kotlinx.coroutines UI thread.
Browse files Browse the repository at this point in the history
It is not very ideal, but we run some non-UI code in UI context...
Compose Compiler cannot seem to track JNI invocations.
We use RtMidi input callbacks that runs JVM code through its C(++) callbacks.
As a consequence, the Compose Compiler does not wrap any access to the
composable code with UI thread dispatcher, and Awt threading error occurs
on desktop.
Any MidiAccess implementation could involve JNI invocations (e.g.
AlsaMidiAccess) and the same kind of problem would occur.

The error that could be dumped by this `runInUIContext`, removing
`uiScope.launch { ...}` wrapper is like:

```
java.lang.IllegalStateException: Method should be called from AWT event dispatch thread
	at org.jetbrains.skiko.SkiaLayer.needRedraw(SkiaLayer.awt.kt:517)
	at androidx.compose.ui.awt.WindowComposeBridge.onComposeInvalidation(WindowComposeBridge.desktop.kt:108)
	at androidx.compose.ui.awt.ComposeBridge$scene$1.invoke(ComposeBridge.desktop.kt:164)
	at androidx.compose.ui.awt.ComposeBridge$scene$1.invoke(ComposeBridge.desktop.kt:163)
	at androidx.compose.ui.ComposeScene.invalidateIfNeeded(ComposeScene.skiko.kt:198)
	at androidx.compose.ui.ComposeScene.access$invalidateIfNeeded(ComposeScene.skiko.kt:68)
	at androidx.compose.ui.ComposeScene$snapshotChanges$1.invoke(ComposeScene.skiko.kt:359)
	at androidx.compose.ui.ComposeScene$snapshotChanges$1.invoke(ComposeScene.skiko.kt:359)
	at androidx.compose.ui.CommandList.add(CommandList.desktop.kt:51)
	at androidx.compose.ui.ComposeScene$attach$4.invoke(ComposeScene.skiko.kt:375)
	at androidx.compose.ui.ComposeScene$attach$4.invoke(ComposeScene.skiko.kt:375)
	at androidx.compose.ui.platform.SkiaBasedOwner$snapshotObserver$1.invoke(SkiaBasedOwner.skiko.kt:155)
	at androidx.compose.ui.platform.SkiaBasedOwner$snapshotObserver$1.invoke(SkiaBasedOwner.skiko.kt:154)
	at androidx.compose.runtime.snapshots.SnapshotStateObserver.sendNotifications(SnapshotStateObserver.kt:81)
	at androidx.compose.runtime.snapshots.SnapshotStateObserver.access$sendNotifications(SnapshotStateObserver.kt:41)
	at androidx.compose.runtime.snapshots.SnapshotStateObserver$applyObserver$1.invoke(SnapshotStateObserver.kt:48)
	at androidx.compose.runtime.snapshots.SnapshotStateObserver$applyObserver$1.invoke(SnapshotStateObserver.kt:46)
	at androidx.compose.runtime.snapshots.SnapshotKt.advanceGlobalSnapshot(Snapshot.kt:1825)
	at androidx.compose.runtime.snapshots.SnapshotKt.takeNewSnapshot(Snapshot.kt:1843)
	at androidx.compose.runtime.snapshots.SnapshotKt.access$takeNewSnapshot(Snapshot.kt:1)
	at androidx.compose.runtime.snapshots.GlobalSnapshot.takeNestedMutableSnapshot(Snapshot.kt:1352)
	at androidx.compose.runtime.snapshots.Snapshot$Companion.takeMutableSnapshot(Snapshot.kt:405)
	at androidx.compose.runtime.snapshots.Snapshot$Companion.takeMutableSnapshot$default(Snapshot.kt:401)
	at dev.atsushieno.ktmidi.citool.view.ViewModel$1.invoke(ViewModel.kt:96)
```
  • Loading branch information
atsushieno committed Dec 26, 2023
1 parent 3dc5046 commit 42317b2
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 15 deletions.
6 changes: 5 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
agp = "8.1.4"
agp = "8.2.0"
android-compileSdk = "34"
android-minSdk = "24"
android-targetSdk = "34"
Expand Down Expand Up @@ -42,6 +42,10 @@ alsakt = { module = "dev.atsushieno:alsakt", version.ref = "alsakt" }
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit-jupiter-api" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter-api" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines-core" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-core" }
kotlinx-coroutines-core-js = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-js", version.ref = "kotlinx-coroutines-core" }
kotlinx-coroutines-core-wasmjs = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-wasmjs", version.ref = "kotlinx-coroutines-core" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-core" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor-io" }
Expand Down
2 changes: 2 additions & 0 deletions midi-ci-tool/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ kotlin {
androidMain.dependencies {
implementation(libs.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.kotlinx.coroutines.android)
}
commonMain.dependencies {
implementation(compose.runtime)
Expand All @@ -62,6 +63,7 @@ kotlin {
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(project(":ktmidi-jvm-desktop"))
implementation(libs.kotlinx.coroutines.swing)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package dev.atsushieno.ktmidi.citool

import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import dev.atsushieno.ktmidi.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext

object AppModel {
val midiDeviceManager = MidiDeviceManager()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.atsushieno.ktmidi.citool
import dev.atsushieno.ktmidi.Midi1Status
import dev.atsushieno.ktmidi.MidiInput
import dev.atsushieno.ktmidi.ci.MidiCIConstants
import dev.atsushieno.ktmidi.citool.view.ViewModel

class CIDeviceManager(private val midiDeviceManager: MidiDeviceManager) {
var isResponder = false
Expand All @@ -19,17 +20,19 @@ class CIDeviceManager(private val midiDeviceManager: MidiDeviceManager) {

private fun setupInputEventListener(input: MidiInput) {
input.setMessageReceivedListener { data, start, length, _ ->
if (data.size > 3 &&
data[start] == Midi1Status.SYSEX.toByte() &&
data[start + 1] == MidiCIConstants.UNIVERSAL_SYSEX &&
data[start + 3] == MidiCIConstants.UNIVERSAL_SYSEX_SUB_ID_MIDI_CI) {
// it is a MIDI-CI message
// FIXME: maybe make it exclusive?
if (isResponder)
responder.processCIMessage(data.drop(start + 1).take(length - 2))
else
initiator.processCIMessage(data.drop(start + 1).take(length - 2))
return@setMessageReceivedListener
ViewModel.runInUIContext {
if (data.size > 3 &&
data[start] == Midi1Status.SYSEX.toByte() &&
data[start + 1] == MidiCIConstants.UNIVERSAL_SYSEX &&
data[start + 3] == MidiCIConstants.UNIVERSAL_SYSEX_SUB_ID_MIDI_CI
) {
// it is a MIDI-CI message
// FIXME: maybe make it exclusive?
if (isResponder)
responder.processCIMessage(data.drop(start + 1).take(length - 2))
else
initiator.processCIMessage(data.drop(start + 1).take(length - 2))
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,30 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.Snapshot
import dev.atsushieno.ktmidi.ci.*
import dev.atsushieno.ktmidi.citool.AppModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

object ViewModel {
private val uiScope = CoroutineScope(Dispatchers.Main)

fun runInUIContext(function: () -> Unit) {
// FIXME: It is not very ideal, but we run some non-UI code in UI context...
// Compose Compiler cannot seem to track JNI invocations.
// We use RtMidi input callbacks that runs JVM code through its C(++) callbacks.
// As a consequence, the Compose Compiler does not wrap any access to the composable code with
// UI thread dispatcher, and Awt threading error occurs on desktop.
// Any MidiAccess implementation could involve JNI invocations (e.g. AlsaMidiAccess) and
// the same kind of problem would occur.
// (You can remove `uiScope.launch {...}` wrapping part to replicate the issue.
try {
uiScope.launch { function() }
} catch(ex: Exception) {
ex.printStackTrace() // try to give full information, not wrapped by javacpp_Exception (C++, that hides everything)
throw ex
}
}

private var logText = mutableStateOf("")

val log: MutableState<String>
Expand Down

0 comments on commit 42317b2

Please sign in to comment.