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. */ @OptIn(DelicateCoroutinesApi::class) 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) }