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
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,26 @@ More editor related documentation is available in [the editor docs](https://kool

## Platform Support

| Platform | Backend | Implementation Status |
|-------------|-------------|--------------------------------------------------------|
| Desktop JVM | OpenGL | :white_check_mark: Fully working |
| Desktop JVM | Vulkan | :white_check_mark: Fully working |
| Desktop JVM | WebGPU | :sparkles: Mostly working (using the `wgpu4k` backend) |
| Browser | WebGL 2 | :white_check_mark: Fully working |
| Browser | WebGPU | :white_check_mark: Fully working |
| Android | OpenGL ES 3 | :white_check_mark: Fully working |

**Supported desktop platforms are:**
| Platform | Backend | Implementation Status |
|---------------------|-------------|--------------------------------------------------------|
| Desktop (JVM) | Vulkan | :white_check_mark: Fully working |
| Desktop (JVM) | OpenGL | :white_check_mark: Fully working |
| Desktop (JVM) | WebGPU | :sparkles: Mostly working (using the `wgpu4k` backend) |
| Browser (JS + WASM) | WebGPU | :white_check_mark: Fully working |
| Browser (JS + WASM) | WebGL 2 | :white_check_mark: Fully working |
| Android | OpenGL ES 3 | :white_check_mark: Fully working |

### Supported desktop platforms
- Windows (x64): Vulkan, WebGPU and OpenGL
- Linux (x64): Vulkan, WebGPU and OpenGL
- macOS (ARM + x64): Vulkan and WebGPU (no OpenGL)

### JS vs. WASM Performance

Kool supports plain JavaScript as well as WebAssembly (WASM) as browser targets. However, the WASM backend seems to be
suffering from the many required JS-interop upcalls to various Web APIs. Therefore, in most cases, the WASM backend is
actually slower than the JS backend, and plain JS stays the recommended target for now.

### Java Version

On Desktop, Kool currently uses Java 17 as the minimum language level except the `kool-backend-wgpu4k` module, which
Expand Down
1 change: 1 addition & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
implementation(libs.plugindep.kotlin)
implementation(libs.plugindep.kotlin.serialization)
implementation(libs.plugindep.kotlin.atomicfu)
implementation(libs.plugindep.kotlin.jsplainobjs)
implementation(libs.plugindep.dokka)
implementation(libs.plugindep.android.library)
implementation(libs.plugindep.maven.publish)
Expand Down
28 changes: 24 additions & 4 deletions buildSrc/src/main/kotlin/kool.lib-conventions.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
@file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalWasmDsl::class)

import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl

plugins {
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.kotlin.plugin.serialization")
id("org.jetbrains.kotlin.plugin.atomicfu")
id("org.jetbrains.dokka")
id("org.jetbrains.kotlin.plugin.js-plain-objects")
}

kotlin {
Expand All @@ -19,10 +21,15 @@ kotlin {
target.set("es2015")
}
}
wasmJs {
binaries.library()
browser()
}

compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
freeCompilerArgs.add("-Xcontext-parameters")
freeCompilerArgs.add("-Xexpect-actual-classes")
}

sourceSets.all {
Expand All @@ -33,9 +40,22 @@ kotlin {
optIn("kotlin.ExperimentalStdlibApi")
}
}

compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
sourceSets {
jsMain {
languageSettings {
optIn("kotlin.js.ExperimentalWasmJsInterop")
}
}
webMain {
languageSettings {
optIn("kotlin.js.ExperimentalWasmJsInterop")
}
}
wasmJsMain {
languageSettings {
optIn("kotlin.js.ExperimentalWasmJsInterop")
}
}
}
}

Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ org.gradle.configuration-cache.parallel=true
kotlin.daemon.jvmargs=-Xmx4g
kotlin.incremental.js=true
kotlin.incremental.js.ir=true
kotlin.incremental.wasm=true
kotlin.mpp.stability.nowarn=true

