[orx-compute-graph] Add compute graph code
This commit is contained in:
55
orx-compute-graph/README.md
Normal file
55
orx-compute-graph/README.md
Normal file
@@ -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<Vector2> 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<Vector2> by randomNode.outputs
|
||||
var links: List<LineSegment> 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<LineSegment> by linkNode.outputs
|
||||
drawer.lineSegments(links)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
91
orx-compute-graph/build.gradle.kts
Normal file
91
orx-compute-graph/build.gradle.kts
Normal file
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
175
orx-compute-graph/src/commonMain/kotlin/ComputeGraph.kt
Normal file
175
orx-compute-graph/src/commonMain/kotlin/ComputeGraph.kt
Normal file
@@ -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<String, Any>()
|
||||
val outputs = mutableMapOf<String, Any>()
|
||||
var name = "unnamed-node-${this.hashCode()}"
|
||||
private var lastInputsHash = inputs.hashCode()
|
||||
var receivedComputeRequest = true
|
||||
private val computeFinished = Event<Unit>("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<ComputeNode>()
|
||||
|
||||
val nodes = mutableListOf<ComputeNode>()
|
||||
val inbound = mutableMapOf<ComputeNode, MutableSet<ComputeNode>>()
|
||||
val outbound = mutableMapOf<ComputeNode, MutableSet<ComputeNode>>()
|
||||
|
||||
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<ComputeNode>()
|
||||
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<T : Any>(private val map: MutableMap<String, Any>, 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 <T : Any> MutableMap<String, Any>.withKey(key: String): MutableMapKeyReference<T> {
|
||||
return MutableMapKeyReference(this, key)
|
||||
}
|
||||
Reference in New Issue
Block a user