From a90a63ea9011b16d436d74f71ce54425fe15ea7a Mon Sep 17 00:00:00 2001 From: fabmax Date: Sun, 7 Jan 2024 21:00:51 +0100 Subject: [PATCH] spinning cube with web gpu --- .../kool/modules/ksl/lang/KslShaderStage.kt | 3 +- .../pipeline/backend/webgpu/WgslGenerator.kt | 14 +- .../backend/webgpu/WgslStructHelper.kt | 2 +- .../kotlin/de/fabmax/kool/KoolConfigJvm.kt | 8 +- .../de/fabmax/kool/platform/Lwjgl3Context.kt | 8 +- .../kotlin/de/fabmax/kool/KoolConfigJs.kt | 6 + .../backend/webgpu/RenderBackendWebGpu.kt | 75 ++-- .../backend/webgpu/WebGpuDictionaries.kt | 213 +++++++--- .../pipeline/backend/webgpu/WebGpuEnums.kt | 170 ++++++-- .../backend/webgpu/WebGpuInterfaces.kt | 5 + .../pipeline/backend/webgpu/WgpuRenderPass.kt | 368 ++++++++++++++++++ .../de/fabmax/kool/platform/JsContext.kt | 8 +- kool-demo/build.gradle.kts | 7 +- .../kotlin/de/fabmax/kool/demo/Main.kt | 3 +- kool-demo/src/jsMain/kotlin/Main.kt | 29 +- kool-demo/src/jsMain/kotlin/WebGpuTest.kt | 269 ------------- 16 files changed, 763 insertions(+), 425 deletions(-) create mode 100644 kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgpuRenderPass.kt delete mode 100644 kool-demo/src/jsMain/kotlin/WebGpuTest.kt diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslShaderStage.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslShaderStage.kt index 8a3419c4e..ce79f6d92 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslShaderStage.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ksl/lang/KslShaderStage.kt @@ -179,8 +179,7 @@ class KslFragmentStage(program: KslProgram) : KslShaderStage(program, KslShaderS fun KslScopeBuilder.colorOutput(rgb: KslVectorExpression, a: KslScalarExpression = 1f.const, location: Int = 0) { check (parentStage is KslFragmentStage) { "colorOutput is only available in fragment stage" } val outColor = parentStage.colorOutput(location) - outColor.value.rgb set rgb - outColor.value.a set a + outColor.value set float4Value(rgb, a) } fun KslScopeBuilder.colorOutput(value: KslVectorExpression, location: Int = 0) { diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgslGenerator.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgslGenerator.kt index dc919d9f6..6ea350be8 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgslGenerator.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgslGenerator.kt @@ -90,7 +90,7 @@ class WgslGenerator : KslGenerator() { // src.generateFunctions(fragmentStage) src.appendLine("@fragment") - src.appendLine("fn fragmentMain(input: FragmentInput) -> FragmentOutput {") + src.appendLine("fn fragmentMain(fragmentInput: FragmentInput) -> FragmentOutput {") src.appendLine(" var fragmentOutput: FragmentOutput;") fragmentOutput.generateExplodedMembers(src) @@ -563,12 +563,12 @@ class WgslGenerator : KslGenerator() { return when (stateName) { KslVertexStage.NAME_IN_VERTEX_INDEX -> "vertexInput.vertexIndex" // vertex_index KslVertexStage.NAME_IN_INSTANCE_INDEX -> "vertexInput.instanceIndex" // instance_index - KslVertexStage.NAME_OUT_POSITION -> "vertexOutput.position" // position - KslVertexStage.NAME_OUT_POINT_SIZE -> "vertexOutput.pointSize" // + KslVertexStage.NAME_OUT_POSITION -> "position" // position + KslVertexStage.NAME_OUT_POINT_SIZE -> "pointSize" // KslFragmentStage.NAME_IN_FRAG_POSITION -> "fragmentInput.position" // position KslFragmentStage.NAME_IN_IS_FRONT_FACING -> "fragmentInput.isFrontFacing" // front_facing - KslFragmentStage.NAME_OUT_DEPTH -> "fragmentOutput.fragDepth" // frag_depth + KslFragmentStage.NAME_OUT_DEPTH -> "fragDepth" // frag_depth // not-implemented: sample_index // not-implemented: sample_mask @@ -627,7 +627,11 @@ class WgslGenerator : KslGenerator() { } } - class WgslGeneratorOutput : GeneratedSourceOutput() { + class WgslGeneratorOutput( + val vertexEntryPoint: String = "vertexMain", + val fragmentEntryPoint: String = "fragmentMain", + val computeEntryPoint: String = "computeMain" + ) : GeneratedSourceOutput() { companion object { fun shaderOutput(vertexSrc: String, fragmentSrc: String) = WgslGeneratorOutput().apply { stages[KslShaderStageType.VertexShader] = vertexSrc diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgslStructHelper.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgslStructHelper.kt index 46406b82b..16980e737 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgslStructHelper.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgslStructHelper.kt @@ -35,7 +35,7 @@ interface WgslStructHelper { data class WgslStructMember(val structName: String, val name: String, val type: String, val annotation: String = "") { fun generateStructMember(builder: StringBuilder) { - builder.appendLine(" ${annotation}${name}: ${type};") + builder.appendLine(" ${annotation}${name}: ${type},") } fun createExplodedMember(builder: StringBuilder) { diff --git a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/KoolConfigJvm.kt b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/KoolConfigJvm.kt index 7a89b6c5f..0ca3b6d66 100644 --- a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/KoolConfigJvm.kt +++ b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/KoolConfigJvm.kt @@ -1,7 +1,6 @@ package de.fabmax.kool import de.fabmax.kool.math.Vec2i -import de.fabmax.kool.platform.Lwjgl3Context import de.fabmax.kool.util.MsdfFontInfo import de.fabmax.kool.util.MsdfMeta import kotlinx.serialization.json.Json @@ -30,7 +29,7 @@ data class KoolConfigJvm( val storageDir: String = "./.storage", val httpCacheDir: String = "./.httpCache", - val renderBackend: Lwjgl3Context.Backend = Lwjgl3Context.Backend.OPEN_GL, + val renderBackend: Backend = Backend.OPEN_GL, val windowTitle: String = "Kool App", val windowSize: Vec2i = Vec2i(1600, 900), val isFullscreen: Boolean = false, @@ -61,4 +60,9 @@ data class KoolConfigJvm( } } } + + enum class Backend(val displayName: String) { + VULKAN("Vulkan"), + OPEN_GL("OpenGL") + } } \ No newline at end of file diff --git a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/platform/Lwjgl3Context.kt b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/platform/Lwjgl3Context.kt index 5660f5741..412affff4 100644 --- a/kool-core/src/desktopMain/kotlin/de/fabmax/kool/platform/Lwjgl3Context.kt +++ b/kool-core/src/desktopMain/kotlin/de/fabmax/kool/platform/Lwjgl3Context.kt @@ -1,5 +1,6 @@ package de.fabmax.kool.platform +import de.fabmax.kool.KoolConfigJvm import de.fabmax.kool.KoolContext import de.fabmax.kool.KoolSystem import de.fabmax.kool.configJvm @@ -75,7 +76,7 @@ class Lwjgl3Context : KoolContext() { } init { - backend = if (KoolSystem.configJvm.renderBackend == Backend.VULKAN) { + backend = if (KoolSystem.configJvm.renderBackend == KoolConfigJvm.Backend.VULKAN) { VkRenderBackend(this) } else { RenderBackendGlImpl(this) @@ -160,10 +161,5 @@ class Lwjgl3Context : KoolContext() { } override fun getSysInfos(): List = SysInfo.lines - - enum class Backend(val displayName: String) { - VULKAN("Vulkan"), - OPEN_GL("OpenGL") - } } diff --git a/kool-core/src/jsMain/kotlin/de/fabmax/kool/KoolConfigJs.kt b/kool-core/src/jsMain/kotlin/de/fabmax/kool/KoolConfigJs.kt index b9f661fd4..4d9e8795c 100644 --- a/kool-core/src/jsMain/kotlin/de/fabmax/kool/KoolConfigJs.kt +++ b/kool-core/src/jsMain/kotlin/de/fabmax/kool/KoolConfigJs.kt @@ -16,6 +16,7 @@ data class KoolConfigJs( override val defaultFont: MsdfFontInfo = DEFAULT_MSDF_FONT_INFO, val canvasName: String = "glCanvas", + val renderBackend: Backend = Backend.WEB_GL2, val isGlobalKeyEventGrabbing: Boolean = true, val isJsCanvasToWindowFitting: Boolean = true, val loaderTasks: List Unit> = emptyList(), @@ -32,4 +33,9 @@ data class KoolConfigJs( MsdfFontInfo(meta, "fonts/font-roboto-regular.png") } } + + enum class Backend(val displayName: String) { + WEB_GL2("WebGL2"), + WEB_GPU("WebGPU") + } } \ No newline at end of file diff --git a/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/RenderBackendWebGpu.kt b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/RenderBackendWebGpu.kt index 003a66754..c247532c0 100644 --- a/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/RenderBackendWebGpu.kt +++ b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/RenderBackendWebGpu.kt @@ -9,7 +9,6 @@ import de.fabmax.kool.pipeline.backend.RenderBackend import de.fabmax.kool.pipeline.backend.RenderBackendJs import de.fabmax.kool.pipeline.backend.stats.BackendStats import de.fabmax.kool.platform.JsContext -import de.fabmax.kool.scene.Scene import de.fabmax.kool.util.LongHash import de.fabmax.kool.util.logE import de.fabmax.kool.util.logI @@ -26,9 +25,17 @@ class RenderBackendWebGpu(val ctx: KoolContext, val canvas: HTMLCanvasElement) : override val canBlitRenderPasses: Boolean = false override val isOnscreenInfiniteDepthCapable: Boolean = false // actually it can... - private lateinit var adapter: GPUAdapter - private lateinit var device: GPUDevice - private lateinit var gpuContext: GPUCanvasContext + lateinit var adapter: GPUAdapter + private set + lateinit var device: GPUDevice + private set + lateinit var gpuContext: GPUCanvasContext + private set + private var _canvasFormat: GPUTextureFormat? = null + val canvasFormat: GPUTextureFormat + get() = _canvasFormat!! + + private val sceneRenderer = WgpuRenderPass(this) init { check(!js("!navigator.gpu") as Boolean) { @@ -48,7 +55,7 @@ class RenderBackendWebGpu(val ctx: KoolContext, val canvas: HTMLCanvasElement) : device = adapter.requestDevice().await() gpuContext = canvas.getContext("webgpu") as GPUCanvasContext - val canvasFormat = navigator.gpu.getPreferredCanvasFormat() + _canvasFormat = navigator.gpu.getPreferredCanvasFormat() gpuContext.configure(GPUCanvasConfiguration(device, canvasFormat)) logI { "WebGPU context created" } @@ -74,40 +81,37 @@ class RenderBackendWebGpu(val ctx: KoolContext, val canvas: HTMLCanvasElement) : // captureFramebuffer(scene) // } // doOffscreenPasses(scene, ctx) - doForegroundPass(scene) + sceneRenderer.doForegroundPass(scene) } } } - fun doForegroundPass(scene: Scene) { - val clearColor = scene.mainRenderPass.clearColor ?: return - - val encoder = device.createCommandEncoder() - val pass = encoder.beginRenderPass(GPURenderPassDescriptor(arrayOf( - GPURenderPassColorAttachment( - view = gpuContext.getCurrentTexture().createView(), - clearValue = GPUColorDict(clearColor.r.toDouble(), clearColor.g.toDouble(), clearColor.b.toDouble(), clearColor.a.toDouble()), - loadOp = GPULoadOp.clear, - storeOp = GPUStoreOp.store - ) - ))) - - pass.end() - device.queue.submit(arrayOf(encoder.finish())) - } - override fun cleanup(ctx: KoolContext) { // do nothing for now } override fun generateKslShader(shader: KslShader, pipeline: Pipeline): ShaderCode { - logE { "Not yet implemented: WebGpuShaderCode()" } - return WebGpuShaderCode() + val output = WgslGenerator().generateProgram(shader.program, pipeline) + if (shader.program.dumpCode) { + output.dump() + } + return WebGpuShaderCode( + vertexSrc = output.vertexSrc, + vertexEntryPoint = output.vertexEntryPoint, + fragmentSrc = output.fragmentSrc, + fragmentEntryPoint = output.fragmentEntryPoint + ) } override fun generateKslComputeShader(shader: KslComputeShader, pipeline: ComputePipeline): ComputeShaderCode { - logE { "Not yet implemented: WebGpuComputeShaderCode()" } - return WebGpuComputeShaderCode() + val output = WgslGenerator().generateComputeProgram(shader.program, pipeline) + if (shader.program.dumpCode) { + output.dump() + } + return WebGpuComputeShaderCode( + computeSrc = output.computeSrc, + computeEntryPoint = output.computeEntryPoint + ) } override fun createOffscreenPass2d(parentPass: OffscreenRenderPass2d): OffscreenPass2dImpl { @@ -146,11 +150,20 @@ class RenderBackendWebGpu(val ctx: KoolContext, val canvas: HTMLCanvasElement) : } } - class WebGpuShaderCode: ShaderCode { - override val hash: LongHash = LongHash() + data class WebGpuShaderCode( + val vertexSrc: String, + val vertexEntryPoint: String, + val fragmentSrc: String, + val fragmentEntryPoint: String + ): ShaderCode { + override val hash = LongHash().apply { + this += vertexSrc.hashCode().toLong() shl 32 or fragmentSrc.hashCode().toLong() + } } - class WebGpuComputeShaderCode: ComputeShaderCode { - override val hash: LongHash = LongHash() + data class WebGpuComputeShaderCode(val computeSrc: String, val computeEntryPoint: String): ComputeShaderCode { + override val hash = LongHash().apply { + this += computeSrc + } } } \ No newline at end of file diff --git a/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuDictionaries.kt b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuDictionaries.kt index 2f1185bd6..bfa7e8105 100644 --- a/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuDictionaries.kt +++ b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuDictionaries.kt @@ -1,37 +1,73 @@ package de.fabmax.kool.pipeline.backend.webgpu +import de.fabmax.kool.util.Color + +interface GPUDictionary { + @JsName("label") + val label: String +} + class GPUBindGroupLayoutDescriptor( @JsName("entries") val entries: Array, -) + override val label: String = "GPUBindGroupLayoutDescriptor" +) : GPUDictionary -class GPUBindGroupLayoutEntry( +sealed interface GPUBindGroupLayoutEntry + +data class GPUBindGroupLayoutEntryBuffer( @JsName("binding") val binding: Int, @JsName("visibility") val visibility: Int, @JsName("buffer") val buffer: GPUBufferBindingLayout, -// @JsName("sampler") -// val sampler: GPUSamplerBindingLayout? = null, -// @JsName("texture") -// val texture: GPUTextureBindingLayout? = null, -// @JsName("storageTexture") -// val storageTexture: GPUStorageTextureBindingLayout? = null, -// @JsName("externalTexture") -// val externalTexture: GPUExternalTextureBindingLayout? = null, -) +) : GPUBindGroupLayoutEntry + +data class GPUBindGroupLayoutEntrySampler( + @JsName("binding") + val binding: Int, + @JsName("visibility") + val visibility: Int, + @JsName("sampler") + val sampler: GPUSamplerBindingLayout, +) : GPUBindGroupLayoutEntry + +data class GPUBindGroupLayoutEntryTexture( + @JsName("binding") + val binding: Int, + @JsName("visibility") + val visibility: Int, + @JsName("texture") + val texture: GPUTextureBindingLayout, +) : GPUBindGroupLayoutEntry + +data class GPUBindGroupLayoutEntryStorageTexture( + @JsName("binding") + val binding: Int, + @JsName("visibility") + val visibility: Int, + @JsName("storageTexture") + val storageTexture: GPUStorageTextureBindingLayout, +) : GPUBindGroupLayoutEntry -class GPUBufferBindingLayout( - type: GPUBufferBindingType, +data class GPUBindGroupLayoutEntryExternaleTexture( + @JsName("binding") + val binding: Int, + @JsName("visibility") + val visibility: Int, + @JsName("externalTexture") + val externalTexture: GPUExternalTextureBindingLayout, +) : GPUBindGroupLayoutEntry + +data class GPUBufferBindingLayout( + @JsName("type") + val type: GPUBufferBindingType = GPUBufferBindingType.uniform, @JsName("hasDynamicOffset") val hasDynamicOffset: Boolean = false, @JsName("minBindingSize") val minBindingSize: Long = 0 -) { - @JsName("type") - val type: String = type.typeName -} +) class GPUSamplerBindingLayout @@ -45,8 +81,9 @@ class GPUBindGroupDescriptor( @JsName("layout") val layout: GPUBindGroupLayout, @JsName("entries") - val entries: Array -) + val entries: Array, + override val label: String = "GPUBindGroupDescriptor" +) : GPUDictionary class GPUBindGroupEntry( @JsName("binding") @@ -64,14 +101,15 @@ class GPUBufferBinding( // val size: Long = 0 ) : GPUBindingResource -class GPUBufferDescriptor( +data class GPUBufferDescriptor( @JsName("size") val size: Long, @JsName("usage") val usage: Int, @JsName("mappedAtCreation") - val mappedAtCreation: Boolean = true, -) + val mappedAtCreation: Boolean = false, + override val label: String = "GPUBufferDescriptor" +) : GPUDictionary class GPUCanvasConfiguration( @JsName("device") @@ -86,15 +124,17 @@ class GPUCanvasConfiguration( class GPUColorDict( @JsName("r") - val r: Double, + val r: Float, @JsName("g") - val g: Double, + val g: Float, @JsName("b") - val b: Double, + val b: Float, @JsName("a") - val a: Double, + val a: Float, ) +fun GPUColorDict(color: Color): GPUColorDict = GPUColorDict(color.r, color.g, color.b, color.a) + class GPUColorTargetState( @JsName("format") val format: GPUTextureFormat @@ -109,80 +149,125 @@ class GPUFragmentState( val targets: Array, ) -class GPUMultisampleState( +data class GPUMultisampleState( @JsName("count") - val count: Int, + val count: Int = 1, +// @JsName("mask") +// val mask: UInt = 0xffffffffu, + @JsName("alphaToCoverageEnabled") + val alphaToCoverageEnabled: Boolean = false ) class GPUPipelineLayoutDescriptor( @JsName("bindGroupLayouts") val bindGroupLayouts: Array, -) + override val label: String = "GPUPipelineLayoutDescriptor" +) : GPURenderPassColorAttachment -class GPUPrimitiveState(topology: GPUPrimitiveTopology) { +data class GPUPrimitiveState( @JsName("topology") - val topology: String = topology.topoName -} + val topology: GPUPrimitiveTopology = GPUPrimitiveTopology.triangleList, + //stripIndexFormat: GPUIndexFormat + //frontFace: GPUFrontFace= 'ccw' + //cullMode: GPUCullMode= 'none' + //unclippedDepth: boolean= 'false' +) -class GPURenderPassColorAttachment( +sealed interface GPURenderPassColorAttachment : GPUDictionary + +data class GPURenderPassColorAttachmentClear( @JsName("view") val view: GPUTextureView, -// @JsName("resolveTarget") -// val resolveTarget: GPUTextureView, + @JsName("resolveTarget") + val resolveTarget: GPUTextureView, @JsName("clearValue") val clearValue: GPUColorDict, + @JsName("storeOp") + val storeOp: GPUStoreOp = GPUStoreOp.store, + override val label: String = "GPURenderPassColorAttachmentClear" +) : GPURenderPassColorAttachment { @JsName("loadOp") - val loadOp: GPULoadOp, + val loadOp: GPULoadOp = GPULoadOp.clear +} + +data class GPURenderPassColorAttachmentLoad( + @JsName("view") + val view: GPUTextureView, + @JsName("resolveTarget") + val resolveTarget: GPUTextureView, @JsName("storeOp") - val storeOp: GPUStoreOp, + val storeOp: GPUStoreOp = GPUStoreOp.store, + override val label: String = "GPURenderPassColorAttachmentClear" +) : GPURenderPassColorAttachment { + @JsName("loadOp") + val loadOp: GPULoadOp = GPULoadOp.load +} + +data class GPURenderPassDepthStencilAttachment( + @JsName("view") + val view: GPUTextureView, + @JsName("depthLoadOp") + val depthLoadOp: GPULoadOp, + @JsName("depthStoreOp") + val depthStoreOp: GPUStoreOp, + @JsName("depthClearValue") + val depthClearValue: Float = 1f, ) class GPURenderPassDescriptor( @JsName("colorAttachments") val colorAttachments: Array, -) + @JsName("depthStencilAttachment") + val depthStencilAttachment: GPURenderPassDepthStencilAttachment, + override val label: String = "GPURenderPassDescriptor" +) : GPUDictionary -class GPURenderPipelineDescriptor private constructor( +data class GPURenderPipelineDescriptor( @JsName("layout") - val layout: Any, - @JsName("fragment") - val fragment: GPUFragmentState, + val layout: GPUPipelineLayout, @JsName("vertex") val vertex: GPUVertexState, + @JsName("fragment") + val fragment: GPUFragmentState, + @JsName("depthStencil") + val depthStencil: GPUDepthStencilState, @JsName("primitive") - val primitive: GPUPrimitiveState, + val primitive: GPUPrimitiveState = GPUPrimitiveState(), @JsName("multisample") - val multisample: GPUMultisampleState -) { - constructor(layout: GPUAutoLayoutMode, - vertex: GPUVertexState, - primitive: GPUPrimitiveState, - multisample: GPUMultisampleState, - fragment: GPUFragmentState - ) : this(layout, fragment, vertex, primitive, multisample) - - constructor(layout: GPUPipelineLayout, - vertex: GPUVertexState, - primitive: GPUPrimitiveState, - multisample: GPUMultisampleState, - fragment: GPUFragmentState - ) : this(layout, fragment, vertex, primitive, multisample) -} + val multisample: GPUMultisampleState = GPUMultisampleState(), + override val label: String = "GPURenderPipelineDescriptor" +) : GPUDictionary -class GPUShaderModuleDescriptor( - @JsName("code") - val code: String +data class GPUDepthStencilState( + @JsName("format") + val format: GPUTextureFormat, + @JsName("depthWriteEnabled") + val depthWriteEnabled: Boolean, + @JsName("depthCompare") + val depthCompare: GPUCompareFunction, + @JsName("depthBias") + val depthBias: Int = 0, + @JsName("depthBiasSlopeScale") + val depthBiasSlopeScale: Float = 0f, + @JsName("depthBiasClamp") + val depthBiasClamp: Float = 0f ) +data class GPUShaderModuleDescriptor( + @JsName("code") + val code: String, + override val label: String = "GPUShaderModuleDescriptor" +) : GPUDictionary + class GPUTextureDescriptor( @JsName("size") val size: IntArray, - @JsName("sampleCount") - val sampleCount: Int, @JsName("format") val format: GPUTextureFormat, @JsName("usage") val usage: Int, + @JsName("sampleCount") + val sampleCount: Int = 1, ) class GPUVertexAttribute( diff --git a/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuEnums.kt b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuEnums.kt index c4da06866..9579d32ca 100644 --- a/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuEnums.kt +++ b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuEnums.kt @@ -1,57 +1,157 @@ package de.fabmax.kool.pipeline.backend.webgpu -enum class GPUAutoLayoutMode { - auto +value class GPUBufferBindingType(val enumValue: String) { + companion object { + val uniform = GPUBufferBindingType("uniform") + val storage = GPUBufferBindingType("storage") + val readOnlyStorage = GPUBufferBindingType("read-only-storage") + } } -enum class GPUBufferBindingType(val typeName: String) { - uniform("uniform"), - storage("storage"), - readOnlyStorage("read-only-storage") +value class GPUCanvasAlphaMode(val enumValue: String) { + companion object { + val opaque = GPUCanvasAlphaMode("opaque") + val premultiplied = GPUCanvasAlphaMode("premultiplied") + } } -enum class GPUCanvasAlphaMode { - opaque, - premultiplied +value class GPUCompareFunction(val funcName: String) { + companion object { + val never = GPUCompareFunction("never") + val less = GPUCompareFunction("less") + val equal = GPUCompareFunction("equal") + val lessEqual = GPUCompareFunction("less-equal") + val greater = GPUCompareFunction("greater") + val notEqual = GPUCompareFunction("not-equal") + val greaterEqual = GPUCompareFunction("greater-equal") + val always = GPUCompareFunction("always") + } } -enum class GPUIndexFormat { - uint16, uint32 +value class GPUIndexFormat(val enumValue: String) { + companion object { + val uint16 = GPUIndexFormat("uint16") + val uint32 = GPUIndexFormat("uint32") + } } -enum class GPULoadOp { - load, - clear +value class GPULoadOp(val enumValue: String) { + companion object { + val load = GPULoadOp("load") + val clear = GPULoadOp("clear") + } } -enum class GPUPredefinedColorSpace { - srgb +value class GPUPredefinedColorSpace(val enumValue: String) { + companion object { + val srgb = GPUPredefinedColorSpace("srgb") + } } -enum class GPUPrimitiveTopology(val topoName: String) { - pointList("point-list"), - lineList("line-list"), - lineStrip("line-strip"), - triangleList("triangle-list"), - triangleStrip("triangle-strip") +value class GPUPrimitiveTopology(val enumValue: String) { + companion object { + val pointList = GPUPrimitiveTopology("point-list") + val lineList = GPUPrimitiveTopology("line-list") + val lineStrip = GPUPrimitiveTopology("line-strip") + val triangleList = GPUPrimitiveTopology("triangle-list") + val triangleStrip = GPUPrimitiveTopology("triangle-strip") + } } -enum class GPUStoreOp { - store, - discard +value class GPUStoreOp(val enumValue: String) { + companion object { + val store = GPUStoreOp("store") + val discard = GPUStoreOp("discard") + } } -external class GPUTextureFormat +value class GPUTextureFormat(val enumValue: String) { + companion object { + val r8unorm = GPUTextureFormat("r8unorm") + val r8snorm = GPUTextureFormat("r8snorm") + val r8uint = GPUTextureFormat("r8uint") + val r8sint = GPUTextureFormat("r8sint") + val r16uint = GPUTextureFormat("r16uint") + val r16sint = GPUTextureFormat("r16sint") + val r16float = GPUTextureFormat("r16float") + val rg8unorm = GPUTextureFormat("rg8unorm") + val rg8snorm = GPUTextureFormat("rg8snorm") + val rg8uint = GPUTextureFormat("rg8uint") + val rg8sint = GPUTextureFormat("rg8sint") + val r32uint = GPUTextureFormat("r32uint") + val r32sint = GPUTextureFormat("r32sint") + val r32float = GPUTextureFormat("r32float") + val rg16uint = GPUTextureFormat("rg16uint") + val rg16sint = GPUTextureFormat("rg16sint") + val rg16float = GPUTextureFormat("rg16float") + val rgba8unorm = GPUTextureFormat("rgba8unorm") + val rgba8unormSrgb = GPUTextureFormat("rgba8unorm-srgb") + val rgba8snorm = GPUTextureFormat("rgba8snorm") + val rgba8uint = GPUTextureFormat("rgba8uint") + val rgba8sint = GPUTextureFormat("rgba8sint") + val bgra8unorm = GPUTextureFormat("bgra8unorm") + val bgra8unormSrgb = GPUTextureFormat("bgra8unorm-srgb") + val rgb9e5ufloat = GPUTextureFormat("rgb9e5ufloat") + val rgb10a2uint = GPUTextureFormat("rgb10a2uint") + val rgb10a2unorm = GPUTextureFormat("rgb10a2unorm") + val rg11b10ufloat = GPUTextureFormat("rg11b10ufloat") + val rg32uint = GPUTextureFormat("rg32uint") + val rg32sint = GPUTextureFormat("rg32sint") + val rg32float = GPUTextureFormat("rg32float") + val rgba16uint = GPUTextureFormat("rgba16uint") + val rgba16sint = GPUTextureFormat("rgba16sint") + val rgba16float = GPUTextureFormat("rgba16float") + val rgba32uint = GPUTextureFormat("rgba32uint") + val rgba32sint = GPUTextureFormat("rgba32sint") + val rgba32float = GPUTextureFormat("rgba32float") + val stencil8 = GPUTextureFormat("stencil8") + val depth16unorm = GPUTextureFormat("depth16unorm") + val depth24plus = GPUTextureFormat("depth24plus") + val depth24plusStencil8 = GPUTextureFormat("depth24plus-stencil8") + val depth32float = GPUTextureFormat("depth32float") + val depth32floatStencil8 = GPUTextureFormat("depth32float-stencil8") + // todo: many more compressed formats... + } +} -enum class GPUVertexFormat { - uint8x2, uint8x4, sint8x2, sint8x4, unorm8x2, unorm8x4, snorm8x2, snorm8x4, - uint16x2, uint16x4, sint16x2, sint16x4, unorm16x2, unorm16x4, snorm16x2, snorm16x4, - float16x2, float16x4, - float32, float32x2, float32x3, float32x4, - uint32, uint32x2, uint32x3, uint32x4, sint32, sint32x2, sint32x3, sint32x4 +value class GPUVertexFormat(val enumValue: String) { + companion object { + val uint8x2 = GPUVertexFormat("uint8x2") + val uint8x4 = GPUVertexFormat("uint8x4") + val sint8x2 = GPUVertexFormat("sint8x2") + val sint8x4 = GPUVertexFormat("sint8x4") + val unorm8x2 = GPUVertexFormat("unorm8x2") + val unorm8x4 = GPUVertexFormat("unorm8x4") + val snorm8x2 = GPUVertexFormat("snorm8x2") + val snorm8x4 = GPUVertexFormat("snorm8x4") + val uint16x2 = GPUVertexFormat("uint16x2") + val uint16x4 = GPUVertexFormat("uint16x4") + val sint16x2 = GPUVertexFormat("sint16x2") + val sint16x4 = GPUVertexFormat("sint16x4") + val unorm16x2 = GPUVertexFormat("unorm16x2") + val unorm16x4 = GPUVertexFormat("unorm16x4") + val snorm16x2 = GPUVertexFormat("snorm16x2") + val snorm16x4 = GPUVertexFormat("snorm16x4") + val float16x2 = GPUVertexFormat("float16x2") + val float16x4 = GPUVertexFormat("float16x4") + val float32 = GPUVertexFormat("float32") + val float32x2 = GPUVertexFormat("float32x2") + val float32x3 = GPUVertexFormat("float32x3") + val float32x4 = GPUVertexFormat("float32x4") + val uint32 = GPUVertexFormat("uint32") + val uint32x2 = GPUVertexFormat("uint32x2") + val uint32x3 = GPUVertexFormat("uint32x3") + val uint32x4 = GPUVertexFormat("uint32x4") + val sint32 = GPUVertexFormat("sint32") + val sint32x2 = GPUVertexFormat("sint32x2") + val sint32x3 = GPUVertexFormat("sint32x3") + val sint32x4 = GPUVertexFormat("sint32x4") + } } -enum class GPUVertexStepMode { - vertex, - instance +value class GPUVertexStepMode(val enumValue: String) { + companion object { + val vertex = GPUVertexStepMode("vertex") + val instance = GPUVertexStepMode("instance") + } } diff --git a/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuInterfaces.kt b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuInterfaces.kt index 2a7181e26..8111973c3 100644 --- a/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuInterfaces.kt +++ b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WebGpuInterfaces.kt @@ -1,6 +1,9 @@ +@file:Suppress("INLINE_CLASS_IN_EXTERNAL_DECLARATION_WARNING") + package de.fabmax.kool.pipeline.backend.webgpu import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.ArrayBufferView import org.w3c.dom.RenderingContext import kotlin.js.Promise @@ -71,6 +74,7 @@ external class GPUPipelineLayout external class GPUQueue { fun submit(commandBuffers: Array) + fun writeBuffer(buffer: GPUBuffer, bufferOffset: Long, data: ArrayBufferView, dataOffset: Long = definedExternally, size: Long = definedExternally) } external class GPURenderPassEncoder { @@ -91,6 +95,7 @@ external class GPUSampler : GPUBindingResource external class GPUShaderModule external object GPUShaderStage { + val COMPUTE: Int val FRAGMENT: Int val VERTEX: Int } diff --git a/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgpuRenderPass.kt b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgpuRenderPass.kt new file mode 100644 index 000000000..e45689b72 --- /dev/null +++ b/kool-core/src/jsMain/kotlin/de/fabmax/kool/pipeline/backend/webgpu/WgpuRenderPass.kt @@ -0,0 +1,368 @@ +package de.fabmax.kool.pipeline.backend.webgpu + +import de.fabmax.kool.pipeline.* +import de.fabmax.kool.pipeline.backend.GpuGeometry +import de.fabmax.kool.pipeline.drawqueue.DrawCommand +import de.fabmax.kool.scene.Mesh +import de.fabmax.kool.scene.Scene +import de.fabmax.kool.util.* + +class WgpuRenderPass(val backend: RenderBackendWebGpu, val multiSamples: Int = 4) { + + private val device: GPUDevice + get() = backend.device + private val gpuContext: GPUCanvasContext + get() = backend.gpuContext + + private var colorTexture: GPUTexture? = null + private var colorView: GPUTextureView? = null + + private val depthFormat = GPUTextureFormat.depth32float + private var depthTexture: GPUTexture? = null + private var depthView: GPUTextureView? = null + + fun createColorTexture( + width: Int = backend.canvas.width, + height: Int = backend.canvas.height + ) { + colorTexture?.destroy() + + val descriptor = GPUTextureDescriptor( + size = intArrayOf(width, height), + format = backend.canvasFormat, + usage = GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount = multiSamples + ) + colorTexture = device.createTexture(descriptor).also { + colorView = it.createView() + } + } + + fun createDepthTexture( + width: Int = backend.canvas.width, + height: Int = backend.canvas.height + ) { + depthTexture?.destroy() + + val descriptor = GPUTextureDescriptor( + size = intArrayOf(width, height), + format = depthFormat, + usage = GPUTextureUsage.RENDER_ATTACHMENT, + sampleCount = multiSamples + ) + depthTexture = device.createTexture(descriptor).also { + depthView = it.createView() + } + } + + fun doForegroundPass(scene: Scene) { + if (depthTexture == null) { + createDepthTexture() + } + if (colorTexture == null) { + createColorTexture() + } + + val scenePass = when (val pass = scene.mainRenderPass) { + is Scene.OnscreenSceneRenderPass -> pass + else -> TODO() + } + + device.queue.submit(scenePass.views.map { it.encodeQueue() }.toTypedArray()) + } + + private fun RenderPass.View.encodeQueue(): GPUCommandBuffer { + val encoder = device.createCommandEncoder() + + val colorAttachments = clearColors.map { clearColor -> + val resolveTarget = gpuContext.getCurrentTexture().createView() + clearColor?.let { + GPURenderPassColorAttachmentClear(colorView!!, resolveTarget, GPUColorDict(it)) + } ?: GPURenderPassColorAttachmentLoad(colorView!!, resolveTarget) + }.toTypedArray() + + val depthAttachment = GPURenderPassDepthStencilAttachment( + view = depthView!!, + depthLoadOp = GPULoadOp.clear, + depthStoreOp = GPUStoreOp.store, + depthClearValue = 1f + ) + + val pass = encoder.beginRenderPass(GPURenderPassDescriptor(colorAttachments, depthAttachment)) + //pass.setViewport() + //pass.setScissor() + + for (cmd in drawQueue.commands) { + val t = Time.precisionTime + + if (cmd.geometry.numIndices == 0) continue + val pipeline = cmd.pipeline ?: continue + if (!pipeline.setup(pass, cmd)) continue + +// val insts = cmd.mesh.instances +// if (insts == null) { + pass.drawIndexed(cmd.geometry.numIndices) +// } else { +// pass.drawIndexed(cmd.geometry.numIndices, insts.numInstances) +// } + + cmd.mesh.drawTime = Time.precisionTime - t + } + + pass.end() + return encoder.finish() + } + + private fun Pipeline.setup(pass: GPURenderPassEncoder, drawCmd: DrawCommand): Boolean { + // call onUpdate callbacks + for (i in onUpdate.indices) { + onUpdate[i].invoke(drawCmd) + } + + val gpuPipeline = getOrCreateWgpuPipeline() + pass.setPipeline(gpuPipeline.pipeline) + gpuPipeline.bindVertexBuffers(pass, drawCmd.mesh) + gpuPipeline.bindBindGroups(pass, this) + return true + } + + private val pipelines = mutableMapOf() + private fun Pipeline.getOrCreateWgpuPipeline(): WgpuPipeline { + return pipelines.getOrPut(pipelineHash) { + logD { "create pipeline: $name (hash=$pipelineHash)" } + WgpuPipeline(this) + } + } + + inner class WgpuPipeline(pipeline: Pipeline) { + val bindGroupLayout: GPUBindGroupLayout = device.createBindGroupLayout(bindGroupLayoutDescriptor(pipeline)) + val pipelineLayout: GPUPipelineLayout = device.createPipelineLayout(pipelineLayoutDescriptor(pipeline)) + val vertexBufferLayout: Array = vertexBufferLayout(pipeline) + val vertexShaderModule: GPUShaderModule = device.createShaderModule(vertexShaderModuleDescriptor(pipeline)) + val fragemntShaderModule: GPUShaderModule = device.createShaderModule(fragmentShaderModuleDescriptor(pipeline)) + val pipeline: GPURenderPipeline = device.createRenderPipeline(renderPipelineDescriptor(pipeline)) + + fun bindGroupLayoutDescriptor(pipeline: Pipeline): GPUBindGroupLayoutDescriptor { + val layoutEntries = pipeline.bindGroupLayout.items.map { binding -> + val visibility = binding.stages.fold(0) { acc, stage -> + acc or when (stage) { + ShaderStage.VERTEX_SHADER -> GPUShaderStage.VERTEX + ShaderStage.FRAGMENT_SHADER -> GPUShaderStage.FRAGMENT + ShaderStage.COMPUTE_SHADER -> GPUShaderStage.COMPUTE + else -> error("unsupported shader stage: $stage") + } + } + + when (binding) { + is Storage1d -> TODO() + is Storage2d -> TODO() + is Storage3d -> TODO() + is TextureSampler1d -> TODO() + is TextureSampler2d -> TODO() + is TextureSampler3d -> TODO() + is TextureSamplerCube -> TODO() + is UniformBuffer -> GPUBindGroupLayoutEntryBuffer(binding.binding, visibility, GPUBufferBindingLayout()) + } + } + + return GPUBindGroupLayoutDescriptor( + label = "${pipeline.name}-bindGroupLayout", + entries = layoutEntries.toTypedArray() + ) + } + + fun pipelineLayoutDescriptor(pipeline: Pipeline) = GPUPipelineLayoutDescriptor( + label = "${pipeline.name}-bindGroupLayout", + bindGroupLayouts = arrayOf(bindGroupLayout) + ) + + fun renderPipelineDescriptor(pipeline: Pipeline): GPURenderPipelineDescriptor { + val shaderCode = pipeline.shaderCode as RenderBackendWebGpu.WebGpuShaderCode + val vertexState = GPUVertexState( + module = vertexShaderModule, + entryPoint = shaderCode.vertexEntryPoint, + buffers = vertexBufferLayout + ) + val fragmentState = GPUFragmentState( + module = fragemntShaderModule, + entryPoint = shaderCode.fragmentEntryPoint, + targets = arrayOf(GPUColorTargetState(backend.canvasFormat)) + ) + + val depthStencil = GPUDepthStencilState( + format = depthFormat, + depthWriteEnabled = true, + depthCompare = GPUCompareFunction.less + ) + + return GPURenderPipelineDescriptor( + label = "${pipeline.name}-layout", + layout = pipelineLayout, + vertex = vertexState, + fragment = fragmentState, + depthStencil = depthStencil, + multisample = GPUMultisampleState(multiSamples) + ) + } + + fun vertexBufferLayout(pipeline: Pipeline): Array { + return pipeline.vertexLayout.bindings.map { vertexBinding -> + val attributes = vertexBinding.vertexAttributes.map { attr -> + val format = when (attr.type) { + GlslType.FLOAT -> GPUVertexFormat.float32 + GlslType.VEC_2F -> GPUVertexFormat.float32x2 + GlslType.VEC_3F -> GPUVertexFormat.float32x3 + GlslType.VEC_4F -> GPUVertexFormat.float32x4 + GlslType.INT -> GPUVertexFormat.sint32 + GlslType.VEC_2I -> GPUVertexFormat.sint32x2 + GlslType.VEC_3I -> GPUVertexFormat.sint32x3 + GlslType.VEC_4I -> GPUVertexFormat.sint32x4 + else -> error("Invalid vertex attribute type: ${attr.type}") + } + GPUVertexAttribute( + format = format, + offset = attr.offset.toLong(), + shaderLocation = attr.location + ) + }.toTypedArray() + + GPUVertexBufferLayout( + arrayStride = vertexBinding.strideBytes.toLong(), + attributes = attributes, + stepMode = when (vertexBinding.inputRate) { + InputRate.VERTEX -> GPUVertexStepMode.vertex + InputRate.INSTANCE -> GPUVertexStepMode.instance + } + ) + }.toTypedArray() + } + + fun vertexShaderModuleDescriptor(pipeline: Pipeline) = GPUShaderModuleDescriptor( + label = "${pipeline.name} vertex shader", + code = (pipeline.shaderCode as RenderBackendWebGpu.WebGpuShaderCode).vertexSrc + ) + + fun fragmentShaderModuleDescriptor(pipeline: Pipeline) = GPUShaderModuleDescriptor( + label = "${pipeline.name} fragment shader", + code = (pipeline.shaderCode as RenderBackendWebGpu.WebGpuShaderCode).fragmentSrc + ) + + fun bindVertexBuffers(pass: GPURenderPassEncoder, mesh: Mesh) { + val gpuGeom = (mesh.geometry.gpuGeometry as WgpuGeometry?) ?: WgpuGeometry(mesh).also { mesh.geometry.gpuGeometry = it } + pass.setVertexBuffer(0, gpuGeom.floatBuffer) + gpuGeom.intBuffer?.let { pass.setVertexBuffer(1, it) } + pass.setIndexBuffer(gpuGeom.indexBuffer, GPUIndexFormat.uint32) + } + + // fixme: this needs to be associated with mesh / pipeline instance + var bindGroup: GPUBindGroup? = null + val ubos = mutableListOf() + + fun bindBindGroups(pass: GPURenderPassEncoder, pipeline: Pipeline) { + if (bindGroup == null) { + val bindGroupEntries = mutableListOf() + pipeline.bindGroupLayout.items + .filterIsInstance() + .forEach { ubo -> + val layout = Std140BufferLayout(ubo.uniforms) + val gpuBuffer = device.createBuffer(GPUBufferDescriptor( + label = "${pipeline.name} uniforms", + size = layout.size.toLong(), + usage = GPUBufferUsage.UNIFORM or GPUBufferUsage.COPY_DST + )) + val hostBuffer = MixedBuffer(layout.size) + ubos += UboBinding(ubo, layout, hostBuffer, gpuBuffer) + bindGroupEntries += GPUBindGroupEntry(ubo.binding, GPUBufferBinding(gpuBuffer)) + } + + bindGroup = device.createBindGroup(GPUBindGroupDescriptor( + label = "${pipeline.name} bind group", + layout = bindGroupLayout, + entries = bindGroupEntries.toTypedArray() + )) + } + + ubos.forEach { ubo -> + ubo.layout.putToBuffer(ubo.binding.uniforms, ubo.hostBuffer) + device.queue.writeBuffer( + buffer = ubo.gpuBuffer, + bufferOffset = 0L, + data = (ubo.hostBuffer as MixedBufferImpl).buffer + ) + } + pass.setBindGroup(0, bindGroup!!) + } + } + + data class UboBinding( + val binding: UniformBuffer, + val layout: Std140BufferLayout, + val hostBuffer: MixedBuffer, + val gpuBuffer: GPUBuffer + ) + + inner class WgpuGeometry(mesh: Mesh) : GpuGeometry { + val indexBuffer: GPUBuffer + val floatBuffer: GPUBuffer + val intBuffer: GPUBuffer? + + override var isReleased: Boolean = false + + init { + val geom = mesh.geometry + indexBuffer = device.createBuffer(GPUBufferDescriptor( + label = "${mesh.name} index data", + size = 4 * geom.numIndices.toLong(), + usage = GPUBufferUsage.INDEX or GPUBufferUsage.COPY_DST + )) + + floatBuffer = device.createBuffer(GPUBufferDescriptor( + label = "${mesh.name} vertex float data", + size = geom.byteStrideF.toLong() * geom.numVertices, + usage = GPUBufferUsage.VERTEX or GPUBufferUsage.COPY_DST + )) + + intBuffer = if (geom.byteStrideI > 0) { + device.createBuffer(GPUBufferDescriptor( + label = "${mesh.name} vertex int data", + size = geom.byteStrideI.toLong() * geom.numVertices, + usage = GPUBufferUsage.VERTEX or GPUBufferUsage.COPY_DST + )) + } else null + + device.queue.writeBuffer( + buffer = indexBuffer, + bufferOffset = 0L, + data = (geom.indices as Int32BufferImpl).buffer, + dataOffset = 0L, + size = geom.numIndices.toLong() + ) + + device.queue.writeBuffer( + buffer = floatBuffer, + bufferOffset = 0L, + data = (geom.dataF as Float32BufferImpl).buffer, + dataOffset = 0L, + size = geom.vertexSizeF * geom.numVertices.toLong() + ) + + intBuffer?.let { + device.queue.writeBuffer( + buffer = it, + bufferOffset = 0L, + data = (geom.dataI as Int32BufferImpl).buffer, + dataOffset = 0L, + size = geom.vertexSizeI * geom.numVertices.toLong() + ) + } + } + + override fun release() { + indexBuffer.destroy() + floatBuffer.destroy() + intBuffer?.destroy() + isReleased = true + } + + } +} \ No newline at end of file diff --git a/kool-core/src/jsMain/kotlin/de/fabmax/kool/platform/JsContext.kt b/kool-core/src/jsMain/kotlin/de/fabmax/kool/platform/JsContext.kt index 0e96fd4d2..6e1d264ec 100644 --- a/kool-core/src/jsMain/kotlin/de/fabmax/kool/platform/JsContext.kt +++ b/kool-core/src/jsMain/kotlin/de/fabmax/kool/platform/JsContext.kt @@ -5,6 +5,7 @@ import de.fabmax.kool.input.PlatformInputJs import de.fabmax.kool.pipeline.backend.RenderBackend import de.fabmax.kool.pipeline.backend.RenderBackendJs import de.fabmax.kool.pipeline.backend.gl.RenderBackendGlImpl +import de.fabmax.kool.pipeline.backend.webgpu.RenderBackendWebGpu import de.fabmax.kool.util.RenderLoopCoroutineDispatcher import kotlinx.browser.document import kotlinx.browser.window @@ -71,8 +72,11 @@ class JsContext internal constructor() : KoolContext() { canvas.height = (canvasFixedHeight * window.devicePixelRatio).roundToInt() } - backend = RenderBackendGlImpl(this, canvas) - //backend = RenderBackendWebGpu(this, canvas) + backend = if (KoolSystem.configJs.renderBackend == KoolConfigJs.Backend.WEB_GL2) { + RenderBackendGlImpl(this, canvas) + } else { + RenderBackendWebGpu(this, canvas) + } document.onfullscreenchange = { isFullscreenEnabled = document.fullscreenElement != null diff --git a/kool-demo/build.gradle.kts b/kool-demo/build.gradle.kts index 58dde0067..3b46d7ae1 100644 --- a/kool-demo/build.gradle.kts +++ b/kool-demo/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig plugins { alias(commonLibs.plugins.kotlinMultiplatform) + alias(commonLibs.plugins.kotlinSerialization) } kotlin { @@ -83,10 +84,11 @@ task("cacheRuntimeLibs") { else -> "" } - configurations + val runtimeLibs = configurations .filter { it.name == "desktopRuntimeClasspath" } .flatMap { it.copyRecursive().fileCollection { true } } .filter { it.name.endsWith("$platformName.jar") && !it.path.startsWith(projectDir.path) } + runtimeLibs .forEach { if (!File("${projectDir}/runtimeLibs/${it.name}").exists()) { copy { @@ -95,6 +97,9 @@ task("cacheRuntimeLibs") { } } } + File("${projectDir}/runtimeLibs/").listFiles() + ?.filter { exiting -> runtimeLibs.none { exiting.name == it.name } } + ?.forEach { it.delete() } } } diff --git a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/Main.kt b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/Main.kt index ad3784519..10fe274d8 100644 --- a/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/Main.kt +++ b/kool-demo/src/desktopMain/kotlin/de/fabmax/kool/demo/Main.kt @@ -3,14 +3,13 @@ package de.fabmax.kool.demo import de.fabmax.kool.KoolApplication import de.fabmax.kool.KoolConfigJvm import de.fabmax.kool.math.Vec2i -import de.fabmax.kool.platform.Lwjgl3Context /** * @author fabmax */ fun main() = KoolApplication( config = KoolConfigJvm( - renderBackend = Lwjgl3Context.Backend.OPEN_GL, + renderBackend = KoolConfigJvm.Backend.OPEN_GL, windowTitle = "Kool Demo", windowSize = Vec2i(1600, 900) ) diff --git a/kool-demo/src/jsMain/kotlin/Main.kt b/kool-demo/src/jsMain/kotlin/Main.kt index 29980b2de..4b975b897 100644 --- a/kool-demo/src/jsMain/kotlin/Main.kt +++ b/kool-demo/src/jsMain/kotlin/Main.kt @@ -1,15 +1,21 @@ import de.fabmax.kool.KoolApplication import de.fabmax.kool.KoolConfigJs import de.fabmax.kool.demo.demo +import de.fabmax.kool.modules.ksl.KslUnlitShader import de.fabmax.kool.physics.Physics +import de.fabmax.kool.scene.addColorMesh +import de.fabmax.kool.scene.defaultOrbitCamera +import de.fabmax.kool.scene.scene import kotlinx.browser.window import kotlin.collections.set -/** - * @author fabmax - */ +val params = getParams() +val isWebGpu: Boolean + get() = params["backend"] == "webgpu" + fun main() = KoolApplication( KoolConfigJs( + renderBackend = if (isWebGpu) KoolConfigJs.Backend.WEB_GPU else KoolConfigJs.Backend.WEB_GL2, isGlobalKeyEventGrabbing = true, loaderTasks = listOf { Physics.loadAndAwaitPhysics() } ) @@ -22,8 +28,13 @@ fun main() = KoolApplication( //Demo.setProperty("assets.materials", "materials") //Demo.setProperty("assets.models", "models") - // launch demo - demo(getParams()["demo"], it) + if (isWebGpu) { + // web-gpu backend does not yet support everything needed to run the full demo set + it.scenes += helloWebGpu() + } else { + // launch demo + demo(params["demo"], it) + } } @Suppress("UNUSED_VARIABLE") @@ -45,4 +56,12 @@ fun getParams(): Map { } } return params +} + +fun helloWebGpu() = scene { + defaultOrbitCamera() + addColorMesh { + generate { cube { colored(false) } } + shader = KslUnlitShader { color { vertexColor() } } + } } \ No newline at end of file diff --git a/kool-demo/src/jsMain/kotlin/WebGpuTest.kt b/kool-demo/src/jsMain/kotlin/WebGpuTest.kt deleted file mode 100644 index 292b36b58..000000000 --- a/kool-demo/src/jsMain/kotlin/WebGpuTest.kt +++ /dev/null @@ -1,269 +0,0 @@ -import de.fabmax.kool.pipeline.backend.webgpu.* -import kotlinx.browser.document -import kotlinx.browser.window -import kotlinx.coroutines.await -import org.khronos.webgl.Float32Array -import org.khronos.webgl.Uint32Array -import org.w3c.dom.HTMLCanvasElement -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine -import kotlin.math.roundToInt - -val vertexCode = """ - struct VSOut { - @builtin(position) Position: vec4, - @location(0) color: vec3 - }; - - struct UBO { - modelViewProj: mat4x4, - primaryColor: vec4, - accentColor: vec4 - }; - - @group(0) @binding(0) - var uniforms: UBO; - - @stage(vertex) - fn main(@location(0) inPos: vec3, - @location(1) inColor: vec3) -> VSOut { - var vsOut: VSOut; - vsOut.Position = vec4(inPos, 1.0); - vsOut.color = inColor * uniforms.accentColor.rgb + uniforms.primaryColor.rgb; - return vsOut; - } -""".trimIndent() - -val fragmentCode = """ - @stage(fragment) - fn main(@location(0) inColor: vec3) -> @location(0) vec4 { - return vec4(inColor, 1.0); - } -""".trimIndent() - -val vertexData = arrayOf(1f, -1f, 0f, -1f, -1f, 0f, 0f, 1f, 0f) -val colorData = arrayOf(1f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f) -val indexData = arrayOf(0, 1, 2) - -val uniformData = arrayOf( - 1f, 0f, 0f, 0f, - 0f, 1f, 0f, 0f, - 0f, 0f, 1f, 0f, - 0f, 0f, 0f, 1f, - - 0.9f, 0.1f, 0.3f, 1f, - 0.8f, 0.2f, 0.8f, 1f -) - -lateinit var canvas: HTMLCanvasElement -lateinit var device: GPUDevice -lateinit var context: GPUCanvasContext -lateinit var pipeline: GPURenderPipeline -lateinit var view: GPUTextureView - -lateinit var vertexBuffer: GPUBuffer -lateinit var colorBuffer: GPUBuffer -lateinit var indexBuffer: GPUBuffer -lateinit var uniformBuffer: GPUBuffer -lateinit var uniformBindGroup: GPUBindGroup - -/** - * WebGPU hello world test / demo, for now this is not integrated into kool at all... - * - * Only works on Chrome Canary with --enable-unsafe-webgpu - */ -fun webGpuTest() { - launch { - canvas = document.getElementById("glCanvas") as HTMLCanvasElement - val adapter = navigator.gpu.requestAdapter().await() - device = adapter!!.requestDevice().await() - - val ctx = canvas.getContext("webgpu") as? GPUCanvasContext - if (ctx == null) { - js("alert(\"Unable to initialize WebGL2 context. Your browser may not support it.\")") - throw RuntimeException("Unable to obtain WebGPU context from canvas") - } - context = ctx - - val screenScale = window.devicePixelRatio - val presentationSize = intArrayOf((canvas.clientWidth * screenScale).roundToInt(), (canvas.clientHeight * screenScale).roundToInt()) - val presentationFormat = navigator.gpu.getPreferredCanvasFormat() - val sampleCount = 4 - - canvas.width = presentationSize[0] - canvas.height = presentationSize[1] - context.configure( - GPUCanvasConfiguration( - device = device, - format = presentationFormat - ) - ) - println("WebGPU context configured!") - - vertexBuffer = createF32Buffer(vertexData, GPUBufferUsage.VERTEX) - colorBuffer = createF32Buffer(colorData, GPUBufferUsage.VERTEX) - indexBuffer = createU32Buffer(indexData, GPUBufferUsage.INDEX) - uniformBuffer = createF32Buffer(uniformData, GPUBufferUsage.UNIFORM or GPUBufferUsage.COPY_DST) - - val positionBufferDesc = GPUVertexBufferLayout( - attributes = arrayOf( - GPUVertexAttribute( - shaderLocation = 0, - offset = 0, - format = GPUVertexFormat.float32x3 - ) - ), - arrayStride = 4 * 3 - ) - val colorBufferDesc = GPUVertexBufferLayout( - attributes = arrayOf( - GPUVertexAttribute( - shaderLocation = 1, - offset = 0, - format = GPUVertexFormat.float32x3 - ) - ), - arrayStride = 4 * 3 - ) - - val uniformBindGroupLayout = device.createBindGroupLayout( - GPUBindGroupLayoutDescriptor( - entries = arrayOf( - GPUBindGroupLayoutEntry( - binding = 0, - visibility = GPUShaderStage.VERTEX, - buffer = GPUBufferBindingLayout(GPUBufferBindingType.uniform) - ) - ) - ) - ) - uniformBindGroup = device.createBindGroup( - GPUBindGroupDescriptor( - layout = uniformBindGroupLayout, - entries = arrayOf( - GPUBindGroupEntry( - binding = 0, - resource = GPUBufferBinding(uniformBuffer) - ) - ) - ) - ) - - val layout = device.createPipelineLayout( - GPUPipelineLayoutDescriptor( - bindGroupLayouts = arrayOf(uniformBindGroupLayout) - ) - ) - - pipeline = device.createRenderPipeline( - GPURenderPipelineDescriptor( - layout = layout, - vertex = GPUVertexState( - module = device.createShaderModule(GPUShaderModuleDescriptor(vertexCode)), - entryPoint = "main", - buffers = arrayOf(positionBufferDesc, colorBufferDesc) - ), - fragment = GPUFragmentState( - module = device.createShaderModule(GPUShaderModuleDescriptor(fragmentCode)), - entryPoint = "main", - targets = arrayOf(GPUColorTargetState(presentationFormat)) - ), - primitive = GPUPrimitiveState( - topology = GPUPrimitiveTopology.triangleList - ), - multisample = GPUMultisampleState( - count = sampleCount - ) - ) - ) - println("created pipeline!") - - val texture = device.createTexture( - GPUTextureDescriptor( - size = presentationSize, - sampleCount = sampleCount, - format = presentationFormat, - usage = GPUTextureUsage.RENDER_ATTACHMENT - ) - ) - view = texture.createView() - println("created view!") - - window.requestAnimationFrame { frame() } - println("started animation loop") - } -} - -fun frame() { - val commandEncoder = device.createCommandEncoder() - val passEncoder = commandEncoder.beginRenderPass( - GPURenderPassDescriptor(arrayOf( - GPURenderPassColorAttachment( - view = view, - //resolveTarget = context.getCurrentTexture().createView(), - clearValue = GPUColorDict(0.0, 0.0, 0.0, 1.0), - loadOp = GPULoadOp.clear, - storeOp = GPUStoreOp.store - ) - )) - ) - - - passEncoder.setPipeline(pipeline) - - passEncoder.setViewport(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), 0f, 1f) - passEncoder.setVertexBuffer(0, vertexBuffer) - passEncoder.setVertexBuffer(1, colorBuffer) - passEncoder.setIndexBuffer(indexBuffer, GPUIndexFormat.uint32) - passEncoder.setBindGroup(0, uniformBindGroup) - passEncoder.drawIndexed(3) - - //passEncoder.draw(3, 1, 0, 0) - passEncoder.end() - - device.queue.submit(arrayOf(commandEncoder.finish())) - - window.requestAnimationFrame { frame() } -} - -fun createF32Buffer(data: Array, usage: Int): GPUBuffer { - val buffer = device.createBuffer( - GPUBufferDescriptor( - size = data.size * 4L, - usage = usage, - mappedAtCreation = true - ) - ) - val array = Float32Array(buffer.getMappedRange()) - array.set(data) - buffer.unmap() - return buffer -} - -fun createU32Buffer(data: Array, usage: Int): GPUBuffer { - val buffer = device.createBuffer( - GPUBufferDescriptor( - size = data.size * 4L, - usage = usage, - mappedAtCreation = true - ) - ) - val array = Uint32Array(buffer.getMappedRange()) - array.set(data) - buffer.unmap() - return buffer -} - -fun launch(block: suspend () -> Unit) { - block.startCoroutine(object : Continuation { - override val context: CoroutineContext get() = EmptyCoroutineContext - override fun resumeWith(result: Result) { - if (result.isFailure) { - println("resume with failure") - result.exceptionOrNull()?.printStackTrace() - } - } - }) -} \ No newline at end of file