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..0312d9db 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..00a3f615 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..f5975295 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,51 @@ 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..c1459fcf 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,23 +289,24 @@ 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) + case when: WhenExpr[?] => compileWhen(when, ctx) @@ -330,14 +327,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 +343,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..5d54fc2b --- /dev/null +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GIOCompiler.scala @@ -0,0 +1,147 @@ +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)) + + + + + + + \ No newline at end of file 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..2542e1f8 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,19 @@ 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) => + 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..ee523d35 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,22 +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()), ) @@ -83,7 +74,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 @@ -101,13 +94,7 @@ 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 +112,139 @@ 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 + 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 (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 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 = 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 + 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 = - if in then ctx.copy(inBufferBlocks = block :: ctx.inBufferBlocks) else ctx.copy(outBufferBlocks = block :: ctx.outBufferBlocks) + ctx.copy(bufferBlocks = ctx.bufferBlocks + (buff -> block)) ( decAcc ::: decorationInstructions, insnAcc ::: definitionInstructions, - contextWithBlock.copy(nextResultId = contextWithBlock.nextResultId + 5, nextBinding = contextWithBlock.nextBinding + 1), + 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 bdc1d5a7..65b5e367 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 @@ -24,6 +24,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/GExecution.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GExecution.scala index 22418ead..7ea8ccaf 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 @@ -6,7 +6,6 @@ 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..7154e47f 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,15 +8,18 @@ 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]] object GProgram: @@ -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..f60c02e3 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,26 @@ 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 +58,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/GContext.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/GContext.scala index 9f209ea4..7b141690 100644 --- 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 @@ -5,11 +5,9 @@ import io.computenode.cyfra.core.archive.mem.{FloatMem, GMem, IntMem, Vec4FloatM 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 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..76d8ffa8 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 @@ -55,17 +56,18 @@ object LayoutStruct: 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 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..ef723217 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 @@ -111,4 +111,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/binding/GBinding.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GBinding.scala index 60c53cac..ce003c87 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..4ffc51c3 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,13 @@ 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)) + summon[FromExpr[T]].fromExpr(ReadBuffer(underlying, i)) -object GArray: - case class GArrayElem[T <: Value: Tag](index: Int, i: Expression[Int32]) extends Expression[T] 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..e532eea2 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 @@ -7,6 +7,7 @@ import io.computenode.cyfra.dsl.macros.Source import izumi.reflect.Tag import io.computenode.cyfra.dsl.Value.FromExpr +// todo temporary class GArray2D[T <: Value: {Tag, FromExpr}](width: Int, val arr: GArray[T]): def at(x: Int32, y: Int32)(using Source): T = arr.at(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..0448fcf0 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,37 +6,53 @@ 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() + + case class Printf(format: String, args: Value*) extends GIO[Empty]: + override def underlying: Empty = Empty() - def pure[T](value: T): GIO[T] = Pure(value) + def pure[T <: Value](value: T): GIO[T] = Pure(value) - def value[T](value: T): GIO[T] = Pure(value) + def value[T <: Value](value: T): GIO[T] = Pure(value) - def repeat(n: Int32)(f: Int32 => GIO[?]): GIO[Unit] = - Repeat(n, f) + case object CurrentRepeatIndex extends PhantomExpression[Int32] with CustomTreeId: + override val treeid: Int = treeidState.getAndIncrement() - def write[T <: Value](buffer: GBuffer[T], index: Int32, value: T): GIO[Unit] = + 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..f9f6b383 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 @@ -26,9 +26,9 @@ object GStruct: 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/RuntimeEnduranceTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/RuntimeEnduranceTest.scala new file mode 100644 index 00000000..dec27507 --- /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.archive.GContext +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..95833a22 --- /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.archive.GContext +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 95% 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..78843776 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,8 +1,8 @@ -package io.computenode.cyfra.e2e +package io.computenode.cyfra.e2e.dsl import io.computenode.cyfra.core.archive.* -import mem.* -import GMem.fRGBA +import io.computenode.cyfra.core.archive.mem.* +import io.computenode.cyfra.core.archive.mem.GMem.fRGBA import io.computenode.cyfra.dsl.algebra.VectorAlgebra import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} 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 94% 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..990906e8 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,9 +1,10 @@ -package io.computenode.cyfra.e2e +package io.computenode.cyfra.e2e.dsl -import io.computenode.cyfra.core.archive.*, mem.* +import io.computenode.cyfra.core.archive.* +import io.computenode.cyfra.core.archive.mem.* +import io.computenode.cyfra.core.archive.mem.GMem.fRGBA import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} -import GMem.fRGBA class FunctionsE2eTest extends munit.FunSuite: given gc: GContext = GContext() 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/dsl/GStructE2eTest.scala similarity index 96% rename from cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/GStructE2eTest.scala rename to cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/GStructE2eTest.scala index c1183c2a..20e15843 100644 --- a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/GStructE2eTest.scala +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/dsl/GStructE2eTest.scala @@ -1,9 +1,9 @@ -package io.computenode.cyfra.e2e +package io.computenode.cyfra.e2e.dsl +import io.computenode.cyfra.core.archive.* +import io.computenode.cyfra.core.archive.mem.* 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: 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 94% 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..318201b1 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,9 +1,9 @@ -package io.computenode.cyfra.e2e +package io.computenode.cyfra.e2e.dsl +import io.computenode.cyfra.core.archive.* +import io.computenode.cyfra.core.archive.mem.* 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 GseqE2eTest extends munit.FunSuite: 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 91% 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..416f9148 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,8 +1,8 @@ -package io.computenode.cyfra.e2e +package io.computenode.cyfra.e2e.dsl -import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.core.archive.* -import mem.* +import io.computenode.cyfra.core.archive.mem.* +import io.computenode.cyfra.dsl.struct.GStruct import io.computenode.cyfra.dsl.{*, given} class WhenE2eTest extends munit.FunSuite: 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..289426ec --- /dev/null +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/fs2interop/Fs2Tests.scala @@ -0,0 +1,115 @@ +package io.computenode.cyfra.e2e.fs2interop + +import io.computenode.cyfra.core.archive.* +import mem.* +import GMem.fRGBA +import io.computenode.cyfra.dsl.{*, given} +import algebra.VectorAlgebra +import io.computenode.cyfra.fs2interop.* +import Bridge.given +import io.computenode.cyfra.core.CyfraRuntime +import io.computenode.cyfra.runtime.VkCyfraRuntime +import fs2.{io as fs2io, *} +import _root_.io.computenode.cyfra.spirvtools.{SpirvCross, SpirvDisassembler, SpirvToolsRunner} +import _root_.io.computenode.cyfra.spirvtools.SpirvTool.ToFile + +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") + +class Fs2LegacyTests extends munit.FunSuite: + given gc: GContext = GContext() + import GPipeLegacy.* + + test("fs2 Float stream (legacy)"): + val inSeq = (0 to 255).map(_.toFloat) + val inStream = Stream.emits(inSeq) + val outStream = inStream.gPipeFloat(_ + 1f).toList + val expected = inStream.map(_ + 1f).toList + outStream + .zip(expected) + .foreach: (res, exp) => + assert(Math.abs(res - exp) < 0.001f, s"Expected $exp but got $res") + + test("fs2 Int stream (legacy)"): + val inSeq = 0 to 255 + val inStream = Stream.emits(inSeq) + val outStream = inStream.gPipeInt(_ + 1).toList + val expected = inStream.map(_ + 1).toList + outStream + .zip(expected) + .foreach: (res, exp) => + assert(res == exp, s"Expected $exp but got $res") + + test("fs2 Vec4Float stream (legacy)"): + val k = -2.1f + val f = (1.2f, 2.3f, 3.4f, 4.5f) + val v = VectorAlgebra.vec4.tupled(f) + + val inSeq: Seq[fRGBA] = (0 to 1023) + .map(_.toFloat) + .grouped(4) + .map: + case Seq(a, b, c, d) => (a, b, c, d) + .toSeq + val inStream = Stream.emits(inSeq) + val outStream = inStream.gPipeVec4(vec => (-vec).*(k).+(v)).toList + val expected = inStream.map(vec => vec.neg.scl(k).add(f)).toList + + outStream + .zip(expected) + .foreach: (res, exp) => + assert(res.close(exp)(0.001f), s"Expected $exp but got $res") 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 8b9a5014..e431b719 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 @@ -9,9 +9,12 @@ 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.file.Paths import java.util.concurrent.atomic.AtomicInteger import scala.collection.parallel.CollectionConverters.given @@ -89,15 +92,19 @@ 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) @@ -106,173 +113,58 @@ 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), + init = EmitProgramLayout( + in = GBuffer[Int32](buffer), + out = GBuffer[Int32](data.length * 2), ), - onDone = layout => layout.filterBuffer.read(rbb), + onDone = layout => layout.out.read(rbb), ) runtime.close() - 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)), - ), - 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) + @main + def test = + given runtime: VkCyfraRuntime = VkCyfraRuntime( + spirvToolsRunner = SpirvToolsRunner( + crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl"))), + validator = SpirvValidator.Disable + ) + ) - 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 = 0, addB = 1) 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[GBoolean](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() - 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") diff --git a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/Bridge.scala b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/Bridge.scala new file mode 100644 index 00000000..b148a53b --- /dev/null +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/Bridge.scala @@ -0,0 +1,68 @@ +// scala +package io.computenode.cyfra.fs2interop + +import io.computenode.cyfra.core.archive.mem.GMem.fRGBA +import io.computenode.cyfra.dsl.* +import fs2.* + +import java.nio.{ByteBuffer, ByteOrder} +import izumi.reflect.Tag +import scala.reflect.ClassTag + +trait Bridge[CyfraType <: Value: FromExpr: Tag, ScalaType: ClassTag]: + def toByteBuffer(inBuf: ByteBuffer, chunk: Chunk[ScalaType]): ByteBuffer + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[ScalaType]): Array[ScalaType] + +object Bridge: + given Bridge[Int32, Int]: + def toByteBuffer(inBuf: ByteBuffer, chunk: Chunk[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 Bridge[Float32, Float]: + def toByteBuffer(inBuf: ByteBuffer, chunk: Chunk[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 Bridge[Vec4[Float32], fRGBA]: + def toByteBuffer(inBuf: ByteBuffer, chunk: Chunk[fRGBA]): ByteBuffer = + inBuf.clear().order(ByteOrder.nativeOrder()) + val vecs = chunk.toArray[fRGBA] + vecs.foreach: + case (x, y, z, a) => + inBuf.putFloat(x) + inBuf.putFloat(y) + inBuf.putFloat(z) + inBuf.putFloat(a) + 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 Bridge[GBoolean, Boolean]: + def toByteBuffer(inBuf: ByteBuffer, chunk: Chunk[Boolean]): ByteBuffer = + inBuf.put(chunk.toArray.asInstanceOf[Array[Byte]]).flip() + inBuf + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[Boolean]): Array[Boolean] = + outBuf.get(arr.asInstanceOf[Array[Byte]]).flip() + arr \ No newline at end of file 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..0efefce1 --- /dev/null +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -0,0 +1,234 @@ +package io.computenode.cyfra.fs2interop + +import io.computenode.cyfra.core.{Allocation, layout} +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: Bridge[C1, S1], bridge2: Bridge[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) + 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, Bridge[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: Bridge[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.printf("Pred: Element %d -> %d", invocId, result) + _ <- 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], intervalSize: GUniform[ScanArgs] = GUniform.fromParams) extends Layout + + val upsweep = GProgram[ScanParams, ScanLayout]( + layout = params => ScanLayout(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.printf("Upsweep: invocId %d, root %d, size %d, mid %d, end %d, oldValue %d, addValue %d, newValue %d", invocId, root, size, mid, end, oldValue, addValue, newValue) + _ <- GIO.write[Int32](layout.ints, end, newValue) + yield Empty() + + val downsweep = GProgram[ScanParams, ScanLayout]( + layout = params => ScanLayout(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.printf("Downsweep: invocId %d, end %d, mid %d, oldValue %d, addValue %d, newValue %d", invocId, end, mid, oldValue, addValue, newValue) + _ <- 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 => layout) + 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 => layout) + 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.printf("Compact: Element %d, prefix sum %d", invocId, prefixSum) + _ <- 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) + 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)) + println(arr) + Stream.emits(arr) diff --git a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipeLegacy.scala b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipeLegacy.scala new file mode 100644 index 00000000..78d36209 --- /dev/null +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipeLegacy.scala @@ -0,0 +1,39 @@ +package io.computenode.cyfra.fs2interop + +import io.computenode.cyfra.core.archive.*, mem.*, GMem.fRGBA +import io.computenode.cyfra.dsl.{*, given} +import struct.GStruct, GStruct.Empty, Empty.given + +import fs2.* + +// legacy stuff working with GFunction +object GPipeLegacy: + extension (stream: Stream[Pure, Float]) + def gPipeFloat(fn: Float32 => Float32)(using GContext): Stream[Pure, Float] = + val gf: GFunction[Empty, Float32, Float32] = GFunction(fn) + stream + .chunkMin(256) + .flatMap: chunk => + val gmem = FloatMem(chunk.toArray) + val res = gmem.map(gf).asInstanceOf[FloatMem].toArray + Stream.emits(res) + + extension (stream: Stream[Pure, Int]) + def gPipeInt(fn: Int32 => Int32)(using GContext): Stream[Pure, Int] = + val gf: GFunction[Empty, Int32, Int32] = GFunction(fn) + stream + .chunkMin(256) + .flatMap: chunk => + val gmem = IntMem(chunk.toArray) + val res = gmem.map(gf).asInstanceOf[IntMem].toArray + Stream.emits(res) + + extension (stream: Stream[Pure, fRGBA]) + def gPipeVec4(fn: Vec4[Float32] => Vec4[Float32])(using GContext): Stream[Pure, fRGBA] = + val gf: GFunction[Empty, Vec4[Float32], Vec4[Float32]] = GFunction(fn) + stream + .chunkMin(256) + .flatMap: chunk => + val gmem = Vec4FloatMem(chunk.toArray) + val res = gmem.map(gf).asInstanceOf[Vec4FloatMem].toArray + Stream.emits(res) 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 782b2a85..b87033ac 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,15 +8,8 @@ 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.runtime.ExecutionHandler.{ - BindingLogicError, - Dispatch, - DispatchType, - ExecutionBinding, - ExecutionStep, - PipelineBarrier, - ShaderCall, -} +import io.computenode.cyfra.dsl.struct.{GStruct, GStructSchema} +import io.computenode.cyfra.runtime.ExecutionHandler.{BindingLogicError, Dispatch, DispatchType, ExecutionBinding, ExecutionStep, PipelineBarrier, ShaderCall} import io.computenode.cyfra.runtime.ExecutionHandler.DispatchType.* import io.computenode.cyfra.runtime.ExecutionHandler.ExecutionBinding.{BufferBinding, UniformBinding} import io.computenode.cyfra.utility.Utility.timed @@ -45,7 +38,7 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte val (result, shaderCalls) = interpret(execution, params, layout) val descriptorSets = shaderCalls.map: - case ShaderCall(pipeline, layout, _) => + case ShaderCall(pipeline, layout, _, _) => pipeline.pipelineLayout.sets .map(dsManager.allocate) .zip(layout) @@ -57,7 +50,7 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte val dispatches: Seq[Dispatch] = shaderCalls .zip(descriptorSets) .map: - case (ShaderCall(pipeline, layout, dispatch), sets) => + case (ShaderCall(pipeline, layout, dispatch, _), sets) => Dispatch(pipeline, layout, sets, dispatch) val (executeSteps, _) = dispatches.foldLeft((Seq.empty[ExecutionStep], Set.empty[GBinding[?]])): @@ -75,7 +68,7 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte .pCommandBuffers(pCommandBuffer) val fence = new Fence() - timed("Vulkan render command"): + timed("Vulkan execute command"): check(vkQueueSubmit(commandPool.queue.get, submitInfo, fence.get), "Failed to submit command buffer to queue") fence.block().destroy() commandPool.freeCommandBuffer(commandBuffer) @@ -97,9 +90,8 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte case x: ExecutionBinding[?] => x case x: GBinding[?] => val e = ExecutionBinding(x)(using x.fromExpr, x.tag) - bindingsAcc.put(e, mutable.Buffer(x)) + bindingsAcc.put(e, mutable.Buffer(x)) // store only base contribution here e - mapper.fromBindings(res) // noinspection TypeParameterShadow @@ -132,33 +124,51 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte val layoutInit = val initProgram: InitProgramLayout = summon[VkAllocation].getInitProgramLayout program.layout(initProgram)(params) - lb.toBindings(layout) - .zip(lb.toBindings(layoutInit)) - .foreach: - case (binding, initBinding) => - bindingsAcc(binding).append(initBinding) + + val callInits: Map[GBinding[?], Seq[GBinding[?]]] = + lb + .toBindings(layout) + .zip(lb.toBindings(layoutInit)) + .groupMap(_._1)(_._2) + val dispatch = program.dispatch(layout, params) match case GProgram.DynamicDispatch(buffer, offset) => DispatchType.Indirect(buffer, offset) case GProgram.StaticDispatch(size) => DispatchType.Direct(size._1, size._2, size._3) // noinspection ScalaRedundantCast - (layout.asInstanceOf[RL], Seq(ShaderCall(shader.underlying, shader.shaderBindings(layout), dispatch))) + (layout.asInstanceOf[RL], Seq(ShaderCall(shader.underlying, shader.shaderBindings(layout), dispatch, callInits))) case _ => ??? val (rl, steps) = interpretImpl(execution, params, mockBindings(layout)) - val bingingToVk = bindingsAcc.map(x => (x._1, interpretBinding(x._1, x._2.toSeq))) + + val finalBindingForRl: mutable.Map[GBinding[?], GBinding[?]] = mutable.Map.empty val nextSteps = steps.map: - case ShaderCall(pipeline, layout, dispatch) => + case ShaderCall(pipeline, layout, dispatch, callInits) => val nextLayout = layout.map: _.map: - case Binding(binding, operation) => Binding(bingingToVk(binding), operation) + case Binding(binding, operation) => + val base = bindingsAcc.getOrElse(binding, mutable.Buffer.empty).toSeq + val extras = callInits.getOrElse(binding, Seq.empty) + val resolved = interpretBinding(binding, base ++ extras) + finalBindingForRl.update(binding, resolved) + Binding(resolved, operation) + val nextDispatch = dispatch match - case x: Direct => x - case Indirect(buffer, offset) => Indirect(bingingToVk(buffer), offset) - ShaderCall(pipeline, nextLayout, nextDispatch) + case x: DispatchType.Direct => x + case DispatchType.Indirect(buffer, offset) => + val base = bindingsAcc.getOrElse(buffer, mutable.Buffer.empty).toSeq + val extras = callInits.getOrElse(buffer, Seq.empty) + val resolved = interpretBinding(buffer, base ++ extras) + finalBindingForRl.update(buffer, resolved) + DispatchType.Indirect(resolved, offset) + + ShaderCall(pipeline, nextLayout, nextDispatch, Map.empty) val mapper = summon[LayoutBinding[RL]] - val res = mapper.fromBindings(mapper.toBindings(rl).map(bingingToVk.apply)) + val rlBindings = mapper.toBindings(rl).map: b => + finalBindingForRl.getOrElse(b, interpretBinding(b, bindingsAcc.getOrElse(b, mutable.Buffer.empty).toSeq)) + val res = mapper.fromBindings(rlBindings) + (res, nextSteps) private def interpretBinding(binding: GBinding[?], bindings: Seq[GBinding[?]])(using VkAllocation): GBinding[?] = @@ -234,13 +244,23 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte commandBuffer object ExecutionHandler: - case class ShaderCall(pipeline: ComputePipeline, layout: ShaderLayout, dispatch: DispatchType) + case class ShaderCall( + pipeline: ComputePipeline, + layout: ShaderLayout, + dispatch: DispatchType, + callInits: Map[GBinding[?], Seq[GBinding[?]]] // per-program contributions + ) sealed trait ExecutionStep - case class Dispatch(pipeline: ComputePipeline, layout: ShaderLayout, descriptorSets: Seq[DescriptorSet], dispatch: DispatchType) - extends ExecutionStep - case object PipelineBarrier extends ExecutionStep + case class Dispatch( + pipeline: ComputePipeline, + layout: ShaderLayout, + descriptorSets: Seq[DescriptorSet], + dispatch: DispatchType + ) extends ExecutionStep + + case object PipelineBarrier extends ExecutionStep sealed trait DispatchType object DispatchType: case class Direct(x: Int, y: Int, z: Int) extends DispatchType @@ -248,11 +268,16 @@ 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() + 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") 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 a038ac7b..8cb67f8b 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 @@ -50,29 +50,30 @@ class VkAllocation(commandPool: CommandPool, executionHandler: ExecutionHandler) def apply[T <: Value: {Tag, FromExpr}](length: Int): GBuffer[T] = VkBuffer[T](length).tap(bindings += _) - def apply[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GBuffer[T] = + 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/VkCyfraRuntime.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala index ccd6585f..17b90fab 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,40 @@ 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 + case p: GioProgram[Params, L] => compile(p) + case p: SpirvProgram[Params, L] => p + case _ => throw new IllegalArgumentException(s"Unsupported program type: ${program.getClass.getName}") + + 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..d2209fe1 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 => @@ -34,9 +32,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-runtime/src/main/scala/io/computenode/cyfra/runtime/VkUniform.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkUniform.scala index f8c75da7..86ed671e 100644 --- a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkUniform.scala +++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkUniform.scala @@ -3,19 +3,20 @@ package io.computenode.cyfra.runtime 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 import org.lwjgl.vulkan.VK10.* -class VkUniform[T <: Value: {Tag, FromExpr}] private (val underlying: Buffer) extends GUniform[T]: +class VkUniform[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}] private (val underlying: Buffer) extends GUniform[T]: val sizeOfT: Int = 4 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 = 4 // typeStride(summon[Tag[T]]) val buffer = new Buffer.DeviceBuffer(sizeOfT, UsageFlags) new VkUniform[T](buffer) 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..991fda38 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 @@ -20,7 +20,7 @@ object SpirvCross extends SpirvTool("spirv-cross"): case Right(crossCompiledCode) => toolOutput match case Ignore => - case toFile @ SpirvTool.ToFile(_) => + 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..67730961 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 @@ -19,7 +19,7 @@ object SpirvDisassembler extends SpirvTool("spirv-dis"): case Right(disassembledShader) => toolOutput match case Ignore => - case toFile @ SpirvTool.ToFile(_) => + 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..509e763c 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 @@ -20,7 +20,7 @@ object SpirvOptimizer extends SpirvTool("spirv-opt"): case Right(optimizedShaderCode) => toolOutput match case SpirvTool.Ignore => - case toFile @ SpirvTool.ToFile(_) => + 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 9c8c99c6..e979d3ca 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 @@ -16,7 +16,7 @@ import scala.jdk.CollectionConverters.* */ 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 ValidationLayers: Boolean = System.getProperty("io.computenode.cyfra.vulkan.validation", "true").toBoolean if Configuration.STACK_SIZE.get() < 100 then logger.warn(s"Small stack size. Increase with org.lwjgl.system.stackSize") private[cyfra] class VulkanContext: 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 3db65668..863770cf 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 @@ -74,6 +74,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/Instance.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Instance.scala index 43072840..2f3d3df5 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 @@ -8,11 +8,13 @@ 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.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,7 +23,11 @@ import scala.util.chaining.* * MarconZet Created 13.04.2020 */ object Instance: - val ValidationLayersExtensions: Seq[String] = List(VK_EXT_DEBUG_REPORT_EXTENSION_NAME) + val ValidationLayersExtensions: Seq[String] = List( + VK_EXT_DEBUG_REPORT_EXTENSION_NAME, + VK_EXT_DEBUG_UTILS_EXTENSION_NAME, + VK_EXT_VALIDATION_FEATURES_EXTENSION_NAME + ) val MoltenVkExtensions: Seq[String] = List(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) lazy val (extensions, layers): (Seq[String], Seq[String]) = pushStack: stack => @@ -69,7 +75,7 @@ private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObj .ppEnabledLayerNames(ppEnabledLayerNames) if enableValidationLayers then - val layerSettings = VkLayerSettingEXT.calloc(1, stack) + val layerSettings = VkLayerSettingEXT.calloc(2, stack) layerSettings .get(0) .pLayerName(stack.ASCII(ValidationLayer)) @@ -77,13 +83,85 @@ private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObj .`type`(VK_LAYER_SETTING_TYPE_BOOL32_EXT) .valueCount(1) .pValues(MemoryUtil.memByteBuffer(stack.ints(1))) + + layerSettings + .get(1) + .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))) + + val layerSettingsCI = VkLayerSettingsCreateInfoEXT.calloc(stack).sType$Default().pSettings(layerSettings) - pCreateInfo.pNext(layerSettingsCI) + + val validationFeatures = VkValidationFeaturesEXT.calloc(stack) + .sType$Default() + .pEnabledValidationFeatures(stack.ints( + VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT, + )) + .pNext(0) + + val validationFeaturesPCreate = VkValidationFeaturesEXT.calloc(stack) + .sType$Default() + .pEnabledValidationFeatures(stack.ints( + VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_EXT, + VK_VALIDATION_FEATURE_ENABLE_GPU_ASSISTED_RESERVE_BINDING_SLOT_EXT, + )) + .pNext(0) + + layerSettingsCI.pNext(validationFeatures.address()) + pCreateInfo.pNext(layerSettingsCI.address()) + validationFeaturesPCreate.pNext(validationFeaturesPCreate.address()) val pInstance = stack.mallocPointer(1) check(vkCreateInstance(pCreateInfo, null, pInstance), "Failed to create VkInstance") new VkInstance(pInstance.get(0), pCreateInfo) + protected val callback: Option[VkDebugUtilsMessengerCallbackEXT] = + if enableValidationLayers then Some: + new VkDebugUtilsMessengerCallbackEXT(): + override def invoke(messageSeverity: Int, messageTypes: Int, pCallbackData: Long, pUserData: Long): Int = + val message = VkDebugUtilsMessengerCallbackDataEXT.create(pCallbackData).pMessageString() + val debugMessage = "[VK DEBUG] " + message.split("\\|").last + if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0 then + logger.error(debugMessage) + else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) != 0 then + logger.warn(debugMessage) + else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) != 0 then + logger.info(debugMessage) + else + logger.debug(debugMessage) + VK_FALSE + else None + + protected val debugMessenger: Option[LongBuffer] = callback.map: c => + pushStack: stack => + val debugMessengerCreate = VkDebugUtilsMessengerCreateInfoEXT.calloc(1) + .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(c) + + val debugMessengerBuff = MemoryUtil.memAllocLong(1) + check(vkCreateDebugUtilsMessengerEXT( + handle, + debugMessengerCreate.get(0), + null, + debugMessengerBuff + )) + debugMessengerBuff + + lazy val enabledLayers: Seq[String] = List .empty[String] .pipe: x => @@ -93,7 +171,8 @@ private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObj x else x - override protected def close(): Unit = + override protected def close(): Unit = + debugMessenger.foreach(b => vkDestroyDebugUtilsMessengerEXT(handle, b.get(0), null)) vkDestroyInstance(handle, null) private def getInstanceExtensions(stack: MemoryStack) = 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 963aa1cd..78b99dc4 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 @@ -57,7 +57,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)