diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 919e719..2b59a38 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -12,6 +12,9 @@ plugins { dependencies { commonMainApi(projects.math) commonMainApi(projects.util) + + // TODO: change dependency to only include enumerations + commonMainApi(libs.wgpu4k) } kotlin { diff --git a/core/src/commonMain/kotlin/Pipeline.kt b/core/src/commonMain/kotlin/Pipeline.kt index 96c069d..7a7aa81 100644 --- a/core/src/commonMain/kotlin/Pipeline.kt +++ b/core/src/commonMain/kotlin/Pipeline.kt @@ -5,7 +5,8 @@ package quest.laxla.spock * * @since 0.0.1-alpha.4 */ -public data class Pipeline @ExperimentalSpockApi constructor( +@ExperimentalSpockApi +public data class Pipeline( public val mesh: Mesh, public val vertexShader: VertexShader ) diff --git a/core/src/commonMain/kotlin/Renderer.kt b/core/src/commonMain/kotlin/Renderer.kt index 6fa61a1..361df57 100644 --- a/core/src/commonMain/kotlin/Renderer.kt +++ b/core/src/commonMain/kotlin/Renderer.kt @@ -6,9 +6,10 @@ package quest.laxla.spock @SubclassOptInRequired(ExperimentalSpockApi::class) public interface Renderer : Closer { /** - * Render a frame. + * Renders a frame. * * @since 0.0.1-alpha.4 */ + @Throws(UnsupportedShaderException::class) public suspend operator fun invoke() } diff --git a/core/src/commonMain/kotlin/Shader.kt b/core/src/commonMain/kotlin/Shader.kt index cad890a..51501ab 100644 --- a/core/src/commonMain/kotlin/Shader.kt +++ b/core/src/commonMain/kotlin/Shader.kt @@ -1,7 +1,21 @@ package quest.laxla.spock +import kotlinx.collections.immutable.ImmutableSet + /** - * Any kind of shader. + * Represents a program to be run on the GPU. + * + * Shaders are provided to [Renderer]s via [Pipeline]s, and are defined by two parameters; + * * [Kind], that itself consists of two more parameters; + * * [Language] (e.g. [Wgsl]), accessible via [Shader.language]. + * * [FormFactor] (e.g. [StringShader.FormFactor]), accessible via [Shader.formFactor]. + * * Type represented by sub-interfaces that are referred to directly by [Pipeline]s (e.g. [VertexShader]). + * + * Shading languages and form factors often require shaders + * to implement an additional interface carrying additional metadata (e.g. [Wgsl.Shader.entrypoint]), + * or in the case of form factors, the code itself (e.g. [StringShader.code]). + * + * [Renderer]s are expected to deduplicate shaders that have same code and [label]. * * @since 0.0.1-alpha.4 */ @@ -10,11 +24,20 @@ public interface Shader { * The language this [Shader] is written in. * * @since 0.0.1-alpha.4 + * @see Shader.kind */ public val language: Language /** - * The label the graphics API should refer to this [Shader] under for logging purposes. + * The [FormFactor] of this [Shader]. + * + * @since 0.0.1-alpha.4 + * @see Shader.kind + */ + public val formFactor: FormFactor + + /** + * The label this [Shader] should be referred to as for logging purposes. * * @since 0.0.1-alpha.4 */ @@ -42,13 +65,48 @@ public interface Shader { } /** - * Implemented by [Shader]s dynamically compiled from another shader of a different [Language] or form factor. + * Represents a shader form, e.g., a [StringShader]. + * + * Implementations **must** properly implement [equals], [hashCode], and [toString]; + * Therefore, it is recommended to use `data object`s. + * + * @since 0.0.1-alpha.4 + * @sample StringShader + */ + public interface FormFactor { + /** + * Does this [shader] carry all metadata required by this form factor? + * + * This function always returns `false` if the shader's [FormFactor] is not this one. + * + * @since 0.0.1-alpha.4 + * @see Shader.isCarryingRequiredMetadata + */ + public infix fun accepts(shader: Shader): Boolean + } + + /** + * Combination of a [FormFactor] and a [Language]. + * + * @property formFactor the form factor of [Shader]s of this kind. + * @property language the language [Shader]s of this kind are written in. + * @since 0.0.1-alpha.4 + * @see Shader.kind + */ + public data class Kind( + val language: Language, + val formFactor: FormFactor + ) where L : Language, F : FormFactor + + /** + * Implemented by [Shader]s dynamically compiled from another shader of a different [Language] or [FormFactor]. * * @param L the language this shader was transpiled to. + * @param F the form factor this shader was transpiled to. * @since 0.0.1-alpha.4 * @see Transpiler */ - public interface Transpiled : Shader where L : Language { + public interface Transpiled : Shader where L : Language, F : FormFactor { /** * The original [Shader] this one was compiled from. * @@ -57,31 +115,57 @@ public interface Shader { public val original: Shader override val language: L + override val formFactor: F } /** - * Transpiles [Shader] from one [Language] or form factor to another if not supported by the [Renderer]. + * Transpiles [Shader]s of one [Kind] to another. * * @since 0.0.1-alpha.4 * @see Transpiled */ - public interface Transpiler where L : Language { + public sealed interface Transpiler where L : Language, F : FormFactor { /** - * The [Language] the shaders this transpiler outputs are written in. + * Transpiles the [input]ted [Shader] to one of the [outputKinds]. + * + * It is expected but not required that the output [carries all required metadata][Shader.isCarryingRequiredMetadata]. * + * @return `null` if this [Transpiler] does not support the provided [input]. * @since 0.0.1-alpha.4 + * @see transpile */ - public val outputLanguage: L + public suspend operator fun invoke(input: Shader): Transpiled? /** - * Transpiles the [input] [Shader] to the [outputLanguage]. + * Transpiles [Shader]s to a single, constant [outputKind]. * - * It is expected but not required that the output is [accept][Language.accepts]ed; - * However, prefer returning an unaccepted shader over `null` when possible and not expensive. - * - * @return `null` if this [Transpiler] does not support this [input]. * @since 0.0.1-alpha.4 + * @see Transpiler + * @see Transpiler.Dynamic + */ + public interface SingleTarget : Transpiler where L : Language, F : FormFactor { + /** + * The [Kind] of all [Shader]s outputted by this [Transpiler]. + * + * @since 0.0.1-alpha.4 + */ + public val outputKind: Kind + } + + /** + * Transpile [Shader]s to any of the pre-selected [outputKinds]. + * + * It is up to the implementation to select the optimal [Kind]. + * + * @see 0.0.1-alpha.4 */ - public suspend operator fun invoke(input: Shader): Transpiled? + public interface Dynamic : Transpiler where L : Language, F : FormFactor { + /** + * All [Shader.Kind]s this [Transpiler] can output. + * + * @since 0.0.1-alpha.4 + */ + public val outputKinds: ImmutableSet> + } } } diff --git a/core/src/commonMain/kotlin/Shaders.kt b/core/src/commonMain/kotlin/Shaders.kt index 42171ca..d20ed1e 100644 --- a/core/src/commonMain/kotlin/Shaders.kt +++ b/core/src/commonMain/kotlin/Shaders.kt @@ -1,16 +1,172 @@ package quest.laxla.spock +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentHashSetOf + +private val logger = KotlinLogging.logger {} + +/** + * Creates a readable [String] about this [Shader] that does not contain the shader's actual implementation. + * + * @since 0.0.1-alpha.4 + * @see transpilationTraceToString + */ +public val Shader.readableName + get() = buildString { + append(formFactor) + append('(') + append("language = $language") + if (label != null) append(", label = \"$label\"") + append(')') + } + +/** + * @author Laxystem + */ +private fun StringBuilder.appendTranspilationTrace(shader: Shader) { + if (shader is Shader.Transpiled<*, *>) { + appendTranspilationTrace(shader.original) + append(" =>> ") + } + + append(shader.readableName) +} + +/** + * Creates a readable [String] about this [Shader], + * including what shaders it was transpiled from, if any. + * + * @since 0.0.1-alpha.4 + * @see readableName + */ +public fun Shader.transpilationTraceToString(): String = buildString { + appendTranspilationTrace(this@transpilationTraceToString) +} + /** - * Does this [Shader] carry all metadata required by the [language][Shader.language] it is written in? + * Does this [Shader] carry all metadata + * required by the [language][Shader.language] and [form factor][Shader.formFactor] + * it is written in? * * @since 0.0.1-alpha.4 * @see Shader.Language.accepts + * @see Shader.FormFactor.accepts + * @see Shader.Kind.accepts + */ +public val Shader.isCarryingRequiredMetadata: Boolean get() = language accepts this && formFactor accepts this + +/** + * Throws if this [Shader] does not carry all required metadata. + * + * @since 0.0.1-alpha.4 + * @see isCarryingRequiredMetadata + */ +@Throws(UnsupportedShaderException::class) +public fun S.throwingIfMissingMetadata(): S = also { + if (!(language accepts it)) throw UnsupportedShaderException(it, "missing metadata required by language") + if (!(formFactor accepts it)) throw UnsupportedShaderException(it, "missing metadata required by form factor") +} + +/** + * Does the provided [shader] carry all metadata required by this [Shader.Kind]? + * + * @since 0.0.1-alpha.4 + * @see Shader.Language.accepts + * @see Shader.FormFactor.accepts + * @see Shader.isOfKind + * @see Shader.isCarryingRequiredMetadata + */ +public infix fun Shader.Kind<*, *>.accepts(shader: Shader) = language accepts shader && formFactor accepts shader + +/** + * Does this [Shader] belong to this shader [kind]? + * + * Note this does not imply this shader [is carrying all required metadata][Shader.Kind.accepts]. + * + * @since 0.0.1-alpha.4 + * @see Shader.language + * @see Shader.formFactor + */ +public infix fun Shader.isOfKind(kind: Shader.Kind<*, *>): Boolean = + language == kind.language && formFactor == kind.formFactor + +/** + * The [Shader.Kind] of this [Shader]. + * + * @since 0.0.1-alpha.4 + * @see Shader.formFactor + * @see Shader.language + * @see Shader.isOfKind + * @see Shader.Kind.accepts */ -public val Shader.isCarryingRequiredMetadata: Boolean get() = language accepts this +public val Shader.kind: Shader.Kind<*, *> get() = Shader.Kind(language, formFactor) + +/** + * All [Shader.Kind]s this [Shader.Transpiler] can output. + * + * @since 0.0.1-alpha.4 + * @see Shader.isOfAnyKindOutputtedBy + * @see Shader.Transpiler.Dynamic.outputKinds + * @see Shader.Transpiler.SingleTarget.outputKind + */ +public val Shader.Transpiler.outputKinds: ImmutableSet> where L : Shader.Language, F : Shader.FormFactor + get() = when (this) { + is Shader.Transpiler.Dynamic -> outputKinds + is Shader.Transpiler.SingleTarget -> persistentHashSetOf(outputKind) + } + +/** + * @author Laxystem + */ +private inline fun Shader.isAnyKindOutputtedBy( + transpiler: Shader.Transpiler<*, *>, + predicate: Shader.(Shader.Kind<*, *>) -> Boolean +) = when (transpiler) { + is Shader.Transpiler.Dynamic -> transpiler.outputKinds.any { predicate(it) } + is Shader.Transpiler.SingleTarget -> predicate(transpiler.outputKind) +} + +/** + * Does this [Shader] belong to any [Shader.Kind] that the given [transpiler] can output? + * + * This function is faster than checking against [Shader.Transpiler.outputKinds], + * as it does not heap-allocate unnecessarily. + * + * @since 0.0.1-alpha.4 + */ +public infix fun Shader.isOfAnyKindOutputtedBy(transpiler: Shader.Transpiler<*, *>): Boolean = + isAnyKindOutputtedBy(transpiler) { this isOfKind it } + +/** + * Does this [Shader] belong to any [Shader.Kind] that the given [transpiler] can output? + * + * This function is faster than checking against [Shader.Transpiler.outputKinds], + * as it does not heap-allocate unnecessarily. + * + * @since 0.0.1-alpha.4 + */ +public infix fun Shader.isAcceptedByAnyKindOutputtedBy(transpiler: Shader.Transpiler<*, *>): Boolean = + isAnyKindOutputtedBy(transpiler) { it accepts this } + +/** + * Transpiles this [shader] + */ +public suspend fun Shader.Transpiler.transpile(shader: Shader): Shader.Transpiled? where L : Shader.Language, F : Shader.FormFactor = + invoke(input = shader)?.let { output -> + when { + !(output.language accepts output) -> logger.warn { output.transpilationTraceToString() + ": transpilation result missing metadata required by language" } + !(output.formFactor accepts output) -> logger.warn { output.transpilationTraceToString() + ": transpilation result missing metadata required by form factor" } + else -> return output + } + + return null + } /** * @author Laxystem */ +@ExperimentalSpockApi private data class WgslVertexShader( override val vertexKind: VertexKind, override val code: String, @@ -23,9 +179,54 @@ private data class WgslVertexShader( * * @since 0.0.1-alpha.4 */ +@ExperimentalSpockApi public fun wgslVertexShaderOf( wgsl: String, vertexKind: VertexKind, entrypoint: String, label: String? = null ): VertexShader = WgslVertexShader(vertexKind, wgsl, entrypoint, label) + +/** + * Caches transpilation results, filters unsupported shaders and + * fallbacks to the [original][Shader.Transpiled.original] for [transpiled][Shader.Transpiled] shaders. + * + * Similarly to a [ManualCache], transpiled shaders are not forgotten unless manually [remove][Cache.remove]d. + * + * @since 0.0.1-alpha.4 + */ +public fun ShaderCache(transpiler: Shader.Transpiler<*, *>): Cache = + object : Cache { + private val transpiledShaders = + ManualCache { transpiler(it)?.takeIf(Shader::isCarryingRequiredMetadata) } + + /** + * @author Laxystem + */ + private inline fun findAccepted(shader: Shader, findAccepted: (Shader) -> Shader?) = when { + shader isAcceptedByAnyKindOutputtedBy transpiler -> shader + shader is Shader.Transpiled<*, *> -> findAccepted(shader.original) + else -> null + } + + private fun findAccepted(shader: Shader): Shader? = findAccepted(shader, ::findAccepted) + + override suspend fun get(shader: Shader): Shader? = + findAccepted(shader) { get(it) } ?: transpiledShaders[shader] + + override suspend fun contains(shader: Shader): Boolean = + shader in transpiledShaders || shader is Shader.Transpiled<*, *> && shader.original in this + + override suspend fun remove(descriptor: Shader): Unit? { + var result: Unit? = null + if (descriptor is Shader.Transpiled<*, *>) result = remove(descriptor) + result = transpiledShaders.remove(descriptor) ?: result + return result + } + + override suspend fun cacheAll(shaders: Sequence) { + transpiledShaders.cacheAll(shaders.mapNotNull(::findAccepted)) + } + + override suspend fun close() = transpiledShaders.close() + } diff --git a/core/src/commonMain/kotlin/StringShader.kt b/core/src/commonMain/kotlin/StringShader.kt index cc8021b..63fd183 100644 --- a/core/src/commonMain/kotlin/StringShader.kt +++ b/core/src/commonMain/kotlin/StringShader.kt @@ -15,4 +15,29 @@ public interface StringShader : Shader { * @since 0.0.1-alpha.4 */ public val code: String + + override val formFactor: FormFactor get() = StringShader + + /** + * @author Laxystem + * @since 0.0.1-alpha.4 + */ + public companion object FormFactor : Shader.FormFactor { + override fun accepts(shader: Shader): Boolean = shader is StringShader + + override fun toString(): String = "StringShader" + + override fun equals(other: Any?): Boolean = other is FormFactor + + /** + * Kotlin [gives data objects constant hash codes](https://kotlinlang.org/docs/object-declarations.html#data-objects); + * This is the only way to reproduce this, + * as [FormFactor] itself cannot be made into a data object since it is a companion object. + * + * @author Laxystem + */ + private data object HashCodeImpl + + override fun hashCode(): Int = HashCodeImpl.hashCode() + } } diff --git a/core/src/commonMain/kotlin/UnsupportedShaderException.kt b/core/src/commonMain/kotlin/UnsupportedShaderException.kt new file mode 100644 index 0000000..bf55f69 --- /dev/null +++ b/core/src/commonMain/kotlin/UnsupportedShaderException.kt @@ -0,0 +1,10 @@ +package quest.laxla.spock + +/** + * @since 0.0.1-alpha.4 + */ +public open class UnsupportedShaderException( + public val shader: Shader, + message: String? = null, + cause: Throwable? = null +) : IllegalArgumentException(shader.transpilationTraceToString() + message.letOrEmpty { ": $it" }, cause) diff --git a/core/src/commonMain/kotlin/VertexKind.kt b/core/src/commonMain/kotlin/VertexKind.kt index 54ffbb6..9d3a436 100644 --- a/core/src/commonMain/kotlin/VertexKind.kt +++ b/core/src/commonMain/kotlin/VertexKind.kt @@ -1,19 +1,19 @@ package quest.laxla.spock +import io.ygdrasil.webgpu.VertexFormat +import kotlinx.collections.immutable.ImmutableList import kotlinx.io.bytestring.ByteStringBuilder /** - * Describes how [Vertex] serializes as a vertex into buffers to be sent to the GPU. + * Serializes [Vertex] into buffers to be sent to the GPU. * * @since 0.0.1-alpha.4 */ public interface VertexKind { /** - * The total size of a vertex of this kind. - * * @since 0.0.1-alpha.4 */ - public val sizeInBytes: UInt + public val attributes: @FutureImmutableArray ImmutableList /** * Appends this [vertex] into this [ByteStringBuilder]. diff --git a/core/src/commonMain/kotlin/VertexKinds.kt b/core/src/commonMain/kotlin/VertexKinds.kt index 3617da2..9b54dd5 100644 --- a/core/src/commonMain/kotlin/VertexKinds.kt +++ b/core/src/commonMain/kotlin/VertexKinds.kt @@ -1,80 +1,23 @@ package quest.laxla.spock +import io.ygdrasil.webgpu.VertexFormat +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.io.bytestring.ByteStringBuilder -import quest.laxla.spock.math.* /** - * Creates a simple [VertexKind] for [Vector1]s of this space. + * Creates a [VertexKind] for the provided [attributes]. * - * @author Laxystem * @since 0.0.1-alpha.4 - * @see vertexKindOfVector2 - * @see vertexKindOfVector3 - * @see vertexKindOfVector4 */ -public fun S.vertexKindOfVector1(): VertexKind> where S : BufferableSpace = - object : VertexKind> { - override val sizeInBytes: UInt - get() = this@vertexKindOfVector1.sizeInBytes - - override fun ByteStringBuilder.append(vertex: Vector1) = append(vector = vertex) - - override fun toString(): String = "Vector1 of ${this@vertexKindOfVector1}" - } - -/** - * Creates a simple [VertexKind] for [Vector2]s of this space. - * - * @author Laxystem - * @since 0.0.1-alpha.4 - * @see vertexKindOfVector1 - * @see vertexKindOfVector3 - * @see vertexKindOfVector4 - */ -public fun S.vertexKindOfVector2(): VertexKind> where S : BufferableSpace = - object : VertexKind> { - override val sizeInBytes: UInt - get() = this@vertexKindOfVector2.sizeInBytes - - override fun ByteStringBuilder.append(vertex: Vector2) = append(vector = vertex) - - override fun toString(): String = "Vector2 of ${this@vertexKindOfVector2}" - } - -/** - * Creates a simple [VertexKind] for [Vector3]s of this space. - * - * @author Laxystem - * @since 0.0.1-alpha.4 - * @see vertexKindOfVector1 - * @see vertexKindOfVector2 - * @see vertexKindOfVector4 - */ -public fun S.vertexKindOfVector3(): VertexKind> where S : BufferableSpace = - object : VertexKind> { - override val sizeInBytes: UInt - get() = this@vertexKindOfVector3.sizeInBytes - - override fun ByteStringBuilder.append(vertex: Vector3) = append(vector = vertex) - - override fun toString(): String = "Vector3 of ${this@vertexKindOfVector3}" - } - -/** - * Creates a simple [VertexKind] for [Vector4]s of this space. - * - * @author Laxystem - * @since 0.0.1-alpha.4 - * @see vertexKindOfVector1 - * @see vertexKindOfVector2 - * @see vertexKindOfVector3 - */ -public fun S.vertexKindOfVector4(): VertexKind> where S : BufferableSpace = - object : VertexKind> { - override val sizeInBytes: UInt - get() = this@vertexKindOfVector4.sizeInBytes - - override fun ByteStringBuilder.append(vertex: Vector4) = append(vector = vertex) - - override fun toString(): String = "Vector4 of ${this@vertexKindOfVector4}" - } +public inline fun VertexKind( + vararg attributes: VertexFormat, + crossinline append: ByteStringBuilder.(V) -> Unit +): VertexKind = object : VertexKind { + override val attributes: @FutureImmutableArray ImmutableList = persistentListOf(*attributes) + override fun ByteStringBuilder.append(vertex: V) = append(vertex) + + override fun toString(): String = "VertexKind(${this.attributes})" + override fun equals(other: Any?): Boolean = other is VertexKind<*> && this.attributes == other.attributes + override fun hashCode(): Int = attributes.hashCode() +} diff --git a/core/src/commonMain/kotlin/VertexShader.kt b/core/src/commonMain/kotlin/VertexShader.kt index 84beca5..70091a4 100644 --- a/core/src/commonMain/kotlin/VertexShader.kt +++ b/core/src/commonMain/kotlin/VertexShader.kt @@ -5,4 +5,5 @@ package quest.laxla.spock * * @since 0.0.1-alpha.4 */ +@ExperimentalSpockApi // add mesh shader support public interface VertexShader : VertexKind.Bound, Shader diff --git a/example/src/commonMain/kotlin/AdvancedRenderer.kt b/example/src/commonMain/kotlin/AdvancedRenderer.kt index 138395b..ef07892 100644 --- a/example/src/commonMain/kotlin/AdvancedRenderer.kt +++ b/example/src/commonMain/kotlin/AdvancedRenderer.kt @@ -1,55 +1,37 @@ -@file:Suppress("UnusedImport") - package quest.laxla.spock.example import io.github.oshai.kotlinlogging.KotlinLogging import io.ygdrasil.webgpu.Device -import io.ygdrasil.webgpu.ShaderModule import io.ygdrasil.webgpu.ShaderModuleDescriptor import io.ygdrasil.webgpu.TextureFormat -import kotlinx.io.bytestring.buildByteString import quest.laxla.spock.* import quest.laxla.spock.toolkit.Surface import quest.laxla.spock.toolkit.WebGpuRenderer -@Suppress("unused") private val logger = KotlinLogging.logger {} -/* + @OptIn(ExperimentalSpockApi::class) public class AdvancedRenderer( override val device: Device, override val surface: Surface, - override val format: TextureFormat = TODO(), + override val format: TextureFormat = surface.textureFormat, public val scene: RenderScene, - public val vertexKind: VertexKind + public val vertexKind: VertexKind, + transpiler: Shader.Transpiler.SingleTarget ) : WebGpuRenderer, Closer by Closer(surface, device) { - private val shaders = +MultiCache(device::createShaderModule) - private val vertexBuffers = mutableMapOf>>() + private val shaders = +ShaderCache(transpiler) + private val shaderDescriptors = +ManualCache { + ShaderModuleDescriptor( + code = (it as StringShader).code, + label = it.label, + compilationHints = listOf(ShaderModuleDescriptor.CompilationHint(entryPoint = (it as Wgsl.Shader).entrypoint)) + ) + } + private val shaderModules = +ManualCache(device::createShaderModule) - private fun Sequence>.toMeshMap() = this - .filter { (_, shader) -> shader is WgslShader && shader.vertexKind == vertexKind } - .groupingBy(Pipeline::vertexShader) - .aggregate { shader, list: MutableList?, (mesh), _ -> - if (mesh is VertexMesh) list?.apply { addAll(mesh.vertices) } ?: mesh.vertices.toMutableList() - else list ?: mutableListOf() - } + private val vertexBuffers = mutableMapOf>>() override suspend fun invoke() { - for ((shader, vertices) in scene.pipelines.toMeshMap()) { - val shaderModule = shaders[(shader as WgslShader).shaderModule] - - *//* - val vertexBuffer = vertexBuffers[shader.shaderModule] - if (vertexBuffer != null && vertexBuffer.second.size == vertices.size) { - for ((index, vertex) in vertices.withIndex()) if (vertex != vertexBuffer.second[index]) ByteStringBuilder(vertexBuffer.first, index * vertexKind.sizeInBytes) - } else { - - vertexBuffers[shader.shaderModule] = *//* val byteString = buildByteString { - with(vertexKind) { - for (vertex in vertices) append(vertex) - } - }.toByteArray() *//* to vertices - } *//* - } + TODO() } -}*/ +} diff --git a/example/src/commonMain/kotlin/MyRenderer.kt b/example/src/commonMain/kotlin/MyRenderer.kt index ab1ce13..412f70e 100644 --- a/example/src/commonMain/kotlin/MyRenderer.kt +++ b/example/src/commonMain/kotlin/MyRenderer.kt @@ -5,7 +5,7 @@ import quest.laxla.spock.* import quest.laxla.spock.toolkit.Surface import quest.laxla.spock.toolkit.WebGpuRenderer -private val Surface.textureFormat +internal val Surface.textureFormat get() = preferredTextureFormat ?: TextureFormat.RGBA8Unorm.takeIf { it in supportedTextureFormats } ?: TextureFormat.BGRA8Unorm.takeIf { it in supportedTextureFormats } diff --git a/util/src/commonMain/kotlin/Cache.kt b/util/src/commonMain/kotlin/Cache.kt index 06b9fad..c969c94 100644 --- a/util/src/commonMain/kotlin/Cache.kt +++ b/util/src/commonMain/kotlin/Cache.kt @@ -33,4 +33,11 @@ public interface Cache : SuspendCloseable { * @since 0.0.1-alpha.4 */ public suspend fun remove(descriptor: Descriptor): Unit? + + /** + * Ensures all provided [descriptors] have a [Product] cached under them. + * + * @since 0.0.1-alpha.4 + */ + public suspend fun cacheAll(descriptors: Sequence) } diff --git a/util/src/commonMain/kotlin/Caches.kt b/util/src/commonMain/kotlin/Caches.kt index d89bb91..efd8f73 100644 --- a/util/src/commonMain/kotlin/Caches.kt +++ b/util/src/commonMain/kotlin/Caches.kt @@ -1,22 +1,25 @@ package quest.laxla.spock +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock /** - * Creates a [Cache] that'll create missing entries using the given [producer]. + * Creates a [Cache] that never [remove]s (and therefore closes) its content unless + * [remove] is manually called or if the cache itself is [close][SuspendCloseable.close]d. * - * The returned cache will never forget its contents unless [remove] is explicitly called - * or if the cache itself is [close][SuspendCloseable.close]d. - * - * The returned cache is cleared when [close][SuspendCloseable.close]d, and may be reused. + * The returned cache clears when [close][SuspendCloseable.close]d, and may be reused; + * However, this is not recommended if the cache was passed by reference, + * as other users can't know about the closure. * * @author Laxystem * @since 0.0.1-alpha.4 */ -public inline fun MultiCache(crossinline producer: (Descriptor) -> Product): Cache = +public inline fun ManualCache(crossinline producer: suspend (Descriptor) -> Product): Cache = object : Cache { - private val contents = linkedMapOf() + private val contents = hashMapOf() private val mutex = Mutex() override suspend fun get(descriptor: Descriptor): Product = mutex.withLock { @@ -25,7 +28,7 @@ public inline fun MultiCache(crossinline producer: (Descri override suspend fun contains(descriptor: Descriptor): Boolean = mutex.withLock { descriptor in contents } - suspend fun close(removed: Product): Unit? = when (removed) { + private suspend fun close(removed: Product): Unit? = when (removed) { is SuspendCloseable -> removed.close() is AutoCloseable -> removed.close() null -> null @@ -36,6 +39,16 @@ public inline fun MultiCache(crossinline producer: (Descri contents.remove(descriptor)?.let { close(it) } } + override suspend fun cacheAll(descriptors: Sequence) = mutex.withLock { + coroutineScope { + descriptors.filter { it !in contents }.mapTo(mutableListOf()) { + launch { + contents.put(it, producer(it)) + } + }.joinAll() + } + } + override suspend fun close() = mutex.withLock { for (product in contents.values.reversed()) close(product) contents.clear() diff --git a/util/src/commonMain/kotlin/Strings.kt b/util/src/commonMain/kotlin/Strings.kt index c6a8ad9..e3e743a 100644 --- a/util/src/commonMain/kotlin/Strings.kt +++ b/util/src/commonMain/kotlin/Strings.kt @@ -11,8 +11,15 @@ import kotlin.contracts.contract */ public fun emptyString(): String = "" +/** + * If `null`, returns an empty [String]; + * otherwise, returns the result of [block]. + * + * @since 0.0.1-alpha.4 + * @see String.orEmpty + */ @OptIn(ExperimentalContracts::class) -public inline fun String?.letOrEmpty(block: (String) -> String = { it }): String { +public inline fun String?.letOrEmpty(block: (String) -> String): String { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) }