From 0858b8455c12a55006b7d63d951f7d19e06121fa Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Sun, 9 Jan 2022 10:51:21 +0100 Subject: [PATCH] [orx-compute-graph] Add compute graph code --- build.gradle | 7 +- orx-compute-graph-nodes/README.md | 22 +++ orx-compute-graph-nodes/build.gradle.kts | 97 ++++++++++ .../src/commonMain/kotlin/FilterNode.kt | 27 +++ .../src/commonMain/kotlin/FitImageNode.kt | 29 +++ .../src/jvmMain/kotlin/DrawCacheNode.kt | 99 ++++++++++ .../src/jvmMain/kotlin/DropImageNode.kt | 28 +++ orx-compute-graph/README.md | 55 ++++++ orx-compute-graph/build.gradle.kts | 91 +++++++++ .../src/commonMain/kotlin/ComputeGraph.kt | 175 ++++++++++++++++++ settings.gradle | 2 + 11 files changed, 629 insertions(+), 3 deletions(-) create mode 100644 orx-compute-graph-nodes/README.md create mode 100644 orx-compute-graph-nodes/build.gradle.kts create mode 100644 orx-compute-graph-nodes/src/commonMain/kotlin/FilterNode.kt create mode 100644 orx-compute-graph-nodes/src/commonMain/kotlin/FitImageNode.kt create mode 100644 orx-compute-graph-nodes/src/jvmMain/kotlin/DrawCacheNode.kt create mode 100644 orx-compute-graph-nodes/src/jvmMain/kotlin/DropImageNode.kt create mode 100644 orx-compute-graph/README.md create mode 100644 orx-compute-graph/build.gradle.kts create mode 100644 orx-compute-graph/src/commonMain/kotlin/ComputeGraph.kt diff --git a/build.gradle b/build.gradle index bc35ab54..1ee97642 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,8 @@ def multiplatformModules = [ "orx-camera", "orx-color", "orx-compositor", + "orx-compute-graph", + "orx-compute-graph-nodes", "orx-easing", "orx-fx", "orx-gradient-descent", @@ -41,6 +43,7 @@ project.ext { kotlinApiVersion = '1.6' kotlinLanguageVersion = '1.6' kotlinVersion = '1.6.10' + kotlinxCoroutinesVersion = '1.6.0' kotlinLoggingVersion = '2.1.15' kotlinxSerializationVersion = '1.3.1' spekVersion = '2.0.15' @@ -323,10 +326,8 @@ configure(allprojects.findAll { !doNotPublish.contains(it.name) && !multiplatfor } dependencies { - // Note: kotlin-logging and kotlinx-coroutines-core are loaded - // too early and their versions cannot be parametrized implementation 'io.github.microutils:kotlin-logging:2.1.10' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-RC3' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.openrndr:openrndr-application:$openrndrVersion" diff --git a/orx-compute-graph-nodes/README.md b/orx-compute-graph-nodes/README.md new file mode 100644 index 00000000..9ef40d38 --- /dev/null +++ b/orx-compute-graph-nodes/README.md @@ -0,0 +1,22 @@ +# orx-compute-graph-nodes + +A collection of nodes that can be used with orx-computer-graph. + +## List of nodes + +### Multi-platform + +Name | Description | Inputs | Outputs +----------------|-----------------------|--------|--------- +`filterNode` | Wrap around a `Filter`| | `image` +`fitImageNode` | Fit image to window bounds | `image` | `image` + +### JVM only + +Name | Description | Inputs | Outputs +----------------|-------------------|--------|--------- +`drawCacheNode` | Cache drawing in an internal color buffer, commonly used as the final stage node | | `image` +`dropImageNode` | Listen for window file drop events | | `image` + + + diff --git a/orx-compute-graph-nodes/build.gradle.kts b/orx-compute-graph-nodes/build.gradle.kts new file mode 100644 index 00000000..f08ce010 --- /dev/null +++ b/orx-compute-graph-nodes/build.gradle.kts @@ -0,0 +1,97 @@ +import EmbedShadersTask + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") +} + +val kotlinxSerializationVersion: String by rootProject.extra +val kotestVersion: String by rootProject.extra +val junitJupiterVersion: String by rootProject.extra +val jvmTarget: String by rootProject.extra +val kotlinApiVersion: String by rootProject.extra +val kotlinVersion: String by rootProject.extra +val kotlinLoggingVersion: String by rootProject.extra +val kluentVersion: String by rootProject.extra +val openrndrVersion: String by rootProject.extra +val openrndrOS: String by rootProject.extra +val spekVersion: String by rootProject.extra + +kotlin { + jvm { + compilations { + val demo by creating { + defaultSourceSet { + kotlin.srcDir("src/demo") + dependencies { + implementation(project(":orx-camera")) + implementation("org.openrndr:openrndr-application:$openrndrVersion") + implementation("org.openrndr:openrndr-extensions:$openrndrVersion") + runtimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion") + runtimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion") + implementation(compilations["main"]!!.output.allOutputs) + } + } + } + } + compilations.all { + kotlinOptions.jvmTarget = jvmTarget + kotlinOptions.apiVersion = kotlinApiVersion + } + testRuns["test"].executionTask.configure { + useJUnitPlatform() + } + } + js(IR) { + browser() + nodejs() + } + + sourceSets { + @Suppress("UNUSED_VARIABLE") + val commonMain by getting { + dependencies { + implementation(project(":orx-parameters")) + implementation(project(":orx-shader-phrases")) + implementation(project(":orx-compute-graph")) + implementation(project(":orx-image-fit")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$kotlinxSerializationVersion") + implementation("org.openrndr:openrndr-application:$openrndrVersion") + implementation("org.openrndr:openrndr-draw:$openrndrVersion") + implementation("org.openrndr:openrndr-filter:$openrndrVersion") + implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") + implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") + } + } + @Suppress("UNUSED_VARIABLE") + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + implementation("io.kotest:kotest-assertions-core:$kotestVersion") + } + } + + @Suppress("UNUSED_VARIABLE") + val jvmTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation(kotlin("test-junit5")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + runtimeOnly("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion") + runtimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion") + implementation("org.spekframework.spek2:spek-dsl-jvm:$spekVersion") + implementation("org.amshove.kluent:kluent:$kluentVersion") + } + } + + @Suppress("UNUSED_VARIABLE") + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + } +} \ No newline at end of file diff --git a/orx-compute-graph-nodes/src/commonMain/kotlin/FilterNode.kt b/orx-compute-graph-nodes/src/commonMain/kotlin/FilterNode.kt new file mode 100644 index 00000000..03d09b44 --- /dev/null +++ b/orx-compute-graph-nodes/src/commonMain/kotlin/FilterNode.kt @@ -0,0 +1,27 @@ +package org.openrndr.extra.computegraph.nodes + +import org.openrndr.draw.ColorBuffer +import org.openrndr.draw.Filter +import org.openrndr.draw.createEquivalent +import org.openrndr.extra.computegraph.ComputeGraph +import org.openrndr.extra.computegraph.ComputeNode +import org.openrndr.extra.computegraph.withKey + +fun ComputeGraph.filterNode( + filter: T, input: ComputeNode, inputKey: String = "image", outputKey: String = "image", + config: ComputeNode.(f: Filter) -> Unit +): ComputeNode { + return node { + name = "filter-${filter::class.simpleName}" + inputs = filter.parameters + config(filter) + val inputImage by input.outputs.withKey(inputKey) + var outputImage by outputs.withKey(outputKey) + outputImage = inputImage.createEquivalent() + compute { + filter.apply(inputImage, outputImage) + } + dependOn(input) + + } +} \ No newline at end of file diff --git a/orx-compute-graph-nodes/src/commonMain/kotlin/FitImageNode.kt b/orx-compute-graph-nodes/src/commonMain/kotlin/FitImageNode.kt new file mode 100644 index 00000000..75ee74cf --- /dev/null +++ b/orx-compute-graph-nodes/src/commonMain/kotlin/FitImageNode.kt @@ -0,0 +1,29 @@ +package org.openrndr.extra.computegraph.nodes + +import org.openrndr.Program +import org.openrndr.draw.ColorBuffer +import org.openrndr.draw.isolatedWithTarget +import org.openrndr.draw.renderTarget +import org.openrndr.extra.computegraph.ComputeGraph +import org.openrndr.extra.computegraph.ComputeNode +import org.openrndr.extra.computegraph.withKey +import org.openrndr.extras.imageFit.imageFit + +fun ComputeGraph.fitImageNode(program: Program, input: ComputeNode) : ComputeNode { + return node { + name = "fit-image" + val rt = renderTarget(program.width, program.height) { + colorBuffer() + } + val inputImage: ColorBuffer by input.outputs.withKey("image") + var outputImage:ColorBuffer by outputs.withKey("image") + outputImage = rt.colorBuffer(0) + compute { + program.drawer.isolatedWithTarget(rt) { + ortho(rt) + imageFit(inputImage, bounds) + } + } + dependOn(input) + } +} \ No newline at end of file diff --git a/orx-compute-graph-nodes/src/jvmMain/kotlin/DrawCacheNode.kt b/orx-compute-graph-nodes/src/jvmMain/kotlin/DrawCacheNode.kt new file mode 100644 index 00000000..f9ae57cd --- /dev/null +++ b/orx-compute-graph-nodes/src/jvmMain/kotlin/DrawCacheNode.kt @@ -0,0 +1,99 @@ +package org.openrndr.extra.computegraph.nodes + +import mu.KotlinLogging +import org.openrndr.KEY_SPACEBAR +import org.openrndr.Program +import org.openrndr.RequestAssetsEvent +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* +import org.openrndr.extra.computegraph.ComputeGraph +import org.openrndr.extra.computegraph.ComputeNode +import org.openrndr.extra.computegraph.withKey +import java.io.File + +val logger = KotlinLogging.logger { } + +private data class RenderTargetDescription(val width: Int, val height: Int, val contentScale: Double) + +private fun RenderTarget.description() = RenderTargetDescription(width, height, contentScale) + + +fun ComputeGraph.drawCacheNode( + program: Program, + inputNodes: List, + draw: Program.(node: ComputeNode) -> Unit +): ComputeNode { + return node { + var producingAssets: Boolean by inputs + producingAssets = false + + program.keyboard.keyDown.listen { + if (!it.propagationCancelled) { + logger.info { "requesting assets" } + if (it.key == KEY_SPACEBAR) { + program.requestAssets.trigger(RequestAssetsEvent(this, program)) + } + } + } + + var screenshotTarget = "" + program.produceAssets.listen { + producingAssets = true + screenshotTarget = "screenshots/${it.assetMetadata.assetBaseName}.png" + } + + name = "draw-cache" + var rt = renderTarget(program.width, program.height) { + colorBuffer() + depthBuffer() + } + var outputImage: ColorBuffer by outputs.withKey("image") + outputImage = rt.colorBuffer(0) + + var description: RenderTargetDescription by inputs + description = RenderTarget.active.description() + + update { + description = RenderTarget.active.description() + } + val defaultContentScale = program.window.contentScale + + compute { + rt.colorBuffer(0).destroy() + rt.depthBuffer?.destroy() + rt.detachColorAttachments() + rt.detachDepthBuffer() + rt.destroy() + rt = renderTarget( + program.width, + program.height, + contentScale = if (producingAssets) 6.0 else defaultContentScale + ) { + colorBuffer() + depthBuffer() + } + program.drawer.isolatedWithTarget(rt) { + clear(ColorRGBa.WHITE) + + draw(program, this@node) + } + outputImage = rt.colorBuffer(0) + println(outputImage) + + if (producingAssets) { + logger.info { "saving draw cache to file" } + val directory = File("screenshots") + if (!directory.exists()) { + directory.mkdirs() + } + + outputImage.saveToFile(File(screenshotTarget), async = false) + producingAssets = false + screenshotTarget = "" + } + } + for (input in inputNodes) { + dependOn(input) + } + } +} \ No newline at end of file diff --git a/orx-compute-graph-nodes/src/jvmMain/kotlin/DropImageNode.kt b/orx-compute-graph-nodes/src/jvmMain/kotlin/DropImageNode.kt new file mode 100644 index 00000000..a32384f6 --- /dev/null +++ b/orx-compute-graph-nodes/src/jvmMain/kotlin/DropImageNode.kt @@ -0,0 +1,28 @@ +package org.openrndr.extra.computegraph.nodes + +import org.openrndr.Program +import org.openrndr.draw.ColorBuffer +import org.openrndr.draw.colorBuffer +import org.openrndr.draw.loadImage +import org.openrndr.extra.computegraph.ComputeGraph +import org.openrndr.extra.computegraph.ComputeNode +import java.io.File + +fun ComputeGraph.dropImageNode(program: Program): ComputeNode { + return node { + name = "drop-image" + var file: File by inputs + file = File("data/images/cheeta.jpg") + program.window.drop.listen { + file = File(it.files.first()) + } + var image: ColorBuffer by outputs + + fun loadFileOrEmpty() = if (file.exists()) loadImage(file) else colorBuffer(256, 256) + image = loadFileOrEmpty() + compute { + image.destroy() + image = loadFileOrEmpty() + } + } +} \ No newline at end of file diff --git a/orx-compute-graph/README.md b/orx-compute-graph/README.md new file mode 100644 index 00000000..0227afe3 --- /dev/null +++ b/orx-compute-graph/README.md @@ -0,0 +1,55 @@ +# orx-compute-graph + +A graph for computation. + +## Status + +In development. Things may change without prior notice. + +## Usage + +``` +fun main() = application { + program { + val linkNode: ComputeNode + val cg = computeGraph { + val randomNode = node { + var seed: Int by inputs + seed = 0 + var points: List by outputs + points = emptyList() + compute { + val r = Random(seed) + points = (0 until 1000).map { + val x = r.nextDouble(0.0, width.toDouble()) + val y = r.nextDouble(0.0, height.toDouble()) + Vector2(x, y) + } + } + } + linkNode = node { + var seed: Int by inputs + seed = 0 + val points: List by randomNode.outputs + var links: List by outputs + compute { + val r = Random(seed) + val shuffled = points.shuffled(r) + links = shuffled.windowed(2, 2).map { + LineSegment(it[0], it[1]) + } + } + } + randomNode.dependOn(root) + linkNode.dependOn(randomNode) + } + cg.dispatch(dispatcher) {} + extend { + drawer.clear(ColorRGBa.WHITE) + drawer.stroke = ColorRGBa.BLACK + val links: List by linkNode.outputs + drawer.lineSegments(links) + } + } +} +``` \ No newline at end of file diff --git a/orx-compute-graph/build.gradle.kts b/orx-compute-graph/build.gradle.kts new file mode 100644 index 00000000..33e2e271 --- /dev/null +++ b/orx-compute-graph/build.gradle.kts @@ -0,0 +1,91 @@ +import EmbedShadersTask + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") +} + +val kotlinxSerializationVersion: String by rootProject.extra +val kotlinxCoroutinesVersion: String by rootProject.extra +val kotestVersion: String by rootProject.extra +val junitJupiterVersion: String by rootProject.extra +val jvmTarget: String by rootProject.extra +val kotlinApiVersion: String by rootProject.extra +val kotlinVersion: String by rootProject.extra +val kotlinLoggingVersion: String by rootProject.extra +val kluentVersion: String by rootProject.extra +val openrndrVersion: String by rootProject.extra +val openrndrOS: String by rootProject.extra +val spekVersion: String by rootProject.extra + +kotlin { + jvm { + compilations { + val demo by creating { + defaultSourceSet { + kotlin.srcDir("src/demo") + dependencies { + implementation(project(":orx-camera")) + implementation("org.openrndr:openrndr-application:$openrndrVersion") + implementation("org.openrndr:openrndr-extensions:$openrndrVersion") + runtimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion") + runtimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion") + implementation(compilations["main"]!!.output.allOutputs) + } + } + } + } + compilations.all { + kotlinOptions.jvmTarget = jvmTarget + kotlinOptions.apiVersion = kotlinApiVersion + } + testRuns["test"].executionTask.configure { + useJUnitPlatform() + } + } + js(IR) { + browser() + nodejs() + } + + sourceSets { + @Suppress("UNUSED_VARIABLE") + val commonMain by getting { + dependencies { + api("org.openrndr:openrndr-event:$openrndrVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + implementation("io.github.microutils:kotlin-logging:$kotlinLoggingVersion") + } + } + @Suppress("UNUSED_VARIABLE") + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + implementation("io.kotest:kotest-assertions-core:$kotestVersion") + } + } + + @Suppress("UNUSED_VARIABLE") + val jvmTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation(kotlin("test-junit5")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") + runtimeOnly("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion") + runtimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion") + implementation("org.spekframework.spek2:spek-dsl-jvm:$spekVersion") + implementation("org.amshove.kluent:kluent:$kluentVersion") + } + } + + @Suppress("UNUSED_VARIABLE") + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + } +} \ No newline at end of file diff --git a/orx-compute-graph/src/commonMain/kotlin/ComputeGraph.kt b/orx-compute-graph/src/commonMain/kotlin/ComputeGraph.kt new file mode 100644 index 00000000..989d44d4 --- /dev/null +++ b/orx-compute-graph/src/commonMain/kotlin/ComputeGraph.kt @@ -0,0 +1,175 @@ +package org.openrndr.extra.computegraph + +import kotlinx.coroutines.* +import mu.KotlinLogging +import org.openrndr.events.Event +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.reflect.KProperty + +private val logger = KotlinLogging.logger { } + +data class ComputeEvent(val source: ComputeNode) + +open class ComputeNode(val graph: ComputeGraph, var computeFunction: suspend () -> Unit = {}) { + internal var updateFunction = {} + + var inputs = mutableMapOf() + val outputs = mutableMapOf() + var name = "unnamed-node-${this.hashCode()}" + private var lastInputsHash = inputs.hashCode() + var receivedComputeRequest = true + private val computeFinished = Event("compute-finished") + + fun needsRecompute(): Boolean { + return receivedComputeRequest || (inputs.hashCode() != lastInputsHash) + } + + fun dependOn(node: ComputeNode) { + graph.nodes.add(this) + graph.inbound.getOrPut(this) { mutableSetOf() }.add(node) + graph.outbound.getOrPut(node) { mutableSetOf() }.add(node) + + node.computeFinished.listen { + receivedComputeRequest = true + graph.requireCompute.add(this) + computeFinished.trigger(Unit) + } + } + + /** + * Set an update function, this function is called unconditionally by the compute-graph processor. This update + * function can be used to change values of [inputs] to trigger compute of the node. + */ + fun update(updateFunction: () -> Unit) { + this.updateFunction = updateFunction + } + + fun compute(computeFunction: suspend () -> Unit) { + this.computeFunction = computeFunction + } + + suspend fun execute() { + receivedComputeRequest = false + lastInputsHash = inputs.hashCode() + computeFunction() + computeFinished.trigger(Unit) + } + + override fun toString(): String { + return "ComputeNode(name='$name', receivedComputeRequest=$receivedComputeRequest)" + } +} + +class ComputeGraph { + val root = ComputeNode(this, {}) + internal val requireCompute = ArrayDeque() + + val nodes = mutableListOf() + val inbound = mutableMapOf>() + val outbound = mutableMapOf>() + + var job: Job? = null + fun node(builder: ComputeNode.() -> Unit): ComputeNode { + val cn = ComputeNode(this) + cn.builder() + return cn + } + + private var computeHash = -1 + + /** + * Run the compute graph in [context]. + * + * Eventually we likely want to separate compute-graph definitions from the compute-graph processor. + */ + fun dispatch(context: CoroutineDispatcher, delayBeforeCompute: Long = 500) { + var firstRodeo = true + GlobalScope.launch(context, CoroutineStart.DEFAULT) { + while (true) { + for (node in nodes) { + node.updateFunction() + } + val testHash = nodes.map { it.inputs.hashCode() }.reduce { acc, computeNode -> acc * 31 + computeNode } + if (testHash != computeHash) { + logger.info { "canceling job $job" } + job?.cancel() + job = null + } + if (testHash != computeHash && job == null) { + computeHash = testHash + job = GlobalScope.launch(context) { + if (!firstRodeo) { + delay(delayBeforeCompute) + } + logger.info { "compute started" } + compute() + logger.info { "compute finished" } + firstRodeo = false + } + } + yield() + } + } + } + + suspend fun compute() { + for (node in nodes) { + if (node.needsRecompute()) { + if (node !in requireCompute) { + logger.info { "node '${node.name}' needs computation" } + requireCompute.add(node) + } + } + } + val processed = mutableListOf() + root.receivedComputeRequest = false + while (requireCompute.isNotEmpty()) { + val node = requireCompute.first { + val deps = (inbound[it] ?: emptyList()) + if (deps.isEmpty()) { + true + } else { + deps.none { dep -> dep in requireCompute } + } + } + requireCompute.remove(node) + if (node !in processed) { + logger.info { "computing ${node.name}" } + node.execute() + processed.add(node) + } + } + } +} + +@OptIn(ExperimentalContracts::class) +fun computeGraph(builder: ComputeGraph.() -> Unit): ComputeGraph { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + val cg = ComputeGraph() + cg.builder() + return cg +} + + +class MutableMapKeyReference(private val map: MutableMap, private val key: String) { + operator fun getValue(any: Any?, property: KProperty<*>): T { + @Suppress("UNCHECKED_CAST") + return map[key] as T + } + + operator fun setValue(any: Any?, property: KProperty<*>, value: Any) { + @Suppress("UNCHECKED_CAST") + map[key] = value as T + } +} + +/** + * Create a map delegation by [key] + */ +fun MutableMap.withKey(key: String): MutableMapKeyReference { + return MutableMapKeyReference(this, key) +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 5f730f04..d551fb84 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,6 +6,8 @@ include 'openrndr-demos', 'orx-jvm:orx-chataigne', 'orx-color', 'orx-compositor', + 'orx-compute-graph', + 'orx-compute-graph-nodes', 'orx-jvm:orx-dnk3', 'orx-easing', 'orx-jvm:orx-file-watcher',