kotlinx.atomicfu.enableJvmIrTransformation=true
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ kotlin-serialization = "1.9.0"
kotlin-datetime = "0.7.1-0.6.x-compat"
kotlin-atomicfu = "0.29.0"
kotlin-dokka = "2.1.0"
kotlinx-browser = "0.5.0"
lwjgl = "3.3.6"
jsvg = "2.0.0"
androidsvg = "1.4"
Expand All @@ -29,6 +30,7 @@ kotlin-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime",
kotlin-atomicfu = { group = "org.jetbrains.kotlinx", name = "atomicfu", version.ref = "kotlin-atomicfu" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { group = "org.jetbrains.kotlin", name = "kotlin-test-junit", version.ref = "kotlin" }
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinx-browser" }
jsvg = { group = "com.github.weisj", name = "jsvg", version.ref = "jsvg" }
jspecify-annotations = { module = "org.jspecify:jspecify", version = "1.0.0" }
androidsvg = { group = "com.caverock", name = "androidsvg-aar", version.ref = "androidsvg" }
Expand Down Expand Up @@ -62,6 +64,7 @@ jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
plugindep-kotlin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
plugindep-kotlin-serialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" }
plugindep-kotlin-atomicfu = { group = "org.jetbrains.kotlin", name = "atomicfu", version.ref = "kotlin" }
plugindep-kotlin-jsplainobjs = { module = "org.jetbrains.kotlin.plugin.js-plain-objects:org.jetbrains.kotlin.plugin.js-plain-objects.gradle.plugin", version.ref = "kotlin" }
plugindep-dokka = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugin", version.ref = "kotlin-dokka" }
plugindep-android-library = { group = "com.android.library", name = "com.android.library.gradle.plugin", version.ref = "agp" }
plugindep-maven-publish = { group = "com.vanniktech", name = "gradle-maven-publish-plugin", version = "0.35.0" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package de.fabmax.kool.pipeline.backend.wgpu

import de.fabmax.kool.util.*
import io.ygdrasil.webgpu.ArrayBuffer
import org.khronos.webgl.*

actual fun ArrayBuffer.writeInto(target: Buffer): Unit = when (target) {
is Float32BufferImpl -> target.buffer.set(Float32Array(this))
is Int32BufferImpl -> target.buffer.set(Int32Array(this))
is MixedBufferImpl -> Uint8Array(target.buffer.buffer).set(Uint8Array(this))
is Uint16BufferImpl -> target.buffer.set(Uint16Array(this))
is Uint8BufferImpl -> target.buffer.set(Uint8Array(this))
else -> error("Unsupported buffer type ${target::class.simpleName}")
}

private fun Buffer.asArrayBuffer(): ArrayBuffer = when (this) {
is Float32BufferImpl -> this.buffer.buffer
is Int32BufferImpl -> this.buffer.buffer
is MixedBufferImpl -> this.buffer.buffer
is Uint16BufferImpl -> this.buffer.buffer
is Uint8BufferImpl -> this.buffer.buffer
else -> error("Unsupported buffer type ${this::class.simpleName}")
}

@OptIn(ExperimentalUnsignedTypes::class)
actual fun ArrayBuffer.asUIntArray(): UIntArray = Int32Array(this).let {
val ktArray = UIntArray(it.length)
for (i in 0 until it.length) {
ktArray[i] = it[i].toUInt()
}
ktArray
}

actual fun Buffer.asArrayBuffer(block: (ArrayBuffer) -> Unit) {
block(asArrayBuffer())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package de.fabmax.kool.pipeline.backend.wgpu

import de.fabmax.kool.FrameData
import de.fabmax.kool.KoolContext
import de.fabmax.kool.KoolSystem
import de.fabmax.kool.configWasm
import de.fabmax.kool.platform.WasmContext
import de.fabmax.kool.util.ApplicationScope
import io.ygdrasil.webgpu.*
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import org.w3c.dom.HTMLCanvasElement

internal actual fun isRenderBackendWgpu4kSupported(): Boolean = true //!(js("!navigator.gpu") as Boolean)

internal actual suspend fun createRenderBackendWgpu4k(ctx: KoolContext): RenderBackendWgpu4k {
ctx as WasmContext
val backend = WasmRenderBackendWgpu4kWebGpu(ctx, ctx.window.canvas)
backend.initContext()
with(backend) {
surface.configure(
SurfaceConfiguration(
device,
surface.format,
viewFormats = setOf(surface.format)
)
)
}
return backend
}

private fun getSurface(canvas: HTMLCanvasElement): WgpuSurface {
val canvasSurface = canvas.unsafeCast<io.ygdrasil.webgpu.HTMLCanvasElement>().getCanvasSurface()
return WgpuSurface(canvasSurface)
}

private suspend fun getAdapter(): Adapter {
val adapterDescriptor = createJsObject<WGPURequestAdapterOptions>().apply {
powerPreference = KoolSystem.configWasm.powerPreference.value
}
val selectedAdapter: WGPUAdapter? = navigator.gpu?.requestAdapter(adapterDescriptor)?.wait()
?: navigator.gpu?.requestAdapter()?.wait()
checkNotNull(selectedAdapter) { "No appropriate GPUAdapter found." }

val adapter = Adapter(selectedAdapter)
return adapter
}

@OptIn(DelicateCoroutinesApi::class)
internal class WasmRenderBackendWgpu4kWebGpu(ctx: KoolContext, canvas: HTMLCanvasElement) :
RenderBackendWgpu4k(
ctx,
getSurface(canvas),
KoolSystem.configWasm.numSamples,
{ getAdapter() }
)
{
override val isAsyncRendering: Boolean = false

override fun renderFrame(frameData: FrameData, ctx: KoolContext) {
ApplicationScope.launch {
renderFrameSuspending(frameData, ctx)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package de.fabmax.kool.pipeline.backend.wgpu

import io.ygdrasil.webgpu.*

actual class WgpuSurface(private val handler: WGPUCanvasContext) : AutoCloseable {
actual val width: UInt
get() = handler.canvas.castAs<HTMLCanvasElement>().width.asUInt()
actual val height: UInt
get() = handler.canvas.castAs<HTMLCanvasElement>().height.asUInt()

actual val supportedAlphaMode: Set<CompositeAlphaMode> =
setOf(CompositeAlphaMode.Opaque, CompositeAlphaMode.Premultiplied)

actual val format: GPUTextureFormat
get() = navigator.gpu?.getPreferredCanvasFormat()
?.let { GPUTextureFormat.of(it) ?: error("Unsupported surface format: $it") }
?: error("WebGPU not supported")

actual fun getCurrentTextureView(): GPUTextureView {
return handler.getCurrentTexture()
.let { Texture(it, canBeDestroy = false)}
.createView()
}

actual fun present() { /* does not exists on Web */ }

actual fun configure(surfaceConfiguration: SurfaceConfiguration) {
handler.configure(map(surfaceConfiguration))
}

actual override fun close() { /* does not exists on Web */ }
}

private fun map(input: SurfaceConfiguration) = createJsObject<WGPUCanvasConfiguration>().apply {
device = (input.device as Device).handler
format = input.format.value
usage = input.usage.toFlagInt().asJsNumber()
viewFormats = input.viewFormats.mapJsArray { it.value.asJsString().castAs() }
colorSpace = input.colorSpace.value.asJsString().castAs()
toneMapping = createJsObject<WGPUCanvasToneMapping>().apply {
// GPUCanvasToneMappingMode.Standard is the default value on specification, should we allow to use extends for HDR ?
mode = GPUCanvasToneMappingMode.Standard.value.asJsString().castAs()
}
alphaMode = input.alphaMode.value.asJsString().castAs()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package de.fabmax.kool.pipeline.backend.wgpu

import de.fabmax.kool.math.float32ToFloat16
import de.fabmax.kool.pipeline.BufferedImageData
import de.fabmax.kool.pipeline.ImageData
import de.fabmax.kool.pipeline.isF16
import de.fabmax.kool.platform.ImageTextureData
import de.fabmax.kool.util.Float32BufferImpl
import de.fabmax.kool.util.Uint16BufferImpl
import de.fabmax.kool.util.Uint8BufferImpl
import io.ygdrasil.webgpu.*
import org.khronos.webgl.*
import org.khronos.webgl.ArrayBuffer

internal actual fun copyNativeTextureData(
src: ImageData,
dst: GPUTexture,
size: Extent3D,
dstOrigin: GPUOrigin3D,
device: GPUDevice
): Unit = when (src) {
is BufferedImageData -> {
device.queue.writeTexture(
data = src.arrayBufferView.unsafeCast<ArrayBuffer>(),
destination = TexelCopyTextureInfo(dst, origin = dstOrigin),
dataLayout = src.gpuImageDataLayout,
size = size
)
}
is ImageTextureData -> copyTextureData(src, dst, size, dstOrigin, device)
else -> error("Not implemented: ${src::class.simpleName}")
}

internal fun copyTextureData(
src: ImageTextureData,
dst: GPUTexture,
size: Extent3D,
dstOrigin: GPUOrigin3D,
device: GPUDevice
) {
val queue = (device.queue as Queue).handler
queue.copyExternalImageToTexture(
source = createJsObject<WGPUCopyExternalImageSourceInfo>().apply {
source = src.data
},
destination = createJsObject<WGPUCopyExternalImageDestInfo>().apply {
texture = (dst as Texture).handler
mipLevel = 0.toJsNumber()
origin = createJsObject<WGPUOrigin3D>().apply {
x = dstOrigin.x.toJsNumber()
y = dstOrigin.y.toJsNumber()
z = dstOrigin.z.toJsNumber()
}
},
copySize = createJsObject<WGPUExtent3D>().apply {
width = size.width.toJsNumber()
height = size.height.toJsNumber()
depthOrArrayLayers = size.depthOrArrayLayers.toJsNumber()
}
)
}

private val ImageData.arrayBufferView: ArrayBufferView get() {
check(this is BufferedImageData)

val bufData = data
return when {
format.isF16 && bufData is Float32BufferImpl -> {
val f32Array = bufData.buffer
val f16Buffer = Uint8Array(f32Array.length * 2)
for (i in 0 until f32Array.length) {
f16Buffer.putF16(i, f32Array[i])
}
f16Buffer
}
bufData is Uint8BufferImpl -> bufData.buffer
bufData is Uint16BufferImpl -> bufData.buffer
bufData is Float32BufferImpl -> bufData.buffer
else -> throw IllegalArgumentException("Unsupported buffer type")
}
}

private fun Uint8Array.putF16(index: Int, f32: Float) {
float32ToFloat16(f32) { high, low ->
val byteI = index * 2
set(byteI, low)
set(byteI+1, high)
}
}
3 changes: 2 additions & 1 deletion kool-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ kotlin {
implementation(libs.kotlin.test.junit)
}

jsMain.dependencies {
webMain.dependencies {
implementation(libs.kotlinx.browser)
implementation(npm("pako", "2.0.4"))
implementation(npm("jszip", "3.10.1"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ class WgslGenerator private constructor(
KslStateType.VertexOutput -> "vertexOutput."
KslStateType.FragmentInput -> "fragmentInput."
KslStateType.FragmentOutput -> "fragmentOutput."
KslStateType.ComputeInput if state.stateName == KslComputeStage.NAME_IN_WORK_GROUP_SIZE -> ""
KslStateType.ComputeInput -> "computeInput."
KslStateType.Other -> ""
}
Expand Down
Loading