diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c47d94a8..e0807fa9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,8 @@ on: tags: - "v*" pull_request: + branches: + - dev jobs: format_and_compile: diff --git a/build.sbt b/build.sbt index 419d75a4..645d9d68 100644 --- a/build.sbt +++ b/build.sbt @@ -58,6 +58,8 @@ lazy val commonSettings = Seq( lazy val runnerSettings = Seq(libraryDependencies += "org.apache.logging.log4j" % "log4j-slf4j2-impl" % "2.24.3") +lazy val fs2Settings = Seq(libraryDependencies ++= Seq("co.fs2" %% "fs2-core" % "3.12.0", "co.fs2" %% "fs2-io" % "3.12.0")) + lazy val utility = (project in file("cyfra-utility")) .settings(commonSettings) @@ -98,13 +100,17 @@ lazy val vscode = (project in file("cyfra-vscode")) .settings(commonSettings) .dependsOn(foton) +lazy val fs2interop = (project in file("cyfra-fs2")) + .settings(commonSettings, fs2Settings) + .dependsOn(runtime) + lazy val e2eTest = (project in file("cyfra-e2e-test")) .settings(commonSettings, runnerSettings) - .dependsOn(runtime) + .dependsOn(runtime, fs2interop) lazy val root = (project in file(".")) .settings(name := "Cyfra") - .aggregate(compiler, dsl, foton, core, runtime, vulkan, examples) + .aggregate(compiler, dsl, foton, core, runtime, vulkan, examples, fs2interop) e2eTest / Test / javaOptions ++= Seq("-Dorg.lwjgl.system.stackSize=1024", "-DuniqueLibraryNames=true") diff --git a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/Context.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/Context.scala index 974f045f..96490071 100644 --- a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/Context.scala +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/Context.scala @@ -1,5 +1,6 @@ package io.computenode.cyfra.spirv +import io.computenode.cyfra.dsl.binding.{GBuffer, GUniform} import io.computenode.cyfra.dsl.macros.FnCall.FnIdentifier import io.computenode.cyfra.spirv.SpirvConstants.HEADER_REFS_TOP import io.computenode.cyfra.spirv.compilers.FunctionCompiler.SprivFunction @@ -16,16 +17,17 @@ private[cyfra] case class Context( voidTypeRef: Int = -1, voidFuncTypeRef: Int = -1, workerIndexRef: Int = -1, - uniformVarRef: Int = -1, + uniformVarRefs: Map[GUniform[?], Int] = Map.empty, + bindingToStructType: Map[Int, Int] = Map.empty, constRefs: Map[(Tag[?], Any), Int] = Map(), exprRefs: Map[Int, Int] = Map(), - inBufferBlocks: List[ArrayBufferBlock] = List(), - outBufferBlocks: List[ArrayBufferBlock] = List(), + bufferBlocks: Map[GBuffer[?], ArrayBufferBlock] = Map(), nextResultId: Int = HEADER_REFS_TOP, nextBinding: Int = 0, exprNames: Map[Int, String] = Map(), - memberNames: Map[Int, String] = Map(), + names: Set[String] = Set(), functions: Map[FnIdentifier, SprivFunction] = Map(), + stringLiterals: Map[String, Int] = Map(), ): def joinNested(ctx: Context): Context = this.copy(nextResultId = ctx.nextResultId, exprNames = ctx.exprNames ++ this.exprNames, functions = ctx.functions ++ this.functions) diff --git a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/SpirvConstants.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/SpirvConstants.scala index 6711afff..ec3c4d0b 100644 --- a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/SpirvConstants.scala +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/SpirvConstants.scala @@ -9,10 +9,13 @@ private[cyfra] object SpirvConstants: val BOUND_VARIABLE = "bound" val GLSL_EXT_NAME = "GLSL.std.450" + val NON_SEMANTIC_DEBUG_PRINTF = "NonSemantic.DebugPrintf" val GLSL_EXT_REF = 1 val TYPE_VOID_REF = 2 val VOID_FUNC_TYPE_REF = 3 val MAIN_FUNC_REF = 4 val GL_GLOBAL_INVOCATION_ID_REF = 5 val GL_WORKGROUP_SIZE_REF = 6 - val HEADER_REFS_TOP = 7 + val DEBUG_PRINTF_REF = 7 + + val HEADER_REFS_TOP = 8 diff --git a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/DSLCompiler.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/DSLCompiler.scala index 07ae9aab..8bdafb24 100644 --- a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/DSLCompiler.scala +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/DSLCompiler.scala @@ -4,6 +4,8 @@ import io.computenode.cyfra.* import io.computenode.cyfra.dsl.* import io.computenode.cyfra.dsl.Expression.E import io.computenode.cyfra.dsl.Value.Scalar +import io.computenode.cyfra.dsl.binding.{GBinding, GBuffer, GUniform, WriteBuffer, WriteUniform} +import io.computenode.cyfra.dsl.gio.GIO import io.computenode.cyfra.dsl.struct.GStruct.* import io.computenode.cyfra.dsl.struct.GStructSchema import io.computenode.cyfra.spirv.Context @@ -24,6 +26,28 @@ import scala.runtime.stdLibPatches.Predef.summon private[cyfra] object DSLCompiler: + @tailrec + private def getAllExprsFlattened(pending: List[GIO[?]], acc: List[E[?]], visitDetached: Boolean): List[E[?]] = + pending match + case Nil => acc + case GIO.Pure(v) :: tail => + getAllExprsFlattened(tail, getAllExprsFlattened(v.tree, visitDetached) ::: acc, visitDetached) + case GIO.FlatMap(v, n) :: tail => + getAllExprsFlattened(v :: n :: tail, acc, visitDetached) + case GIO.Repeat(n, gio) :: tail => + val nAllExprs = getAllExprsFlattened(n.tree, visitDetached) + getAllExprsFlattened(gio :: tail, nAllExprs ::: acc, visitDetached) + case WriteBuffer(_, index, value) :: tail => + val indexAllExprs = getAllExprsFlattened(index.tree, visitDetached) + val valueAllExprs = getAllExprsFlattened(value.tree, visitDetached) + getAllExprsFlattened(tail, indexAllExprs ::: valueAllExprs ::: acc, visitDetached) + case WriteUniform(_, value) :: tail => + val valueAllExprs = getAllExprsFlattened(value.tree, visitDetached) + getAllExprsFlattened(tail, valueAllExprs ::: acc, visitDetached) + case GIO.Printf(_, args*) :: tail => + val argsAllExprs = args.flatMap(a => getAllExprsFlattened(a.tree, visitDetached)).toList + getAllExprsFlattened(tail, argsAllExprs ::: acc, visitDetached) + // TODO: Not traverse same fn scopes for each fn call private def getAllExprsFlattened(root: E[?], visitDetached: Boolean): List[E[?]] = var blockI = 0 @@ -33,7 +57,7 @@ private[cyfra] object DSLCompiler: def getAllScopesExprsAcc(toVisit: List[E[?]], acc: List[E[?]] = Nil): List[E[?]] = toVisit match case Nil => acc case e :: tail if visited.contains(e.treeid) => getAllScopesExprsAcc(tail, acc) - case e :: tail => + case e :: tail => // todo i don't think this really works (tail not used???) if allScopesCache.contains(root.treeid) then return allScopesCache(root.treeid) val eScopes = e.introducedScopes val filteredScopes = if visitDetached then eScopes else eScopes.filterNot(_.isDetached) @@ -47,33 +71,52 @@ private[cyfra] object DSLCompiler: allScopesCache(root.treeid) = result result - def compile(tree: Value, inTypes: List[Tag[?]], outTypes: List[Tag[?]], uniformSchema: GStructSchema[?]): ByteBuffer = - val treeExpr = tree.tree - val allExprs = getAllExprsFlattened(treeExpr, visitDetached = true) + // So far only used for printf + private def getAllStrings(pending: List[GIO[?]], acc: Set[String]): Set[String] = + pending match + case Nil => acc + case GIO.FlatMap(v, n) :: tail => + getAllStrings(v :: n :: tail, acc) + case GIO.Repeat(_, gio) :: tail => + getAllStrings(gio :: tail, acc) + case GIO.Printf(format, _*) :: tail => + getAllStrings(tail, acc + format) + case _ :: tail => getAllStrings(tail, acc) + + def compile(bodyIo: GIO[?], bindings: List[GBinding[?]]): ByteBuffer = + val allExprs = getAllExprsFlattened(List(bodyIo), Nil, visitDetached = true) val typesInCode = allExprs.map(_.tag).distinct - val allTypes = (typesInCode ::: inTypes ::: outTypes).distinct + val allTypes = (typesInCode ::: bindings.map(_.tag)).distinct def scalarTypes = allTypes.filter(_.tag <:< summon[Tag[Scalar]].tag) val (typeDefs, typedContext) = defineScalarTypes(scalarTypes, Context.initialContext) + val allStrings = getAllStrings(List(bodyIo), Set.empty) + val (stringDefs, ctxWithStrings) = defineStrings(allStrings.toList, typedContext) + val (buffersWithIndices, uniformsWithIndices) = bindings.zipWithIndex + .partition: + case (_: GBuffer[?], _) => true + case (_: GUniform[?], _) => false + .asInstanceOf[(List[(GBuffer[?], Int)], List[(GUniform[?], Int)])] + val uniforms = uniformsWithIndices.map(_._1) + val uniformSchemas = uniforms.map(_.schema) val structsInCode = (allExprs.collect { case cs: ComposeStruct[?] => cs.resultSchema case gf: GetField[?, ?] => gf.resultSchema - } :+ uniformSchema).distinct - val (structDefs, structCtx) = defineStructTypes(structsInCode, typedContext) - val structNames = getStructNames(structsInCode, structCtx) - val (decorations, uniformDefs, uniformContext) = initAndDecorateUniforms(inTypes, outTypes, structCtx) - val (uniformStructDecorations, uniformStructInsns, uniformStructContext) = createAndInitUniformBlock(uniformSchema, uniformContext) - val blockNames = getBlockNames(uniformContext, uniformSchema) + } ::: uniformSchemas).distinct + val (structDefs, structCtx) = defineStructTypes(structsInCode, ctxWithStrings) + val (structNames, structNamesCtx) = getStructNames(structsInCode, structCtx) + val (decorations, uniformDefs, uniformContext) = initAndDecorateBuffers(buffersWithIndices, structNamesCtx) + val (uniformStructDecorations, uniformStructInsns, uniformStructContext) = createAndInitUniformBlocks(uniformsWithIndices, uniformContext) + val blockNames = getBlockNames(uniformContext, uniforms) val (inputDefs, inputContext) = createInvocationId(uniformStructContext) val (constDefs, constCtx) = defineConstants(allExprs, inputContext) val (varDefs, varCtx) = defineVarNames(constCtx) - val resultType = tree.tree.tag - val (main, ctxAfterMain) = compileMain(tree, resultType, varCtx) + val (main, ctxAfterMain) = compileMain(bodyIo, varCtx) val (fnTypeDefs, fnDefs, ctxWithFnDefs) = compileFunctions(ctxAfterMain) val nameDecorations = getNameDecorations(ctxWithFnDefs) val code: List[Words] = - SpirvProgramCompiler.headers ::: blockNames ::: nameDecorations ::: structNames ::: SpirvProgramCompiler.workgroupDecorations ::: + SpirvProgramCompiler.headers ::: stringDefs ::: blockNames ::: nameDecorations ::: structNames ::: SpirvProgramCompiler.workgroupDecorations ::: decorations ::: uniformStructDecorations ::: typeDefs ::: structDefs ::: fnTypeDefs ::: uniformDefs ::: uniformStructInsns ::: inputDefs ::: constDefs ::: varDefs ::: main ::: fnDefs diff --git a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/ExpressionCompiler.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/ExpressionCompiler.scala index 1a8cd62b..6e859bd3 100644 --- a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/ExpressionCompiler.scala +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/ExpressionCompiler.scala @@ -3,7 +3,7 @@ package io.computenode.cyfra.spirv.compilers import io.computenode.cyfra.dsl.* import io.computenode.cyfra.dsl.Expression.* import io.computenode.cyfra.dsl.Value.* -import io.computenode.cyfra.dsl.collections.GArray.GArrayElem +import io.computenode.cyfra.dsl.binding.* import io.computenode.cyfra.dsl.collections.GSeq import io.computenode.cyfra.dsl.macros.Source import io.computenode.cyfra.dsl.struct.GStruct.{ComposeStruct, GetField} @@ -22,10 +22,6 @@ private[cyfra] object ExpressionCompiler: val WorkerIndexTag = "worker_index" - val WorkerIndex: Int32 = Int32(Dynamic(WorkerIndexTag)) - val UniformStructRefTag = "uniform_struct" - def UniformStructRef[G <: Value: Tag] = Dynamic(UniformStructRefTag) - private def binaryOpOpcode(expr: BinaryOpExpression[?]) = expr match case _: Sum[?] => (Op.OpIAdd, Op.OpFAdd) case _: Diff[?] => (Op.OpISub, Op.OpFSub) @@ -110,11 +106,11 @@ private[cyfra] object ExpressionCompiler: val updatedContext = ctx.copy(exprRefs = ctx.exprRefs + (c.treeid -> constRef)) (List(), updatedContext) - case d @ Dynamic(WorkerIndexTag) => - (Nil, ctx.copy(exprRefs = ctx.exprRefs + (d.treeid -> ctx.workerIndexRef))) + case w @ InvocationId => + (Nil, ctx.copy(exprRefs = ctx.exprRefs + (w.treeid -> ctx.workerIndexRef))) - case d @ Dynamic(UniformStructRefTag) => - (Nil, ctx.copy(exprRefs = ctx.exprRefs + (d.treeid -> ctx.uniformVarRef))) + case d @ ReadUniform(u) => + (Nil, ctx.copy(exprRefs = ctx.exprRefs + (d.treeid -> ctx.uniformVarRefs(u)))) case c: ConvertExpression[?, ?] => compileConvertExpression(c, ctx) @@ -293,19 +289,19 @@ private[cyfra] object ExpressionCompiler: case fc: FunctionCall[?] => compileFunctionCall(fc, ctx) - case ga @ GArrayElem(index, i) => + case ReadBuffer(buffer, i) => val instructions = List( Instruction( Op.OpAccessChain, List( - ResultRef(ctx.uniformPointerMap(ctx.valueTypeMap(ga.tag.tag))), + ResultRef(ctx.uniformPointerMap(ctx.valueTypeMap(buffer.tag.tag))), ResultRef(ctx.nextResultId), - ResultRef(ctx.inBufferBlocks(index).blockVarRef), + ResultRef(ctx.bufferBlocks(buffer).blockVarRef), ResultRef(ctx.constRefs((Int32Tag, 0))), ResultRef(ctx.exprRefs(i.treeid)), ), ), - Instruction(Op.OpLoad, List(IntWord(ctx.valueTypeMap(ga.tag.tag)), ResultRef(ctx.nextResultId + 1), ResultRef(ctx.nextResultId))), + Instruction(Op.OpLoad, List(IntWord(ctx.valueTypeMap(buffer.tag.tag)), ResultRef(ctx.nextResultId + 1), ResultRef(ctx.nextResultId))), ) val updatedContext = ctx.copy(exprRefs = ctx.exprRefs + (expr.treeid -> (ctx.nextResultId + 1)), nextResultId = ctx.nextResultId + 2) (instructions, updatedContext) @@ -330,14 +326,15 @@ private[cyfra] object ExpressionCompiler: ) val updatedContext = ctx.copy(exprRefs = ctx.exprRefs + (cs.treeid -> ctx.nextResultId), nextResultId = ctx.nextResultId + 1) (insns, updatedContext) - case gf @ GetField(dynamic @ Dynamic(UniformStructRefTag), fieldIndex) => + + case gf @ GetField(binding @ ReadUniform(uf), fieldIndex) => val insns: List[Instruction] = List( Instruction( Op.OpAccessChain, List( ResultRef(ctx.uniformPointerMap(ctx.valueTypeMap(gf.tag.tag))), ResultRef(ctx.nextResultId), - ResultRef(ctx.uniformVarRef), + ResultRef(ctx.uniformVarRefs(uf)), ResultRef(ctx.constRefs((Int32Tag, gf.fieldIndex))), ), ), @@ -345,6 +342,7 @@ private[cyfra] object ExpressionCompiler: ) val updatedContext = ctx.copy(exprRefs = ctx.exprRefs + (expr.treeid -> (ctx.nextResultId + 1)), nextResultId = ctx.nextResultId + 2) (insns, updatedContext) + case gf: GetField[?, ?] => val insns: List[Instruction] = List( Instruction( diff --git a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GIOCompiler.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GIOCompiler.scala new file mode 100644 index 00000000..11adc24c --- /dev/null +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GIOCompiler.scala @@ -0,0 +1,125 @@ +package io.computenode.cyfra.spirv.compilers + +import io.computenode.cyfra.dsl.gio.GIO +import io.computenode.cyfra.spirv.Context +import io.computenode.cyfra.spirv.Opcodes.* +import io.computenode.cyfra.dsl.binding.* +import io.computenode.cyfra.dsl.gio.GIO.CurrentRepeatIndex +import io.computenode.cyfra.spirv.SpirvConstants.{DEBUG_PRINTF_REF, TYPE_VOID_REF} +import io.computenode.cyfra.spirv.SpirvTypes.{GBooleanTag, Int32Tag, LInt32Tag} + +object GIOCompiler: + + def compileGio(gio: GIO[?], ctx: Context, acc: List[Words] = Nil): (List[Words], Context) = + gio match + + case GIO.Pure(v) => + val (insts, updatedCtx) = ExpressionCompiler.compileBlock(v.tree, ctx) + (acc ::: insts, updatedCtx) + + case WriteBuffer(buffer, index, value) => + val (valueInsts, ctxWithValue) = ExpressionCompiler.compileBlock(value.tree, ctx) + val (indexInsts, ctxWithIndex) = ExpressionCompiler.compileBlock(index.tree, ctxWithValue) + + val insns = List( + Instruction( + Op.OpAccessChain, + List( + ResultRef(ctxWithIndex.uniformPointerMap(ctxWithIndex.valueTypeMap(buffer.tag.tag))), + ResultRef(ctxWithIndex.nextResultId), + ResultRef(ctxWithIndex.bufferBlocks(buffer).blockVarRef), + ResultRef(ctxWithIndex.constRefs((Int32Tag, 0))), + ResultRef(ctxWithIndex.exprRefs(index.tree.treeid)), + ), + ), + Instruction(Op.OpStore, List(ResultRef(ctxWithIndex.nextResultId), ResultRef(ctxWithIndex.exprRefs(value.tree.treeid)))), + ) + val updatedCtx = ctxWithIndex.copy(nextResultId = ctxWithIndex.nextResultId + 1) + (acc ::: indexInsts ::: valueInsts ::: insns, updatedCtx) + + case GIO.FlatMap(v, n) => + val (vInsts, ctxAfterV) = compileGio(v, ctx, acc) + compileGio(n, ctxAfterV, vInsts) + + case GIO.Repeat(n, f) => + // Compile 'n' first (so we can use its id in the comparison) + val (nInsts, ctxWithN) = ExpressionCompiler.compileBlock(n.tree, ctx) + + // Types and constants + val intTy = ctxWithN.valueTypeMap(Int32Tag.tag) + val boolTy = ctxWithN.valueTypeMap(GBooleanTag.tag) + val zeroId = ctxWithN.constRefs((Int32Tag, 0)) + val oneId = ctxWithN.constRefs((Int32Tag, 1)) + val nId = ctxWithN.exprRefs(n.tree.treeid) + + // Reserve ids for blocks and results + val baseId = ctxWithN.nextResultId + val preHeaderId = baseId + val headerId = baseId + 1 + val bodyId = baseId + 2 + val continueId = baseId + 3 + val mergeId = baseId + 4 + val phiId = baseId + 5 + val cmpId = baseId + 6 + val addId = baseId + 7 + + // Bind CurrentRepeatIndex to the phi result for body compilation + val bodyCtx = ctxWithN.copy(nextResultId = baseId + 8, exprRefs = ctxWithN.exprRefs + (CurrentRepeatIndex.treeid -> phiId)) + val (bodyInsts, ctxAfterBody) = compileGio(f, bodyCtx) // ← Capture the context after body compilation + + // Preheader: close current block and jump to header through a dedicated block + val preheader = List( + Instruction(Op.OpBranch, List(ResultRef(preHeaderId))), + Instruction(Op.OpLabel, List(ResultRef(preHeaderId))), + Instruction(Op.OpBranch, List(ResultRef(headerId))), + ) + + // Header: OpPhi first, then compute condition, then OpLoopMerge and the terminating branch + val header = List( + Instruction(Op.OpLabel, List(ResultRef(headerId))), + // OpPhi must be first in the block + Instruction( + Op.OpPhi, + List(ResultRef(intTy), ResultRef(phiId), ResultRef(zeroId), ResultRef(preHeaderId), ResultRef(addId), ResultRef(continueId)), + ), + // cmp = (counter < n) + Instruction(Op.OpSLessThan, List(ResultRef(boolTy), ResultRef(cmpId), ResultRef(phiId), ResultRef(nId))), + // OpLoopMerge must be the second-to-last instruction, before the terminating branch + Instruction(Op.OpLoopMerge, List(ResultRef(mergeId), ResultRef(continueId), LoopControlMask.MaskNone)), + Instruction(Op.OpBranchConditional, List(ResultRef(cmpId), ResultRef(bodyId), ResultRef(mergeId))), + ) + + val bodyBlk = List(Instruction(Op.OpLabel, List(ResultRef(bodyId)))) ::: bodyInsts ::: List(Instruction(Op.OpBranch, List(ResultRef(continueId)))) + + val contBlk = List( + Instruction(Op.OpLabel, List(ResultRef(continueId))), + Instruction(Op.OpIAdd, List(ResultRef(intTy), ResultRef(addId), ResultRef(phiId), ResultRef(oneId))), + Instruction(Op.OpBranch, List(ResultRef(headerId))), + ) + + val mergeBlk = List(Instruction(Op.OpLabel, List(ResultRef(mergeId)))) + + // Use the highest nextResultId to avoid ID collisions + val finalNextId = math.max(ctxAfterBody.nextResultId, addId + 1) // ← Use ctxAfterBody.nextResultId + // Use ctxWithN as base to prevent loop-local values from being referenced outside + val finalCtx = ctxWithN.copy(nextResultId = finalNextId) + + (acc ::: nInsts ::: preheader ::: header ::: bodyBlk ::: contBlk ::: mergeBlk, finalCtx) + + case GIO.Printf(format, args*) => + val (argsInsts, ctxAfterArgs) = args.foldLeft((List.empty[Words], ctx)) { case ((instsAcc, cAcc), arg) => + val (argInsts, cAfterArg) = ExpressionCompiler.compileBlock(arg.tree, cAcc) + (instsAcc ::: argInsts, cAfterArg) + } + val argResults = args.map(a => ResultRef(ctxAfterArgs.exprRefs(a.tree.treeid))).toList + val printf = Instruction( + Op.OpExtInst, + List( + ResultRef(TYPE_VOID_REF), + ResultRef(ctxAfterArgs.nextResultId), + ResultRef(DEBUG_PRINTF_REF), + IntWord(1), + ResultRef(ctx.stringLiterals(format)), + ) ::: argResults, + ) + (acc ::: argsInsts ::: List(printf), ctxAfterArgs.copy(nextResultId = ctxAfterArgs.nextResultId + 1)) diff --git a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GStructCompiler.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GStructCompiler.scala index fe3faacc..78683deb 100644 --- a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GStructCompiler.scala +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GStructCompiler.scala @@ -29,13 +29,20 @@ private[cyfra] object GStructCompiler: ) } - def getStructNames(schemas: List[GStructSchema[?]], context: Context): List[Words] = - schemas.flatMap { schema => - val structName = schema.structTag.tag.shortName + def getStructNames(schemas: List[GStructSchema[?]], context: Context): (List[Words], Context) = + schemas.distinctBy(_.structTag).foldLeft((List.empty[Words], context)) { case ((wordsAcc, currCtx), schema) => + var structName = schema.structTag.tag.shortName + var nameSuffix = 0 + while currCtx.names.contains(structName) do + structName = s"${schema.structTag.tag.shortName}_$nameSuffix" + nameSuffix += 1 val structType = context.valueTypeMap(schema.structTag.tag) - Instruction(Op.OpName, List(ResultRef(structType), Text(structName))) :: schema.fields.zipWithIndex.map { case ((name, _, tag), i) => - Instruction(Op.OpMemberName, List(ResultRef(structType), IntWord(i), Text(name))) + val words = Instruction(Op.OpName, List(ResultRef(structType), Text(structName))) :: schema.fields.zipWithIndex.map { + case ((name, _, tag), i) => + Instruction(Op.OpMemberName, List(ResultRef(structType), IntWord(i), Text(name))) } + val updatedCtx = currCtx.copy(names = currCtx.names + structName) + (wordsAcc ::: words, updatedCtx) } private def sortSchemasDag(schemas: List[GStructSchema[?]]): List[GStructSchema[?]] = diff --git a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/SpirvProgramCompiler.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/SpirvProgramCompiler.scala index 8d16743c..e80ed296 100644 --- a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/SpirvProgramCompiler.scala +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/SpirvProgramCompiler.scala @@ -4,6 +4,8 @@ import io.computenode.cyfra.spirv.Opcodes.* import io.computenode.cyfra.dsl.Expression.{Const, E} import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Value.* +import io.computenode.cyfra.dsl.binding.{GBuffer, GUniform} +import io.computenode.cyfra.dsl.gio.GIO import io.computenode.cyfra.dsl.struct.{GStructConstructor, GStructSchema} import io.computenode.cyfra.spirv.Context import io.computenode.cyfra.spirv.SpirvConstants.* @@ -18,7 +20,7 @@ private[cyfra] object SpirvProgramCompiler: case Instruction(Op.OpVariable, _) => true case _ => false - def compileMain(tree: Value, resultType: Tag[?], ctx: Context): (List[Words], Context) = + def compileMain(bodyIo: GIO[?], ctx: Context): (List[Words], Context) = val init = List( Instruction(Op.OpFunction, List(ResultRef(ctx.voidTypeRef), ResultRef(MAIN_FUNC_REF), SamplerAddressingMode.None, ResultRef(VOID_FUNC_TYPE_REF))), @@ -38,25 +40,11 @@ private[cyfra] object SpirvProgramCompiler: Instruction(Op.OpLoad, List(ResultRef(ctx.valueTypeMap(Int32Tag.tag)), ResultRef(ctx.nextResultId + 2), ResultRef(ctx.nextResultId + 1))), ) - val (body, codeCtx) = compileBlock(tree.tree, ctx.copy(nextResultId = ctx.nextResultId + 3, workerIndexRef = ctx.nextResultId + 2)) + val (body, codeCtx) = GIOCompiler.compileGio(bodyIo, ctx.copy(nextResultId = ctx.nextResultId + 3, workerIndexRef = ctx.nextResultId + 2)) val (vars, nonVarsBody) = bubbleUpVars(body) - val end = List( - Instruction( - Op.OpAccessChain, - List( - ResultRef(codeCtx.uniformPointerMap(codeCtx.valueTypeMap(resultType.tag))), - ResultRef(codeCtx.nextResultId), - ResultRef(codeCtx.outBufferBlocks.head.blockVarRef), - ResultRef(codeCtx.constRefs((Int32Tag, 0))), - ResultRef(codeCtx.workerIndexRef), - ), - ), - Instruction(Op.OpStore, List(ResultRef(codeCtx.nextResultId), ResultRef(codeCtx.exprRefs(tree.tree.treeid)))), - Instruction(Op.OpReturn, List()), - Instruction(Op.OpFunctionEnd, List()), - ) + val end = List(Instruction(Op.OpReturn, List()), Instruction(Op.OpFunctionEnd, List())) (init ::: vars ::: initWorkerIndex ::: nonVarsBody ::: end, codeCtx.copy(nextResultId = codeCtx.nextResultId + 1)) def getNameDecorations(ctx: Context): List[Instruction] = @@ -83,7 +71,9 @@ private[cyfra] object SpirvProgramCompiler: WordVariable(BOUND_VARIABLE) :: // Bound: To be calculated Word(Array(0x00, 0x00, 0x00, 0x00)) :: // Schema: 0 Instruction(Op.OpCapability, List(Capability.Shader)) :: // OpCapability Shader + Instruction(Op.OpExtension, List(Text("SPV_KHR_non_semantic_info"))) :: // OpExtension "SPV_KHR_non_semantic_info" Instruction(Op.OpExtInstImport, List(ResultRef(GLSL_EXT_REF), Text(GLSL_EXT_NAME))) :: // OpExtInstImport "GLSL.std.450" + Instruction(Op.OpExtInstImport, List(ResultRef(DEBUG_PRINTF_REF), Text(NON_SEMANTIC_DEBUG_PRINTF))) :: // OpExtInstImport "NonSemantic.DebugPrintf" Instruction(Op.OpMemoryModel, List(AddressingModel.Logical, MemoryModel.GLSL450)) :: // OpMemoryModel Logical GLSL450 Instruction(Op.OpEntryPoint, List(ExecutionModel.GLCompute, ResultRef(MAIN_FUNC_REF), Text("main"), ResultRef(GL_GLOBAL_INVOCATION_ID_REF))) :: // OpEntryPoint GLCompute %MAIN_FUNC_REF "main" %GL_GLOBAL_INVOCATION_ID_REF Instruction(Op.OpExecutionMode, List(ResultRef(MAIN_FUNC_REF), ExecutionMode.LocalSize, IntWord(256), IntWord(1), IntWord(1))) :: // OpExecutionMode %4 LocalSize 128 1 1 @@ -102,12 +92,6 @@ private[cyfra] object SpirvProgramCompiler: val ctxWithVoid = context.copy(voidTypeRef = TYPE_VOID_REF, voidFuncTypeRef = VOID_FUNC_TYPE_REF) (voidDef, ctxWithVoid) - def initAndDecorateUniforms(ins: List[Tag[?]], outs: List[Tag[?]], context: Context): (List[Words], List[Words], Context) = - val (inDecor, inDef, inCtx) = createAndInitBlocks(ins, in = true, context) - val (outDecor, outDef, outCtx) = createAndInitBlocks(outs, in = false, inCtx) - val (voidsDef, voidCtx) = defineVoids(outCtx) - (inDecor ::: outDecor, voidsDef ::: inDef ::: outDef, voidCtx) - def createInvocationId(context: Context): (List[Words], Context) = val definitionInstructions = List( Instruction(Op.OpConstant, List(ResultRef(context.valueTypeMap(UInt32Tag.tag)), ResultRef(context.nextResultId + 0), IntWord(localSizeX))), @@ -125,88 +109,134 @@ private[cyfra] object SpirvProgramCompiler: ), ) (definitionInstructions, context.copy(nextResultId = context.nextResultId + 3)) + def initAndDecorateBuffers(buffers: List[(GBuffer[?], Int)], context: Context): (List[Words], List[Words], Context) = + val (blockDecor, blockDef, inCtx) = createAndInitBlocks(buffers, context) + val (voidsDef, voidCtx) = defineVoids(inCtx) + (blockDecor, voidsDef ::: blockDef, voidCtx) - def createAndInitBlocks(blocks: List[Tag[?]], in: Boolean, context: Context): (List[Words], List[Words], Context) = - val (decoration, definition, newContext) = blocks.foldLeft((List[Words](), List[Words](), context)) { case ((decAcc, insnAcc, ctx), tpe) => - val block = ArrayBufferBlock(ctx.nextResultId, ctx.nextResultId + 1, ctx.nextResultId + 2, ctx.nextResultId + 3, ctx.nextBinding) + def createAndInitBlocks(blocks: List[(GBuffer[?], Int)], context: Context): (List[Words], List[Words], Context) = + var membersVisited = Set[Int]() + var structsVisited = Set[Int]() + val (decoration, definition, newContext) = blocks.foldLeft((List[Words](), List[Words](), context)) { + case ((decAcc, insnAcc, ctx), (buff, binding)) => + val tpe = buff.tag + val block = ArrayBufferBlock(ctx.nextResultId, ctx.nextResultId + 1, ctx.nextResultId + 2, ctx.nextResultId + 3, binding) - val decorationInstructions = List[Words]( - Instruction(Op.OpDecorate, List(ResultRef(block.memberArrayTypeRef), Decoration.ArrayStride, IntWord(typeStride(tpe)))), // OpDecorate %_runtimearr_X ArrayStride [typeStride(type)] - Instruction(Op.OpMemberDecorate, List(ResultRef(block.structTypeRef), IntWord(0), Decoration.Offset, IntWord(0))), // OpMemberDecorate %BufferX 0 Offset 0 - Instruction(Op.OpDecorate, List(ResultRef(block.structTypeRef), Decoration.BufferBlock)), // OpDecorate %BufferX BufferBlock - Instruction(Op.OpDecorate, List(ResultRef(block.blockVarRef), Decoration.DescriptorSet, IntWord(0))), // OpDecorate %_X DescriptorSet 0 - Instruction(Op.OpDecorate, List(ResultRef(block.blockVarRef), Decoration.Binding, IntWord(block.binding))), // OpDecorate %_X Binding [binding] - ) + val (structDecoration, structDefinition) = + if structsVisited.contains(block.structTypeRef) then (Nil, Nil) + else + structsVisited += block.structTypeRef + ( + List( + Instruction(Op.OpMemberDecorate, List(ResultRef(block.structTypeRef), IntWord(0), Decoration.Offset, IntWord(0))), // OpMemberDecorate %BufferX 0 Offset 0 + Instruction(Op.OpDecorate, List(ResultRef(block.structTypeRef), Decoration.BufferBlock)), // OpDecorate %BufferX BufferBlock + ), + List( + Instruction(Op.OpTypeStruct, List(ResultRef(block.structTypeRef), IntWord(block.memberArrayTypeRef))), // %BufferX = OpTypeStruct %_runtimearr_X + ), + ) - val definitionInstructions = List[Words]( - Instruction(Op.OpTypeRuntimeArray, List(ResultRef(block.memberArrayTypeRef), IntWord(context.valueTypeMap(tpe.tag)))), // %_runtimearr_X = OpTypeRuntimeArray %[typeOf(tpe)] - Instruction(Op.OpTypeStruct, List(ResultRef(block.structTypeRef), IntWord(block.memberArrayTypeRef))), // %BufferX = OpTypeStruct %_runtimearr_X - Instruction(Op.OpTypePointer, List(ResultRef(block.blockPointerRef), StorageClass.Uniform, ResultRef(block.structTypeRef))), // %_ptr_Uniform_BufferX= OpTypePointer Uniform %BufferX - Instruction(Op.OpVariable, List(ResultRef(block.blockPointerRef), ResultRef(block.blockVarRef), StorageClass.Uniform)), // %_X = OpVariable %_ptr_Uniform_X Uniform - ) + val (memberDecoration, memberDefinition) = + if membersVisited.contains(block.memberArrayTypeRef) then (Nil, Nil) + else + membersVisited += block.memberArrayTypeRef + ( + List( + Instruction(Op.OpDecorate, List(ResultRef(block.memberArrayTypeRef), Decoration.ArrayStride, IntWord(typeStride(tpe)))), // OpDecorate %_runtimearr_X ArrayStride [typeStride(type)] + ), + List( + Instruction(Op.OpTypeRuntimeArray, List(ResultRef(block.memberArrayTypeRef), IntWord(context.valueTypeMap(tpe.tag)))), // %_runtimearr_X = OpTypeRuntimeArray %[typeOf(tpe)] + ), + ) - val contextWithBlock = - if in then ctx.copy(inBufferBlocks = block :: ctx.inBufferBlocks) else ctx.copy(outBufferBlocks = block :: ctx.outBufferBlocks) - ( - decAcc ::: decorationInstructions, - insnAcc ::: definitionInstructions, - contextWithBlock.copy(nextResultId = contextWithBlock.nextResultId + 5, nextBinding = contextWithBlock.nextBinding + 1), - ) + val decorationInstructions = memberDecoration ::: structDecoration ::: List[Words]( + Instruction(Op.OpDecorate, List(ResultRef(block.blockVarRef), Decoration.DescriptorSet, IntWord(0))), // OpDecorate %_X DescriptorSet 0 + Instruction(Op.OpDecorate, List(ResultRef(block.blockVarRef), Decoration.Binding, IntWord(block.binding))), // OpDecorate %_X Binding [binding] + ) + + val definitionInstructions = memberDefinition ::: structDefinition ::: List[Words]( + Instruction(Op.OpTypePointer, List(ResultRef(block.blockPointerRef), StorageClass.Uniform, ResultRef(block.structTypeRef))), // %_ptr_Uniform_BufferX= OpTypePointer Uniform %BufferX + Instruction(Op.OpVariable, List(ResultRef(block.blockPointerRef), ResultRef(block.blockVarRef), StorageClass.Uniform)), // %_X = OpVariable %_ptr_Uniform_X Uniform + ) + + val contextWithBlock = + ctx.copy(bufferBlocks = ctx.bufferBlocks + (buff -> block)) + (decAcc ::: decorationInstructions, insnAcc ::: definitionInstructions, contextWithBlock.copy(nextResultId = contextWithBlock.nextResultId + 5)) } (decoration, definition, newContext) - def getBlockNames(context: Context, uniformSchema: GStructSchema[?]): List[Words] = + def getBlockNames(context: Context, uniformSchemas: List[GUniform[?]]): List[Words] = def namesForBlock(block: ArrayBufferBlock, tpe: String): List[Words] = Instruction(Op.OpName, List(ResultRef(block.structTypeRef), Text(s"Buffer$tpe"))) :: Instruction(Op.OpName, List(ResultRef(block.blockVarRef), Text(s"data$tpe"))) :: Nil // todo name uniform - context.inBufferBlocks.flatMap(namesForBlock(_, "In")) ::: context.outBufferBlocks.flatMap(namesForBlock(_, "Out")) - - def createAndInitUniformBlock(schema: GStructSchema[?], ctx: Context): (List[Words], List[Words], Context) = - def totalStride(gs: GStructSchema[?]): Int = gs.fields - .map: - case (_, fromExpr, t) if t <:< gs.gStructTag => - val constructor = fromExpr.asInstanceOf[GStructConstructor[?]] - totalStride(constructor.schema) - case (_, _, t) => - typeStride(t) - .sum - val uniformStructTypeRef = ctx.valueTypeMap(schema.structTag.tag) - - val (offsetDecorations, _) = schema.fields.zipWithIndex.foldLeft[(List[Words], Int)](List.empty[Word], 0): - case ((acc, offset), ((name, fromExpr, tag), idx)) => - val stride = - if tag <:< schema.gStructTag then - val constructor = fromExpr.asInstanceOf[GStructConstructor[?]] - totalStride(constructor.schema) - else typeStride(tag) - val offsetDecoration = Instruction(Op.OpMemberDecorate, List(ResultRef(uniformStructTypeRef), IntWord(idx), Decoration.Offset, IntWord(offset))) - (acc :+ offsetDecoration, offset + stride) - - val uniformBlockDecoration = Instruction(Op.OpDecorate, List(ResultRef(uniformStructTypeRef), Decoration.Block)) - - val uniformPointerUniformRef = ctx.nextResultId - val uniformPointerUniform = - Instruction(Op.OpTypePointer, List(ResultRef(uniformPointerUniformRef), StorageClass.Uniform, ResultRef(uniformStructTypeRef))) - - val uniformVarRef = ctx.nextResultId + 1 - val uniformVar = Instruction(Op.OpVariable, List(ResultRef(uniformPointerUniformRef), ResultRef(uniformVarRef), StorageClass.Uniform)) - - val uniformDecorateDescriptorSet = Instruction(Op.OpDecorate, List(ResultRef(uniformVarRef), Decoration.DescriptorSet, IntWord(0))) - - assert(ctx.nextBinding == 2, "Currently the only legal layout is (in, out, uniform)") - val uniformDecorateBinding = Instruction(Op.OpDecorate, List(ResultRef(uniformVarRef), Decoration.Binding, IntWord(ctx.nextBinding))) + // context.inBufferBlocks.flatMap(namesForBlock(_, "In")) ::: context.outBufferBlocks.flatMap(namesForBlock(_, "Out")) + List() - ( - offsetDecorations ::: List(uniformDecorateDescriptorSet, uniformDecorateBinding, uniformBlockDecoration), - List(uniformPointerUniform, uniformVar), - ctx.copy( - nextResultId = ctx.nextResultId + 2, - nextBinding = ctx.nextBinding + 1, - uniformVarRef = uniformVarRef, - uniformPointerMap = ctx.uniformPointerMap + (uniformStructTypeRef -> uniformPointerUniformRef), - ), - ) + def totalStride(gs: GStructSchema[?]): Int = gs.fields + .map: + case (_, fromExpr, t) if t <:< gs.gStructTag => + val constructor = fromExpr.asInstanceOf[GStructConstructor[?]] + totalStride(constructor.schema) + case (_, _, t) => + typeStride(t) + .sum + + def defineStrings(strings: List[String], ctx: Context): (List[Words], Context) = + strings.foldLeft((List.empty[Words], ctx)): + case ((insnsAcc, currentCtx), str) => + if currentCtx.stringLiterals.contains(str) then (insnsAcc, currentCtx) + else + val strRef = currentCtx.nextResultId + val strInsns = List(Instruction(Op.OpString, List(ResultRef(strRef), Text(str)))) + val newCtx = currentCtx.copy(stringLiterals = currentCtx.stringLiterals + (str -> strRef), nextResultId = currentCtx.nextResultId + 1) + (insnsAcc ::: strInsns, newCtx) + + def createAndInitUniformBlocks(schemas: List[(GUniform[?], Int)], ctx: Context): (List[Words], List[Words], Context) = { + var decoratedOffsets = Set[Int]() + schemas.foldLeft((List.empty[Words], List.empty[Words], ctx)) { case ((decorationsAcc, definitionsAcc, currentCtx), (uniform, binding)) => + val schema = uniform.schema + val uniformStructTypeRef = currentCtx.valueTypeMap(schema.structTag.tag) + + val structDecorations = + if decoratedOffsets.contains(uniformStructTypeRef) then Nil + else + decoratedOffsets += uniformStructTypeRef + schema.fields.zipWithIndex + .foldLeft[(List[Words], Int)](List.empty[Words], 0): + case ((acc, offset), ((name, fromExpr, tag), idx)) => + val stride = + if tag <:< schema.gStructTag then + val constructor = fromExpr.asInstanceOf[GStructConstructor[?]] + totalStride(constructor.schema) + else typeStride(tag) + val offsetDecoration = + Instruction(Op.OpMemberDecorate, List(ResultRef(uniformStructTypeRef), IntWord(idx), Decoration.Offset, IntWord(offset))) + (acc :+ offsetDecoration, offset + stride) + ._1 ::: List(Instruction(Op.OpDecorate, List(ResultRef(uniformStructTypeRef), Decoration.Block))) + + val uniformPointerUniformRef = currentCtx.nextResultId + val uniformPointerUniform = + Instruction(Op.OpTypePointer, List(ResultRef(uniformPointerUniformRef), StorageClass.Uniform, ResultRef(uniformStructTypeRef))) + + val uniformVarRef = currentCtx.nextResultId + 1 + val uniformVar = Instruction(Op.OpVariable, List(ResultRef(uniformPointerUniformRef), ResultRef(uniformVarRef), StorageClass.Uniform)) + + val uniformDecorateDescriptorSet = Instruction(Op.OpDecorate, List(ResultRef(uniformVarRef), Decoration.DescriptorSet, IntWord(0))) + val uniformDecorateBinding = Instruction(Op.OpDecorate, List(ResultRef(uniformVarRef), Decoration.Binding, IntWord(binding))) + + val newDecorations = decorationsAcc ::: structDecorations ::: List(uniformDecorateDescriptorSet, uniformDecorateBinding) + val newDefinitions = definitionsAcc ::: List(uniformPointerUniform, uniformVar) + val newCtx = currentCtx.copy( + nextResultId = currentCtx.nextResultId + 2, + uniformVarRefs = currentCtx.uniformVarRefs + (uniform -> uniformVarRef), + uniformPointerMap = currentCtx.uniformPointerMap + (uniformStructTypeRef -> uniformPointerUniformRef), + bindingToStructType = currentCtx.bindingToStructType + (binding -> uniformStructTypeRef), + ) + + (newDecorations, newDefinitions, newCtx) + } + } val predefinedConsts = List((Int32Tag, 0), (UInt32Tag, 0), (Int32Tag, 1)) def defineConstants(exprs: List[E[?]], ctx: Context): (List[Words], Context) = diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/Allocation.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/Allocation.scala index 493b6a6e..ea7200e1 100644 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/Allocation.scala +++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/Allocation.scala @@ -4,7 +4,7 @@ import io.computenode.cyfra.core.layout.{Layout, LayoutBinding} import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Value.FromExpr import io.computenode.cyfra.dsl.binding.{GBinding, GBuffer, GUniform} -import io.computenode.cyfra.dsl.struct.GStruct +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} import izumi.reflect.Tag import java.nio.ByteBuffer @@ -26,6 +26,6 @@ trait Allocation: def apply[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GBuffer[T] extension (buffers: GUniform.type) - def apply[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GUniform[T] + def apply[T <: GStruct[T]: {Tag, FromExpr, GStructSchema}](buff: ByteBuffer): GUniform[T] - def apply[T <: Value: {Tag, FromExpr}](): GUniform[T] + def apply[T <: GStruct[T]: {Tag, FromExpr, GStructSchema}](): GUniform[T] diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GCodec.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GCodec.scala new file mode 100644 index 00000000..9d4d4bb9 --- /dev/null +++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GCodec.scala @@ -0,0 +1,140 @@ +// scala +package io.computenode.cyfra.core + +import io.computenode.cyfra.dsl.* +import io.computenode.cyfra.dsl.macros.Source +import io.computenode.cyfra.dsl.struct.GStruct.ComposeStruct +import io.computenode.cyfra.dsl.struct.{GStruct, GStructConstructor, GStructSchema} +import io.computenode.cyfra.spirv.SpirvTypes.typeStride +import izumi.reflect.Tag + +import java.nio.{ByteBuffer, ByteOrder} +import scala.reflect.ClassTag + +trait GCodec[CyfraType <: Value: {FromExpr, Tag}, ScalaType: ClassTag]: + def toByteBuffer(inBuf: ByteBuffer, arr: Array[ScalaType]): ByteBuffer + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[ScalaType]): Array[ScalaType] + def fromByteBufferUnchecked(outBuf: ByteBuffer, arr: Array[Any]): Array[ScalaType] = + fromByteBuffer(outBuf, arr.asInstanceOf[Array[ScalaType]]) + +object GCodec: + + def totalStride(gs: GStructSchema[?]): Int = gs.fields + .map: + case (_, fromExpr, t) if t <:< gs.gStructTag => + val constructor = fromExpr.asInstanceOf[GStructConstructor[?]] + totalStride(constructor.schema) + case (_, _, t) => + typeStride(t) + .sum + + given GCodec[Int32, Int]: + def toByteBuffer(inBuf: ByteBuffer, chunk: Array[Int]): ByteBuffer = + inBuf.clear().order(ByteOrder.nativeOrder()) + val ib = inBuf.asIntBuffer() + ib.put(chunk.toArray[Int]) + inBuf.position(ib.position() * java.lang.Integer.BYTES).flip() + inBuf + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[Int]): Array[Int] = + outBuf.order(ByteOrder.nativeOrder()) + outBuf.asIntBuffer().get(arr) + outBuf.rewind() + arr + + given GCodec[Float32, Float]: + def toByteBuffer(inBuf: ByteBuffer, chunk: Array[Float]): ByteBuffer = + inBuf.clear().order(ByteOrder.nativeOrder()) + val fb = inBuf.asFloatBuffer() + fb.put(chunk.toArray[Float]) + inBuf.position(fb.position() * java.lang.Float.BYTES).flip() + inBuf + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[Float]): Array[Float] = + outBuf.order(ByteOrder.nativeOrder()) + outBuf.asFloatBuffer().get(arr) + outBuf.rewind() + arr + + given GCodec[Vec4[Float32], fRGBA]: + def toByteBuffer(inBuf: ByteBuffer, arr: Array[fRGBA]): ByteBuffer = + inBuf.clear().order(ByteOrder.nativeOrder()) + arr.foreach: tuple => + writePrimitive(inBuf, tuple) + inBuf.flip() + inBuf + + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[fRGBA]): Array[fRGBA] = + val res = outBuf.asFloatBuffer() + for i <- 0 until arr.size do arr(i) = (res.get(), res.get(), res.get(), res.get()) + outBuf.rewind() + arr + + given GCodec[GBoolean, Boolean]: + def toByteBuffer(inBuf: ByteBuffer, arr: Array[Boolean]): ByteBuffer = + inBuf.put(arr.asInstanceOf[Array[Byte]]).flip() + inBuf + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[Boolean]): Array[Boolean] = + outBuf.get(arr.asInstanceOf[Array[Byte]]).flip() + arr + + given [T <: GStruct[T]: {GStructSchema as schema, Tag, ClassTag}]: GCodec[T, T] with + def toByteBuffer(inBuf: ByteBuffer, arr: Array[T]): ByteBuffer = + inBuf.clear().order(ByteOrder.nativeOrder()) + for + struct <- arr + field <- struct.productIterator + do writeConstPrimitive(inBuf, field.asInstanceOf[Value]) + inBuf.flip() + inBuf + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[T]): Array[T] = + val stride = totalStride(schema) + val nElems = outBuf.remaining() / stride + for _ <- 0 to nElems do + val values = schema.fields.map[Value] { case (_, fromExpr, t) => + t match + case t if t <:< schema.gStructTag => + val constructor = fromExpr.asInstanceOf[GStructConstructor[T]] + val nestedValues = constructor.schema.fields.map { case (_, _, nt) => + readPrimitive(outBuf, nt) + } + constructor.fromExpr(ComposeStruct(nestedValues, constructor.schema)) + case _ => + readPrimitive(outBuf, t) + } + val newStruct = schema.create(values, schema.copy(dependsOn = None))(using Source("Input")) + arr.appended(newStruct) + outBuf.rewind() + arr + + private def readPrimitive(buffer: ByteBuffer, value: Tag[?]): Value = + value.tag match + case t if t =:= summon[Tag[Int]].tag => Int32(ConstInt32(buffer.getInt())) + case t if t =:= summon[Tag[Float]].tag => Float32(ConstFloat32(buffer.getFloat())) + case t if t =:= summon[Tag[Boolean]].tag => GBoolean(ConstGB(buffer.get() != 0)) + case t if t =:= summon[Tag[(Float, Float, Float, Float)]].tag => // todo other tuples + Vec4( + ComposeVec4( + Float32(ConstFloat32(buffer.getFloat())), + Float32(ConstFloat32(buffer.getFloat())), + Float32(ConstFloat32(buffer.getFloat())), + Float32(ConstFloat32(buffer.getFloat())), + ), + ) + case illegal => + throw new IllegalArgumentException(s"Unable to deserialize value of type $illegal") + + private def writeConstPrimitive(buff: ByteBuffer, value: Value): Unit = value.tree match + case c: Const[?] => writePrimitive(buff, c.value) + case compose: ComposeVec[?] => + compose.productIterator.foreach: v => + writeConstPrimitive(buff, v.asInstanceOf[Value]) + case illegal => + throw new IllegalArgumentException(s"Only constant Cyfra values can be serialized (got $illegal)") + + private def writePrimitive(buff: ByteBuffer, value: Any): Unit = value match + case i: Int => buff.putInt(i) + case f: Float => buff.putFloat(f) + case b: Boolean => buff.put(if b then 1.toByte else 0.toByte) + case t: Tuple => + t.productIterator.foreach(writePrimitive(buff, _)) + case illegal => + throw new IllegalArgumentException(s"Unable to serialize value $illegal of type ${illegal.getClass}") diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GExecution.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GExecution.scala index 22418ead..9fab9d52 100644 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GExecution.scala +++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GExecution.scala @@ -1,12 +1,10 @@ package io.computenode.cyfra.core import io.computenode.cyfra.core.GExecution.* -import io.computenode.cyfra.core.archive.GContext import io.computenode.cyfra.core.layout.* import io.computenode.cyfra.dsl.binding.GBuffer import io.computenode.cyfra.dsl.gio.GIO import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} -import io.computenode.cyfra.spirv.compilers.ExpressionCompiler.UniformStructRef import izumi.reflect.Tag import GExecution.* diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GProgram.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GProgram.scala index da5407df..ffd87858 100644 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GProgram.scala +++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GProgram.scala @@ -8,16 +8,19 @@ import GProgram.* import io.computenode.cyfra.dsl.{Expression, Value} import io.computenode.cyfra.dsl.Value.{FromExpr, GBoolean, Int32} import io.computenode.cyfra.dsl.binding.{GBinding, GBuffer, GUniform} -import io.computenode.cyfra.dsl.struct.GStruct +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} import io.computenode.cyfra.dsl.struct.GStruct.Empty import izumi.reflect.Tag +import java.io.FileInputStream +import java.nio.file.Path +import scala.util.Using + trait GProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}] extends GExecution[Params, L, L]: val layout: InitProgramLayout => Params => L val dispatch: (L, Params) => ProgramDispatch val workgroupSize: WorkDimensions - private[cyfra] def cacheKey: String // TODO better type - def layoutStruct = summon[LayoutStruct[L]] + def layoutStruct: LayoutStruct[L] = summon[LayoutStruct[L]] object GProgram: type WorkDimensions = (Int, Int, Int) @@ -33,9 +36,22 @@ object GProgram: )(body: L => GIO[?]): GProgram[Params, L] = new GioProgram[Params, L](body, s => layout(using s), dispatch, workgroupSize) + def fromSpirvFile[Params, L <: Layout: {LayoutBinding, LayoutStruct}]( + layout: InitProgramLayout ?=> Params => L, + dispatch: (L, Params) => ProgramDispatch, + path: Path, + ): SpirvProgram[Params, L] = + Using.resource(new FileInputStream(path.toFile)): fis => + val fc = fis.getChannel + val size = fc.size().toInt + val bb = ByteBuffer.allocateDirect(size) + fc.read(bb) + bb.flip() + SpirvProgram(layout, dispatch, bb) + private[cyfra] class BufferLengthSpec[T <: Value: {Tag, FromExpr}](val length: Int) extends GBuffer[T]: private[cyfra] def materialise()(using Allocation): GBuffer[T] = GBuffer.apply[T](length) - private[cyfra] class DynamicUniform[T <: GStruct[T]: {Tag, FromExpr}]() extends GUniform[T] + private[cyfra] class DynamicUniform[T <: GStruct[T]: {Tag, FromExpr, GStructSchema}]() extends GUniform[T] trait InitProgramLayout: extension (_buffers: GBuffer.type) @@ -43,6 +59,6 @@ object GProgram: BufferLengthSpec[T](length) extension (_uniforms: GUniform.type) - def apply[T <: GStruct[T]: {Tag, FromExpr}](): GUniform[T] = + def apply[T <: GStruct[T]: {Tag, FromExpr, GStructSchema}](): GUniform[T] = DynamicUniform[T]() - def apply[T <: GStruct[T]: {Tag, FromExpr}](value: T): GUniform[T] + def apply[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}](value: T): GUniform[T] diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GioProgram.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GioProgram.scala index d97f97b2..03158fea 100644 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GioProgram.scala +++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GioProgram.scala @@ -11,9 +11,4 @@ case class GioProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}]( layout: InitProgramLayout => Params => L, dispatch: (L, Params) => ProgramDispatch, workgroupSize: WorkDimensions, -) extends GProgram[Params, L]: - private[cyfra] def cacheKey: String = summon[LayoutStruct[L]].elementTypes match - case x if x.size == 12 => "addOne" - case x if x.contains(summon[Tag[GBoolean]]) && x.size == 3 => "filter" - case x if x.size == 3 => "emit" - case _ => ??? +) extends GProgram[Params, L] diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/SpirvProgram.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/SpirvProgram.scala index c7b9f29a..0cfacd43 100644 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/SpirvProgram.scala +++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/SpirvProgram.scala @@ -14,6 +14,8 @@ import java.io.File import java.io.FileInputStream import java.nio.ByteBuffer import java.nio.channels.FileChannel +import java.nio.file.Path +import java.security.MessageDigest import java.util.Objects import scala.util.Try import scala.util.Using @@ -26,8 +28,27 @@ case class SpirvProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}] priv code: ByteBuffer, entryPoint: String, shaderBindings: L => ShaderLayout, - cacheKey: String, -) extends GProgram[Params, L] +) extends GProgram[Params, L]: + + /** A hash of the shader code, entry point, workgroup size, and layout bindings. Layout and dispatch are not taken into account. + */ + lazy val shaderHash: (Long, Long) = + val md = MessageDigest.getInstance("SHA-256") + md.update(code) + code.rewind() + md.update(entryPoint.getBytes) + md.update( + workgroupSize.toList + .flatMap(BigInt(_).toByteArray) + .toArray, + ) + val layout = shaderBindings(summon[LayoutStruct[L]].layoutRef) + layout.flatten.foreach: binding => + md.update(binding.binding.tag.toString.getBytes) + md.update(binding.operation.toString.getBytes) + val digest = md.digest() + val bb = java.nio.ByteBuffer.wrap(digest) + (bb.getLong(), bb.getLong()) object SpirvProgram: type ShaderLayout = Seq[Seq[Binding]] @@ -38,24 +59,13 @@ object SpirvProgram: case ReadWrite def apply[Params, L <: Layout: {LayoutBinding, LayoutStruct}]( - path: String, layout: InitProgramLayout ?=> Params => L, dispatch: (L, Params) => ProgramDispatch, + code: ByteBuffer, ): SpirvProgram[Params, L] = - val code = loadShader(path).get - val workgroupSize = (128, 1, 1) // TODO Extract form shader + val workgroupSize = (128, 1, 1) // TODO Extract form shader val main = "main" val f: L => ShaderLayout = { case layout: Product => layout.productIterator.zipWithIndex.map { case (binding: GBinding[?], i) => Binding(binding, ReadWrite) }.toSeq.pipe(Seq(_)) } - val cacheKey = - val x = new File(path).getName - x.substring(0, x.lastIndexOf('.')) - new SpirvProgram[Params, L]((il: InitProgramLayout) => layout(using il), dispatch, workgroupSize, code, main, f, cacheKey) - - def loadShader(path: String, classLoader: ClassLoader = getClass.getClassLoader): Try[ByteBuffer] = - Using.Manager: use => - val file = new File(Objects.requireNonNull(classLoader.getResource(path)).getFile) - val fis = use(new FileInputStream(file)) - val fc = use(fis.getChannel) - fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()) + new SpirvProgram[Params, L]((il: InitProgramLayout) => layout(using il), dispatch, workgroupSize, code, main, f) diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/Executable.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/Executable.scala deleted file mode 100644 index 68e0b273..00000000 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/Executable.scala +++ /dev/null @@ -1,9 +0,0 @@ -package io.computenode.cyfra.core.archive - -import io.computenode.cyfra.core.archive.mem.{GMem, RamGMem} -import io.computenode.cyfra.dsl.Value - -import scala.concurrent.Future - -trait Executable[H <: Value, R <: Value]: - def execute(input: GMem[H], output: RamGMem[R, ?]): Future[Unit] diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/GContext.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/GContext.scala deleted file mode 100644 index 9f209ea4..00000000 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/GContext.scala +++ /dev/null @@ -1,75 +0,0 @@ -package io.computenode.cyfra.core.archive - -import io.computenode.cyfra.core.archive.mem.GMem.totalStride -import io.computenode.cyfra.core.archive.mem.{FloatMem, GMem, IntMem, Vec4FloatMem} -import io.computenode.cyfra.core.archive.{GFunction, UniformContext} -import io.computenode.cyfra.dsl.Value -import io.computenode.cyfra.dsl.Value.{Float32, FromExpr, Int32, Vec4} -import io.computenode.cyfra.dsl.collections.GArray -import io.computenode.cyfra.dsl.struct.* -import io.computenode.cyfra.spirv.SpirvTypes.typeStride -import io.computenode.cyfra.spirv.compilers.DSLCompiler -import io.computenode.cyfra.spirv.compilers.ExpressionCompiler.{UniformStructRef, WorkerIndex} -import io.computenode.cyfra.spirvtools.SpirvToolsRunner - -import izumi.reflect.Tag -import org.lwjgl.system.Configuration - -import java.util.concurrent.Executors -import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} - -class GContext(spirvToolsRunner: SpirvToolsRunner = SpirvToolsRunner()): - Configuration.STACK_SIZE.set(1024) // fix lwjgl stack size - - val vkContext = ??? - - implicit val ec: ExecutionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(16)) - - def compile[G <: GStruct[G]: {Tag, GStructSchema}, H <: Value: {Tag, FromExpr}, R <: Value: {Tag, FromExpr}]( - function: GFunction[G, H, R], - ): Nothing = ??? -// val uniformStructSchema = summon[GStructSchema[G]] -// val uniformStruct = uniformStructSchema.fromTree(UniformStructRef) -// val tree = function.fn -// .apply(uniformStruct, WorkerIndex, GArray[H](0)) -// -// val optimizedShaderCode = -// spirvToolsRunner.processShaderCodeWithSpirvTools(DSLCompiler.compile(tree, function.arrayInputs, function.arrayOutputs, uniformStructSchema)) -// -// val inOut = 0 to 1 map (Binding(_, InputBufferSize(typeStride(summon[Tag[H]])))) -// val uniform = Option.when(uniformStructSchema.fields.nonEmpty)(Binding(2, UniformSize(totalStride(uniformStructSchema)))) -// val layoutInfo = LayoutInfo(Seq(LayoutSet(0, inOut ++ uniform))) -// -// val shader = Shader(optimizedShaderCode, org.joml.Vector3i(256, 1, 1), layoutInfo, "main", vkContext.device) -// ComputePipeline(shader, vkContext) - - def execute[G <: GStruct[G]: {Tag, GStructSchema}, H <: Value, R <: Value](mem: GMem[H], fn: GFunction[G, H, R])(using - uniformContext: UniformContext[G], - ): GMem[R] = ??? -// val isUniformEmpty = uniformContext.uniform.schema.fields.isEmpty -// val actions = Map(LayoutLocation(0, 0) -> BufferAction.LoadTo, LayoutLocation(0, 1) -> BufferAction.LoadFrom) ++ -// ( -// if isUniformEmpty then Map.empty -// else Map(LayoutLocation(0, 2) -> BufferAction.LoadTo) -// ) -// val sequence = ComputationSequence(Seq(Compute(fn.pipeline, actions)), Seq.empty) -// val executor = new SequenceExecutor(sequence, vkContext) -// -// val data = mem.toReadOnlyBuffer -// val inData = -// if isUniformEmpty then Seq(data) -// else Seq(data, GMem.serializeUniform(uniformContext.uniform)) -// val out = executor.execute(inData, mem.size) -// executor.destroy() -// -// val outTags = fn.arrayOutputs -// assert(outTags.size == 1) -// -// outTags.head match -// case t if t == Tag[Float32] => -// new FloatMem(mem.size, out.head).asInstanceOf[GMem[R]] -// case t if t == Tag[Int32] => -// new IntMem(mem.size, out.head).asInstanceOf[GMem[R]] -// case t if t == Tag[Vec4[Float32]] => -// new Vec4FloatMem(mem.size, out.head).asInstanceOf[GMem[R]] -// case _ => assert(false, "Supported output types are Float32 and Vec4[Float32]") diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/GFunction.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/GFunction.scala index c5613bb7..b124bed6 100644 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/GFunction.scala +++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/GFunction.scala @@ -1,30 +1,96 @@ package io.computenode.cyfra.core.archive +import io.computenode.cyfra.core.{CyfraRuntime, GBufferRegion, GCodec, GProgram} +import io.computenode.cyfra.core.GBufferRegion.* +import io.computenode.cyfra.core.GProgram.StaticDispatch import io.computenode.cyfra.core.archive.GFunction +import io.computenode.cyfra.core.archive.GFunction.{GFunctionLayout, GFunctionParams} +import io.computenode.cyfra.core.layout.{Layout, LayoutBinding, LayoutStruct} import io.computenode.cyfra.dsl.Value.* +import io.computenode.cyfra.dsl.binding.{GBuffer, GUniform} import io.computenode.cyfra.dsl.collections.{GArray, GArray2D} +import io.computenode.cyfra.dsl.gio.GIO import io.computenode.cyfra.dsl.struct.* import io.computenode.cyfra.dsl.{*, given} - +import io.computenode.cyfra.spirv.SpirvTypes.typeStride +import io.computenode.cyfra.spirv.compilers.SpirvProgramCompiler.totalStride import izumi.reflect.Tag +import org.lwjgl.BufferUtils + +import scala.reflect.ClassTag +import io.computenode.cyfra.core.GCodec.{*, given} +import io.computenode.cyfra.dsl.struct.GStruct.Empty -case class GFunction[G <: GStruct[G]: {GStructSchema, Tag}, H <: Value: {Tag, FromExpr}, R <: Value: {Tag, FromExpr}](fn: (G, Int32, GArray[H]) => R)( - implicit context: GContext, +case class GFunction[G <: GStruct[G]: {GStructSchema, Tag}, H <: Value: {Tag, FromExpr}, R <: Value: {Tag, FromExpr}]( + underlying: GProgram[GFunctionParams, GFunctionLayout[G, H, R]], ): - def arrayInputs: List[Tag[?]] = List(summon[Tag[H]]) - def arrayOutputs: List[Tag[?]] = List(summon[Tag[R]]) - val pipeline: Nothing = ??? + def run[GS: ClassTag, HS, RS: ClassTag](input: Array[HS], g: GS)(using + gCodec: GCodec[G, GS], + hCodec: GCodec[H, HS], + rCodec: GCodec[R, RS], + runtime: CyfraRuntime, + ): Array[RS] = + + val inTypeSize = typeStride(Tag.apply[H]) + val outTypeSize = typeStride(Tag.apply[R]) + val uniformStride = totalStride(summon[GStructSchema[G]]) + val params = GFunctionParams(size = input.size) + + val in = BufferUtils.createByteBuffer(inTypeSize * input.size) + hCodec.toByteBuffer(in, input) + val out = BufferUtils.createByteBuffer(outTypeSize * input.size) + val uniform = BufferUtils.createByteBuffer(uniformStride) + gCodec.toByteBuffer(uniform, Array(g)) + + GBufferRegion + .allocate[GFunctionLayout[G, H, R]] + .map: layout => + underlying.execute(params, layout) + .runUnsafe( + init = GFunctionLayout(in = GBuffer[H](in), out = GBuffer[R](input.size), uniform = GUniform[G](uniform)), + onDone = layout => layout.out.read(out), + ) + val resultArray = Array.ofDim[RS](input.size) + rCodec.fromByteBuffer(out, resultArray) object GFunction: - def apply[H <: Value: {Tag, FromExpr}, R <: Value: {Tag, FromExpr}](fn: H => R)(using context: GContext): GFunction[GStruct.Empty, H, R] = - new GFunction[GStruct.Empty, H, R]((_, index: Int32, gArray: GArray[H]) => fn(gArray.at(index))) + case class GFunctionParams(size: Int) + + case class GFunctionLayout[G <: GStruct[G], H <: Value, R <: Value](in: GBuffer[H], out: GBuffer[R], uniform: GUniform[G]) extends Layout + + def forEachIndex[G <: GStruct[G]: {GStructSchema, Tag}, H <: Value: {Tag, FromExpr}, R <: Value: {Tag, FromExpr}]( + fn: (G, Int32, GBuffer[H]) => R, + ): GFunction[G, H, R] = + val body = (layout: GFunctionLayout[G, H, R]) => + val g = layout.uniform.read + val result = fn(g, GIO.invocationId, layout.in) + for _ <- layout.out.write(GIO.invocationId, result) + yield Empty() + + val inTypeSize = typeStride(Tag.apply[H]) + val outTypeSize = typeStride(Tag.apply[R]) + + GFunction(underlying = + GProgram.apply[GFunctionParams, GFunctionLayout[G, H, R]]( + layout = (p: GFunctionParams) => GFunctionLayout[G, H, R](in = GBuffer[H](p.size), out = GBuffer[R](p.size), uniform = GUniform[G]()), + dispatch = (l, p) => StaticDispatch((p.size + 255) / 256, 1, 1), + workgroupSize = (256, 1, 1), + )(body), + ) + + def apply[H <: Value: {Tag, FromExpr}, R <: Value: {Tag, FromExpr}](fn: H => R): GFunction[GStruct.Empty, H, R] = + GFunction.forEachIndex[GStruct.Empty, H, R]((g: GStruct.Empty, index: Int32, a: GBuffer[H]) => fn(a.read(index))) def from2D[G <: GStruct[G]: {GStructSchema, Tag}, H <: Value: {Tag, FromExpr}, R <: Value: {Tag, FromExpr}]( width: Int, - )(fn: (G, (Int32, Int32), GArray2D[H]) => R)(using context: GContext): GFunction[G, H, R] = - GFunction[G, H, R]((g: G, index: Int32, a: GArray[H]) => + )(fn: (G, (Int32, Int32), GArray2D[H]) => R): GFunction[G, H, R] = + GFunction.forEachIndex[G, H, R]((g: G, index: Int32, a: GBuffer[H]) => val x: Int32 = index mod width val y: Int32 = index / width val arr = GArray2D(width, a) fn(g, (x, y), arr), ) + + extension [H <: Value: {Tag, FromExpr}, R <: Value: {Tag, FromExpr}](gf: GFunction[GStruct.Empty, H, R]) + def run[HS, RS: ClassTag](input: Array[HS])(using hCodec: GCodec[H, HS], rCodec: GCodec[R, RS], runtime: CyfraRuntime): Array[RS] = + gf.run(input, GStruct.Empty()) diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/UniformContext.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/UniformContext.scala deleted file mode 100644 index 093698ae..00000000 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/UniformContext.scala +++ /dev/null @@ -1,13 +0,0 @@ -package io.computenode.cyfra.core.archive - -import io.computenode.cyfra.core.archive.UniformContext -import io.computenode.cyfra.dsl.struct.* -import io.computenode.cyfra.dsl.struct.GStruct.Empty -import izumi.reflect.Tag - -class UniformContext[G <: GStruct[G]: {Tag, GStructSchema}](val uniform: G) - -object UniformContext: - def withUniform[G <: GStruct[G]: {Tag, GStructSchema}, T](uniform: G)(fn: UniformContext[G] ?=> T): T = - fn(using UniformContext(uniform)) - given empty: UniformContext[Empty] = new UniformContext(Empty()) diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/FloatMem.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/FloatMem.scala deleted file mode 100644 index 2e51f2ee..00000000 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/FloatMem.scala +++ /dev/null @@ -1,27 +0,0 @@ -package io.computenode.cyfra.core.archive.mem - -import io.computenode.cyfra.dsl.Value.Float32 -import org.lwjgl.BufferUtils - -import java.nio.ByteBuffer - -class FloatMem(val size: Int, protected val data: ByteBuffer) extends RamGMem[Float32, Float]: - def toArray: Array[Float] = - val res = data.asFloatBuffer() - val result = new Array[Float](size) - res.get(result) - result - -object FloatMem: - val FloatSize = 4 - - def apply(floats: Array[Float]): FloatMem = - val size = floats.length - val data = BufferUtils.createByteBuffer(size * FloatSize) - data.asFloatBuffer().put(floats) - data.rewind() - new FloatMem(size, data) - - def apply(size: Int): FloatMem = - val data = BufferUtils.createByteBuffer(size * FloatSize) - new FloatMem(size, data) diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/GMem.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/GMem.scala deleted file mode 100644 index 41961aa4..00000000 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/GMem.scala +++ /dev/null @@ -1,52 +0,0 @@ -package io.computenode.cyfra.core.archive.mem - -import io.computenode.cyfra.core.archive.{GContext, GFunction, UniformContext} -import io.computenode.cyfra.dsl.Value.FromExpr -import io.computenode.cyfra.dsl.struct.* -import io.computenode.cyfra.dsl.{*, given} -import io.computenode.cyfra.spirv.SpirvTypes.typeStride -import izumi.reflect.Tag -import org.lwjgl.BufferUtils - -import java.nio.ByteBuffer - -trait GMem[H <: Value]: - def size: Int - def toReadOnlyBuffer: ByteBuffer - def map[G <: GStruct[G]: {Tag, GStructSchema}, R <: Value: {FromExpr, Tag}]( - fn: GFunction[G, H, R], - )(using context: GContext, uc: UniformContext[G]): GMem[R] = - context.execute(this, fn) - -object GMem: - type fRGBA = (Float, Float, Float, Float) - - def totalStride(gs: GStructSchema[?]): Int = gs.fields.map { - case (_, fromExpr, t) if t <:< gs.gStructTag => - val constructor = fromExpr.asInstanceOf[GStructConstructor[?]] - totalStride(constructor.schema) - case (_, _, t) => - typeStride(t) - }.sum - - def serializeUniform(g: GStruct[?]): ByteBuffer = - val data = BufferUtils.createByteBuffer(totalStride(g.schema)) - g.productIterator.foreach: - case Int32(ConstInt32(i)) => data.putInt(i) - case Float32(ConstFloat32(f)) => data.putFloat(f) - case Vec4(ComposeVec4(Float32(ConstFloat32(x)), Float32(ConstFloat32(y)), Float32(ConstFloat32(z)), Float32(ConstFloat32(a)))) => - data.putFloat(x) - data.putFloat(y) - data.putFloat(z) - data.putFloat(a) - case Vec3(ComposeVec3(Float32(ConstFloat32(x)), Float32(ConstFloat32(y)), Float32(ConstFloat32(z)))) => - data.putFloat(x) - data.putFloat(y) - data.putFloat(z) - case Vec2(ComposeVec2(Float32(ConstFloat32(x)), Float32(ConstFloat32(y)))) => - data.putFloat(x) - data.putFloat(y) - case illegal => - throw new IllegalArgumentException(s"Uniform must be constructed from constants (got field $illegal)") - data.rewind() - data diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/IntMem.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/IntMem.scala deleted file mode 100644 index ee9e61e8..00000000 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/IntMem.scala +++ /dev/null @@ -1,27 +0,0 @@ -package io.computenode.cyfra.core.archive.mem - -import io.computenode.cyfra.dsl.Value.Int32 -import org.lwjgl.BufferUtils - -import java.nio.ByteBuffer - -class IntMem(val size: Int, protected val data: ByteBuffer) extends RamGMem[Int32, Int]: - def toArray: Array[Int] = - val res = data.asIntBuffer() - val result = new Array[Int](size) - res.get(result) - result - -object IntMem: - val IntSize = 4 - - def apply(ints: Array[Int]): IntMem = - val size = ints.length - val data = BufferUtils.createByteBuffer(size * IntSize) - data.asIntBuffer().put(ints) - data.rewind() - new IntMem(size, data) - - def apply(size: Int): IntMem = - val data = BufferUtils.createByteBuffer(size * IntSize) - new IntMem(size, data) diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/RamGMem.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/RamGMem.scala deleted file mode 100644 index a136d7d4..00000000 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/RamGMem.scala +++ /dev/null @@ -1,9 +0,0 @@ -package io.computenode.cyfra.core.archive.mem - -import io.computenode.cyfra.dsl.Value - -import java.nio.ByteBuffer - -trait RamGMem[T <: Value, R] extends GMem[T]: - protected val data: ByteBuffer - def toReadOnlyBuffer: ByteBuffer = data.asReadOnlyBuffer() diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/Vec4FloatMem.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/Vec4FloatMem.scala deleted file mode 100644 index 0fdb63cb..00000000 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/mem/Vec4FloatMem.scala +++ /dev/null @@ -1,33 +0,0 @@ -package io.computenode.cyfra.core.archive.mem - -import io.computenode.cyfra.core.archive.mem.GMem.fRGBA -import io.computenode.cyfra.dsl.Value.{Float32, Vec4} -import org.lwjgl.BufferUtils - -import java.nio.ByteBuffer - -class Vec4FloatMem(val size: Int, protected val data: ByteBuffer) extends RamGMem[Vec4[Float32], fRGBA]: - def toArray: Array[fRGBA] = - val res = data.asFloatBuffer() - val result = new Array[fRGBA](size) - for i <- 0 until size do result(i) = (res.get(), res.get(), res.get(), res.get()) - result - -object Vec4FloatMem: - val Vec4FloatSize = 16 - - def apply(vecs: Array[fRGBA]): Vec4FloatMem = - val size = vecs.length - val data = BufferUtils.createByteBuffer(size * Vec4FloatSize) - vecs.foreach { case (x, y, z, a) => - data.putFloat(x) - data.putFloat(y) - data.putFloat(z) - data.putFloat(a) - } - data.rewind() - new Vec4FloatMem(size, data) - - def apply(size: Int): Vec4FloatMem = - val data = BufferUtils.createByteBuffer(size * Vec4FloatSize) - new Vec4FloatMem(size, data) diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/binding/UniformRef.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/binding/UniformRef.scala index d7c3b308..8fc86c2f 100644 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/binding/UniformRef.scala +++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/binding/UniformRef.scala @@ -3,8 +3,8 @@ package io.computenode.cyfra.core.binding import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Value.FromExpr import io.computenode.cyfra.dsl.binding.{GBuffer, GUniform} -import io.computenode.cyfra.dsl.struct.GStruct +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} import izumi.reflect.Tag import izumi.reflect.macrortti.LightTypeTag -case class UniformRef[T <: Value: {Tag, FromExpr}](layoutOffset: Int, valueTag: Tag[T]) extends GUniform[T] +case class UniformRef[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}](layoutOffset: Int, valueTag: Tag[T]) extends GUniform[T] diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/LayoutStruct.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/LayoutStruct.scala index 3d79b409..1b460121 100644 --- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/LayoutStruct.scala +++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/LayoutStruct.scala @@ -4,6 +4,7 @@ import io.computenode.cyfra.core.binding.{BufferRef, UniformRef} import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Value.FromExpr import io.computenode.cyfra.dsl.binding.{GBinding, GBuffer, GUniform} +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} import izumi.reflect.Tag import izumi.reflect.macrortti.LightTypeTag @@ -35,7 +36,18 @@ object LayoutStruct: report.errorAndAbort("LayoutStruct can only be derived for case classes with GBinding elements") val valueTypes = fieldTypes.map: ftype => - (ftype, ftype.typeArgs.headOption.getOrElse(report.errorAndAbort("GBuffer must have a value type"))) + ftype match + case AppliedType(_, args) if args.nonEmpty => + val valueType = args.head + // Ensure we're working with the original type parameter, not the instance type + val resolvedType = valueType match + case tr if tr.typeSymbol.isTypeParam => + // Find the corresponding type parameter from the original class + tpe.typeArgs.find(_.typeSymbol.name == tr.typeSymbol.name).getOrElse(tr) + case tr => tr + (ftype, resolvedType) + case _ => + report.errorAndAbort("GBinding must have a value type") // summon izumi tags val typeGivens = valueTypes.map: @@ -47,29 +59,39 @@ object LayoutStruct: farg.asType, Expr.summon[Tag[t]] match case Some(tagExpr) => tagExpr - case None => report.errorAndAbort(s"Cannot summon Tag for type ${tpe.show}"), + case None => report.errorAndAbort(s"Cannot summon Tag for type ${farg.show}"), Expr.summon[FromExpr[t]] match case Some(fromExpr) => fromExpr - case None => report.errorAndAbort(s"Cannot summon FromExpr for type ${tpe.show}"), + case None => report.errorAndAbort(s"Cannot summon FromExpr for type ${farg.show}"), ) val buffers = typeGivens.zipWithIndex.map: case ((ftype, tpe, tag, fromExpr), i) => - tpe match - case '[type t <: Value; t] => - ftype match - case '[type tg <: GBuffer[?]; tg] => - '{ - BufferRef[t](${ Expr(i) }, ${ tag.asExprOf[Tag[t]] })(using summon[Tag[t]], ${ fromExpr.asExprOf[FromExpr[t]] }) - } - case '[type tg <: GUniform[?]; tg] => - '{ - UniformRef[t](${ Expr(i) }, ${ tag.asExprOf[Tag[t]] })(using summon[Tag[t]], ${ fromExpr.asExprOf[FromExpr[t]] }) - } + (tpe, ftype) match + case ('[type t <: Value; t], '[type tg <: GBuffer[?]; tg]) => + '{ + BufferRef[t](${ Expr(i) }, ${ tag.asExprOf[Tag[t]] })(using ${ tag.asExprOf[Tag[t]] }, ${ fromExpr.asExprOf[FromExpr[t]] }) + } + case ('[type t <: GStruct[?]; t], '[type tg <: GUniform[?]; tg]) => + val structSchema = Expr.summon[GStructSchema[t]] match + case Some(s) => s + case None => report.errorAndAbort(s"Cannot summon GStructSchema for type") + '{ + UniformRef[t](${ Expr(i) }, ${ tag.asExprOf[Tag[t]] })(using + ${ tag.asExprOf[Tag[t]] }, + ${ fromExpr.asExprOf[FromExpr[t]] }, + ${ structSchema }, + ) + } val constructor = sym.primaryConstructor + report.info(s"Constructor: ${constructor.fullName} with params ${constructor.paramSymss.flatten.map(_.name).mkString(", ")}") + + val typeArgs = tpe.typeArgs - val layoutInstance = Apply(Select(New(TypeIdent(sym)), constructor), buffers.map(_.asTerm)) + val layoutInstance = + if typeArgs.isEmpty then Apply(Select(New(TypeIdent(sym)), constructor), buffers.map(_.asTerm)) + else Apply(TypeApply(Select(New(TypeIdent(sym)), constructor), typeArgs.map(arg => TypeTree.of(using arg.asType))), buffers.map(_.asTerm)) val layoutRef = layoutInstance.asExprOf[T] diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/Expression.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/Expression.scala index 18f81033..7d52eb5e 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/Expression.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/Expression.scala @@ -101,9 +101,11 @@ object Expression: case class ConstUInt32(value: Int) extends Const[UInt32] case class ConstGB(value: Boolean) extends Const[GBoolean] - case class ComposeVec2[T <: Scalar: Tag](a: T, b: T) extends Expression[Vec2[T]] - case class ComposeVec3[T <: Scalar: Tag](a: T, b: T, c: T) extends Expression[Vec3[T]] - case class ComposeVec4[T <: Scalar: Tag](a: T, b: T, c: T, d: T) extends Expression[Vec4[T]] + trait ComposeVec[T <: Vec[?]: Tag] extends Expression[T] + + case class ComposeVec2[T <: Scalar: Tag](a: T, b: T) extends ComposeVec[Vec2[T]] + case class ComposeVec3[T <: Scalar: Tag](a: T, b: T, c: T) extends ComposeVec[Vec3[T]] + case class ComposeVec4[T <: Scalar: Tag](a: T, b: T, c: T, d: T) extends ComposeVec[Vec4[T]] case class ExtFunctionCall[R <: Value: Tag](fn: FunctionName, args: List[Value]) extends Expression[R] case class FunctionCall[R <: Value: Tag](fn: FnIdentifier, body: Scope[R], args: List[Value]) extends E[R] @@ -111,4 +113,5 @@ object Expression: case class Pass[T <: Value: Tag](value: T) extends E[T] - case class Dynamic[T <: Value: Tag](source: String) extends E[T] + case object WorkerIndex extends E[Int32] + case class Binding[T <: Value: Tag](binding: Int) extends E[T] diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/Value.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/Value.scala index 985357e7..1e8a0e92 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/Value.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/Value.scala @@ -1,6 +1,5 @@ package io.computenode.cyfra.dsl -import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Expression.{E, E as T} import io.computenode.cyfra.dsl.macros.Source import izumi.reflect.Tag @@ -56,3 +55,5 @@ object Value: case class Vec4[T <: Value](tree: E[Vec4[T]])(using val source: Source) extends Vec[T] given [T <: Scalar]: FromExpr[Vec4[T]] with def fromExpr(f: E[Vec4[T]])(using Source) = Vec4(f) + + type fRGBA = (Float, Float, Float, Float) diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GBinding.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GBinding.scala index 60c53cac..27f25d04 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GBinding.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GBinding.scala @@ -4,7 +4,8 @@ import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Value.FromExpr.fromExpr as fromExprEval import io.computenode.cyfra.dsl.Value.{FromExpr, Int32} import io.computenode.cyfra.dsl.gio.GIO -import io.computenode.cyfra.dsl.struct.GStruct +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} +import io.computenode.cyfra.dsl.struct.GStruct.Empty import izumi.reflect.Tag sealed trait GBinding[T <: Value: {Tag, FromExpr}]: @@ -14,17 +15,19 @@ sealed trait GBinding[T <: Value: {Tag, FromExpr}]: trait GBuffer[T <: Value: {FromExpr, Tag}] extends GBinding[T]: def read(index: Int32): T = FromExpr.fromExpr(ReadBuffer(this, index)) - def write(index: Int32, value: T): GIO[Unit] = GIO.write(this, index, value) + def write(index: Int32, value: T): GIO[Empty] = GIO.write(this, index, value) object GBuffer -trait GUniform[T <: Value: {Tag, FromExpr}] extends GBinding[T]: +trait GUniform[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}] extends GBinding[T]: def read: T = fromExprEval(ReadUniform(this)) - def write(value: T): GIO[Unit] = WriteUniform(this, value) + def write(value: T): GIO[Empty] = WriteUniform(this, value) + + def schema = summon[GStructSchema[T]] object GUniform: - class ParamUniform[T <: GStruct[T]: {Tag, FromExpr}]() extends GUniform[T] + class ParamUniform[T <: GStruct[T]: {Tag, FromExpr, GStructSchema}]() extends GUniform[T] - def fromParams[T <: GStruct[T]: {Tag, FromExpr}] = ParamUniform[T]() + def fromParams[T <: GStruct[T]: {Tag, FromExpr, GStructSchema}] = ParamUniform[T]() diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/ReadUniform.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/ReadUniform.scala index 9f75a278..85b2b53e 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/ReadUniform.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/ReadUniform.scala @@ -1,6 +1,7 @@ package io.computenode.cyfra.dsl.binding +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} import io.computenode.cyfra.dsl.{Expression, Value} import izumi.reflect.Tag -case class ReadUniform[T <: Value: Tag](uniform: GUniform[T]) extends Expression[T] +case class ReadUniform[T <: GStruct[?]: {Tag, GStructSchema}](uniform: GUniform[T]) extends Expression[T] diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/WriteBuffer.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/WriteBuffer.scala index 53b0abf9..1856079a 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/WriteBuffer.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/WriteBuffer.scala @@ -3,6 +3,7 @@ package io.computenode.cyfra.dsl.binding import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Value.Int32 import io.computenode.cyfra.dsl.gio.GIO +import io.computenode.cyfra.dsl.struct.GStruct.Empty -case class WriteBuffer[T <: Value](buffer: GBuffer[T], index: Int32, value: T) extends GIO[Unit]: - override def underlying: Unit = () +case class WriteBuffer[T <: Value](buffer: GBuffer[T], index: Int32, value: T) extends GIO[Empty]: + override def underlying: Empty = Empty() diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/WriteUniform.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/WriteUniform.scala index 240aa643..f176014a 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/WriteUniform.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/WriteUniform.scala @@ -2,8 +2,9 @@ package io.computenode.cyfra.dsl.binding import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.gio.GIO -import io.computenode.cyfra.dsl.struct.GStruct +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} +import io.computenode.cyfra.dsl.struct.GStruct.Empty import izumi.reflect.Tag -case class WriteUniform[T <: Value: Tag](uniform: GUniform[T], value: T) extends GIO[Unit]: - override def underlying: Unit = () +case class WriteUniform[T <: GStruct[?]: {Tag, GStructSchema}](uniform: GUniform[T], value: T) extends GIO[Empty]: + override def underlying: Empty = Empty() diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray.scala index 6e9daf13..dfca871b 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray.scala @@ -1,14 +1,12 @@ package io.computenode.cyfra.dsl.collections import io.computenode.cyfra.dsl.Value.* -import io.computenode.cyfra.dsl.collections.GArray.GArrayElem +import io.computenode.cyfra.dsl.binding.{GBuffer, ReadBuffer} import io.computenode.cyfra.dsl.macros.Source import io.computenode.cyfra.dsl.{Expression, Value} import izumi.reflect.Tag -case class GArray[T <: Value: {Tag, FromExpr}](index: Int): +// todo temporary +case class GArray[T <: Value: {Tag, FromExpr}](underlying: GBuffer[T]): def at(i: Int32)(using Source): T = - summon[FromExpr[T]].fromExpr(GArrayElem(index, i.tree)) - -object GArray: - case class GArrayElem[T <: Value: Tag](index: Int, i: Expression[Int32]) extends Expression[T] + summon[FromExpr[T]].fromExpr(ReadBuffer(underlying, i)) diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala index 70d6df19..9671e288 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala @@ -6,7 +6,9 @@ import io.computenode.cyfra.dsl.algebra.ScalarAlgebra.{*, given} import io.computenode.cyfra.dsl.macros.Source import izumi.reflect.Tag import io.computenode.cyfra.dsl.Value.FromExpr +import io.computenode.cyfra.dsl.binding.GBuffer -class GArray2D[T <: Value: {Tag, FromExpr}](width: Int, val arr: GArray[T]): +// todo temporary +class GArray2D[T <: Value: {Tag, FromExpr}](width: Int, val arr: GBuffer[T]): def at(x: Int32, y: Int32)(using Source): T = - arr.at(y * width + x) + arr.read(y * width + x) diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/gio/GIO.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/gio/GIO.scala index 02a018f8..09373068 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/gio/GIO.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/gio/GIO.scala @@ -6,38 +6,54 @@ import io.computenode.cyfra.dsl.Value.FromExpr.fromExpr import io.computenode.cyfra.dsl.binding.{GBuffer, ReadBuffer, WriteBuffer} import io.computenode.cyfra.dsl.collections.GSeq import io.computenode.cyfra.dsl.gio.GIO.* +import io.computenode.cyfra.dsl.struct.GStruct.Empty +import io.computenode.cyfra.dsl.control.When import izumi.reflect.Tag -trait GIO[T]: +trait GIO[T <: Value]: - def flatMap[U](f: T => GIO[U]): GIO[U] = FlatMap(this, f(this.underlying)) + def flatMap[U <: Value](f: T => GIO[U]): GIO[U] = FlatMap(this, f(this.underlying)) - def map[U](f: T => U): GIO[U] = flatMap(t => GIO.pure(f(t))) + def map[U <: Value](f: T => U): GIO[U] = flatMap(t => GIO.pure(f(t))) private[cyfra] def underlying: T object GIO: - case class Pure[T](value: T) extends GIO[T]: + case class Pure[T <: Value](value: T) extends GIO[T]: override def underlying: T = value - case class FlatMap[T, U](gio: GIO[T], next: GIO[U]) extends GIO[U]: + case class FlatMap[T <: Value, U <: Value](gio: GIO[T], next: GIO[U]) extends GIO[U]: override def underlying: U = next.underlying // TODO repeat that collects results - case class Repeat(n: Int32, f: Int32 => GIO[?]) extends GIO[Unit]: - override def underlying: Unit = () + case class Repeat(n: Int32, f: GIO[?]) extends GIO[Empty]: + override def underlying: Empty = Empty() - def pure[T](value: T): GIO[T] = Pure(value) + case class Printf(format: String, args: Value*) extends GIO[Empty]: + override def underlying: Empty = Empty() - def value[T](value: T): GIO[T] = Pure(value) + def pure[T <: Value](value: T): GIO[T] = Pure(value) - def repeat(n: Int32)(f: Int32 => GIO[?]): GIO[Unit] = - Repeat(n, f) + def value[T <: Value](value: T): GIO[T] = Pure(value) - def write[T <: Value](buffer: GBuffer[T], index: Int32, value: T): GIO[Unit] = + case object CurrentRepeatIndex extends PhantomExpression[Int32] with CustomTreeId: + override val treeid: Int = treeidState.getAndIncrement() + + def repeat(n: Int32)(f: Int32 => GIO[?]): GIO[Empty] = + Repeat(n, f(fromExpr(CurrentRepeatIndex))) + + def write[T <: Value](buffer: GBuffer[T], index: Int32, value: T): GIO[Empty] = WriteBuffer(buffer, index, value) + def printf(format: String, args: Value*): GIO[Empty] = + Printf(s"|$format", args*) + + def when(cond: GBoolean)(thenCode: GIO[?]): GIO[Empty] = + val n = When.when(cond)(1: Int32).otherwise(0) + repeat(n): _ => + thenCode + def read[T <: Value: {FromExpr, Tag}](buffer: GBuffer[T], index: Int32): T = fromExpr(ReadBuffer(buffer, index)) diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/struct/GStruct.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/struct/GStruct.scala index 9ec4199b..38a642ee 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/struct/GStruct.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/struct/GStruct.scala @@ -2,7 +2,7 @@ package io.computenode.cyfra.dsl.struct import io.computenode.cyfra.* import io.computenode.cyfra.dsl.Expression.* -import io.computenode.cyfra.dsl.{Expression, Value} +import io.computenode.cyfra.dsl.{*, given} import io.computenode.cyfra.dsl.Value.* import io.computenode.cyfra.dsl.macros.Source import izumi.reflect.Tag @@ -21,14 +21,14 @@ abstract class GStruct[T <: GStruct[T]: {Tag, GStructSchema}] extends Value with override def source: Source = _name object GStruct: - case class Empty() extends GStruct[Empty] + case class Empty(_placeholder: Int32 = 0) extends GStruct[Empty] object Empty: given GStructSchema[Empty] = GStructSchema.derived - case class ComposeStruct[T <: GStruct[T]: Tag](fields: List[Value], resultSchema: GStructSchema[T]) extends Expression[T] + case class ComposeStruct[T <: GStruct[?]: Tag](fields: List[Value], resultSchema: GStructSchema[T]) extends Expression[T] - case class GetField[S <: GStruct[S]: GStructSchema, T <: Value: Tag](struct: E[S], fieldIndex: Int) extends Expression[T]: + case class GetField[S <: GStruct[?]: GStructSchema, T <: Value: Tag](struct: E[S], fieldIndex: Int) extends Expression[T]: val resultSchema: GStructSchema[S] = summon[GStructSchema[S]] given [T <: GStruct[T]: GStructSchema]: GStructConstructor[T] with diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/struct/GStructSchema.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/struct/GStructSchema.scala index e0cd5d8f..8c26aa4f 100644 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/struct/GStructSchema.scala +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/struct/GStructSchema.scala @@ -10,7 +10,7 @@ import izumi.reflect.Tag import scala.compiletime.{constValue, erasedValue, error, summonAll} import scala.deriving.Mirror -case class GStructSchema[T <: GStruct[T]: Tag](fields: List[(String, FromExpr[?], Tag[?])], dependsOn: Option[E[T]], fromTuple: (Tuple, Source) => T): +case class GStructSchema[T <: GStruct[?]: Tag](fields: List[(String, FromExpr[?], Tag[?])], dependsOn: Option[E[T]], fromTuple: (Tuple, Source) => T): given GStructSchema[T] = this val structTag = summon[Tag[T]] @@ -23,7 +23,7 @@ case class GStructSchema[T <: GStruct[T]: Tag](fields: List[(String, FromExpr[?] def create(values: List[Value], schema: GStructSchema[T])(using name: Source): T = val valuesTuple = Tuple.fromArray(values.toArray) val newStruct = fromTuple(valuesTuple, name) - newStruct._schema = schema + newStruct._schema = schema.asInstanceOf newStruct.tree.of = Some(newStruct) newStruct diff --git a/cyfra-e2e-test/src/test/resources/addOne.comp b/cyfra-e2e-test/src/test/resources/addOne.comp new file mode 100644 index 00000000..091de31f --- /dev/null +++ b/cyfra-e2e-test/src/test/resources/addOne.comp @@ -0,0 +1,48 @@ +#version 450 + +layout (local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +layout (set = 0, binding = 0) buffer In1 { + int in1[]; +}; +layout (set = 0, binding = 1) buffer In2 { + int in2[]; +}; +layout (set = 0, binding = 2) buffer In3 { + int in3[]; +}; +layout (set = 0, binding = 3) buffer In4 { + int in4[]; +}; +layout (set = 0, binding = 4) buffer In5 { + int in5[]; +}; +layout (set = 0, binding = 5) buffer Out1 { + int out1[]; +}; +layout (set = 0, binding = 6) buffer Out2 { + int out2[]; +}; +layout (set = 0, binding = 7) buffer Out3 { + int out3[]; +}; +layout (set = 0, binding = 8) buffer Out4 { + int out4[]; +}; +layout (set = 0, binding = 9) buffer Out5 { + int out5[]; +}; +layout (set = 0, binding = 10) uniform U1 { + int a; +}; +layout (set = 0, binding = 11) uniform U2 { + int b; +}; +void main(void) { + uint index = gl_GlobalInvocationID.x; + out1[index] = in1[index] + a + b; + out2[index] = in2[index] + a + b; + out3[index] = in3[index] + a + b; + out4[index] = in4[index] + a + b; + out5[index] = in5[index] + a + b; +} diff --git a/cyfra-e2e-test/src/test/resources/compileAll.ps1 b/cyfra-e2e-test/src/test/resources/compileAll.ps1 new file mode 100644 index 00000000..e1755a32 --- /dev/null +++ b/cyfra-e2e-test/src/test/resources/compileAll.ps1 @@ -0,0 +1,4 @@ +Get-ChildItem -Filter *.comp -Name | ForEach-Object -Process { + $name = $_.Replace(".comp", "") + "$Env:VULKAN_SDK\Bin\glslangValidator.exe -V $name.comp -o $name.spv" | Invoke-Expression +} diff --git a/cyfra-e2e-test/src/test/resources/compileAll.sh b/cyfra-e2e-test/src/test/resources/compileAll.sh new file mode 100644 index 00000000..e4f70140 --- /dev/null +++ b/cyfra-e2e-test/src/test/resources/compileAll.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +for f in *.comp +do + prefix=$(echo "$f" | cut -f 1 -d '.') + glslangValidator -V "$prefix.comp" -o "$prefix.spv" +done diff --git a/cyfra-e2e-test/src/test/resources/emit.comp b/cyfra-e2e-test/src/test/resources/emit.comp new file mode 100644 index 00000000..5789c424 --- /dev/null +++ b/cyfra-e2e-test/src/test/resources/emit.comp @@ -0,0 +1,23 @@ +#version 450 + +layout (local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +layout (set = 0, binding = 0) buffer InputBuffer { + int inBuffer[]; +}; +layout (set = 0, binding = 1) buffer OutputBuffer { + int outBuffer[]; +}; + +layout (set = 0, binding = 2) uniform InputUniform { + int emitN; +}; + +void main(void) { + uint index = gl_GlobalInvocationID.x; + int element = inBuffer[index]; + uint offset = index * uint(emitN); + for (int i = 0; i < emitN; i++) { + outBuffer[offset + uint(i)] = element; + } +} diff --git a/cyfra-e2e-test/src/test/resources/filter.comp b/cyfra-e2e-test/src/test/resources/filter.comp new file mode 100644 index 00000000..37beef64 --- /dev/null +++ b/cyfra-e2e-test/src/test/resources/filter.comp @@ -0,0 +1,20 @@ +#version 450 + +layout (local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +layout (set = 0, binding = 0) buffer InputBuffer { + int inBuffer[]; +}; +layout (set = 0, binding = 1) buffer OutputBuffer { + bool outBuffer[]; +}; + +layout (set = 0, binding = 2) uniform InputUniform { + int filterValue; +}; + +void main(void) { + uint index = gl_GlobalInvocationID.x; + int element = inBuffer[index]; + outBuffer[index] = (element == filterValue); +} diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/GStructE2eTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/GStructE2eTest.scala deleted file mode 100644 index c1183c2a..00000000 --- a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/GStructE2eTest.scala +++ /dev/null @@ -1,65 +0,0 @@ -package io.computenode.cyfra.e2e - -import io.computenode.cyfra.dsl.collections.GSeq -import io.computenode.cyfra.dsl.struct.GStruct -import io.computenode.cyfra.core.archive.* -import mem.* -import io.computenode.cyfra.dsl.{*, given} - -class GStructE2eTest extends munit.FunSuite: - case class Custom(f: Float32, v: Vec4[Float32]) extends GStruct[Custom] - val custom1 = Custom(2f, (1f, 2f, 3f, 4f)) - val custom2 = Custom(-0.5f, (-0.5f, -1.5f, -2.5f, -3.5f)) - - case class Nested(c1: Custom, c2: Custom) extends GStruct[Nested] - val nested = Nested(custom1, custom2) - - given gc: GContext = GContext() - - test("GStruct passed as uniform"): - UniformContext.withUniform(custom1): - val gf: GFunction[Custom, Float32, Float32] = GFunction: - case (Custom(f, v), index, gArray) => v.*(f).dot(v) + gArray.at(index) * f - - val inArr = (0 to 255).map(_.toFloat).toArray - val gmem = FloatMem(inArr) - val result = gmem.map(gf).asInstanceOf[FloatMem].toArray - - val expected = inArr.map(f => 2f * f + 60f) - result - .zip(expected) - .foreach: (res, exp) => - assert(Math.abs(res - exp) < 0.001f, s"Expected $exp but got $res") - - test("GStruct of GStructs".ignore): - UniformContext.withUniform(nested): - val gf: GFunction[Nested, Float32, Float32] = GFunction: - case (Nested(Custom(f1, v1), Custom(f2, v2)), index, gArray) => - v1.*(f2).dot(v2) + gArray.at(index) * f1 - - val inArr = (0 to 255).map(_.toFloat).toArray - val gmem = FloatMem(inArr) - val result = gmem.map(gf).asInstanceOf[FloatMem].toArray - - val expected = inArr.map(f => 2f * f + 12.5f) - result - .zip(expected) - .foreach: (res, exp) => - assert(Math.abs(res - exp) < 0.001f, s"Expected $exp but got $res") - - test("GSeq of GStructs"): - val gf: GFunction[GStruct.Empty, Float32, Float32] = GFunction: fl => - GSeq - .gen(custom1, c => Custom(c.f * 2f, c.v.*(2f))) - .limit(3) - .fold[Float32](0f, (f, c) => f + c.f * (c.v.w + c.v.x + c.v.y + c.v.z)) + fl - - val inArr = (0 to 255).map(_.toFloat).toArray - val gmem = FloatMem(inArr) - val result = gmem.map(gf).asInstanceOf[FloatMem].toArray - - val expected = inArr.map(f => f + 420f) - result - .zip(expected) - .foreach: (res, exp) => - assert(res == exp, s"Expected $exp but got $res") diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/RuntimeEnduranceTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/RuntimeEnduranceTest.scala new file mode 100644 index 00000000..d298a839 --- /dev/null +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/RuntimeEnduranceTest.scala @@ -0,0 +1,228 @@ +package io.computenode.cyfra.e2e + +import io.computenode.cyfra.core.layout.* +import io.computenode.cyfra.core.{GBufferRegion, GExecution, GProgram} +import io.computenode.cyfra.dsl.Value.{GBoolean, Int32} +import io.computenode.cyfra.dsl.binding.{GBuffer, GUniform} +import io.computenode.cyfra.dsl.gio.GIO +import io.computenode.cyfra.dsl.struct.GStruct +import io.computenode.cyfra.dsl.struct.GStruct.Empty +import io.computenode.cyfra.dsl.{*, given} +import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.spirvtools.{SpirvCross, SpirvDisassembler, SpirvToolsRunner} +import io.computenode.cyfra.spirvtools.SpirvTool.ToFile +import io.computenode.cyfra.utility.Logger.logger +import org.lwjgl.BufferUtils +import org.lwjgl.system.MemoryUtil + +import java.nio.file.Paths +import scala.concurrent.ExecutionContext.Implicits.global +import java.util.concurrent.atomic.AtomicInteger +import scala.concurrent.{Await, Future} + +class RuntimeEnduranceTest extends munit.FunSuite: + + test("Endurance test for GExecution with multiple programs"): + runEnduranceTest(10000) + + // === Emit program === + + case class EmitProgramParams(inSize: Int, emitN: Int) + + case class EmitProgramUniform(emitN: Int32) extends GStruct[EmitProgramUniform] + + case class EmitProgramLayout( + in: GBuffer[Int32], + out: GBuffer[Int32], + args: GUniform[EmitProgramUniform] = GUniform.fromParams, // todo will be different in the future + ) extends Layout + + val emitProgram = GProgram[EmitProgramParams, EmitProgramLayout]( + layout = params => + EmitProgramLayout( + in = GBuffer[Int32](params.inSize), + out = GBuffer[Int32](params.inSize * params.emitN), + args = GUniform(EmitProgramUniform(params.emitN)), + ), + dispatch = (_, args) => GProgram.StaticDispatch((args.inSize / 128, 1, 1)), + ): layout => + val EmitProgramUniform(emitN) = layout.args.read + val invocId = GIO.invocationId + val element = GIO.read(layout.in, invocId) + val bufferOffset = invocId * emitN + GIO.repeat(emitN): i => + GIO.write(layout.out, bufferOffset + i, element) + + // === Filter program === + + case class FilterProgramParams(inSize: Int, filterValue: Int) + + case class FilterProgramUniform(filterValue: Int32) extends GStruct[FilterProgramUniform] + + case class FilterProgramLayout(in: GBuffer[Int32], out: GBuffer[GBoolean], params: GUniform[FilterProgramUniform] = GUniform.fromParams) + extends Layout + + val filterProgram = GProgram[FilterProgramParams, FilterProgramLayout]( + layout = params => + FilterProgramLayout( + in = GBuffer[Int32](params.inSize), + out = GBuffer[GBoolean](params.inSize), + params = GUniform(FilterProgramUniform(params.filterValue)), + ), + dispatch = (_, args) => GProgram.StaticDispatch((args.inSize / 128, 1, 1)), + ): layout => + val invocId = GIO.invocationId + val element = GIO.read(layout.in, invocId) + val isMatch = element === layout.params.read.filterValue + GIO.write(layout.out, invocId, isMatch) + + // === GExecution === + + case class EmitFilterParams(inSize: Int, emitN: Int, filterValue: Int) + + case class EmitFilterLayout(inBuffer: GBuffer[Int32], emitBuffer: GBuffer[Int32], filterBuffer: GBuffer[GBoolean]) extends Layout + + case class EmitFilterResult(out: GBuffer[GBoolean]) extends Layout + + val emitFilterExecution = GExecution[EmitFilterParams, EmitFilterLayout]() + .addProgram(emitProgram)( + params => EmitProgramParams(inSize = params.inSize, emitN = params.emitN), + layout => EmitProgramLayout(in = layout.inBuffer, out = layout.emitBuffer), + ) + .addProgram(filterProgram)( + params => FilterProgramParams(inSize = 2 * params.inSize, filterValue = params.filterValue), + layout => FilterProgramLayout(in = layout.emitBuffer, out = layout.filterBuffer), + ) + + // Test case: Use one program 10 times, copying values from five input buffers to five output buffers and adding values from two uniforms + case class AddProgramParams(bufferSize: Int, addA: Int, addB: Int) + + case class AddProgramUniform(a: Int32) extends GStruct[AddProgramUniform] + + case class AddProgramLayout( + in1: GBuffer[Int32], + in2: GBuffer[Int32], + in3: GBuffer[Int32], + in4: GBuffer[Int32], + in5: GBuffer[Int32], + out1: GBuffer[Int32], + out2: GBuffer[Int32], + out3: GBuffer[Int32], + out4: GBuffer[Int32], + out5: GBuffer[Int32], + u1: GUniform[AddProgramUniform] = GUniform.fromParams, + u2: GUniform[AddProgramUniform] = GUniform.fromParams, + ) extends Layout + + case class AddProgramExecLayout( + in1: GBuffer[Int32], + in2: GBuffer[Int32], + in3: GBuffer[Int32], + in4: GBuffer[Int32], + in5: GBuffer[Int32], + out1: GBuffer[Int32], + out2: GBuffer[Int32], + out3: GBuffer[Int32], + out4: GBuffer[Int32], + out5: GBuffer[Int32], + ) extends Layout + + val addProgram: GProgram[AddProgramParams, AddProgramLayout] = GProgram[AddProgramParams, AddProgramLayout]( + layout = params => + AddProgramLayout( + in1 = GBuffer[Int32](params.bufferSize), + in2 = GBuffer[Int32](params.bufferSize), + in3 = GBuffer[Int32](params.bufferSize), + in4 = GBuffer[Int32](params.bufferSize), + in5 = GBuffer[Int32](params.bufferSize), + out1 = GBuffer[Int32](params.bufferSize), + out2 = GBuffer[Int32](params.bufferSize), + out3 = GBuffer[Int32](params.bufferSize), + out4 = GBuffer[Int32](params.bufferSize), + out5 = GBuffer[Int32](params.bufferSize), + u1 = GUniform(AddProgramUniform(params.addA)), + u2 = GUniform(AddProgramUniform(params.addB)), + ), + dispatch = (layout, args) => GProgram.StaticDispatch((args.bufferSize / 128, 1, 1)), + ): + case AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5, u1, u2) => + val index = GIO.invocationId + val a = u1.read.a + val b = u2.read.a + for + _ <- GIO.write(out1, index, GIO.read(in1, index) + a + b) + _ <- GIO.write(out2, index, GIO.read(in2, index) + a + b) + _ <- GIO.write(out3, index, GIO.read(in3, index) + a + b) + _ <- GIO.write(out4, index, GIO.read(in4, index) + a + b) + _ <- GIO.write(out5, index, GIO.read(in5, index) + a + b) + yield Empty() + + def swap(l: AddProgramLayout): AddProgramLayout = + val AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5, u1, u2) = l + AddProgramLayout(out1, out2, out3, out4, out5, in1, in2, in3, in4, in5, u1, u2) + + def fromExecLayout(l: AddProgramExecLayout): AddProgramLayout = + val AddProgramExecLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5) = l + AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5) + + val execution = (0 until 11).foldLeft( + GExecution[AddProgramParams, AddProgramExecLayout]().asInstanceOf[GExecution[AddProgramParams, AddProgramExecLayout, AddProgramExecLayout]], + )((x, i) => + if i % 2 == 0 then x.addProgram(addProgram)(mapParams = identity[AddProgramParams], mapLayout = fromExecLayout) + else x.addProgram(addProgram)(mapParams = identity, mapLayout = x => swap(fromExecLayout(x))), + ) + + def runEnduranceTest(nRuns: Int): Unit = + logger.info(s"Starting endurance test with $nRuns runs...") + + given runtime: VkCyfraRuntime = VkCyfraRuntime(spirvToolsRunner = + SpirvToolsRunner( + crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl"))), + disassembler = SpirvDisassembler.Enable(toolOutput = ToFile(Paths.get("output/dis.spvdis"))), + ), + ) + + val bufferSize = 1280 + val params = AddProgramParams(bufferSize, addA = 0, addB = 1) + val region = GBufferRegion + .allocate[AddProgramExecLayout] + .map: region => + execution.execute(params, region) + val aInt = new AtomicInteger(0) + val runs = (1 to nRuns).map: i => + Future: + val inBuffers = List.fill(5)(BufferUtils.createIntBuffer(bufferSize)) + val wbbList = inBuffers.map(MemoryUtil.memByteBuffer) + val rbbList = List.fill(5)(BufferUtils.createByteBuffer(bufferSize * 4)) + + val inData = (0 until bufferSize).toArray + inBuffers.foreach(_.put(inData).flip()) + region.runUnsafe( + init = AddProgramExecLayout( + in1 = GBuffer[Int32](wbbList(0)), + in2 = GBuffer[Int32](wbbList(1)), + in3 = GBuffer[Int32](wbbList(2)), + in4 = GBuffer[Int32](wbbList(3)), + in5 = GBuffer[Int32](wbbList(4)), + out1 = GBuffer[Int32](bufferSize), + out2 = GBuffer[Int32](bufferSize), + out3 = GBuffer[Int32](bufferSize), + out4 = GBuffer[Int32](bufferSize), + out5 = GBuffer[Int32](bufferSize), + ), + onDone = layout => { + layout.out1.read(rbbList(0)) + layout.out2.read(rbbList(1)) + layout.out3.read(rbbList(2)) + layout.out4.read(rbbList(3)) + layout.out5.read(rbbList(4)) + }, + ) + val prev = aInt.getAndAdd(1) + if prev % 50 == 0 then logger.info(s"Iteration $prev completed") + + val allRuns = Future.sequence(runs) + Await.result(allRuns, scala.concurrent.duration.Duration.Inf) + + runtime.close() + logger.info("Endurance test completed successfully") diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/SpirvRuntimeEnduranceTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/SpirvRuntimeEnduranceTest.scala new file mode 100644 index 00000000..cca59242 --- /dev/null +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/SpirvRuntimeEnduranceTest.scala @@ -0,0 +1,209 @@ +package io.computenode.cyfra.e2e + +import io.computenode.cyfra.core.layout.* +import io.computenode.cyfra.core.{GBufferRegion, GExecution, GProgram} +import io.computenode.cyfra.dsl.Value.{GBoolean, Int32} +import io.computenode.cyfra.dsl.binding.{GBuffer, GUniform} +import io.computenode.cyfra.dsl.gio.GIO +import io.computenode.cyfra.dsl.struct.GStruct +import io.computenode.cyfra.dsl.struct.GStruct.Empty +import io.computenode.cyfra.dsl.{*, given} +import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.spirvtools.{SpirvCross, SpirvDisassembler, SpirvToolsRunner} +import io.computenode.cyfra.spirvtools.SpirvTool.ToFile +import io.computenode.cyfra.utility.Logger.logger +import org.lwjgl.BufferUtils +import org.lwjgl.system.MemoryUtil + +import java.nio.file.Paths +import scala.concurrent.ExecutionContext.Implicits.global +import java.util.concurrent.atomic.AtomicInteger +import scala.concurrent.{Await, Future} + +class SpirvRuntimeEnduranceTest extends munit.FunSuite: + + test("Endurance test for GExecution with multiple SPIRV programs loaded from files"): + runEnduranceTest(10000) + + // === Emit program === + + case class EmitProgramParams(inSize: Int, emitN: Int) + + case class EmitProgramUniform(emitN: Int32) extends GStruct[EmitProgramUniform] + + case class EmitProgramLayout( + in: GBuffer[Int32], + out: GBuffer[Int32], + args: GUniform[EmitProgramUniform] = GUniform.fromParams, // todo will be different in the future + ) extends Layout + + val emitProgram = GProgram.fromSpirvFile[EmitProgramParams, EmitProgramLayout]( + layout = params => + EmitProgramLayout( + in = GBuffer[Int32](params.inSize), + out = GBuffer[Int32](params.inSize * params.emitN), + args = GUniform(EmitProgramUniform(params.emitN)), + ), + dispatch = (_, args) => GProgram.StaticDispatch((args.inSize / 128, 1, 1)), + Paths.get(getClass.getResource("/emit.spv").toURI), + ) + + // === Filter program === + + case class FilterProgramParams(inSize: Int, filterValue: Int) + + case class FilterProgramUniform(filterValue: Int32) extends GStruct[FilterProgramUniform] + + case class FilterProgramLayout(in: GBuffer[Int32], out: GBuffer[GBoolean], params: GUniform[FilterProgramUniform] = GUniform.fromParams) + extends Layout + + val filterProgram = GProgram.fromSpirvFile[FilterProgramParams, FilterProgramLayout]( + layout = params => + FilterProgramLayout( + in = GBuffer[Int32](params.inSize), + out = GBuffer[GBoolean](params.inSize), + params = GUniform(FilterProgramUniform(params.filterValue)), + ), + dispatch = (_, args) => GProgram.StaticDispatch((args.inSize / 128, 1, 1)), + Paths.get(getClass.getResource("/filter.spv").toURI), + ) + // === GExecution === + + case class EmitFilterParams(inSize: Int, emitN: Int, filterValue: Int) + + case class EmitFilterLayout(inBuffer: GBuffer[Int32], emitBuffer: GBuffer[Int32], filterBuffer: GBuffer[GBoolean]) extends Layout + + case class EmitFilterResult(out: GBuffer[GBoolean]) extends Layout + + val emitFilterExecution = GExecution[EmitFilterParams, EmitFilterLayout]() + .addProgram(emitProgram)( + params => EmitProgramParams(inSize = params.inSize, emitN = params.emitN), + layout => EmitProgramLayout(in = layout.inBuffer, out = layout.emitBuffer), + ) + .addProgram(filterProgram)( + params => FilterProgramParams(inSize = 2 * params.inSize, filterValue = params.filterValue), + layout => FilterProgramLayout(in = layout.emitBuffer, out = layout.filterBuffer), + ) + + // Test case: Use one program 10 times, copying values from five input buffers to five output buffers and adding values from two uniforms + case class AddProgramParams(bufferSize: Int, addA: Int, addB: Int) + + case class AddProgramUniform(a: Int32) extends GStruct[AddProgramUniform] + + case class AddProgramLayout( + in1: GBuffer[Int32], + in2: GBuffer[Int32], + in3: GBuffer[Int32], + in4: GBuffer[Int32], + in5: GBuffer[Int32], + out1: GBuffer[Int32], + out2: GBuffer[Int32], + out3: GBuffer[Int32], + out4: GBuffer[Int32], + out5: GBuffer[Int32], + u1: GUniform[AddProgramUniform] = GUniform.fromParams, + u2: GUniform[AddProgramUniform] = GUniform.fromParams, + ) extends Layout + + case class AddProgramExecLayout( + in1: GBuffer[Int32], + in2: GBuffer[Int32], + in3: GBuffer[Int32], + in4: GBuffer[Int32], + in5: GBuffer[Int32], + out1: GBuffer[Int32], + out2: GBuffer[Int32], + out3: GBuffer[Int32], + out4: GBuffer[Int32], + out5: GBuffer[Int32], + ) extends Layout + + val addProgram: GProgram[AddProgramParams, AddProgramLayout] = GProgram.fromSpirvFile[AddProgramParams, AddProgramLayout]( + layout = params => + AddProgramLayout( + in1 = GBuffer[Int32](params.bufferSize), + in2 = GBuffer[Int32](params.bufferSize), + in3 = GBuffer[Int32](params.bufferSize), + in4 = GBuffer[Int32](params.bufferSize), + in5 = GBuffer[Int32](params.bufferSize), + out1 = GBuffer[Int32](params.bufferSize), + out2 = GBuffer[Int32](params.bufferSize), + out3 = GBuffer[Int32](params.bufferSize), + out4 = GBuffer[Int32](params.bufferSize), + out5 = GBuffer[Int32](params.bufferSize), + u1 = GUniform(AddProgramUniform(params.addA)), + u2 = GUniform(AddProgramUniform(params.addB)), + ), + dispatch = (layout, args) => GProgram.StaticDispatch((args.bufferSize / 128, 1, 1)), + Paths.get(getClass.getResource("/addOne.spv").toURI), + ) + + def swap(l: AddProgramLayout): AddProgramLayout = + val AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5, u1, u2) = l + AddProgramLayout(out1, out2, out3, out4, out5, in1, in2, in3, in4, in5, u1, u2) + + def fromExecLayout(l: AddProgramExecLayout): AddProgramLayout = + val AddProgramExecLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5) = l + AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5) + + val execution = (0 until 11).foldLeft( + GExecution[AddProgramParams, AddProgramExecLayout]().asInstanceOf[GExecution[AddProgramParams, AddProgramExecLayout, AddProgramExecLayout]], + )((x, i) => + if i % 2 == 0 then x.addProgram(addProgram)(mapParams = identity[AddProgramParams], mapLayout = fromExecLayout) + else x.addProgram(addProgram)(mapParams = identity, mapLayout = x => swap(fromExecLayout(x))), + ) + + def runEnduranceTest(nRuns: Int): Unit = + logger.info(s"Starting endurance test with $nRuns runs...") + + given runtime: VkCyfraRuntime = VkCyfraRuntime(spirvToolsRunner = + SpirvToolsRunner( + crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl"))), + disassembler = SpirvDisassembler.Enable(toolOutput = ToFile(Paths.get("output/dis.spvdis"))), + ), + ) + + val bufferSize = 1280 + val params = AddProgramParams(bufferSize, addA = 0, addB = 1) + val region = GBufferRegion + .allocate[AddProgramExecLayout] + .map: region => + execution.execute(params, region) + val aInt = new AtomicInteger(0) + val runs = (1 to nRuns).map: i => + Future: + val inBuffers = List.fill(5)(BufferUtils.createIntBuffer(bufferSize)) + val wbbList = inBuffers.map(MemoryUtil.memByteBuffer) + val rbbList = List.fill(5)(BufferUtils.createByteBuffer(bufferSize * 4)) + + val inData = (0 until bufferSize).toArray + inBuffers.foreach(_.put(inData).flip()) + region.runUnsafe( + init = AddProgramExecLayout( + in1 = GBuffer[Int32](wbbList(0)), + in2 = GBuffer[Int32](wbbList(1)), + in3 = GBuffer[Int32](wbbList(2)), + in4 = GBuffer[Int32](wbbList(3)), + in5 = GBuffer[Int32](wbbList(4)), + out1 = GBuffer[Int32](bufferSize), + out2 = GBuffer[Int32](bufferSize), + out3 = GBuffer[Int32](bufferSize), + out4 = GBuffer[Int32](bufferSize), + out5 = GBuffer[Int32](bufferSize), + ), + onDone = layout => { + layout.out1.read(rbbList(0)) + layout.out2.read(rbbList(1)) + layout.out3.read(rbbList(2)) + layout.out4.read(rbbList(3)) + layout.out5.read(rbbList(4)) + }, + ) + val prev = aInt.getAndAdd(1) + if prev % 50 == 0 then logger.info(s"Iteration $prev completed") + + val allRuns = Future.sequence(runs) + Await.result(allRuns, scala.concurrent.duration.Duration.Inf) + + runtime.close() + logger.info("Endurance test completed successfully") diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/ArithmeticsE2eTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/ArithmeticsE2eTest.scala similarity index 86% rename from cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/ArithmeticsE2eTest.scala rename to cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/ArithmeticsE2eTest.scala index 17797a77..5a54d8ee 100644 --- a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/ArithmeticsE2eTest.scala +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/ArithmeticsE2eTest.scala @@ -1,14 +1,15 @@ -package io.computenode.cyfra.e2e +package io.computenode.cyfra.e2e.dsl +import io.computenode.cyfra.core.CyfraRuntime import io.computenode.cyfra.core.archive.* -import mem.* -import GMem.fRGBA import io.computenode.cyfra.dsl.algebra.VectorAlgebra import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} +import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.core.GCodec.{*, given} class ArithmeticsE2eTest extends munit.FunSuite: - given gc: GContext = GContext() + given CyfraRuntime = VkCyfraRuntime() test("Float32 arithmetics"): val gf: GFunction[GStruct.Empty, Float32, Float32] = GFunction: fl => @@ -16,8 +17,7 @@ class ArithmeticsE2eTest extends munit.FunSuite: // We need to use multiples of 256 for Vulkan buffer alignment. val inArr = (0 to 255).map(_.toFloat).toArray - val gmem = FloatMem(inArr) - val result = gmem.map(gf).asInstanceOf[FloatMem].toArray + val result: Array[Float] = gf.run(inArr) val expected = inArr.map(f => (f + 1.2f) * (f - 3.4f) / 5.6f) result @@ -30,8 +30,7 @@ class ArithmeticsE2eTest extends munit.FunSuite: ((n + 2) * (n - 3) / 5).mod(7) val inArr = (0 to 255).toArray - val gmem = IntMem(inArr) - val result = gmem.map(gf).asInstanceOf[IntMem].toArray + val result: Array[Int] = gf.run(inArr) // With negative values and mod, Scala and Vulkan behave differently val expected = inArr.map: n => @@ -63,8 +62,7 @@ class ArithmeticsE2eTest extends munit.FunSuite: case Seq(a, b, c, d) => (a, b, c, d) .toArray - val gmem = Vec4FloatMem(inArr) - val result = gmem.map(gf).asInstanceOf[FloatMem].toArray + val result: Array[Float] = gf.run(inArr) extension (f: fRGBA) def neg = (-f._1, -f._2, -f._3, -f._4) diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/FunctionsE2eTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/FunctionsE2eTest.scala similarity index 89% rename from cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/FunctionsE2eTest.scala rename to cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/FunctionsE2eTest.scala index 31966edf..4cbc71d8 100644 --- a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/FunctionsE2eTest.scala +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/FunctionsE2eTest.scala @@ -1,12 +1,14 @@ -package io.computenode.cyfra.e2e +package io.computenode.cyfra.e2e.dsl -import io.computenode.cyfra.core.archive.*, mem.* +import io.computenode.cyfra.core.CyfraRuntime +import io.computenode.cyfra.core.archive.* import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} -import GMem.fRGBA +import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.core.GCodec.{*, given} class FunctionsE2eTest extends munit.FunSuite: - given gc: GContext = GContext() + given CyfraRuntime = VkCyfraRuntime() test("Functions"): val gf: GFunction[GStruct.Empty, Float32, Float32] = GFunction: f => @@ -15,8 +17,7 @@ class FunctionsE2eTest extends munit.FunSuite: abs(min(res1, res2) - max(res1, res2)) val inArr = (0 to 255).map(_.toFloat).toArray - val gmem = FloatMem(inArr) - val result = gmem.map(gf).asInstanceOf[FloatMem].toArray + val result: Array[Float] = gf.run(inArr) val expected = inArr.map: f => val res1 = math.pow(math.sqrt(math.exp(math.sin(math.cos(math.tan(f))))), 2) @@ -41,8 +42,7 @@ class FunctionsE2eTest extends munit.FunSuite: v5.dot(v1) val inArr = (0 to 255).map(_.toFloat).toArray - val gmem = FloatMem(inArr) - val result = gmem.map(gf).asInstanceOf[FloatMem].toArray + val result: Array[Float] = gf.run(inArr) extension (f: fRGBA) def neg: fRGBA = (-f._1, -f._2, -f._3, -f._4) diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/GStructE2eTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/GStructE2eTest.scala new file mode 100644 index 00000000..750085fe --- /dev/null +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/GStructE2eTest.scala @@ -0,0 +1,63 @@ +package io.computenode.cyfra.e2e.dsl + +import io.computenode.cyfra.core.CyfraRuntime +import io.computenode.cyfra.core.archive.* +import io.computenode.cyfra.dsl.binding.GBuffer +import io.computenode.cyfra.dsl.collections.GSeq +import io.computenode.cyfra.dsl.struct.GStruct +import io.computenode.cyfra.dsl.{*, given} +import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.core.GCodec.{*, given} + +class GStructE2eTest extends munit.FunSuite: + given CyfraRuntime = VkCyfraRuntime() + + case class Custom(f: Float32, v: Vec4[Float32]) extends GStruct[Custom] + val custom1 = Custom(2f, (1f, 2f, 3f, 4f)) + val custom2 = Custom(-0.5f, (-0.5f, -1.5f, -2.5f, -3.5f)) + + case class Nested(c1: Custom, c2: Custom) extends GStruct[Nested] + val nested = Nested(custom1, custom2) + + test("GStruct passed as uniform"): + val gf: GFunction[Custom, Float32, Float32] = GFunction.forEachIndex: + case (Custom(f, v), index: Int32, buff: GBuffer[Float32]) => v.*(f).dot(v) + buff.read(index) * f + + val inArr = (0 to 255).map(_.toFloat).toArray + val result: Array[Float] = gf.run(inArr, custom1) + + val expected = inArr.map(f => 2f * f + 60f) + result + .zip(expected) + .foreach: (res, exp) => + assert(Math.abs(res - exp) < 0.001f, s"Expected $exp but got $res") + + test("GStruct of GStructs".ignore): + val gf: GFunction[Nested, Float32, Float32] = GFunction.forEachIndex[Nested, Float32, Float32]: + case (Nested(Custom(f1, v1), Custom(f2, v2)), index: Int32, buff: GBuffer[Float32]) => + v1.*(f2).dot(v2) + buff.read(index) * f1 + + val inArr = (0 to 255).map(_.toFloat).toArray + val result: Array[Float] = gf.run(inArr, nested) + + val expected = inArr.map(f => 2f * f + 12.5f) + result + .zip(expected) + .foreach: (res, exp) => + assert(Math.abs(res - exp) < 0.001f, s"Expected $exp but got $res") + + test("GSeq of GStructs"): + val gf: GFunction[GStruct.Empty, Float32, Float32] = GFunction: fl => + GSeq + .gen(custom1, c => Custom(c.f * 2f, c.v.*(2f))) + .limit(3) + .fold[Float32](0f, (f, c) => f + c.f * (c.v.w + c.v.x + c.v.y + c.v.z)) + fl + + val inArr = (0 to 255).map(_.toFloat).toArray + val result: Array[Float] = gf.run(inArr) + + val expected = inArr.map(f => f + 420f) + result + .zip(expected) + .foreach: (res, exp) => + assert(res == exp, s"Expected $exp but got $res") diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/GseqE2eTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/GseqE2eTest.scala similarity index 79% rename from cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/GseqE2eTest.scala rename to cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/GseqE2eTest.scala index d10cca51..f63f077e 100644 --- a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/GseqE2eTest.scala +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/GseqE2eTest.scala @@ -1,13 +1,15 @@ -package io.computenode.cyfra.e2e +package io.computenode.cyfra.e2e.dsl +import io.computenode.cyfra.core.CyfraRuntime +import io.computenode.cyfra.core.archive.* import io.computenode.cyfra.dsl.collections.GSeq import io.computenode.cyfra.dsl.struct.GStruct -import io.computenode.cyfra.core.archive.* -import mem.* import io.computenode.cyfra.dsl.{*, given} +import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.core.GCodec.{*, given} class GseqE2eTest extends munit.FunSuite: - given gc: GContext = GContext() + given CyfraRuntime = VkCyfraRuntime() test("GSeq gen limit map fold"): val gf: GFunction[GStruct.Empty, Float32, Float32] = GFunction: f => @@ -18,8 +20,7 @@ class GseqE2eTest extends munit.FunSuite: .fold[Float32](0f, _ + _) val inArr = (0 to 255).map(_.toFloat).toArray - val gmem = FloatMem(inArr) - val result = gmem.map(gf).asInstanceOf[FloatMem].toArray + val result: Array[Float] = gf.run(inArr) val expected = inArr.map(f => 10 * f + 65.0f) result @@ -36,8 +37,7 @@ class GseqE2eTest extends munit.FunSuite: .count val inArr = (0 to 255).toArray - val gmem = IntMem(inArr) - val result = gmem.map(gf).asInstanceOf[IntMem].toArray + val result: Array[Int] = gf.run(inArr) val expected = inArr.map: n => List diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/WhenE2eTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/WhenE2eTest.scala similarity index 69% rename from cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/WhenE2eTest.scala rename to cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/WhenE2eTest.scala index 0a374c26..ce202128 100644 --- a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/WhenE2eTest.scala +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/WhenE2eTest.scala @@ -1,12 +1,14 @@ -package io.computenode.cyfra.e2e +package io.computenode.cyfra.e2e.dsl +import io.computenode.cyfra.core.CyfraRuntime +import io.computenode.cyfra.core.archive.GFunction import io.computenode.cyfra.dsl.struct.GStruct -import io.computenode.cyfra.core.archive.* -import mem.* import io.computenode.cyfra.dsl.{*, given} +import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.core.GCodec.{*, given} class WhenE2eTest extends munit.FunSuite: - given gc: GContext = GContext() + given CyfraRuntime = VkCyfraRuntime() test("when elseWhen otherwise"): val oneHundred = 100.0f @@ -17,8 +19,7 @@ class WhenE2eTest extends munit.FunSuite: .otherwise(2.0f) val inArr: Array[Float] = (0 to 255).map(_.toFloat).toArray - val gmem = FloatMem(inArr) - val result = gmem.map(gf).asInstanceOf[FloatMem].toArray + val result: Array[Float] = gf.run(inArr) val expected = inArr.map: f => if f <= oneHundred then 0.0f else if f <= twoHundred then 1.0f else 2.0f diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/fs2interop/Fs2Tests.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/fs2interop/Fs2Tests.scala new file mode 100644 index 00000000..6c6e5b14 --- /dev/null +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/fs2interop/Fs2Tests.scala @@ -0,0 +1,69 @@ +package io.computenode.cyfra.e2e.fs2interop + +import io.computenode.cyfra.core.archive.* +import io.computenode.cyfra.dsl.{*, given} +import algebra.VectorAlgebra +import io.computenode.cyfra.fs2interop.* +import io.computenode.cyfra.core.CyfraRuntime +import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.spirvtools.{SpirvCross, SpirvDisassembler, SpirvToolsRunner} +import io.computenode.cyfra.spirvtools.SpirvTool.ToFile + +import fs2.* + +import java.nio.file.Paths + +extension (f: fRGBA) + def neg = (-f._1, -f._2, -f._3, -f._4) + def scl(s: Float) = (f._1 * s, f._2 * s, f._3 * s, f._4 * s) + def add(g: fRGBA) = (f._1 + g._1, f._2 + g._2, f._3 + g._3, f._4 + g._4) + def close(g: fRGBA)(eps: Float): Boolean = + Math.abs(f._1 - g._1) < eps && Math.abs(f._2 - g._2) < eps && Math.abs(f._3 - g._3) < eps && Math.abs(f._4 - g._4) < eps + +class Fs2Tests extends munit.FunSuite: + given cr: VkCyfraRuntime = VkCyfraRuntime(spirvToolsRunner = + SpirvToolsRunner( + crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl"))), + disassembler = SpirvDisassembler.Enable(toolOutput = ToFile(Paths.get("output/disassembled.spv"))), + ), + ) + + override def afterAll(): Unit = + // cr.close() + super.afterAll() + + test("fs2 through GPipe map, just ints"): + val inSeq = (0 until 256).toSeq + val stream = Stream.emits(inSeq) + val pipe = GPipe.map[Pure, Int32, Int](_ + 1) + val result = stream.through(pipe).compile.toList + val expected = inSeq.map(_ + 1) + result + .zip(expected) + .foreach: (res, exp) => + assert(res == exp, s"Expected $exp, got $res") + + test("fs2 through GPipe map, floats and vectors"): + val n = 16 + val inSeq = (0 until n * 256).map(_.toFloat) + val stream = Stream.emits(inSeq) + val pipe = GPipe.map[Pure, Float32, Vec4[Float32], Float, fRGBA](f => (f, f + 1f, f + 2f, f + 3f)) + val result = stream.through(pipe).compile.toList + val expected = inSeq.map(f => (f, f + 1f, f + 2f, f + 3f)) + println("DONE!") + result + .zip(expected) + .foreach: (res, exp) => + assert(res.close(exp)(0.001f), s"Expected $exp, got $res") + + test("fs2 through GPipe filter, just ints"): + val n = 16 + val inSeq = 0 until n * 256 + val stream = Stream.emits(inSeq) + val pipe = GPipe.filter[Pure, Int32, Int](_.mod(7) === 0) + val result = stream.through(pipe).compile.toList + val expected = inSeq.filter(_ % 7 == 0) + result + .zip(expected) + .foreach: (res, exp) => + assert(res == exp, s"Expected $exp, got $res") diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/juliaset/JuliaSet.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/juliaset/JuliaSet.scala index df9b0191..b0d70672 100644 --- a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/juliaset/JuliaSet.scala +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/juliaset/JuliaSet.scala @@ -2,12 +2,14 @@ package io.computenode.cyfra.e2e.juliaset import io.computenode.cyfra.dsl.{*, given} import io.computenode.cyfra.* +import io.computenode.cyfra.core.GCodec.{*, given} +import io.computenode.cyfra.core.CyfraRuntime import io.computenode.cyfra.dsl.collections.GSeq import io.computenode.cyfra.dsl.control.Pure.pure import io.computenode.cyfra.dsl.struct.GStruct.Empty import io.computenode.cyfra.e2e.ImageTests -import io.computenode.cyfra.core.archive.mem.Vec4FloatMem -import io.computenode.cyfra.core.archive.{GContext, GFunction} +import io.computenode.cyfra.core.archive.GFunction +import io.computenode.cyfra.runtime.VkCyfraRuntime import io.computenode.cyfra.spirvtools.* import io.computenode.cyfra.spirvtools.SpirvTool.{Param, ToFile} import io.computenode.cyfra.utility.ImageUtility @@ -21,7 +23,7 @@ import scala.concurrent.ExecutionContext.Implicits class JuliaSet extends FunSuite: given ExecutionContext = Implicits.global - def runJuliaSet(referenceImgName: String)(using GContext): Unit = + def runJuliaSet(referenceImgName: String)(using CyfraRuntime): Unit = val dim = 4096 val max = 1 val RECURSION_LIMIT = 1000 @@ -65,18 +67,19 @@ class JuliaSet extends FunSuite: .otherwise: (8f / 255f, 22f / 255f, 104f / 255f, 1.0f) - val r = Vec4FloatMem(dim * dim).map(function).asInstanceOf[Vec4FloatMem].toArray + val vec4arr = Array.ofDim[fRGBA](dim * dim) + val r: Array[fRGBA] = function.run(vec4arr, Empty()) val outputTemp = File.createTempFile("julia", ".png") ImageUtility.renderToImage(r, dim, outputTemp.toPath) val referenceImage = getClass.getResource(referenceImgName) ImageTests.assertImagesEquals(outputTemp, new File(referenceImage.getPath)) test("Render julia set"): - given GContext = new GContext + given CyfraRuntime = VkCyfraRuntime() runJuliaSet("/julia.png") test("Render julia set optimized"): - given GContext = new GContext( + given CyfraRuntime = new VkCyfraRuntime( SpirvToolsRunner( validator = SpirvValidator.Enable(throwOnFail = true), optimizer = SpirvOptimizer.Enable(toolOutput = ToFile(Paths.get("output/optimized.spv")), settings = Seq(Param("-O"))), diff --git a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala index 2f1b2799..0e1781df 100644 --- a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala +++ b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala @@ -1,6 +1,5 @@ package io.computenode.cyfra.samples -import io.computenode.cyfra.core.archive.GContext import io.computenode.cyfra.core.layout.* import io.computenode.cyfra.core.{GBufferRegion, GExecution, GProgram} import io.computenode.cyfra.dsl.Value.{GBoolean, Int32} @@ -9,23 +8,17 @@ import io.computenode.cyfra.dsl.gio.GIO import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.spirvtools.SpirvTool.ToFile +import io.computenode.cyfra.spirvtools.{SpirvCross, SpirvToolsRunner, SpirvValidator} import org.lwjgl.BufferUtils import org.lwjgl.system.MemoryUtil -import java.nio.ByteBuffer +import java.nio.file.Paths import java.util.concurrent.atomic.AtomicInteger import scala.collection.parallel.CollectionConverters.given -def printBuffer(bb: ByteBuffer): Unit = - val l = bb.asIntBuffer() - val a = new Array[Int](l.remaining()) - l.get(a) - println(a.mkString(" ")) - object TestingStuff: - given GContext = GContext() - // === Emit program === case class EmitProgramParams(inSize: Int, emitN: Int) @@ -60,14 +53,13 @@ object TestingStuff: case class FilterProgramUniform(filterValue: Int32) extends GStruct[FilterProgramUniform] - case class FilterProgramLayout(in: GBuffer[Int32], out: GBuffer[GBoolean], params: GUniform[FilterProgramUniform] = GUniform.fromParams) - extends Layout + case class FilterProgramLayout(in: GBuffer[Int32], out: GBuffer[Int32], params: GUniform[FilterProgramUniform] = GUniform.fromParams) extends Layout val filterProgram = GProgram[FilterProgramParams, FilterProgramLayout]( layout = params => FilterProgramLayout( in = GBuffer[Int32](params.inSize), - out = GBuffer[GBoolean](params.inSize), + out = GBuffer[Int32](params.inSize), params = GUniform(FilterProgramUniform(params.filterValue)), ), dispatch = (_, args) => GProgram.StaticDispatch((args.inSize / 128, 1, 1)), @@ -75,15 +67,16 @@ object TestingStuff: val invocId = GIO.invocationId val element = GIO.read(layout.in, invocId) val isMatch = element === layout.params.read.filterValue - GIO.write(layout.out, invocId, isMatch) + val a: Int32 = when[Int32](isMatch)(1).otherwise(0) + GIO.write(layout.out, invocId, a) // === GExecution === case class EmitFilterParams(inSize: Int, emitN: Int, filterValue: Int) - case class EmitFilterLayout(inBuffer: GBuffer[Int32], emitBuffer: GBuffer[Int32], filterBuffer: GBuffer[GBoolean]) extends Layout + case class EmitFilterLayout(inBuffer: GBuffer[Int32], emitBuffer: GBuffer[Int32], filterBuffer: GBuffer[Int32]) extends Layout - case class EmitFilterResult(out: GBuffer[GBoolean]) extends Layout + case class EmitFilterResult(out: GBuffer[Int32]) extends Layout val emitFilterExecution = GExecution[EmitFilterParams, EmitFilterLayout]() .addProgram(emitProgram)( @@ -96,15 +89,16 @@ object TestingStuff: ) @main - def test = - given runtime: VkCyfraRuntime = VkCyfraRuntime() + def testEmit = + given runtime: VkCyfraRuntime = + VkCyfraRuntime(spirvToolsRunner = SpirvToolsRunner(crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl"))))) - val emitFilterParams = EmitFilterParams(inSize = 1024, emitN = 2, filterValue = 42) + val emitParams = EmitProgramParams(inSize = 1024, emitN = 2) val region = GBufferRegion - .allocate[EmitFilterLayout] + .allocate[EmitProgramLayout] .map: region => - emitFilterExecution.execute(emitFilterParams, region) + emitProgram.execute(emitParams, region) val data = (0 until 1024).toArray val buffer = BufferUtils.createByteBuffer(data.length * 4) @@ -113,176 +107,56 @@ object TestingStuff: val result = BufferUtils.createIntBuffer(data.length * 2) val rbb = MemoryUtil.memByteBuffer(result) region.runUnsafe( - init = EmitFilterLayout( - inBuffer = GBuffer[Int32](buffer), - emitBuffer = GBuffer[Int32](data.length * 2), - filterBuffer = GBuffer[GBoolean](data.length * 2), - ), - onDone = layout => layout.filterBuffer.read(rbb), + init = EmitProgramLayout(in = GBuffer[Int32](buffer), out = GBuffer[Int32](data.length * 2)), + onDone = layout => layout.out.read(rbb), ) runtime.close() - printBuffer(rbb) - val actual = (0 until 2 * 1024).map(i => result.get(i * 1) != 0) - val expected = (0 until 1024).flatMap(x => Seq.fill(emitFilterParams.emitN)(x)).map(_ == emitFilterParams.filterValue) + val actual = (0 until 2 * 1024).map(i => result.get(i * 1)) + val expected = (0 until 1024).flatMap(x => Seq.fill(emitParams.emitN)(x)) expected .zip(actual) .zipWithIndex .foreach: case ((e, a), i) => assert(e == a, s"Mismatch at index $i: expected $e, got $a") -// Test case: Use one program 10 times, copying values from five input buffers to five output buffers and adding values from two uniforms - case class AddProgramParams(bufferSize: Int, addA: Int, addB: Int) - case class AddProgramUniform(a: Int32) extends GStruct[AddProgramUniform] - case class AddProgramLayout( - in1: GBuffer[Int32], - in2: GBuffer[Int32], - in3: GBuffer[Int32], - in4: GBuffer[Int32], - in5: GBuffer[Int32], - out1: GBuffer[Int32], - out2: GBuffer[Int32], - out3: GBuffer[Int32], - out4: GBuffer[Int32], - out5: GBuffer[Int32], - u1: GUniform[AddProgramUniform] = GUniform.fromParams, - u2: GUniform[AddProgramUniform] = GUniform.fromParams, - ) extends Layout - - case class AddProgramExecLayout( - in1: GBuffer[Int32], - in2: GBuffer[Int32], - in3: GBuffer[Int32], - in4: GBuffer[Int32], - in5: GBuffer[Int32], - out1: GBuffer[Int32], - out2: GBuffer[Int32], - out3: GBuffer[Int32], - out4: GBuffer[Int32], - out5: GBuffer[Int32], - ) extends Layout - - val addProgram: GProgram[AddProgramParams, AddProgramLayout] = GProgram[AddProgramParams, AddProgramLayout]( - layout = params => - AddProgramLayout( - in1 = GBuffer[Int32](params.bufferSize), - in2 = GBuffer[Int32](params.bufferSize), - in3 = GBuffer[Int32](params.bufferSize), - in4 = GBuffer[Int32](params.bufferSize), - in5 = GBuffer[Int32](params.bufferSize), - out1 = GBuffer[Int32](params.bufferSize), - out2 = GBuffer[Int32](params.bufferSize), - out3 = GBuffer[Int32](params.bufferSize), - out4 = GBuffer[Int32](params.bufferSize), - out5 = GBuffer[Int32](params.bufferSize), - u1 = GUniform(AddProgramUniform(params.addA)), - u2 = GUniform(AddProgramUniform(params.addB)), + @main + def test = + given runtime: VkCyfraRuntime = VkCyfraRuntime(spirvToolsRunner = + SpirvToolsRunner( + crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl"))), + validator = SpirvValidator.Disable, ), - dispatch = (layout, args) => GProgram.StaticDispatch((args.bufferSize / 128, 1, 1)), - )(_ => ???) - def swap(l: AddProgramLayout): AddProgramLayout = - val AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5, u1, u2) = l - AddProgramLayout(out1, out2, out3, out4, out5, in1, in2, in3, in4, in5, u1, u2) - - def fromExecLayout(l: AddProgramExecLayout): AddProgramLayout = - val AddProgramExecLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5) = l - AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5) + ) - val execution = (0 until 11).foldLeft( - GExecution[AddProgramParams, AddProgramExecLayout]().asInstanceOf[GExecution[AddProgramParams, AddProgramExecLayout, AddProgramExecLayout]], - )((x, i) => - if i % 2 == 0 then x.addProgram(addProgram)(mapParams = identity[AddProgramParams], mapLayout = fromExecLayout) - else x.addProgram(addProgram)(mapParams = identity, mapLayout = x => swap(fromExecLayout(x))), - ) + val emitFilterParams = EmitFilterParams(inSize = 1024, emitN = 2, filterValue = 42) - @main - def testAddProgram10Times = - given runtime: VkCyfraRuntime = VkCyfraRuntime() - val bufferSize = 1280 - val params = AddProgramParams(bufferSize, addA = 5, addB = 10) val region = GBufferRegion - .allocate[AddProgramExecLayout] + .allocate[EmitFilterLayout] .map: region => - execution.execute(params, region) + emitFilterExecution.execute(emitFilterParams, region) - val inBuffers = List.fill(5)(BufferUtils.createIntBuffer(bufferSize)) - val wbbList = inBuffers.map(MemoryUtil.memByteBuffer) - val outBuffers = List.fill(5)(BufferUtils.createIntBuffer(bufferSize)) - val rbbList = outBuffers.map(MemoryUtil.memByteBuffer) + val data = (0 until 1024).toArray + val buffer = BufferUtils.createByteBuffer(data.length * 4) + buffer.asIntBuffer().put(data).flip() - val inData = (0 until bufferSize).toArray - inBuffers.foreach(_.put(inData).flip()) + val result = BufferUtils.createIntBuffer(data.length * 2) + val rbb = MemoryUtil.memByteBuffer(result) region.runUnsafe( - init = AddProgramExecLayout( - in1 = GBuffer[Int32](wbbList(0)), - in2 = GBuffer[Int32](wbbList(1)), - in3 = GBuffer[Int32](wbbList(2)), - in4 = GBuffer[Int32](wbbList(3)), - in5 = GBuffer[Int32](wbbList(4)), - out1 = GBuffer[Int32](bufferSize), - out2 = GBuffer[Int32](bufferSize), - out3 = GBuffer[Int32](bufferSize), - out4 = GBuffer[Int32](bufferSize), - out5 = GBuffer[Int32](bufferSize), + init = EmitFilterLayout( + inBuffer = GBuffer[Int32](buffer), + emitBuffer = GBuffer[Int32](data.length * 2), + filterBuffer = GBuffer[Int32](data.length * 2), ), - onDone = layout => { - layout.out1.read(rbbList(0)) - layout.out2.read(rbbList(1)) - layout.out3.read(rbbList(2)) - layout.out4.read(rbbList(3)) - layout.out5.read(rbbList(4)) - }, + onDone = layout => layout.filterBuffer.read(rbb), ) runtime.close() - printBuffer(rbbList(0)) - val expected = inData.map(_ + 11 * (params.addA + params.addB)) - outBuffers.foreach { buf => - (0 until bufferSize).foreach { i => - assert(buf.get(i) == expected(i), s"Mismatch at index $i: expected ${expected(i)}, got ${buf.get(i)}") - } - } - - @main - def enduranceTest = - given runtime: VkCyfraRuntime = VkCyfraRuntime() - val bufferSize = 1280 - val params = AddProgramParams(bufferSize, addA = 0, addB = 1) - val region = GBufferRegion - .allocate[AddProgramExecLayout] - .map: region => - execution.execute(params, region) - val aInt = new AtomicInteger(0) - (1 to 10000).par.foreach: i => - val inBuffers = List.fill(5)(BufferUtils.createIntBuffer(bufferSize)) - val wbbList = inBuffers.map(MemoryUtil.memByteBuffer) - val rbbList = List.fill(5)(BufferUtils.createByteBuffer(bufferSize * 4)) - - val inData = (0 until bufferSize).toArray - inBuffers.foreach(_.put(inData).flip()) - region.runUnsafe( - init = AddProgramExecLayout( - in1 = GBuffer[Int32](wbbList(0)), - in2 = GBuffer[Int32](wbbList(1)), - in3 = GBuffer[Int32](wbbList(2)), - in4 = GBuffer[Int32](wbbList(3)), - in5 = GBuffer[Int32](wbbList(4)), - out1 = GBuffer[Int32](bufferSize), - out2 = GBuffer[Int32](bufferSize), - out3 = GBuffer[Int32](bufferSize), - out4 = GBuffer[Int32](bufferSize), - out5 = GBuffer[Int32](bufferSize), - ), - onDone = layout => { - layout.out1.read(rbbList(0)) - layout.out2.read(rbbList(1)) - layout.out3.read(rbbList(2)) - layout.out4.read(rbbList(3)) - layout.out5.read(rbbList(4)) - }, - ) - val prev = aInt.getAndAdd(1) - if prev % 100 == 0 then println(s"Iteration $prev completed") - - runtime.close() - println("Endurance test completed successfully") + val actual = (0 until 2 * 1024).map(i => result.get(i) != 0) + val expected = (0 until 1024).flatMap(x => Seq.fill(emitFilterParams.emitN)(x)).map(_ == emitFilterParams.filterValue) + expected + .zip(actual) + .zipWithIndex + .foreach: + case ((e, a), i) => assert(e == a, s"Mismatch at index $i: expected $e, got $a") + println("DONE") diff --git a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/oldsamples/Raytracing.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/oldsamples/Raytracing.scala deleted file mode 100644 index ab35810e..00000000 --- a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/oldsamples/Raytracing.scala +++ /dev/null @@ -1,535 +0,0 @@ -package io.computenode.cyfra.samples.oldsamples - -import io.computenode.cyfra.dsl.collections.GSeq -import io.computenode.cyfra.dsl.{*, given} -import io.computenode.cyfra.dsl.struct.GStruct -import io.computenode.cyfra.core.archive.* -import io.computenode.cyfra.core.archive.mem.Vec4FloatMem -import io.computenode.cyfra.utility.ImageUtility - -import java.nio.file.Paths -import scala.annotation.tailrec -import scala.collection.mutable -import scala.concurrent.ExecutionContext -import scala.concurrent.ExecutionContext.Implicits - -given GContext = new GContext() -given ExecutionContext = Implicits.global - -/** Raytracing example - */ - -@main -def main = - - val dim = 2048 - val minRayHitTime = 0.01f - val rayPosNormalNudge = 0.01f - val superFar = 1000.0f - val fovDeg = 60 - val fovRad = fovDeg * math.Pi.toFloat / 180.0f - val maxBounces = 8 - val pixelIterationsPerFrame = 1000 - val bgColor = (0.2f, 0.2f, 0.2f) - val exposure = 1f - - case class Random[T <: Value](value: T, nextSeed: UInt32) - - def lessThan(f: Vec3[Float32], f2: Float32): Vec3[Float32] = - (when(f.x < f2)(1.0f).otherwise(0.0f), when(f.y < f2)(1.0f).otherwise(0.0f), when(f.z < f2)(1.0f).otherwise(0.0f)) - - def linearToSRGB(rgb: Vec3[Float32]): Vec3[Float32] = - val clampedRgb = vclamp(rgb, 0.0f, 1.0f) - mix(pow(clampedRgb, vec3(1.0f / 2.4f)) * 1.055f - vec3(0.055f), clampedRgb * 12.92f, lessThan(clampedRgb, 0.0031308f)) - - def SRGBToLinear(rgb: Vec3[Float32]): Vec3[Float32] = - val clampedRgb = vclamp(rgb, 0.0f, 1.0f) - mix(pow((clampedRgb + vec3(0.055f)) * (1.0f / 1.055f), vec3(2.4f)), clampedRgb * (1.0f / 12.92f), lessThan(clampedRgb, 0.04045f)) - - def ACESFilm(x: Vec3[Float32]): Vec3[Float32] = - val a = 2.51f - val b = 0.03f - val c = 2.43f - val d = 0.59f - val e = 0.14f - vclamp((x mulV (x * a + vec3(b))) divV (x mulV (x * c + vec3(d)) + vec3(e)), 0.0f, 1.0f) - - case class RayHitInfo( - dist: Float32, - normal: Vec3[Float32], - albedo: Vec3[Float32], - emissive: Vec3[Float32], - percentSpecular: Float32 = 0f, - roughness: Float32 = 0f, - specularColor: Vec3[Float32] = vec3(0f), - indexOfRefraction: Float32 = 1.0f, - refractionChance: Float32 = 0f, - refractionRoughness: Float32 = 0f, - refractionColor: Vec3[Float32] = vec3(0f), - fromInside: GBoolean = false, - ) extends GStruct[RayHitInfo] - - case class Sphere( - center: Vec3[Float32], - radius: Float32, - color: Vec3[Float32], - emissive: Vec3[Float32], - percentSpecular: Float32 = 0f, - roughness: Float32 = 0f, - specularColor: Vec3[Float32] = vec3(0f), - indexOfRefraction: Float32 = 1f, - refractionChance: Float32 = 0f, - refractionRoughness: Float32 = 0f, - refractionColor: Vec3[Float32] = vec3(0f), - ) extends GStruct[Sphere] - - case class Quad( - a: Vec3[Float32], - b: Vec3[Float32], - c: Vec3[Float32], - d: Vec3[Float32], - color: Vec3[Float32], - emissive: Vec3[Float32], - percentSpecular: Float32 = 0f, - roughness: Float32 = 0f, - specularColor: Vec3[Float32] = vec3(0f), - indexOfRefraction: Float32 = 1f, - refractionChance: Float32 = 0f, - refractionRoughness: Float32 = 0f, - refractionColor: Vec3[Float32] = vec3(0f), - ) extends GStruct[Quad] - - case class RayTraceState( - rayPos: Vec3[Float32], - rayDir: Vec3[Float32], - color: Vec3[Float32], - throughput: Vec3[Float32], - rngState: UInt32, - finished: GBoolean = false, - ) extends GStruct[RayTraceState] - - val sceneTranslation = vec4(0f, 0f, 10f, 0f) - // 7 is cool - val rd = scala.util.Random(3) - - def scalaTwoSpheresIntersect(sphereA: (Float, Float, Float), radiusA: Float, sphereB: (Float, Float, Float), radiusB: Float): Boolean = - val dist = Math.sqrt( - (sphereA._1 - sphereB._1) * - (sphereA._1 - sphereB._1) + - (sphereA._2 - sphereB._2) * - (sphereA._2 - sphereB._2) + - (sphereA._3 - sphereB._3) * - (sphereA._3 - sphereB._3), - ) - dist < radiusA + radiusB - - val existingSpheres = mutable.Set.empty[((Float, Float, Float), Float)] - @tailrec - def randomSphere(iter: Int = 0): Sphere = - if iter > 1000 then throw new Exception("Could not find a non-intersecting sphere") - def nextFloatAny = rd.nextFloat() * 2f - 1f - - def nextFloatPos = rd.nextFloat() - - val center = (nextFloatAny * 10, nextFloatAny * 10, nextFloatPos * 10 + 8f) - val radius = nextFloatPos + 1.5f - if existingSpheres.exists(s => scalaTwoSpheresIntersect(s._1, s._2, center, radius)) then randomSphere(iter + 1) - else - existingSpheres.add((center, radius)) - def color = (nextFloatPos * 0.5f + 0.5f, nextFloatPos * 0.5f + 0.5f, nextFloatPos * 0.5f + 0.5f) - val emissive = (0f, 0f, 0f) - Sphere( - center, - radius, - color, - emissive, - 0.45f, - 0.1f, - (nextFloatPos + 0.2f, nextFloatPos + 0.2f, nextFloatPos + 0.2f), - 1.1f, - 0.6f, - 0.1f, - (nextFloatPos, nextFloatPos, nextFloatPos), - ) - - def randomSpheres(n: Int) = List.fill(n)(randomSphere()) - - val flash = // flash - val x = -10f - val mX = -5f - val y = -10f - val mY = 0f - val z = -5f - Sphere((-7.5f, -12f, -5f), 3f, (1f, 1f, 1f), (20f, 20f, 20f)) - val spheres = (flash :: randomSpheres(20)).map(sp => sp.copy(center = sp.center + sceneTranslation.xyz)) - - val walls = List( - Quad( // back - (-15.5f, -15.5f, 25.0f), - (15.5f, -15.5f, 25.0f), - (15.5f, 15.5f, 25.0f), - (-15.5f, 15.5f, 25.0f), - (0.8f, 0.8f, 0.8f), - (0f, 0f, 0f), - ), - Quad( // right - (15f, -15.5f, 25.5f), - (15f, -15.5f, -15.5f), - (15f, 15.5f, -15.5f), - (15f, 15.5f, 25.5f), - (0.0f, 0.8f, 0.0f), - (0f, 0f, 0f), - ), - Quad( // left - (-15f, -15.5f, 25.5f), - (-15f, -15.5f, -15.5f), - (-15f, 15.5f, -15.5f), - (-15f, 15.5f, 25.5f), - (0.8f, 0.0f, 0.0f), - (0f, 0f, 0f), - ), - Quad( // bottom - (-15.5f, 15f, 25.5f), - (15.5f, 15f, 25.5f), - (15.5f, 15f, -15.5f), - (-15.5f, 15f, -15.5f), - (0.8f, 0.8f, 0.8f), - (0f, 0f, 0f), - ), - Quad( // top - (-15.5f, -15f, 25.5f), - (15.5f, -15f, 25.5f), - (15.5f, -15f, -15.5f), - (-15.5f, -15f, -15.5f), - (0.8f, 0.8f, 0.8f), - (0f, 0f, 0f), - ), - Quad( // front - (-15.5f, -15.5f, -15.5f), - (15.5f, -15.5f, -15.5f), - (15.5f, 15.5f, -15.5f), - (-15.5f, 15.5f, -15.5f), - (0.8f, 0.8f, 0.8f), - (0f, 0f, 0f), - ), - Quad( // light - (-2.5f, -14.95f, 17.5f), - (2.5f, -14.95f, 17.5f), - (2.5f, -14.95f, 12.5f), - (-2.5f, -14.95f, 12.5f), - (1f, 1f, 1f), - (20f, 18f, 14f), - ), - ).map(quad => - quad.copy( - a = quad.a + sceneTranslation.xyz, - b = quad.b + sceneTranslation.xyz, - c = quad.c + sceneTranslation.xyz, - d = quad.d + sceneTranslation.xyz, - ), - ) - - case class RaytracingIteration(frame: Int32) extends GStruct[RaytracingIteration] - - def function(): GFunction[RaytracingIteration, Vec4[Float32], Vec4[Float32]] = GFunction.from2D(dim): - case (RaytracingIteration(frame), (xi: Int32, yi: Int32), lastFrame) => - def wangHash(seed: UInt32): UInt32 = - val s1 = (seed ^ 61) ^ (seed >> 16) - val s2 = s1 * 9 - val s3 = s2 ^ (s2 >> 4) - val s4 = s3 * 0x27d4eb2d - s4 ^ (s4 >> 15) - - def randomFloat(seed: UInt32): Random[Float32] = - val nextSeed = wangHash(seed) - val f = nextSeed.asFloat / 4294967296.0f - Random(f, nextSeed) - - def randomVector(seed: UInt32): Random[Vec3[Float32]] = - val Random(z, seed1) = randomFloat(seed) - val z2 = z * 2.0f - 1.0f - val Random(a, seed2) = randomFloat(seed1) - val a2 = a * 2.0f * math.Pi.toFloat - val r = sqrt(1.0f - z2 * z2) - val x = r * cos(a2) - val y = r * sin(a2) - Random((x, y, z2), seed2) - - def scalarTriple(u: Vec3[Float32], v: Vec3[Float32], w: Vec3[Float32]): Float32 = (u cross v) dot w - - def testQuadTrace(rayPos: Vec3[Float32], rayDir: Vec3[Float32], currentHit: RayHitInfo, quad: Quad): RayHitInfo = - val normal = normalize((quad.c - quad.a) cross (quad.c - quad.b)) - val fixedQuad = - when((normal dot rayDir) > 0f): - Quad(quad.d, quad.c, quad.b, quad.a, quad.color, quad.emissive) - .otherwise: - quad - - val fixedNormal = when((normal dot rayDir) > 0f)(-normal).otherwise(normal) - val p = rayPos - val q = rayPos + rayDir - val pq = q - p - val pa = fixedQuad.a - p - val pb = fixedQuad.b - p - val pc = fixedQuad.c - p - val m = pc cross pq - val v = pa dot m - - def checkHit(intersectPoint: Vec3[Float32]): RayHitInfo = - val dist = - when(abs(rayDir.x) > 0.1f): - (intersectPoint.x - rayPos.x) / rayDir.x - .elseWhen(abs(rayDir.y) > 0.1f): - (intersectPoint.y - rayPos.y) / rayDir.y - .otherwise: - (intersectPoint.z - rayPos.z) / rayDir.z - - when(dist > minRayHitTime && dist < currentHit.dist): - RayHitInfo( - dist, - fixedNormal, - quad.color, - quad.emissive, - quad.percentSpecular, - quad.roughness, - quad.specularColor, - quad.indexOfRefraction, - quad.refractionChance, - quad.refractionRoughness, - quad.refractionColor, - ) - .otherwise: - currentHit - - when(v >= 0f): - val u = -(pb dot m) - val w = scalarTriple(pq, pb, pa) - when(u >= 0f && w >= 0f): - val denom = 1f / (u + v + w) - val uu = u * denom - val vv = v * denom - val ww = w * denom - val intersectPos = fixedQuad.a * uu + fixedQuad.b * vv + fixedQuad.c * ww - checkHit(intersectPos) - .otherwise: - currentHit - .otherwise: - val pd = fixedQuad.d - p - val u = pd dot m - val w = scalarTriple(pq, pa, pd) - when(u >= 0f && w >= 0f): - val negV = -v - val denom = 1f / (u + negV + w) - val uu = u * denom - val vv = negV * denom - val ww = w * denom - val intersectPos = fixedQuad.a * uu + fixedQuad.d * vv + fixedQuad.c * ww - checkHit(intersectPos) - .otherwise: - currentHit - - def testSphereTrace(rayPos: Vec3[Float32], rayDir: Vec3[Float32], currentHit: RayHitInfo, sphere: Sphere): RayHitInfo = - val toRay = rayPos - sphere.center - val b = toRay dot rayDir - val c = (toRay dot toRay) - (sphere.radius * sphere.radius) - val notHit = currentHit - when(c > 0f && b > 0f): - notHit - .otherwise: - val discr = b * b - c - when(discr > 0f): - val initDist = -b - sqrt(discr) - val fromInside = initDist < 0f - val dist = when(fromInside)(-b + sqrt(discr)).otherwise(initDist) - when(dist > minRayHitTime && dist < currentHit.dist): - val normal = normalize((rayPos + rayDir * dist - sphere.center) * when(fromInside)(-1f).otherwise(1f)) - RayHitInfo( - dist, - normal, - sphere.color, - sphere.emissive, - sphere.percentSpecular, - sphere.roughness, - sphere.specularColor, - sphere.indexOfRefraction, - sphere.refractionChance, - sphere.refractionRoughness, - sphere.refractionColor, - fromInside, - ) - .otherwise: - notHit - .otherwise: - notHit - - def testScene(rayPos: Vec3[Float32], rayDir: Vec3[Float32], currentHit: RayHitInfo): RayHitInfo = - - val spheresHit = GSeq - .of(spheres) - .fold( - currentHit, - { case (hit, sphere) => - testSphereTrace(rayPos, rayDir, hit, sphere) - }, - ) - - GSeq.of(walls).fold(spheresHit, (hit, wall) => testQuadTrace(rayPos, rayDir, hit, wall)) - - def fresnelReflectAmount(n1: Float32, n2: Float32, normal: Vec3[Float32], incident: Vec3[Float32], f0: Float32, f90: Float32): Float32 = - val r0 = ((n1 - n2) / (n1 + n2)) * ((n1 - n2) / (n1 + n2)) - val cosX = -(normal dot incident) - when(n1 > n2): - val n = n1 / n2 - val sinT2 = n * n * (1f - cosX * cosX) - when(sinT2 > 1f): - f90 - .otherwise: - val cosX2 = sqrt(1.0f - sinT2) - val x = 1.0f - cosX2 - val ret = r0 + ((1.0f - r0) * x * x * x * x * x) - mix(f0, f90, ret) - .otherwise: - val x = 1.0f - cosX - val ret = r0 + ((1.0f - r0) * x * x * x * x * x) - mix(f0, f90, ret) - - val MaxBounces = 8 - def getColorForRay(startRayPos: Vec3[Float32], startRayDir: Vec3[Float32], initRngState: UInt32): RayTraceState = - val initState = RayTraceState(startRayPos, startRayDir, (0f, 0f, 0f), (1f, 1f, 1f), initRngState) - GSeq - .gen[RayTraceState]( - first = initState, - next = - case state @ RayTraceState(rayPos, rayDir, color, throughput, rngState, _) => - val noHit = RayHitInfo(superFar, (0f, 0f, 0f), (0f, 0f, 0f), (0f, 0f, 0f)) - val testResult = testScene(rayPos, rayDir, noHit) - when(testResult.dist < superFar): - val throughput2 = when(testResult.fromInside): - throughput mulV exp[Vec3[Float32]](-testResult.refractionColor * testResult.dist) - .otherwise: - throughput - - val specularChance = when(testResult.percentSpecular > 0.0f): - fresnelReflectAmount( - when(testResult.fromInside)(testResult.indexOfRefraction).otherwise(1.0f), - when(!testResult.fromInside)(testResult.indexOfRefraction).otherwise(1.0f), - rayDir, - testResult.normal, - testResult.percentSpecular, - 1.0f, - ) - .otherwise: - 0f - - val refractionChance = when(specularChance > 0.0f): - testResult.refractionChance * ((1.0f - specularChance) / (1.0f - testResult.percentSpecular)) - .otherwise: - testResult.refractionChance - - val Random(rayRoll, nextRngState1) = randomFloat(rngState) - val doSpecular = when(specularChance > 0.0f && rayRoll < specularChance): - 1.0f - .otherwise: - 0.0f - - val doRefraction = when(refractionChance > 0.0f && doSpecular === 0.0f && rayRoll < specularChance + refractionChance): - 1.0f - .otherwise: - 0.0f - - val rayProbability = when(doSpecular === 1.0f): - specularChance - .elseWhen(doRefraction === 1.0f): - refractionChance - .otherwise: - 1.0f - (specularChance + refractionChance) - - val rayProbabilityCorrected = max(rayProbability, 0.01f) - - val nextRayPos = when(doRefraction === 1.0f): - (rayPos + rayDir * testResult.dist) - (testResult.normal * rayPosNormalNudge) - .otherwise: - (rayPos + rayDir * testResult.dist) + (testResult.normal * rayPosNormalNudge) - - val Random(randomVec1, nextRngState2) = randomVector(nextRngState1) - val diffuseRayDir = normalize(testResult.normal + randomVec1) - val specularRayDirPerfect = reflect(rayDir, testResult.normal) - val specularRayDir = normalize(mix(specularRayDirPerfect, diffuseRayDir, testResult.roughness * testResult.roughness)) - - val Random(randomVec2, nextRngState3) = randomVector(nextRngState2) - val refractionRayDirPerfect = - refract( - rayDir, - testResult.normal, - when(testResult.fromInside)(testResult.indexOfRefraction).otherwise(1.0f / testResult.indexOfRefraction), - ) - val refractionRayDir = - normalize( - mix( - refractionRayDirPerfect, - normalize(-testResult.normal + randomVec2), - testResult.refractionRoughness * testResult.refractionRoughness, - ), - ) - - val rayDirSpecular = mix(diffuseRayDir, specularRayDir, doSpecular) - val rayDirRefracted = mix(rayDirSpecular, refractionRayDir, doRefraction) - - val nextColor = (throughput2 mulV testResult.emissive) addV color - - val nextThroughput = when(doRefraction === 0.0f): - throughput2 mulV mix[Vec3[Float32]](testResult.albedo, testResult.specularColor, doSpecular) - .otherwise: - throughput2 - - val throughputRayProb = nextThroughput * (1.0f / rayProbabilityCorrected) - - RayTraceState(nextRayPos, rayDirRefracted, nextColor, throughputRayProb, nextRngState3) - .otherwise: - RayTraceState(rayPos, rayDir, color, throughput, rngState, true), - ) - .limit(MaxBounces) - .takeWhile(!_.finished) - .lastOr(initState) - - val rngState = xi * 1973 + yi * 9277 + frame * 26699 | 1 - case class RenderIteration(color: Vec3[Float32], rngState: UInt32) extends GStruct[RenderIteration] - val color = - GSeq - .gen( - first = RenderIteration((0f, 0f, 0f), rngState.unsigned), - next = { case RenderIteration(_, rngState) => - val Random(wiggleX, rngState1) = randomFloat(rngState) - val Random(wiggleY, rngState2) = randomFloat(rngState1) - val x = ((xi.asFloat + wiggleX) / dim.toFloat) * 2f - 1f - val y = ((yi.asFloat + wiggleY) / dim.toFloat) * 2f - 1f - val xy = (x, y) - - val rayPosition = (0f, 0f, 0f) - val cameraDist = 1.0f / tan(fovDeg * 0.6f * math.Pi.toFloat / 180.0f) - val rayTarget = (x, y, cameraDist) - - val rayDir = normalize(rayTarget - rayPosition) - val rtResult = getColorForRay(rayPosition, rayDir, rngState) - val withBg = vclamp(rtResult.color + (SRGBToLinear(bgColor) mulV rtResult.throughput), 0.0f, 20.0f) - RenderIteration(withBg, rtResult.rngState) - }, - ) - .limit(pixelIterationsPerFrame) - .fold((0f, 0f, 0f), { case (acc, RenderIteration(color, _)) => acc + (color * (1.0f / pixelIterationsPerFrame.toFloat)) }) - - when(frame === 0): - (color, 1.0f) - .otherwise: - mix(lastFrame.at(xi, yi), (color, 1.0f), vec4(1.0f / (frame.asFloat + 1f))) - - val initialMem = Array.fill(dim * dim)((0.5f, 0.5f, 0.5f, 0.5f)) - val renders = 100 - val code = function() - List.range(0, renders).foldLeft(initialMem) { case (mem, i) => - UniformContext.withUniform(RaytracingIteration(i)): - val newMem = Vec4FloatMem(mem).map(code).asInstanceOf[Vec4FloatMem].toArray - ImageUtility.renderToImage(newMem, dim, Paths.get(s"generated.png")) - println(s"Finished render $i") - newMem - } diff --git a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/1sample.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/1sample.scala deleted file mode 100644 index 16121da4..00000000 --- a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/1sample.scala +++ /dev/null @@ -1,17 +0,0 @@ -package io.computenode.cyfra.samples.slides - -import io.computenode.cyfra.dsl.{*, given} -import io.computenode.cyfra.core.archive.* -import io.computenode.cyfra.core.archive.mem.FloatMem - -given GContext = new GContext() - -@main -def sample() = - val gpuFunction = GFunction: (value: Float32) => - value * 2f - - val data = FloatMem((1 to 128).map(_.toFloat).toArray) - - val result = data.map(gpuFunction).asInstanceOf[FloatMem].toArray - println(result.mkString(", ")) diff --git a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/2simpleray.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/2simpleray.scala deleted file mode 100644 index 145fe5e6..00000000 --- a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/2simpleray.scala +++ /dev/null @@ -1,44 +0,0 @@ -package io.computenode.cyfra.samples.slides - -import io.computenode.cyfra.dsl.{*, given} -import io.computenode.cyfra.dsl.struct.GStruct -import io.computenode.cyfra.dsl.struct.GStruct.Empty -import io.computenode.cyfra.core.archive.* -import io.computenode.cyfra.core.archive.mem.Vec4FloatMem -import io.computenode.cyfra.utility.ImageUtility - -import java.nio.file.Paths - -@main -def simpleRay() = - val dim = 1024 - val fovDeg = 60 - - case class Sphere(center: Vec3[Float32], radius: Float32, color: Vec3[Float32], emissive: Vec3[Float32]) extends GStruct[Sphere] - - def getColorForRay(rayPos: Vec3[Float32], rayDirection: Vec3[Float32]): Vec4[Float32] = - val sphereCenter = (0f, 0.5f, 3f) - val sphereRadius = 1f - val toRay = rayPos - sphereCenter - val b = toRay dot rayDirection - val c = (toRay dot toRay) - (sphereRadius * sphereRadius) - when((c < 0f || b < 0f) && b * b - c > 0f): - (1f, 1f, 1f, 1f) - .otherwise: - (0f, 0f, 0f, 1f) - - val raytracing: GFunction[Empty, Vec4[Float32], Vec4[Float32]] = GFunction.from2D(dim): - case (_, (xi: Int32, yi: Int32), _) => - val x = (xi.asFloat / dim.toFloat) * 2f - 1f - val y = (yi.asFloat / dim.toFloat) * 2f - 1f - - val rayPosition = (0f, 0f, 0f) - val cameraDist = 1.0f / tan(fovDeg * 0.6f * math.Pi.toFloat / 180.0f) - val rayTarget = (x, y, cameraDist) - - val rayDir = normalize(rayTarget - rayPosition) - getColorForRay(rayPosition, rayDir) - - val mem = Vec4FloatMem(Array.fill(dim * dim)((0f, 0f, 0f, 0f))) - val result = mem.map(raytracing).asInstanceOf[Vec4FloatMem].toArray - ImageUtility.renderToImage(result, dim, Paths.get(s"generated2.png")) diff --git a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/3rays.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/3rays.scala deleted file mode 100644 index 49ff0d46..00000000 --- a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/3rays.scala +++ /dev/null @@ -1,153 +0,0 @@ -package io.computenode.cyfra.samples.slides - -import io.computenode.cyfra.* -import io.computenode.cyfra.dsl.collections.GSeq -import io.computenode.cyfra.dsl.{*, given} -import io.computenode.cyfra.dsl.struct.GStruct -import io.computenode.cyfra.dsl.struct.GStruct.Empty -import io.computenode.cyfra.core.archive.* -import io.computenode.cyfra.core.archive.mem.Vec4FloatMem -import io.computenode.cyfra.utility.ImageUtility - -import java.nio.file.Paths - -@main -def rays() = - val raysPerPixel = 10 - val dim = 1024 - val fovDeg = 60 - val minRayHitTime = 0.01f - val superFar = 999f - val maxBounces = 10 - val rayPosNudge = 0.001f - - def scalarTriple(u: Vec3[Float32], v: Vec3[Float32], w: Vec3[Float32]): Float32 = (u cross v) dot w - - case class Sphere(center: Vec3[Float32], radius: Float32, color: Vec3[Float32], emissive: Vec3[Float32]) extends GStruct[Sphere] - - case class Quad(a: Vec3[Float32], b: Vec3[Float32], c: Vec3[Float32], d: Vec3[Float32], color: Vec3[Float32], emissive: Vec3[Float32]) - extends GStruct[Quad] - - case class RayHitInfo(dist: Float32, normal: Vec3[Float32], albedo: Vec3[Float32], emissive: Vec3[Float32]) extends GStruct[RayHitInfo] - - case class RayTraceState(rayPos: Vec3[Float32], rayDir: Vec3[Float32], color: Vec3[Float32], throughput: Vec3[Float32], finished: GBoolean = false) - extends GStruct[RayTraceState] - - def testSphereTrace(rayPos: Vec3[Float32], rayDir: Vec3[Float32], currentHit: RayHitInfo, sphere: Sphere): RayHitInfo = - val toRay = rayPos - sphere.center - val b = toRay dot rayDir - val c = (toRay dot toRay) - (sphere.radius * sphere.radius) - val notHit = currentHit - when(c > 0f && b > 0f): - notHit - .otherwise: - val discr = b * b - c - when(discr > 0f): - val initDist = -b - sqrt(discr) - val fromInside = initDist < 0f - val dist = when(fromInside)(-b + sqrt(discr)).otherwise(initDist) - when(dist > minRayHitTime && dist < currentHit.dist): - val normal = normalize(rayPos + rayDir * dist - sphere.center) - RayHitInfo(dist, normal, sphere.color, sphere.emissive) - .otherwise: - notHit - .otherwise: - notHit - - def testQuadTrace(rayPos: Vec3[Float32], rayDir: Vec3[Float32], currentHit: RayHitInfo, quad: Quad): RayHitInfo = - val normal = normalize((quad.c - quad.a) cross (quad.c - quad.b)) - val fixedQuad = when((normal dot rayDir) > 0f): - Quad(quad.d, quad.c, quad.b, quad.a, quad.color, quad.emissive) - .otherwise: - quad - val fixedNormal = when((normal dot rayDir) > 0f)(-normal).otherwise(normal) - val p = rayPos - val q = rayPos + rayDir - val pq = q - p - val pa = fixedQuad.a - p - val pb = fixedQuad.b - p - val pc = fixedQuad.c - p - val m = pc cross pq - val v = pa dot m - - def checkHit(intersectPoint: Vec3[Float32]): RayHitInfo = - val dist = when(abs(rayDir.x) > 0.1f): - (intersectPoint.x - rayPos.x) / rayDir.x - .elseWhen(abs(rayDir.y) > 0.1f): - (intersectPoint.y - rayPos.y) / rayDir.y - .otherwise: - (intersectPoint.z - rayPos.z) / rayDir.z - - when(dist > minRayHitTime && dist < currentHit.dist): - RayHitInfo(dist, fixedNormal, quad.color, quad.emissive) - .otherwise: - currentHit - - when(v >= 0f): - val u = -(pb dot m) - val w = scalarTriple(pq, pb, pa) - when(u >= 0f && w >= 0f): - val denom = 1f / (u + v + w) - val uu = u * denom - val vv = v * denom - val ww = w * denom - val intersectPos = fixedQuad.a * uu + fixedQuad.b * vv + fixedQuad.c * ww - checkHit(intersectPos) - .otherwise: - currentHit - .otherwise: - val pd = fixedQuad.d - p - val u = pd dot m - val w = scalarTriple(pq, pa, pd) - when(u >= 0f && w >= 0f): - val negV = -v - val denom = 1f / (u + negV + w) - val uu = u * denom - val vv = negV * denom - val ww = w * denom - val intersectPos = fixedQuad.a * uu + fixedQuad.d * vv + fixedQuad.c * ww - checkHit(intersectPos) - .otherwise: - currentHit - - val sphere = Sphere(center = (1.5f, 1.5f, 4f), radius = 0.5f, color = (1f, 1f, 1f), emissive = (3f, 3f, 3f)) - - val backWall = Quad(a = (-2f, -2f, 5f), b = (2f, -2f, 5f), c = (2f, 2f, 5f), d = (-2f, 2f, 5f), color = (0f, 1f, 1f), emissive = (0f, 0f, 0f)) - - def getColorForRay(rayPos: Vec3[Float32], rayDirection: Vec3[Float32]): Vec4[Float32] = - GSeq - .gen[RayTraceState]( - first = RayTraceState(rayPos = rayPos, rayDir = rayDirection, color = (0f, 0f, 0f), throughput = (1f, 1f, 1f)), - next = { case state @ RayTraceState(rayPos, rayDir, color, throughput, _) => - val noHit = RayHitInfo(1000f, (0f, 0f, 0f), (0f, 0f, 0f), (0f, 0f, 0f)) - val sphereHit = testSphereTrace(rayPos, rayDir, noHit, sphere) - val wallHit = testQuadTrace(rayPos, rayDir, sphereHit, backWall) - RayTraceState( - rayPos = rayPos + rayDir * wallHit.dist + wallHit.normal * rayPosNudge, - rayDir = reflect(rayDir, wallHit.normal), - color = color + wallHit.emissive mulV throughput, - throughput = throughput mulV wallHit.albedo, - finished = wallHit.dist > superFar, - ) - }, - ) - .limit(maxBounces) - .takeWhile(!_.finished) - .map(state => (state.color, 1f)) - .lastOr((0f, 0f, 0f, 1f)) - - val raytracing: GFunction[Empty, Vec4[Float32], Vec4[Float32]] = GFunction.from2D(dim): - case (_, (xi: Int32, yi: Int32), _) => - val x = (xi.asFloat / dim.toFloat) * 2f - 1f - val y = (yi.asFloat / dim.toFloat) * 2f - 1f - - val rayPosition = (0f, 0f, 0f) - val cameraDist = 1.0f / tan(fovDeg * 0.6f * math.Pi.toFloat / 180.0f) - val rayTarget = (x, y, cameraDist) - - val rayDir = normalize(rayTarget - rayPosition) - getColorForRay(rayPosition, rayDir) - - val mem = Vec4FloatMem(Array.fill(dim * dim)((0f, 0f, 0f, 0f))) - val result = mem.map(raytracing).asInstanceOf[Vec4FloatMem].toArray - ImageUtility.renderToImage(result, dim, Paths.get(s"generated3.png")) diff --git a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/4random.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/4random.scala index bac16f3b..8d3488a7 100644 --- a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/4random.scala +++ b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/slides/4random.scala @@ -1,11 +1,12 @@ package io.computenode.cyfra.samples.slides +import io.computenode.cyfra.core.CyfraRuntime import io.computenode.cyfra.dsl.collections.GSeq import io.computenode.cyfra.dsl.{*, given} import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.struct.GStruct.Empty import io.computenode.cyfra.core.archive.* -import io.computenode.cyfra.core.archive.mem.Vec4FloatMem +import io.computenode.cyfra.runtime.VkCyfraRuntime import io.computenode.cyfra.utility.ImageUtility import java.nio.file.Paths @@ -36,6 +37,9 @@ def randomVector(seed: UInt32): Random[Vec3[Float32]] = @main def randomRays() = + + given CyfraRuntime = VkCyfraRuntime() + val raysPerPixel = 10 val dim = 1024 val fovDeg = 80 @@ -202,6 +206,6 @@ def randomRays() = .fold((0f, 0f, 0f), { case (acc, RenderIteration(color, _)) => acc + (color * (1.0f / pixelIterationsPerFrame.toFloat)) }) (color, 1f) - val mem = Vec4FloatMem(Array.fill(dim * dim)((0f, 0f, 0f, 0f))) - val result = mem.map(raytracing).asInstanceOf[Vec4FloatMem].toArray + val mem = Array.fill(dim * dim)((0f, 0f, 0f, 0f)) + val result: Array[fRGBA] = raytracing.run(mem) ImageUtility.renderToImage(result, dim, Paths.get(s"generated4.png")) diff --git a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/animation/AnimatedFunctionRenderer.scala b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/animation/AnimatedFunctionRenderer.scala index 1e18cd28..49a5feed 100644 --- a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/animation/AnimatedFunctionRenderer.scala +++ b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/animation/AnimatedFunctionRenderer.scala @@ -1,14 +1,14 @@ package io.computenode.cyfra.foton.animation import io.computenode.cyfra +import io.computenode.cyfra.core.CyfraRuntime import io.computenode.cyfra.dsl.Value.* import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} import io.computenode.cyfra.foton.animation.AnimatedFunctionRenderer.{AnimationIteration, RenderFn} import io.computenode.cyfra.foton.animation.AnimationFunctions.AnimationInstant -import io.computenode.cyfra.core.archive.mem.GMem.fRGBA -import io.computenode.cyfra.core.archive.mem.Vec4FloatMem -import io.computenode.cyfra.core.archive.{GContext, GFunction, UniformContext} +import io.computenode.cyfra.core.archive.GFunction +import io.computenode.cyfra.runtime.VkCyfraRuntime import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext.Implicits @@ -16,15 +16,13 @@ import scala.concurrent.ExecutionContext.Implicits class AnimatedFunctionRenderer(params: AnimatedFunctionRenderer.Parameters) extends AnimationRenderer[AnimatedFunction, AnimatedFunctionRenderer.RenderFn](params): - given GContext = new GContext() + given CyfraRuntime = new VkCyfraRuntime() given ExecutionContext = Implicits.global override protected def renderFrame(scene: AnimatedFunction, time: Float32, fn: RenderFn): Array[fRGBA] = val mem = Array.fill(params.width * params.height)((0.5f, 0.5f, 0.5f, 0.5f)) - UniformContext.withUniform(AnimationIteration(time)): - val fmem = Vec4FloatMem(mem) - fmem.map(fn).asInstanceOf[Vec4FloatMem].toArray + fn.run(mem, AnimationIteration(time)) override protected def renderFunction(scene: AnimatedFunction): RenderFn = GFunction.from2D(params.width): diff --git a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/animation/AnimationRenderer.scala b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/animation/AnimationRenderer.scala index 9a262e95..015be533 100644 --- a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/animation/AnimationRenderer.scala +++ b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/animation/AnimationRenderer.scala @@ -4,7 +4,6 @@ import io.computenode.cyfra import io.computenode.cyfra.dsl.Value.* import io.computenode.cyfra.dsl.{*, given} import io.computenode.cyfra.core.archive.GFunction -import io.computenode.cyfra.core.archive.mem.GMem.fRGBA import io.computenode.cyfra.utility.ImageUtility import io.computenode.cyfra.utility.Units.Milliseconds import io.computenode.cyfra.utility.Utility.timed diff --git a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/ImageRtRenderer.scala b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/ImageRtRenderer.scala index 8f4d2b70..3ad661dc 100644 --- a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/ImageRtRenderer.scala +++ b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/ImageRtRenderer.scala @@ -2,13 +2,13 @@ package io.computenode.cyfra.foton.rt import io.computenode.cyfra import io.computenode.cyfra.* +import io.computenode.cyfra.core.CyfraRuntime import io.computenode.cyfra.dsl.Value.* import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} import io.computenode.cyfra.foton.rt.ImageRtRenderer.RaytracingIteration -import io.computenode.cyfra.core.archive.mem.GMem.fRGBA -import io.computenode.cyfra.core.archive.mem.Vec4FloatMem -import io.computenode.cyfra.core.archive.{GFunction, UniformContext} +import io.computenode.cyfra.core.archive.GFunction +import io.computenode.cyfra.runtime.VkCyfraRuntime import io.computenode.cyfra.utility.ImageUtility import io.computenode.cyfra.utility.Utility.timed @@ -16,6 +16,8 @@ import java.nio.file.Path class ImageRtRenderer(params: ImageRtRenderer.Parameters) extends RtRenderer(params): + given CyfraRuntime = VkCyfraRuntime() + def renderToFile(scene: Scene, destinationPath: Path): Unit = val images = render(scene) for image <- images do ImageUtility.renderToImage(image, params.width, params.height, destinationPath) @@ -26,12 +28,11 @@ class ImageRtRenderer(params: ImageRtRenderer.Parameters) extends RtRenderer(par private def render(scene: Scene, fn: GFunction[RaytracingIteration, Vec4[Float32], Vec4[Float32]]): LazyList[Array[fRGBA]] = val initialMem = Array.fill(params.width * params.height)((0.5f, 0.5f, 0.5f, 0.5f)) LazyList - .iterate((initialMem, 0), params.iterations + 1) { case (mem, render) => - UniformContext.withUniform(RaytracingIteration(render)): - val fmem = Vec4FloatMem(mem) - val result = timed(s"Rendered iteration $render")(fmem.map(fn).asInstanceOf[Vec4FloatMem].toArray) + .iterate((initialMem, 0), params.iterations + 1): + case (mem, render) => + val result: Array[fRGBA] = timed(s"Render iteration $render"): + fn.run(mem, RaytracingIteration(render)) (result, render + 1) - } .drop(1) .map(_._1) diff --git a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/RtRenderer.scala b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/RtRenderer.scala index b59765b1..de38af62 100644 --- a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/RtRenderer.scala +++ b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/RtRenderer.scala @@ -10,15 +10,12 @@ import io.computenode.cyfra.dsl.library.Random import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} import io.computenode.cyfra.foton.rt.RtRenderer.RayHitInfo -import io.computenode.cyfra.core.archive.GContext import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext.Implicits class RtRenderer(params: RtRenderer.Parameters): - given GContext = new GContext() - given ExecutionContext = Implicits.global private case class RayTraceState( diff --git a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/animation/AnimationRtRenderer.scala b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/animation/AnimationRtRenderer.scala index c339bec9..19ee393b 100644 --- a/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/animation/AnimationRtRenderer.scala +++ b/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/animation/AnimationRtRenderer.scala @@ -1,29 +1,29 @@ package io.computenode.cyfra.foton.rt.animation import io.computenode.cyfra +import io.computenode.cyfra.core.CyfraRuntime import io.computenode.cyfra.dsl.Value.* import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} import io.computenode.cyfra.foton.animation.AnimationRenderer import io.computenode.cyfra.foton.rt.RtRenderer import io.computenode.cyfra.foton.rt.animation.AnimationRtRenderer.RaytracingIteration -import io.computenode.cyfra.core.archive.mem.GMem.fRGBA -import io.computenode.cyfra.core.archive.mem.Vec4FloatMem -import io.computenode.cyfra.core.archive.{GFunction, UniformContext} +import io.computenode.cyfra.core.archive.GFunction +import io.computenode.cyfra.runtime.VkCyfraRuntime class AnimationRtRenderer(params: AnimationRtRenderer.Parameters) extends RtRenderer(params) with AnimationRenderer[AnimatedScene, AnimationRtRenderer.RenderFn](params): + given CyfraRuntime = VkCyfraRuntime() + protected def renderFrame(scene: AnimatedScene, time: Float32, fn: GFunction[RaytracingIteration, Vec4[Float32], Vec4[Float32]]): Array[fRGBA] = val initialMem = Array.fill(params.width * params.height)((0.5f, 0.5f, 0.5f, 0.5f)) List - .iterate((initialMem, 0), params.iterations + 1) { case (mem, render) => - UniformContext.withUniform(RaytracingIteration(render, time)): - val fmem = Vec4FloatMem(mem) - val result = fmem.map(fn).asInstanceOf[Vec4FloatMem].toArray + .iterate((initialMem, 0), params.iterations + 1): + case (mem, render) => + val result: Array[fRGBA] = fn.run(mem, RaytracingIteration(render, time)) (result, render + 1) - } .map(_._1) .last diff --git a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala new file mode 100644 index 00000000..0aef22d3 --- /dev/null +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -0,0 +1,222 @@ +package io.computenode.cyfra.fs2interop + +import io.computenode.cyfra.core.{Allocation, layout, GCodec} +import layout.Layout +import io.computenode.cyfra.core.{CyfraRuntime, GBufferRegion, GExecution, GProgram} +import io.computenode.cyfra.dsl.{*, given} +import io.computenode.cyfra.core.layout.LayoutBinding +import io.computenode.cyfra.core.layout.LayoutStruct +import gio.GIO +import binding.{GBinding, GBuffer, GUniform} +import io.computenode.cyfra.spirv.SpirvTypes.typeStride +import struct.GStruct +import GStruct.Empty +import Empty.given +import fs2.* + +import java.nio.ByteBuffer +import org.lwjgl.BufferUtils +import izumi.reflect.Tag + +import scala.reflect.ClassTag + +object GPipe: + def map[F[_], C1 <: Value: {FromExpr, Tag}, C2 <: Value: {FromExpr, Tag}, S1: ClassTag, S2: ClassTag]( + f: C1 => C2, + )(using cr: CyfraRuntime, bridge1: GCodec[C1, S1], bridge2: GCodec[C2, S2]): Pipe[F, S1, S2] = + (stream: Stream[F, S1]) => + case class Params(inSize: Int) + case class PLayout(in: GBuffer[C1], out: GBuffer[C2]) extends Layout + + val params = Params(inSize = 256) + val inTypeSize = typeStride(Tag.apply[C1]) + val outTypeSize = typeStride(Tag.apply[C2]) + + val gProg = GProgram[Params, PLayout]( + layout = params => PLayout(in = GBuffer[C1](params.inSize), out = GBuffer[C2](params.inSize)), + dispatch = (layout, params) => GProgram.StaticDispatch((Math.ceil(params.inSize / 256f).toInt, 1, 1)), + ) { layout => + val invocId = GIO.invocationId + val element = GIO.read[C1](layout.in, invocId) + val res = f(element) + for _ <- GIO.write[C2](layout.out, invocId, res) + yield Empty() + } + + val execution = GExecution[Params, PLayout]() + .addProgram(gProg)(params => Params(params.inSize), layout => PLayout(layout.in, layout.out)) + + val region = GBufferRegion + .allocate[PLayout] + .map: pLayout => + execution.execute(params, pLayout) + + // these are allocated once, reused for many chunks + val inBuf = BufferUtils.createByteBuffer(params.inSize * inTypeSize) + val outBuf = BufferUtils.createByteBuffer(params.inSize * outTypeSize) + + stream + .chunkN(params.inSize) + .flatMap: chunk => + bridge1.toByteBuffer(inBuf, chunk.toArray) + region.runUnsafe(init = PLayout(in = GBuffer[C1](inBuf), out = GBuffer[C2](outBuf)), onDone = layout => layout.out.read(outBuf)) + Stream.emits(bridge2.fromByteBuffer(outBuf, new Array[S2](params.inSize))) + + // Overload for convenient single type version + def map[F[_], C <: Value: FromExpr: Tag, S: ClassTag](f: C => C)(using CyfraRuntime, GCodec[C, S]): Pipe[F, S, S] = + map[F, C, C, S, S](f) + + def filter[F[_], C <: Value: FromExpr: Tag, S: ClassTag](pred: C => GBoolean)(using cr: CyfraRuntime, bridge: GCodec[C, S]): Pipe[F, S, S] = + (stream: Stream[F, S]) => + val chunkInSize = 256 + + // Predicate mapping + case class PredParams(inSize: Int) + case class PredLayout(in: GBuffer[C], out: GBuffer[Int32]) extends Layout + + val predicateProgram = GProgram[PredParams, PredLayout]( + layout = params => PredLayout(in = GBuffer[C](params.inSize), out = GBuffer[Int32](params.inSize)), + dispatch = (layout, params) => GProgram.StaticDispatch((Math.ceil(params.inSize.toFloat / 256).toInt, 1, 1)), + ): layout => + val invocId = GIO.invocationId + val element = GIO.read[C](layout.in, invocId) + val result = when(pred(element))(1: Int32).otherwise(0) + for _ <- GIO.write[Int32](layout.out, invocId, result) + yield Empty() + + // Prefix sum (inclusive), upsweep/downsweep + case class ScanParams(inSize: Int, intervalSize: Int) + case class ScanArgs(intervalSize: Int32) extends GStruct[ScanArgs] + case class ScanLayout(ints: GBuffer[Int32]) extends Layout + case class ScanProgramLayout(ints: GBuffer[Int32], intervalSize: GUniform[ScanArgs] = GUniform.fromParams) extends Layout + + val upsweep = GProgram[ScanParams, ScanProgramLayout]( + layout = params => ScanProgramLayout(ints = GBuffer[Int32](params.inSize), intervalSize = GUniform(ScanArgs(params.intervalSize))), + dispatch = (layout, params) => GProgram.StaticDispatch((Math.ceil(params.inSize.toFloat / params.intervalSize / 256).toInt, 1, 1)), + ): layout => + val ScanArgs(size) = layout.intervalSize.read + GIO.when(GIO.invocationId < ((chunkInSize: Int32) / size)): + val invocId = GIO.invocationId + val root = invocId * size + val mid = root + (size / 2) - 1 + val end = root + size - 1 + val oldValue = GIO.read[Int32](layout.ints, end) + val addValue = GIO.read[Int32](layout.ints, mid) + val newValue = oldValue + addValue + for _ <- GIO.write[Int32](layout.ints, end, newValue) + yield Empty() + + val downsweep = GProgram[ScanParams, ScanProgramLayout]( + layout = params => ScanProgramLayout(ints = GBuffer[Int32](params.inSize), intervalSize = GUniform(ScanArgs(params.intervalSize))), + dispatch = (layout, params) => GProgram.StaticDispatch((Math.ceil(params.inSize.toFloat / params.intervalSize / 256).toInt, 1, 1)), + ): layout => + val ScanArgs(size) = layout.intervalSize.read + GIO.when(GIO.invocationId < ((chunkInSize: Int32) / size)): + val invocId = GIO.invocationId + val end = invocId * size - 1 // if invocId = 0, this is -1 (out of bounds) + val mid = end + (size / 2) + val oldValue = GIO.read[Int32](layout.ints, mid) + val addValue = when(end > 0)(GIO.read[Int32](layout.ints, end)).otherwise(0) + val newValue = oldValue + addValue + for _ <- GIO.write[Int32](layout.ints, mid, newValue) + yield Empty() + + // Stitch together many upsweep / downsweep program phases recursively + @annotation.tailrec + def upsweepPhases( + exec: GExecution[ScanParams, ScanLayout, ScanLayout], + inSize: Int, + intervalSize: Int, + ): GExecution[ScanParams, ScanLayout, ScanLayout] = + if intervalSize > inSize then exec + else + val newExec = exec.addProgram(upsweep)(params => ScanParams(inSize, intervalSize), layout => ScanProgramLayout(layout.ints)) + upsweepPhases(newExec, inSize, intervalSize * 2) + + @annotation.tailrec + def downsweepPhases( + exec: GExecution[ScanParams, ScanLayout, ScanLayout], + inSize: Int, + intervalSize: Int, + ): GExecution[ScanParams, ScanLayout, ScanLayout] = + if intervalSize < 2 then exec + else + val newExec = exec.addProgram(downsweep)(params => ScanParams(inSize, intervalSize), layout => ScanProgramLayout(layout.ints)) + downsweepPhases(newExec, inSize, intervalSize / 2) + + val initExec = GExecution[ScanParams, ScanLayout]() // no program + val upsweepExec = upsweepPhases(initExec, 256, 2) // add all upsweep phases + val scanExec = downsweepPhases(upsweepExec, 256, 128) // add all downsweep phases + + // Stream compaction + case class CompactParams(inSize: Int) + case class CompactLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C]) extends Layout + + val compactProgram = GProgram[CompactParams, CompactLayout]( + layout = params => CompactLayout(in = GBuffer[C](params.inSize), scan = GBuffer[Int32](params.inSize), out = GBuffer[C](params.inSize)), + dispatch = (layout, params) => GProgram.StaticDispatch((Math.ceil(params.inSize.toFloat / 256).toInt, 1, 1)), + ): layout => + val invocId = GIO.invocationId + val element = GIO.read[C](layout.in, invocId) + val prefixSum = GIO.read[Int32](layout.scan, invocId) + for + _ <- GIO.when(invocId > 0): + val prevScan = GIO.read[Int32](layout.scan, invocId - 1) + GIO.when(prevScan < prefixSum): + GIO.write(layout.out, prevScan, element) + _ <- GIO.when(invocId === 0): + GIO.when(prefixSum > 0): + GIO.write(layout.out, invocId, element) + yield Empty() + + // connect all the layouts/executions into one + case class FilterParams(inSize: Int, intervalSize: Int) + case class FilterLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C]) extends Layout + + val filterExec = GExecution[FilterParams, FilterLayout]() + .addProgram(predicateProgram)( + filterParams => PredParams(filterParams.inSize), + filterLayout => PredLayout(in = filterLayout.in, out = filterLayout.scan), + ) + .flatMap[FilterLayout, FilterParams]: filterLayout => + scanExec + .contramap[FilterLayout]: filterLayout => + ScanLayout(filterLayout.scan) + .contramapParams[FilterParams](filterParams => ScanParams(filterParams.inSize, filterParams.intervalSize)) + .map(scanLayout => filterLayout) + .flatMap[FilterLayout, FilterParams]: filterLayout => + compactProgram + .contramap[FilterLayout]: filterLayout => + CompactLayout(filterLayout.in, filterLayout.scan, filterLayout.out) + .contramapParams[FilterParams](filterParams => CompactParams(filterParams.inSize)) + .map(compactLayout => filterLayout) + + // finally setup buffers, region, parameters, and run the program + val filterParams = FilterParams(chunkInSize, 2) + val region = GBufferRegion + .allocate[FilterLayout] + .map: filterLayout => + filterExec.execute(filterParams, filterLayout) + + val typeSize = typeStride(Tag.apply[C]) + val intSize = typeStride(Tag.apply[Int32]) + + // these are allocated once, reused for many chunks + val predBuf = BufferUtils.createByteBuffer(filterParams.inSize * typeSize) + val filteredCount = BufferUtils.createByteBuffer(intSize) + val compactBuf = BufferUtils.createByteBuffer(filterParams.inSize * typeSize) + + stream + .chunkN(chunkInSize) + .flatMap: chunk => + bridge.toByteBuffer(predBuf, chunk.toArray) + region.runUnsafe( + init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](filterParams.inSize), out = GBuffer[C](filterParams.inSize)), + onDone = layout => { + layout.scan.read(filteredCount, (filterParams.inSize - 1) * intSize) + layout.out.read(compactBuf) + }, + ) + val filteredN = filteredCount.getInt(0) + val arr = bridge.fromByteBuffer(compactBuf, new Array[S](filteredN)) + Stream.emits(arr) diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/ExecutionHandler.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/ExecutionHandler.scala index 9b9b385d..7f2c6cff 100644 --- a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/ExecutionHandler.scala +++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/ExecutionHandler.scala @@ -8,6 +8,7 @@ import io.computenode.cyfra.core.layout.{Layout, LayoutBinding, LayoutStruct} import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Value.FromExpr import io.computenode.cyfra.dsl.binding.{GBinding, GBuffer, GUniform} +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} import io.computenode.cyfra.runtime.ExecutionHandler.{ BindingLogicError, Dispatch, @@ -95,7 +96,6 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte val e = ExecutionBinding(x)(using x.fromExpr, x.tag) bindingsAcc.put(e, mutable.Buffer(x)) e - mapper.fromBindings(res) // noinspection TypeParameterShadow @@ -187,7 +187,7 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte case _: GUniform.ParamUniform[?] => false case x => throw BindingLogicError(x, "Unsupported binding type") if allocations.size > 1 then throw BindingLogicError(allocations, "Multiple allocations for uniform") - allocations.headOption.getOrElse(throw new IllegalStateException("Uniform never allocated")) + allocations.headOption.getOrElse(throw new BindingLogicError(Seq(), "Uniform never allocated")) case x => throw new IllegalArgumentException(s"Binding of type ${x.getClass.getName} should not be here") private def recordCommandBuffer(steps: Seq[ExecutionStep]): VkCommandBuffer = pushStack: stack => @@ -250,12 +250,15 @@ object ExecutionHandler: sealed trait ExecutionBinding[T <: Value: {FromExpr, Tag}] object ExecutionBinding: - class UniformBinding[T <: Value: {FromExpr, Tag}] extends ExecutionBinding[T] with GUniform[T] + class UniformBinding[T <: GStruct[?]: {FromExpr, Tag, GStructSchema}] extends ExecutionBinding[T] with GUniform[T] class BufferBinding[T <: Value: {FromExpr, Tag}] extends ExecutionBinding[T] with GBuffer[T] - def apply[T <: Value: {FromExpr, Tag}](binding: GBinding[T]): ExecutionBinding[T] & GBinding[T] = binding match - case _: GUniform[T] => new UniformBinding() - case _: GBuffer[T] => new BufferBinding() + def apply[T <: Value: {FromExpr as fe, Tag as t}](binding: GBinding[T]): ExecutionBinding[T] & GBinding[T] = binding match + // todo types are a mess here + case u: GUniform[GStruct[?]] => + new UniformBinding[GStruct[?]](using fe.asInstanceOf[FromExpr[GStruct[?]]], t.asInstanceOf[Tag[GStruct[?]]], u.schema.asInstanceOf) + .asInstanceOf[UniformBinding[T]] + case _: GBuffer[T] => new BufferBinding() case class BindingLogicError(bindings: Seq[GBinding[?]], message: String) extends RuntimeException(s"Error in binding logic for $bindings: $message") object BindingLogicError: diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkAllocation.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkAllocation.scala index ea80f7c6..6f1dd91a 100644 --- a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkAllocation.scala +++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkAllocation.scala @@ -7,7 +7,7 @@ import io.computenode.cyfra.dsl.Expression.ConstInt32 import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Value.FromExpr import io.computenode.cyfra.dsl.binding.{GBinding, GBuffer, GUniform} -import io.computenode.cyfra.dsl.struct.GStruct +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} import io.computenode.cyfra.runtime.VkAllocation.getUnderlying import io.computenode.cyfra.spirv.SpirvTypes.typeStride import io.computenode.cyfra.vulkan.command.CommandPool @@ -74,27 +74,27 @@ class VkAllocation(commandPool: CommandPool, executionHandler: ExecutionHandler) def apply[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GBuffer[T] = val sizeOfT = typeStride(summon[Tag[T]]) - val length = buff.remaining() / sizeOfT - if buff.remaining() % sizeOfT != 0 then ??? + val length = buff.capacity() / sizeOfT + if buff.capacity() % sizeOfT != 0 then + throw new IllegalArgumentException(s"ByteBuffer size ${buff.capacity()} is not a multiple of element size $sizeOfT") GBuffer[T](length).tap(_.write(buff)) - extension (buffers: GUniform.type) - def apply[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GUniform[T] = + extension (uniforms: GUniform.type) + def apply[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}](buff: ByteBuffer): GUniform[T] = GUniform[T]().tap(_.write(buff)) - def apply[T <: Value: {Tag, FromExpr}](): GUniform[T] = + def apply[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}](): GUniform[T] = VkUniform[T]().tap(bindings += _) extension [Params, EL <: Layout: LayoutBinding, RL <: Layout: LayoutBinding](execution: GExecution[Params, EL, RL]) def execute(params: Params, layout: EL): RL = executionHandler.handle(execution, params, layout) - private def direct[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GUniform[T] = + private def direct[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}](buff: ByteBuffer): GUniform[T] = GUniform[T](buff) - def getInitProgramLayout: GProgram.InitProgramLayout = new GProgram.InitProgramLayout: extension (uniforms: GUniform.type) - def apply[T <: GStruct[T]: {Tag, FromExpr}](value: T): GUniform[T] = pushStack: stack => + def apply[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}](value: T): GUniform[T] = pushStack: stack => val bb = value.productElement(0) match case Int32(tree: ConstInt32) => MemoryUtil.memByteBuffer(stack.ints(tree.value)) case _ => ??? diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBinding.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBinding.scala index 6283ad78..00c2d280 100644 --- a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBinding.scala +++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBinding.scala @@ -17,6 +17,7 @@ import org.lwjgl.vulkan.VK10.{VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_BUFFER_USAG import io.computenode.cyfra.dsl.Value import io.computenode.cyfra.dsl.Value.FromExpr import io.computenode.cyfra.dsl.binding.GUniform +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} import io.computenode.cyfra.vulkan.memory.{Allocator, Buffer} import izumi.reflect.Tag import org.lwjgl.vulkan.VK10 @@ -60,13 +61,13 @@ object VkBuffer: val buffer = new Buffer.DeviceBuffer(size, UsageFlags) new VkBuffer[T](length, buffer) -class VkUniform[T <: Value: {Tag, FromExpr}] private (underlying: Buffer) extends VkBinding[T](underlying) with GUniform[T] +class VkUniform[T <: GStruct[_]: {Tag, FromExpr, GStructSchema}] private (underlying: Buffer) extends VkBinding[T](underlying) with GUniform[T] object VkUniform: private final val UsageFlags = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT - def apply[T <: Value: {Tag, FromExpr}]()(using Allocator): VkUniform[T] = + def apply[T <: GStruct[_]: {Tag, FromExpr, GStructSchema}]()(using Allocator): VkUniform[T] = val sizeOfT = typeStride(summon[Tag[T]]) val buffer = new Buffer.DeviceBuffer(sizeOfT, UsageFlags) new VkUniform[T](buffer) diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala index ccd6585f..2e96e221 100644 --- a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala +++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala @@ -1,19 +1,43 @@ package io.computenode.cyfra.runtime +import io.computenode.cyfra.core.GProgram.InitProgramLayout import io.computenode.cyfra.core.layout.{Layout, LayoutBinding, LayoutStruct} -import io.computenode.cyfra.core.{Allocation, CyfraRuntime, GExecution, GProgram, SpirvProgram} +import io.computenode.cyfra.core.{Allocation, CyfraRuntime, GExecution, GProgram, GioProgram, SpirvProgram} +import io.computenode.cyfra.spirv.compilers.DSLCompiler +import io.computenode.cyfra.spirvtools.SpirvToolsRunner import io.computenode.cyfra.vulkan.VulkanContext import io.computenode.cyfra.vulkan.compute.ComputePipeline +import java.security.MessageDigest import scala.collection.mutable -class VkCyfraRuntime extends CyfraRuntime: +class VkCyfraRuntime(spirvToolsRunner: SpirvToolsRunner = SpirvToolsRunner()) extends CyfraRuntime: private val context = new VulkanContext() import context.given - private val shaderCache = mutable.Map.empty[String, VkShader[?]] - private[cyfra] def getOrLoadProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}](program: GProgram[Params, L]): VkShader[L] = - shaderCache.getOrElseUpdate(program.cacheKey, VkShader(program)).asInstanceOf[VkShader[L]] + private val gProgramCache = mutable.Map[GProgram[?, ?], SpirvProgram[?, ?]]() + private val shaderCache = mutable.Map[(Long, Long), VkShader[?]]() + + private[cyfra] def getOrLoadProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}](program: GProgram[Params, L]): VkShader[L] = synchronized: + + val spirvProgram: SpirvProgram[Params, L] = program match + case p: GioProgram[Params, L] if gProgramCache.contains(p) => + gProgramCache(p).asInstanceOf[SpirvProgram[Params, L]] + case p: GioProgram[Params, L] => compile(p) + case p: SpirvProgram[Params, L] => p + case _ => throw new IllegalArgumentException(s"Unsupported program type: ${program.getClass.getName}") + + gProgramCache.update(program, spirvProgram) + shaderCache.getOrElseUpdate(spirvProgram.shaderHash, VkShader(spirvProgram)).asInstanceOf[VkShader[L]] + + private def compile[Params, L <: Layout: {LayoutBinding as lbinding, LayoutStruct as lstruct}]( + program: GioProgram[Params, L], + ): SpirvProgram[Params, L] = + val GioProgram(_, layout, dispatch, _) = program + val bindings = lbinding.toBindings(lstruct.layoutRef).toList + val compiled = DSLCompiler.compile(program.body(summon[LayoutStruct[L]].layoutRef), bindings) + val optimizedShaderCode = spirvToolsRunner.processShaderCodeWithSpirvTools(compiled) + SpirvProgram((il: InitProgramLayout) ?=> layout(il), dispatch, optimizedShaderCode) override def withAllocation(f: Allocation => Unit): Unit = context.withThreadContext: threadContext => diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkShader.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkShader.scala index f0885cb9..492266e9 100644 --- a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkShader.scala +++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkShader.scala @@ -5,6 +5,7 @@ import io.computenode.cyfra.core.SpirvProgram.* import io.computenode.cyfra.core.GProgram.InitProgramLayout import io.computenode.cyfra.core.layout.{Layout, LayoutBinding, LayoutStruct} import io.computenode.cyfra.dsl.binding.{GBuffer, GUniform} +import io.computenode.cyfra.spirv.compilers.DSLCompiler import io.computenode.cyfra.vulkan.compute.ComputePipeline import io.computenode.cyfra.vulkan.compute.ComputePipeline.* import io.computenode.cyfra.vulkan.core.Device @@ -15,11 +16,8 @@ import scala.util.{Failure, Success} case class VkShader[L](underlying: ComputePipeline, shaderBindings: L => ShaderLayout) object VkShader: - def apply[P, L <: Layout: {LayoutBinding, LayoutStruct}](program: GProgram[P, L])(using Device): VkShader[L] = - val SpirvProgram(layout, dispatch, _workgroupSize, code, entryPoint, shaderBindings, _) = program match - case p: GioProgram[?, ?] => compile(p) - case p: SpirvProgram[?, ?] => p - case _ => throw new IllegalArgumentException(s"Unsupported program type: ${program.getClass.getName}") + def apply[P, L <: Layout: {LayoutBinding, LayoutStruct}](program: SpirvProgram[P, L])(using Device): VkShader[L] = + val SpirvProgram(layout, dispatch, _workgroupSize, code, entryPoint, shaderBindings) = program val shaderLayout = shaderBindings(summon[LayoutStruct[L]].layoutRef) val sets = shaderLayout.map: set => @@ -33,10 +31,3 @@ object VkShader: val pipeline = ComputePipeline(code, entryPoint, LayoutInfo(sets)) VkShader(pipeline, shaderBindings) - - def compile[Params, L <: Layout: {LayoutBinding, LayoutStruct}](program: GioProgram[Params, L]): SpirvProgram[Params, L] = - val GioProgram(_, layout, dispatch, _) = program - val name = program.cacheKey + ".spv" - loadShader(name) match - case Failure(_) => ??? - case Success(_) => SpirvProgram(name, (il: InitProgramLayout) ?=> layout(il), dispatch) diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvCross.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvCross.scala index 73304350..4042b629 100644 --- a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvCross.scala +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvCross.scala @@ -19,8 +19,8 @@ object SpirvCross extends SpirvTool("spirv-cross"): None case Right(crossCompiledCode) => toolOutput match - case Ignore => - case toFile @ SpirvTool.ToFile(_) => + case Ignore => + case toFile @ SpirvTool.ToFile(_, _) => toFile.write(crossCompiledCode) logger.debug(s"Saved cross compiled shader code in ${toFile.filePath}.") case ToLogger => logger.debug(s"SPIR-V Cross Compilation result:\n$crossCompiledCode") diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvDisassembler.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvDisassembler.scala index f0c7c38f..4579db8a 100644 --- a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvDisassembler.scala +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvDisassembler.scala @@ -18,8 +18,8 @@ object SpirvDisassembler extends SpirvTool("spirv-dis"): None case Right(disassembledShader) => toolOutput match - case Ignore => - case toFile @ SpirvTool.ToFile(_) => + case Ignore => + case toFile @ SpirvTool.ToFile(_, _) => toFile.write(disassembledShader) logger.debug(s"Saved disassembled shader code in ${toFile.filePath}.") case ToLogger => logger.debug(s"SPIR-V Assembly:\n$disassembledShader") diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvOptimizer.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvOptimizer.scala index b42c0651..922d5346 100644 --- a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvOptimizer.scala +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvOptimizer.scala @@ -19,8 +19,8 @@ object SpirvOptimizer extends SpirvTool("spirv-opt"): None case Right(optimizedShaderCode) => toolOutput match - case SpirvTool.Ignore => - case toFile @ SpirvTool.ToFile(_) => + case SpirvTool.Ignore => + case toFile @ SpirvTool.ToFile(_, _) => toFile.write(optimizedShaderCode) logger.debug(s"Saved optimized shader code in ${toFile.filePath}.") Some(optimizedShaderCode) diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvTool.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvTool.scala index 729c0efd..58501a42 100644 --- a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvTool.scala +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvTool.scala @@ -95,18 +95,27 @@ object SpirvTool: case class Param(value: String): def asStringParam: String = value - case class ToFile(filePath: Path) extends ToolOutput: + case class ToFile(filePath: Path, hashSuffix: Boolean = true) extends ToolOutput: require(filePath != null, "filePath must not be null") - def write(outputToSave: String | ByteBuffer): Unit = - Option(filePath.getParent).foreach { dir => + def write(outputToSave: String | ByteBuffer): Unit = { + val suffix = if hashSuffix then s"_${outputToSave.hashCode() & 0xffff}" else "" + // prefix before last dot + val suffixedPath = filePath.getFileName.toString.lastIndexOf('.') match + case -1 => filePath.getFileName.toString + suffix + case index => filePath.getFileName.toString.substring(0, index) + suffix + filePath.getFileName.toString.substring(index) + val updatedPath = filePath.getParent match + case null => Path.of(suffixedPath) + case dir => dir.resolve(suffixedPath) + Option(updatedPath.getParent).foreach { dir => if !Files.exists(dir) then Files.createDirectories(dir) logger.debug(s"Created output directory: $dir") outputToSave match - case stringOutput: String => Files.write(filePath, stringOutput.getBytes(StandardCharsets.UTF_8)) - case byteBuffer: ByteBuffer => dumpByteBufferToFile(byteBuffer, filePath) + case stringOutput: String => Files.write(updatedPath, stringOutput.getBytes(StandardCharsets.UTF_8)) + case byteBuffer: ByteBuffer => dumpByteBufferToFile(byteBuffer, updatedPath) } + } private def dumpByteBufferToFile(code: ByteBuffer, path: Path): Unit = Using.resource(new FileOutputStream(path.toAbsolutePath.toString).getChannel) { fc => diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvToolsRunner.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvToolsRunner.scala index 234fca7b..1467350e 100644 --- a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvToolsRunner.scala +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvToolsRunner.scala @@ -20,7 +20,7 @@ class SpirvToolsRunner( SpirvValidator.validateSpirv(code, validator) originalSpirvOutput match - case toFile @ SpirvTool.ToFile(_) => + case toFile @ SpirvTool.ToFile(_, _) => toFile.write(shaderCode) logger.debug(s"Saved original shader code in ${toFile.filePath}.") case Ignore => diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/VulkanContext.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/VulkanContext.scala index 67d612fe..eade4596 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/VulkanContext.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/VulkanContext.scala @@ -1,9 +1,9 @@ package io.computenode.cyfra.vulkan import io.computenode.cyfra.utility.Logger.logger -import io.computenode.cyfra.vulkan.VulkanContext.ValidationLayers +import io.computenode.cyfra.vulkan.VulkanContext.{validation, vulkanPrintf} import io.computenode.cyfra.vulkan.command.CommandPool -import io.computenode.cyfra.vulkan.core.{DebugCallback, Device, Instance, PhysicalDevice, Queue} +import io.computenode.cyfra.vulkan.core.{DebugMessengerCallback, DebugReportCallback, Device, Instance, PhysicalDevice, Queue} import io.computenode.cyfra.vulkan.memory.{Allocator, DescriptorPool, DescriptorPoolManager, DescriptorSetManager} import org.lwjgl.system.Configuration @@ -15,12 +15,13 @@ import scala.jdk.CollectionConverters.* * MarconZet Created 13.04.2020 */ private[cyfra] object VulkanContext: - val ValidationLayer: String = "VK_LAYER_KHRONOS_validation" - private val ValidationLayers: Boolean = System.getProperty("io.computenode.cyfra.vulkan.validation", "false").toBoolean + private val validation: Boolean = System.getProperty("io.computenode.cyfra.vulkan.validation", "false").toBoolean + private val vulkanPrintf: Boolean = System.getProperty("io.computenode.cyfra.vulkan.printf", "false").toBoolean private[cyfra] class VulkanContext: - private val instance: Instance = new Instance(ValidationLayers) - private val debugCallback: Option[DebugCallback] = if ValidationLayers then Some(new DebugCallback(instance)) else None + private val instance: Instance = new Instance(validation, vulkanPrintf) + private val debugReport: Option[DebugReportCallback] = if validation then Some(new DebugReportCallback(instance)) else None + private val debugMessenger: Option[DebugMessengerCallback] = if validation & vulkanPrintf then Some(new DebugMessengerCallback(instance)) else None private val physicalDevice = new PhysicalDevice(instance) physicalDevice.assertRequirements() @@ -54,5 +55,6 @@ private[cyfra] class VulkanContext: descriptorPoolManager.destroy() allocator.destroy() device.destroy() - debugCallback.foreach(_.destroy()) + debugReport.foreach(_.destroy()) + debugMessenger.foreach(_.destroy()) instance.destroy() diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/CommandPool.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/CommandPool.scala index 11d21e1a..0af4cd2a 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/CommandPool.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/CommandPool.scala @@ -57,6 +57,8 @@ private[cyfra] abstract class CommandPool private (flags: Int, val queue: Queue) val pointerBuffer = stack.callocPointer(commandBuffer.length) commandBuffer.foreach(pointerBuffer.put) pointerBuffer.flip() + // TODO remove vkQueueWaitIdle, but currently crashes without it - Likely the printf debug buffer is still in use? + vkQueueWaitIdle(queue.get) vkFreeCommandBuffers(device.get, commandPool, pointerBuffer) protected def close(): Unit = diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugMessengerCallback.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugMessengerCallback.scala new file mode 100644 index 00000000..ad319665 --- /dev/null +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugMessengerCallback.scala @@ -0,0 +1,58 @@ +package io.computenode.cyfra.vulkan.core + +import io.computenode.cyfra.vulkan.util.Util.{check, pushStack} +import io.computenode.cyfra.vulkan.util.VulkanObjectHandle +import org.lwjgl.BufferUtils +import org.lwjgl.system.MemoryUtil +import org.lwjgl.vulkan.EXTDebugUtils.{ + VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT, + VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT, + VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT, + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT, + VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT, + VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT, + VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT, + vkCreateDebugUtilsMessengerEXT, + vkDestroyDebugUtilsMessengerEXT, +} +import org.lwjgl.vulkan.VK10.VK_FALSE +import org.lwjgl.vulkan.{VkDebugUtilsMessengerCallbackDataEXT, VkDebugUtilsMessengerCallbackEXT, VkDebugUtilsMessengerCreateInfoEXT} +import org.slf4j.LoggerFactory + +import java.lang.Integer.highestOneBit +import java.nio.LongBuffer + +class DebugMessengerCallback(instance: Instance) extends VulkanObjectHandle: + private val logger = LoggerFactory.getLogger("Cyfra-DebugMessenger") + + protected val handle: Long = pushStack: stack => + val callback = + new VkDebugUtilsMessengerCallbackEXT(): + override def invoke(messageSeverity: Int, messageTypes: Int, pCallbackData: Long, pUserData: Long): Int = + val message = VkDebugUtilsMessengerCallbackDataEXT.create(pCallbackData).pMessageString() + val debugMessage = message.split("\\|").last + highestOneBit(messageSeverity) match + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT => logger.error(debugMessage) + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT => logger.warn(debugMessage) + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT => logger.info(debugMessage) + case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT => logger.debug(debugMessage) + case x => logger.error(s"Unexpected message severity: $messageSeverity, message: $debugMessage") + VK_FALSE + + val debugMessengerCreate = VkDebugUtilsMessengerCreateInfoEXT + .calloc(stack) + .sType$Default() + .messageSeverity( + VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT, + ) + .messageType( + VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT, + ) + .pfnUserCallback(callback) + + val debugMessengerBuff = stack.callocLong(1) + check(vkCreateDebugUtilsMessengerEXT(instance.get, debugMessengerCreate, null, debugMessengerBuff), "Failed to create debug messenger") + debugMessengerBuff.get() + + override protected def close(): Unit = vkDestroyDebugUtilsMessengerEXT(instance.get, handle, null) diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugCallback.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugReportCallback.scala similarity index 56% rename from cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugCallback.scala rename to cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugReportCallback.scala index 3721e9b3..2e43450d 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugCallback.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugReportCallback.scala @@ -1,24 +1,28 @@ package io.computenode.cyfra.vulkan.core import io.computenode.cyfra.utility.Logger.logger -import io.computenode.cyfra.vulkan.core.DebugCallback.DebugReport +import io.computenode.cyfra.vulkan.core.DebugReportCallback.DebugReport +import io.computenode.cyfra.vulkan.util.Util.{check, pushStack} import io.computenode.cyfra.vulkan.util.{VulkanAssertionError, VulkanObjectHandle} import org.lwjgl.BufferUtils import org.lwjgl.system.MemoryUtil.NULL import org.lwjgl.vulkan.EXTDebugReport.* import org.lwjgl.vulkan.VK10.VK_SUCCESS import org.lwjgl.vulkan.{VkDebugReportCallbackCreateInfoEXT, VkDebugReportCallbackEXT} +import org.slf4j.LoggerFactory import java.lang.Integer.highestOneBit /** @author * MarconZet Created 13.04.2020 */ -object DebugCallback: +object DebugReportCallback: val DebugReport: Int = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT | VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT -private[cyfra] class DebugCallback(instance: Instance) extends VulkanObjectHandle: - override protected val handle: Long = +private[cyfra] class DebugReportCallback(instance: Instance) extends VulkanObjectHandle: + private val logger = LoggerFactory.getLogger("Cyfra-DebugReport") + + protected val handle: Long = pushStack: stack => val debugCallback = new VkDebugReportCallbackEXT(): def invoke( flags: Int, @@ -32,31 +36,23 @@ private[cyfra] class DebugCallback(instance: Instance) extends VulkanObjectHandl ): Int = val decodedMessage = VkDebugReportCallbackEXT.getString(pMessage) highestOneBit(flags) match - case VK_DEBUG_REPORT_DEBUG_BIT_EXT => - logger.debug(decodedMessage) - case VK_DEBUG_REPORT_ERROR_BIT_EXT => - logger.error(decodedMessage) - case VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT => - logger.warn(decodedMessage) - case VK_DEBUG_REPORT_INFORMATION_BIT_EXT => - logger.info(decodedMessage) + case VK_DEBUG_REPORT_DEBUG_BIT_EXT => logger.debug(decodedMessage) + case VK_DEBUG_REPORT_ERROR_BIT_EXT => logger.error(decodedMessage) + case VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT => logger.warn(decodedMessage) + case VK_DEBUG_REPORT_INFORMATION_BIT_EXT => logger.info(decodedMessage) case x => logger.error(s"Unexpected value: x, message: $decodedMessage") 0 - setupDebugging(DebugReport, debugCallback) - - override protected def close(): Unit = - vkDestroyDebugReportCallbackEXT(instance.get, handle, null) - private def setupDebugging(flags: Int, callback: VkDebugReportCallbackEXT): Long = val dbgCreateInfo = VkDebugReportCallbackCreateInfoEXT - .create() + .calloc(stack) .sType$Default() .pNext(0) - .pfnCallback(callback) + .pfnCallback(debugCallback) .pUserData(0) - .flags(flags) - val pCallback = BufferUtils.createLongBuffer(1) - val err = vkCreateDebugReportCallbackEXT(instance.get, dbgCreateInfo, null, pCallback) - val callbackHandle = pCallback.get(0) - if err != VK_SUCCESS then throw new VulkanAssertionError("Failed to create DebugCallback", err) - callbackHandle + .flags(DebugReport) + val pCallback = stack.callocLong(1) + check(vkCreateDebugReportCallbackEXT(instance.get, dbgCreateInfo, null, pCallback), "Failed to create DebugCallback") + pCallback.get() + + override protected def close(): Unit = + vkDestroyDebugReportCallbackEXT(instance.get, handle, null) diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Device.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Device.scala index 6908fcfa..290ac699 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Device.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Device.scala @@ -1,6 +1,5 @@ package io.computenode.cyfra.vulkan.core -import io.computenode.cyfra.vulkan.VulkanContext.ValidationLayer import Device.MacOsExtension import io.computenode.cyfra.vulkan.util.Util.{check, pushStack} import io.computenode.cyfra.vulkan.util.{VulkanObject, VulkanObjectHandle} diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Instance.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Instance.scala index 43072840..f8661f6d 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Instance.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Instance.scala @@ -1,18 +1,21 @@ package io.computenode.cyfra.vulkan.core import io.computenode.cyfra.utility.Logger.logger -import io.computenode.cyfra.vulkan.VulkanContext.ValidationLayer +import io.computenode.cyfra.vulkan.core.Instance.ValidationLayer import io.computenode.cyfra.vulkan.util.Util.{check, pushStack} import io.computenode.cyfra.vulkan.util.VulkanObject import org.lwjgl.system.{MemoryStack, MemoryUtil} import org.lwjgl.system.MemoryUtil.NULL import org.lwjgl.vulkan.* import org.lwjgl.vulkan.EXTDebugReport.VK_EXT_DEBUG_REPORT_EXTENSION_NAME -import org.lwjgl.vulkan.EXTLayerSettings.VK_LAYER_SETTING_TYPE_BOOL32_EXT +import org.lwjgl.vulkan.EXTLayerSettings.{VK_LAYER_SETTING_TYPE_BOOL32_EXT, VK_LAYER_SETTING_TYPE_STRING_EXT, VK_LAYER_SETTING_TYPE_UINT32_EXT} import org.lwjgl.vulkan.KHRPortabilityEnumeration.{VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME} +import org.lwjgl.vulkan.EXTLayerSettings.VK_EXT_LAYER_SETTINGS_EXTENSION_NAME import org.lwjgl.vulkan.VK10.* +import org.lwjgl.vulkan.EXTValidationFeatures.* +import org.lwjgl.vulkan.EXTDebugUtils.* -import java.nio.ByteBuffer +import java.nio.{ByteBuffer, LongBuffer} import scala.collection.mutable import scala.jdk.CollectionConverters.given import scala.util.chaining.* @@ -21,8 +24,10 @@ import scala.util.chaining.* * MarconZet Created 13.04.2020 */ object Instance: - val ValidationLayersExtensions: Seq[String] = List(VK_EXT_DEBUG_REPORT_EXTENSION_NAME) - val MoltenVkExtensions: Seq[String] = List(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) + private val ValidationLayer: String = "VK_LAYER_KHRONOS_validation" + private val ValidationLayersExtensions: Seq[String] = + List(VK_EXT_DEBUG_REPORT_EXTENSION_NAME, VK_EXT_DEBUG_UTILS_EXTENSION_NAME, VK_EXT_LAYER_SETTINGS_EXTENSION_NAME) + private val MoltenVkExtensions: Seq[String] = List(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) lazy val (extensions, layers): (Seq[String], Seq[String]) = pushStack: stack => val ip = stack.ints(1) @@ -40,7 +45,7 @@ object Instance: lazy val version: Int = VK.getInstanceVersionSupported -private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObject[VkInstance]: +private[cyfra] class Instance(enableValidationLayers: Boolean, enablePrinting: Boolean) extends VulkanObject[VkInstance]: protected val handle: VkInstance = pushStack: stack => val appInfo = VkApplicationInfo @@ -69,15 +74,27 @@ private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObj .ppEnabledLayerNames(ppEnabledLayerNames) if enableValidationLayers then - val layerSettings = VkLayerSettingEXT.calloc(1, stack) - layerSettings - .get(0) - .pLayerName(stack.ASCII(ValidationLayer)) - .pSettingName(stack.ASCII("validate_sync")) - .`type`(VK_LAYER_SETTING_TYPE_BOOL32_EXT) - .valueCount(1) - .pValues(MemoryUtil.memByteBuffer(stack.ints(1))) + val layerSettings = VkLayerSettingEXT.calloc(10, stack) + + setTrue(layerSettings.get(), "validate_sync", stack) + setTrue(layerSettings.get(), "gpuav_enable", stack) + setTrue(layerSettings.get(), "validate_best_practices", stack) + + if enablePrinting then + setTrue(layerSettings.get(), "printf_enable", stack) + + layerSettings + .get() + .pLayerName(stack.ASCII(ValidationLayer)) + .pSettingName(stack.ASCII("printf_buffer_size")) + .`type`(VK_LAYER_SETTING_TYPE_UINT32_EXT) + .valueCount(1) + .pValues(MemoryUtil.memByteBuffer(stack.ints(1024 * 1024))) + + layerSettings.flip() + val layerSettingsCI = VkLayerSettingsCreateInfoEXT.calloc(stack).sType$Default().pSettings(layerSettings) + pCreateInfo.pNext(layerSettingsCI) val pInstance = stack.mallocPointer(1) @@ -114,10 +131,18 @@ private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObj val filteredExtensions = extensions.filter(ext => availableExtensions .contains(ext) - .tap: x => // TODO detect when this extension is needed + .tap: x => // TODO better handle missing extensions if !x then logger.warn(s"Requested Vulkan instance extension '$ext' is not available"), ) val ppEnabledExtensionNames = stack.callocPointer(extensions.size) filteredExtensions.foreach(x => ppEnabledExtensionNames.put(stack.ASCII(x))) ppEnabledExtensionNames.flip() + + private def setTrue(setting: VkLayerSettingEXT, name: String, stack: MemoryStack) = + setting + .pLayerName(stack.ASCII(ValidationLayer)) + .pSettingName(stack.ASCII(name)) + .`type`(VK_LAYER_SETTING_TYPE_BOOL32_EXT) + .valueCount(1) + .pValues(MemoryUtil.memByteBuffer(stack.ints(1))) diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/Buffer.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/Buffer.scala index c1f34b40..484b5505 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/Buffer.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/Buffer.scala @@ -58,7 +58,6 @@ object Buffer: def copyTo(dst: ByteBuffer, srcOffset: Int): Unit = pushStack: stack => vmaCopyAllocationToMemory(allocator.get, allocation, srcOffset, dst) - def copyFrom(src: ByteBuffer, dstOffset: Int): Unit = pushStack: stack => vmaCopyMemoryToAllocation(allocator.get, src, allocation, dstOffset)