Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ plugins {
id("kool.androidlib-conventions") apply false
id("kool.lib-conventions") apply false
id("kool.publish-conventions") apply false
alias(libs.plugins.compose.compiler) apply false
}

allprojects {
Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/kool.lib-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ kotlin {
optIn("kotlin.contracts.ExperimentalContracts")
optIn("kotlin.io.encoding.ExperimentalEncodingApi")
optIn("kotlin.ExperimentalStdlibApi")
optIn("de.fabmax.kool.InternalKoolAPI")
}
}
sourceSets {
Expand Down
12 changes: 10 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[versions]
agp = "8.12.3"
compose = "1.9.3"
compose-mini = "0.0.1"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the dependency I mentioned for some compose helpers, repo here: https://github.com/0ffz/compose-mini. I've tried to split things into modules where it makes sense and support all the current targets, the only thing it's necessary for in the core module is a cross platform nanoTime implementation which I've included in the runtime module, so I'd like to hear your preferences:

  • I can split just nanoTime into its own module under this repo or leave it as is (the runtime just contains some helpers for creating a composition)
  • I noticed Kool already has a SystemClock implementation though this doesn't expose nanoseconds. I could update its implementation, it's not too bad for jvm/web but there are a lot of native implementations if Kool ever adds those targets.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding it to Time should be fine. I think SystemClock actually isn't really needed anymore since Clock.System made it it to stdlib. So Time.kt could now look something like this:

val precisionTime: Double get() = nanoTime / 1_000_000_000.0

val nanoTime: Long get() {
    val now = Clock.System.now()
    return now.epochSeconds * 1_000_000_000L + now.nanosecondsOfSecond
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the implementation to use this and removed the dependency in core module 👍

kotlin = "2.3.0"
kotlin-coroutines = "1.10.2"
kotlin-serialization = "1.9.0"
Expand Down Expand Up @@ -51,8 +53,11 @@ physx-wasm = { group = "npm", name = "physx-js-webidl", version.ref = "physx-was
box2d-jni = { group = "de.fabmax.box2d-jni", name = "box2d-jni", version.ref = "box2d-jni" }
box2d-android = { group = "de.fabmax.box2d-jni", name = "box2d-jni-android", version.ref = "box2d-jni" }
box2d-wasm = { group = "npm", name = "kool-box2d-wasm", version.ref = "box2d-wasm" }

# wgpu backend
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" }
compose-mini-runtime = { group = "me.dvyy.compose.mini", name = "runtime", version.ref = "compose-mini"}
compose-mini-modifier = { group = "me.dvyy.compose.mini", name = "modifier", version.ref = "compose-mini" }
compose-mini-modifier-composed = { group = "me.dvyy.compose.mini", name = "modifier-composed", version.ref = "compose-mini"}
# wgpu backcend
wgpu4k = { module = "io.ygdrasil:wgpu4k", version.ref = "wgpu4k" }
webgpu-descriptors = { module = "io.ygdrasil:webgpu-ktypes-descriptors", version.ref = "webgpu-ktypes" }
rococoa = { module = "io.ygdrasil:rococoa", version.ref = "rococoa" }
Expand All @@ -71,6 +76,9 @@ plugindep-maven-publish = { group = "com.vanniktech", name = "gradle-maven-publi

[plugins]
webidl = { id = "de.fabmax.webidl-util", version = "0.10.5" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose" }


[bundles]
lwjgl = ["lwjgl-core", "lwjgl-glfw", "lwjgl-jemalloc", "lwjgl-opengl", "lwjgl-vulkan", "lwjgl-vma", "lwjgl-shaderc", "lwjgl-nfd", "lwjgl-stb", "lwjgl-jawt", "lwjgl-msdfgen"]
Expand Down
1 change: 1 addition & 0 deletions kool-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ kotlin {
api(libs.kotlin.coroutines)
implementation(libs.kotlin.serialization.json)
implementation(libs.kotlin.atomicfu)
implementation(libs.compose.runtime)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
Expand Down

This file was deleted.

15 changes: 15 additions & 0 deletions kool-core/src/commonMain/kotlin/de/fabmax/kool/Annotations.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.fabmax.kool

@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
message = "This is internal Kool API that may lead to unexpected behavior or change without warning."
)
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER,
)
@Retention(AnnotationRetention.BINARY)
annotation class InternalKoolAPI
18 changes: 18 additions & 0 deletions kool-core/src/commonMain/kotlin/de/fabmax/kool/KoolContext.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.fabmax.kool

import androidx.compose.runtime.snapshots.Snapshot
import de.fabmax.kool.input.Input
import de.fabmax.kool.pipeline.ComputePass
import de.fabmax.kool.pipeline.GpuPass
Expand All @@ -11,6 +12,7 @@ import de.fabmax.kool.scene.Scene
import de.fabmax.kool.util.BufferedList
import de.fabmax.kool.util.KoolDispatchers
import de.fabmax.kool.util.Time
import kotlinx.atomicfu.atomic

/**
* @author fabmax
Expand All @@ -25,6 +27,11 @@ abstract class KoolContext {

private var prevFrameTime = Time.precisionTime

private val applyScheduled = atomic(false)
private val snapshotHandle = Snapshot.registerGlobalWriteObserver {
applyScheduled.compareAndSet(expect = false, update = true)
}

val onRender = BufferedList<(KoolContext) -> Unit>()
val onShutdown = BufferedList<(KoolContext) -> Unit>()

Expand All @@ -40,6 +47,10 @@ abstract class KoolContext {
.also { brdf -> onShutdown += { brdf.release() } }
}

init {
onShutdown += { snapshotHandle.dispose() }
}

abstract fun openUrl(url: String, sameWindow: Boolean = true)

abstract fun run()
Expand Down Expand Up @@ -73,7 +84,14 @@ abstract class KoolContext {

Input.poll(this)

// Apply any mutable state changes from user input
if (applyScheduled.compareAndSet(expect = true, update = false)) {
Snapshot.sendApplyNotifications()
}

KoolDispatchers.Frontend.executeDispatchedTasks()
Time.composeFrameClock.sendFrame(Time.nanoTime) // Let recomposer update UI nodes

onRender.update()
for (i in onRender.indices) {
onRender[i](this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ value class Dp(val value: Float): Dimension, Comparable<Dp> {
return fromPx(round(pxf))
}
}
}
}

val Number.dp: Dp get() = Dp(this.toFloat())
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ open class ScrollPaneNode(parent: UiNode?, surface: UiSurface) : UiNode(parent,
var currentScrollY = state.yScrollDp.use()
var desiredScrollX = state.xScrollDpDesired.use()
var desiredScrollY = state.yScrollDpDesired.use()

val parent = parent
if (parent != null) {
state.viewWidthDp.set(parent.widthPx / UiScale.measuredScale)
state.viewHeightDp.set(parent.heightPx / UiScale.measuredScale)
Expand Down Expand Up @@ -221,4 +221,4 @@ open class ScrollPaneNode(parent: UiNode?, surface: UiSurface) : UiNode(parent,
companion object {
val factory: (UiNode, UiSurface) -> ScrollPaneNode = { parent, surface -> ScrollPaneNode(parent, surface) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ open class UiModifier(val surface: UiSurface) {
val onDragStart: MutableList<(PointerEvent) -> Unit> by listProperty()
val onDrag: MutableList<(PointerEvent) -> Unit> by listProperty()
val onDragEnd: MutableList<(PointerEvent) -> Unit> by listProperty()
val onRender: MutableList<UiNode.() -> Unit> by listProperty()

protected fun <T> property(defaultVal: T): PropertyHolder<T> {
val holder = PropertyHolder { defaultVal }
Expand Down Expand Up @@ -309,4 +310,4 @@ fun <T: UiModifier> T.dragListener(draggable: Draggable): T {
onDrag(draggable::onDrag)
onDragEnd(draggable::onDragEnd)
return this
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.fabmax.kool.modules.ui2

import de.fabmax.kool.InternalKoolAPI
import de.fabmax.kool.KoolContext
import de.fabmax.kool.math.MutableVec2f
import de.fabmax.kool.math.MutableVec4f
Expand All @@ -12,15 +13,23 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.reflect.KClass

abstract class UiNode(val parent: UiNode?, override val surface: UiSurface) : UiScope {
abstract class UiNode(parent: UiNode?, override val surface: UiSurface) : UiScope {
@set:InternalKoolAPI
var parent: UiNode? = parent

override val uiNode: UiNode get() = this

var nodeIndex = 0
private set

protected val oldChildren = mutableListOf<UiNode>()
protected val mutChildren = mutableListOf<UiNode>()

@InternalKoolAPI
val mutChildren = mutableListOf<UiNode>()

@OptIn(InternalKoolAPI::class)
val children: List<UiNode> get() = mutChildren

val weakMemory = WeakMemory()

private var scopeName: String? = null
Expand Down Expand Up @@ -131,7 +140,7 @@ abstract class UiNode(val parent: UiNode?, override val surface: UiSurface) : Ui
this.topPx = minY
this.rightPx = maxX
this.bottomPx = maxY

val parent = parent
if (parent != null) {
clipBoundsPx.x = max(parent.clipLeftPx, minX)
clipBoundsPx.y = max(parent.clipTopPx, minY)
Expand Down Expand Up @@ -205,6 +214,7 @@ abstract class UiNode(val parent: UiNode?, override val surface: UiSurface) : Ui
open fun render(ctx: KoolContext) {
modifier.background?.renderUi(this)
modifier.border?.renderUi(this)
modifier.onRender.forEach { render -> render(this) }
}

open fun measureContentSize(ctx: KoolContext) {
Expand Down Expand Up @@ -412,4 +422,4 @@ abstract class UiNode(val parent: UiNode?, override val surface: UiSurface) : Ui
companion object {
val NO_CLIP = Vec4f(-1e9f, -1e9f, 1e9f, 1e9f)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ import de.fabmax.kool.scene.geometry.MeshBuilder
import de.fabmax.kool.scene.geometry.Usage
import de.fabmax.kool.util.*

/**
* @property clearViewportOnUpdateUi
* Whether the viewport node should have defaults applied when updating UI.
* Set to false if managing UI nodes externally (ex. via Compose.)
*/
open class UiSurface(
val parentScene: Scene,
colors: Colors = Colors.darkColors(),
sizes: Sizes = Sizes.medium,
name: String = "uiSurface"
name: String = "uiSurface",
val clearViewportOnUpdateUi: Boolean = true,
) : Node(name) {

constructor(
Expand Down Expand Up @@ -155,7 +161,7 @@ open class UiSurface(
perfPrep = pt.takeMs().also { pt.reset() }

viewport.setBounds(0f, 0f, viewportWidth.use(this), viewportHeight.use(this))
viewport.applyDefaults()
if (clearViewportOnUpdateUi) viewport.applyDefaults()
composeContent()
perfCompose = pt.takeMs().also { pt.reset() }

Expand Down Expand Up @@ -758,4 +764,4 @@ open class UiSurface(
}
}
}
}
}
30 changes: 22 additions & 8 deletions kool-core/src/commonMain/kotlin/de/fabmax/kool/util/Time.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package de.fabmax.kool.util

import androidx.compose.runtime.BroadcastFrameClock
import de.fabmax.kool.InternalKoolAPI
import de.fabmax.kool.util.Time.frameCount
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlin.time.Clock

object Time {
private val systemClock = SystemClock()
/**
* Time since previous frame in seconds.
*/
Expand Down Expand Up @@ -44,7 +46,25 @@ object Time {
* measure time intervals. Precision depends on platform, on JVM it should be around nanoseconds, on JS it should
* be around 0.1 milliseconds.
*/
val precisionTime: Double get() = systemClock.now()
val precisionTime: Double get() = nanoTime / 1_000_000_000.0

/**
* Current system clock time in nanoseconds.
*/
val nanoTime: Long get() {
val now = Clock.System.now()
return now.epochSeconds * 1_000_000_000L + now.nanosecondsOfSecond
}

/**
* Frame clock from Jetpack compose runtime.
* Frames are emitted after polling user input and dispatching any queued frontend scope tasks.
*
* Any blocks waiting for a new frame will run immediately when the frame is emitted, then switch
* to their parent context to return the result.
*/
@InternalKoolAPI
val composeFrameClock = BroadcastFrameClock()

internal fun update(dt: Double) {
gameTime += dt
Expand All @@ -54,10 +74,4 @@ object Time {
for (i in frameTimes.indices) { sum += frameTimes[i] }
fps = (frameTimes.size / sum) * 0.1 + fps * 0.9
}
}

internal expect fun SystemClock(): SystemClock

internal interface SystemClock {
fun now(): Double
}

This file was deleted.

14 changes: 0 additions & 14 deletions kool-core/src/webMain/kotlin/de/fabmax/kool/util/Time.web.kt

This file was deleted.