From 24678fc4fab27fdef49de97a5070c8cb030c48d7 Mon Sep 17 00:00:00 2001 From: spamegg Date: Sun, 27 Jul 2025 14:43:59 +0300 Subject: [PATCH 01/22] Fs2 interop --- .github/workflows/ci.yml | 2 + build.sbt | 10 ++- .../cyfra/core/layout/LayoutStruct.scala | 4 +- .../cyfra/e2e/fs2interop/Fs2Tests.scala | 84 +++++++++++++++++ .../computenode/cyfra/fs2interop/Bridge.scala | 58 ++++++++++++ .../computenode/cyfra/fs2interop/GPipe.scala | 90 +++++++++++++++++++ 6 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/fs2interop/Fs2Tests.scala create mode 100644 cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/Bridge.scala create mode 100644 cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala 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 35a46c2c..d9e1687c 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) @@ -97,13 +99,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-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..f9bd8247 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 @@ -60,11 +60,11 @@ object LayoutStruct: ftype match case '[type tg <: GBuffer[?]; tg] => '{ - BufferRef[t](${ Expr(i) }, ${ tag.asExprOf[Tag[t]] })(using summon[Tag[t]], ${ fromExpr.asExprOf[FromExpr[t]] }) + BufferRef[t](${ Expr(i) }, ${ tag.asExprOf[Tag[t]] })(using ${ tag.asExprOf[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]] }) + UniformRef[t](${ Expr(i) }, ${ tag.asExprOf[Tag[t]] })(using ${ tag.asExprOf[Tag[t]] }, ${ fromExpr.asExprOf[FromExpr[t]] }) } val constructor = sym.primaryConstructor 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..2339a22f --- /dev/null +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/fs2interop/Fs2Tests.scala @@ -0,0 +1,84 @@ +package io.computenode.cyfra.e2e.fs2interop + +import io.computenode.cyfra.core.archive.*, mem.*, GMem.fRGBA +import io.computenode.cyfra.dsl.{*, given}, algebra.VectorAlgebra +import io.computenode.cyfra.fs2interop.*, GPipe.*, Bridge.given +import io.computenode.cyfra.core.CyfraRuntime +import io.computenode.cyfra.runtime.VkCyfraRuntime + +import fs2.{io as fs2io, *} + +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: CyfraRuntime = VkCyfraRuntime() + + test("fs2 through gPipeMap, just ints"): + val inSeq = (0 until 256).toSeq + val stream = Stream.emits(inSeq) + val pipe = gPipeMap[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 gPipeMap, floats and vectors"): + val inSeq = (0 to 255).map(_.toFloat).toSeq + val stream = Stream.emits(inSeq) + val pipe = gPipeMap[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)) + result + .zip(expected) + .foreach: (res, exp) => + assert(res.close(exp)(0.001f), s"Expected $exp, got $res") + +class Fs2LegacyTests extends munit.FunSuite: + given gc: GContext = GContext() + + 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-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..41d366c3 --- /dev/null +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/Bridge.scala @@ -0,0 +1,58 @@ +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 +import org.lwjgl.BufferUtils +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.asIntBuffer().put(chunk.toArray[Int]).flip() + inBuf + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[Int]): Array[Int] = + outBuf.asIntBuffer().get(arr).flip() + arr + + given Bridge[Float32, Float]: + def toByteBuffer(inBuf: ByteBuffer, chunk: Chunk[Float]): ByteBuffer = + inBuf.asFloatBuffer().put(chunk.toArray[Float]).flip() + inBuf + def fromByteBuffer(outBuf: ByteBuffer, arr: Array[Float]): Array[Float] = + outBuf.asFloatBuffer().get(arr).flip() + arr + + given Bridge[Vec4[Float32], fRGBA]: + def toByteBuffer(inBuf: ByteBuffer, chunk: Chunk[fRGBA]): ByteBuffer = + 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.flip() + 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 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..d3868465 --- /dev/null +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -0,0 +1,90 @@ +package io.computenode.cyfra.fs2interop + +import io.computenode.cyfra.core.archive.*, mem.*, GMem.fRGBA +import io.computenode.cyfra.core.{Allocation, layout}, layout.Layout +import io.computenode.cyfra.core.{CyfraRuntime, GBufferRegion, GExecution, GProgram} +import io.computenode.cyfra.dsl.{*, given}, gio.GIO, binding.GBuffer +import io.computenode.cyfra.spirv.SpirvTypes.typeStride +import struct.GStruct, GStruct.Empty, Empty.given + +import fs2.* +import java.nio.ByteBuffer +import org.lwjgl.BufferUtils +import izumi.reflect.Tag + +import scala.reflect.ClassTag + +object GPipe: + def gPipeMap[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 + case class PResult(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((params.inSize, 1, 1)), + ): layout => + val invocId = GIO.invocationId + val element = GIO.read[C1](layout.in, invocId) + GIO.write[C2](layout.out, invocId, f(element)) // implicit bug + + val execution = GExecution[Params, PLayout]() + .addProgram(gProg)(params => Params(params.inSize), layout => PLayout(layout.in, layout.out)) + + val region = GBufferRegion + .allocate[PLayout] // implicit bug + .map: pLayout => + execution.execute(params, pLayout) // implicit bug + + // these are allocated once, reused for many chunks + val inBuf = BufferUtils.createByteBuffer(params.inSize * inTypeSize) + val outBuf = BufferUtils.createByteBuffer(params.inSize * outTypeSize) + + stream + .chunkMin(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)) // implicit bug + Stream.emits(bridge2.fromByteBuffer(outBuf, new Array[S2](params.inSize))) + + // Syntax sugar for convenient single type version + def gPipeMap[F[_], C <: Value: FromExpr: Tag, S: ClassTag](f: C => C)(using CyfraRuntime, Bridge[C, S]): Pipe[F, S, S] = + gPipeMap[F, C, C, S, S](f) + + // legacy stuff working with GFunction + 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) From db2eb0c1094b7d5f30a6fd081ef1992a32919212 Mon Sep 17 00:00:00 2001 From: spamegg Date: Tue, 12 Aug 2025 18:51:13 +0300 Subject: [PATCH 02/22] progress on filter: upsweep and downsweep --- .../computenode/cyfra/fs2interop/GPipe.scala | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) 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 index d3868465..2e0e6a28 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -3,7 +3,7 @@ package io.computenode.cyfra.fs2interop import io.computenode.cyfra.core.archive.*, mem.*, GMem.fRGBA import io.computenode.cyfra.core.{Allocation, layout}, layout.Layout import io.computenode.cyfra.core.{CyfraRuntime, GBufferRegion, GExecution, GProgram} -import io.computenode.cyfra.dsl.{*, given}, gio.GIO, binding.GBuffer +import io.computenode.cyfra.dsl.{*, given}, gio.GIO, binding.{GBuffer, GUniform, GBinding} import io.computenode.cyfra.spirv.SpirvTypes.typeStride import struct.GStruct, GStruct.Empty, Empty.given @@ -33,15 +33,15 @@ object GPipe: ): layout => val invocId = GIO.invocationId val element = GIO.read[C1](layout.in, invocId) - GIO.write[C2](layout.out, invocId, f(element)) // implicit bug + GIO.write[C2](layout.out, invocId, f(element)) val execution = GExecution[Params, PLayout]() .addProgram(gProg)(params => Params(params.inSize), layout => PLayout(layout.in, layout.out)) val region = GBufferRegion - .allocate[PLayout] // implicit bug + .allocate[PLayout] .map: pLayout => - execution.execute(params, pLayout) // implicit bug + execution.execute(params, pLayout) // these are allocated once, reused for many chunks val inBuf = BufferUtils.createByteBuffer(params.inSize * inTypeSize) @@ -58,6 +58,73 @@ object GPipe: def gPipeMap[F[_], C <: Value: FromExpr: Tag, S: ClassTag](f: C => C)(using CyfraRuntime, Bridge[C, S]): Pipe[F, S, S] = gPipeMap[F, C, C, S, S](f) + // https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-39-parallel-prefix-sum-scan-cuda + // Prefix Sum and Stream Compaction + // 11 + // 012345678901 index + // [abcdefghijkl] starting collection + // [tfftfftttftf] convert to booleans + // [100100111010] integer equivalent + // [0111222345566] scan prefixsum, last number tells us the size of filtered collection + // [x..x..xxx.x.] take the indexes where the next scan number is 1 bigger. + // [adhijl] compact the collection + // 012345 prefixsum results are the new indices in compacted collection + def gPipeFilter[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]) => + case class Params(inSize: Int, intervalSize: Int) + case class PredLayout(cIn: GBuffer[C], boolOut: GBuffer[Int32]) extends Layout + case class ScanArgs(intervalSize: Int32) extends GStruct[ScanArgs] + case class ScanLayout(intIn: GBuffer[Int32], intOut: GBuffer[Int32], intervalSize: GUniform[ScanArgs]) extends Layout + case class CompactionLayout(cIn: GBuffer[C], intIn: GBuffer[Int32], out: GBuffer[C]) extends Layout + case class FilterResult(cOut: GBuffer[C]) extends Layout + + val predicateProgram = GProgram[Params, PredLayout]( + layout = params => PredLayout(cIn = GBuffer[C](params.inSize), boolOut = GBuffer[Int32](params.inSize)), + dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize, 1, 1)), + ): layout => + val invocId = GIO.invocationId + val element = GIO.read[C](layout.cIn, invocId) + GIO.write[Int32](layout.boolOut, invocId, when(pred(element))(1: Int32).otherwise(0)) + + val upsweep = GProgram[Params, ScanLayout]( + layout = params => + ScanLayout( + intIn = GBuffer[Int32](params.inSize / params.intervalSize), + intOut = GBuffer[Int32](params.inSize), + intervalSize = GUniform(ScanArgs(params.intervalSize)), + ), + dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / params.intervalSize, 1, 1)), + ): layout => + val ScanArgs(size) = layout.intervalSize.read + 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.intOut, end) + val addValue = GIO.read[Int32](layout.intOut, mid) + val newValue = oldValue + addValue + GIO.write[Int32](layout.intOut, end, newValue) + + val downsweep = GProgram[Params, ScanLayout]( + layout = params => + ScanLayout( + intIn = GBuffer[Int32](params.inSize / params.intervalSize), + intOut = GBuffer[Int32](params.inSize), + intervalSize = GUniform(ScanArgs(params.intervalSize)), + ), + dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / params.intervalSize, 1, 1)), + ): layout => + val ScanArgs(size) = layout.intervalSize.read + val invocId = GIO.invocationId + val root = invocId * size - 1 // if invocId = 0, this is -1 (out of bounds) + val mid = root + (size / 2) + val oldValue = GIO.read[Int32](layout.intOut, mid) + val addValue = when(root > 0)(GIO.read[Int32](layout.intOut, root)).otherwise(0) + val newValue = oldValue + addValue + GIO.write[Int32](layout.intOut, mid, newValue) + + ??? + // legacy stuff working with GFunction extension (stream: Stream[Pure, Float]) def gPipeFloat(fn: Float32 => Float32)(using GContext): Stream[Pure, Float] = From 386e101a30741ff66518c6d0993ef4a66a9ca6df Mon Sep 17 00:00:00 2001 From: spamegg Date: Wed, 13 Aug 2025 18:49:34 +0300 Subject: [PATCH 03/22] progress on upsweep / downsweep phases --- .../computenode/cyfra/fs2interop/GPipe.scala | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) 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 index 2e0e6a28..abc50120 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -61,21 +61,21 @@ object GPipe: // https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-39-parallel-prefix-sum-scan-cuda // Prefix Sum and Stream Compaction // 11 - // 012345678901 index - // [abcdefghijkl] starting collection - // [tfftfftttftf] convert to booleans - // [100100111010] integer equivalent - // [0111222345566] scan prefixsum, last number tells us the size of filtered collection - // [x..x..xxx.x.] take the indexes where the next scan number is 1 bigger. - // [adhijl] compact the collection - // 012345 prefixsum results are the new indices in compacted collection + // 012345678901 index + // [abcdefghijkl] starting collection + // [tfftfftttftf] convert to booleans + // [100100111010] integer equivalent + // [111222345566] prefixsum, last number tells us the size of filtered collection + // [x..x..xxx.x.] take the ones that are 1 bigger than previous. + // [adhijl] compact the collection + // 012345 (prefixsum result - 1) are the new indices in compacted collection def gPipeFilter[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]) => case class Params(inSize: Int, intervalSize: Int) case class PredLayout(cIn: GBuffer[C], boolOut: GBuffer[Int32]) extends Layout case class ScanArgs(intervalSize: Int32) extends GStruct[ScanArgs] case class ScanLayout(intIn: GBuffer[Int32], intOut: GBuffer[Int32], intervalSize: GUniform[ScanArgs]) extends Layout - case class CompactionLayout(cIn: GBuffer[C], intIn: GBuffer[Int32], out: GBuffer[C]) extends Layout + case class CompactLayout(cIn: GBuffer[C], intIn: GBuffer[Int32], out: GBuffer[C]) extends Layout case class FilterResult(cOut: GBuffer[C]) extends Layout val predicateProgram = GProgram[Params, PredLayout]( @@ -86,6 +86,9 @@ object GPipe: val element = GIO.read[C](layout.cIn, invocId) GIO.write[Int32](layout.boolOut, invocId, when(pred(element))(1: Int32).otherwise(0)) + val predicateExec = GExecution[Params, PredLayout]() + .addProgram(predicateProgram)(params => Params(256, 2), layout => PredLayout(layout.cIn, layout.boolOut)) + val upsweep = GProgram[Params, ScanLayout]( layout = params => ScanLayout( @@ -123,6 +126,31 @@ object GPipe: val newValue = oldValue + addValue GIO.write[Int32](layout.intOut, mid, newValue) + @annotation.tailrec + def upsweepPhases(exec: GExecution[Params, ScanLayout, ?], inSize: Int, intervalSize: Int): GExecution[Params, ScanLayout, ?] = + if intervalSize >= inSize then exec + else + val newExec = exec.addProgram(upsweep)(params => Params(inSize, intervalSize), layout => layout) + upsweepPhases(newExec, inSize, intervalSize * 2) + + val upsweepInitialExec = GExecution[Params, ScanLayout]() + val upsweepExec = upsweepPhases(upsweepInitialExec, 256, 2) + + @annotation.tailrec + def downsweepPhases(exec: GExecution[Params, ScanLayout, ?], inSize: Int, intervalSize: Int): GExecution[Params, ScanLayout, ?] = + if intervalSize < 2 then exec + else + val newExec = exec.addProgram(downsweep)(params => Params(inSize, intervalSize), layout => layout) + downsweepPhases(newExec, inSize, intervalSize / 2) + + val downsweepExec = downsweepPhases(upsweepInitialExec, 256, 128) + + val startParams = Params(256, 2) + // val region = GBufferRegion + // .allocate[ScanLayout] + // .map: region => + // upsweepExec.execute(startParams, region) + ??? // legacy stuff working with GFunction From 3ea8eb9e31d1966d86259d18ecb59cef98c5ceee Mon Sep 17 00:00:00 2001 From: spamegg Date: Sat, 16 Aug 2025 15:36:48 +0300 Subject: [PATCH 04/22] factor out legacy gPipe code --- .../cyfra/e2e/fs2interop/Fs2Tests.scala | 1 + .../computenode/cyfra/fs2interop/GPipe.scala | 136 ++++++++---------- .../cyfra/fs2interop/GPipeLegacy.scala | 39 +++++ 3 files changed, 101 insertions(+), 75 deletions(-) create mode 100644 cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipeLegacy.scala 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 index 2339a22f..20c04b9d 100644 --- 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 @@ -42,6 +42,7 @@ class Fs2Tests extends munit.FunSuite: class Fs2LegacyTests extends munit.FunSuite: given gc: GContext = GContext() + import GPipeLegacy.* test("fs2 Float stream (legacy)"): val inSeq = (0 to 255).map(_.toFloat) 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 index abc50120..439973d6 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -51,7 +51,7 @@ object GPipe: .chunkMin(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)) // implicit bug + 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))) // Syntax sugar for convenient single type version @@ -71,31 +71,31 @@ object GPipe: // 012345 (prefixsum result - 1) are the new indices in compacted collection def gPipeFilter[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]) => - case class Params(inSize: Int, intervalSize: Int) - case class PredLayout(cIn: GBuffer[C], boolOut: GBuffer[Int32]) extends Layout - case class ScanArgs(intervalSize: Int32) extends GStruct[ScanArgs] - case class ScanLayout(intIn: GBuffer[Int32], intOut: GBuffer[Int32], intervalSize: GUniform[ScanArgs]) extends Layout - case class CompactLayout(cIn: GBuffer[C], intIn: GBuffer[Int32], out: GBuffer[C]) extends Layout - case class FilterResult(cOut: GBuffer[C]) extends Layout - val predicateProgram = GProgram[Params, PredLayout]( - layout = params => PredLayout(cIn = GBuffer[C](params.inSize), boolOut = GBuffer[Int32](params.inSize)), + // 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((params.inSize, 1, 1)), ): layout => val invocId = GIO.invocationId - val element = GIO.read[C](layout.cIn, invocId) - GIO.write[Int32](layout.boolOut, invocId, when(pred(element))(1: Int32).otherwise(0)) - - val predicateExec = GExecution[Params, PredLayout]() - .addProgram(predicateProgram)(params => Params(256, 2), layout => PredLayout(layout.cIn, layout.boolOut)) - - val upsweep = GProgram[Params, ScanLayout]( - layout = params => - ScanLayout( - intIn = GBuffer[Int32](params.inSize / params.intervalSize), - intOut = GBuffer[Int32](params.inSize), - intervalSize = GUniform(ScanArgs(params.intervalSize)), - ), + val element = GIO.read[C](layout.in, invocId) + val result = when(pred(element))(1: Int32).otherwise(0) + GIO.write[Int32](layout.out, invocId, result) + + val predExec = GExecution[PredParams, PredLayout]() + .addProgram(predicateProgram)(params => params, layout => layout) + val predParams = PredParams(256) + + // 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]) 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((params.inSize / params.intervalSize, 1, 1)), ): layout => val ScanArgs(size) = layout.intervalSize.read @@ -103,83 +103,69 @@ object GPipe: val root = invocId * size val mid = root + (size / 2) - 1 val end = root + size - 1 - val oldValue = GIO.read[Int32](layout.intOut, end) - val addValue = GIO.read[Int32](layout.intOut, mid) + val oldValue = GIO.read[Int32](layout.ints, end) + val addValue = GIO.read[Int32](layout.ints, mid) val newValue = oldValue + addValue - GIO.write[Int32](layout.intOut, end, newValue) - - val downsweep = GProgram[Params, ScanLayout]( - layout = params => - ScanLayout( - intIn = GBuffer[Int32](params.inSize / params.intervalSize), - intOut = GBuffer[Int32](params.inSize), - intervalSize = GUniform(ScanArgs(params.intervalSize)), - ), + GIO.write[Int32](layout.ints, end, newValue) + + val downsweep = GProgram[ScanParams, ScanLayout]( + layout = params => ScanLayout(ints = GBuffer[Int32](params.inSize), intervalSize = GUniform(ScanArgs(params.intervalSize))), dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / params.intervalSize, 1, 1)), ): layout => val ScanArgs(size) = layout.intervalSize.read val invocId = GIO.invocationId val root = invocId * size - 1 // if invocId = 0, this is -1 (out of bounds) val mid = root + (size / 2) - val oldValue = GIO.read[Int32](layout.intOut, mid) - val addValue = when(root > 0)(GIO.read[Int32](layout.intOut, root)).otherwise(0) + val oldValue = GIO.read[Int32](layout.ints, mid) + val addValue = when(root > 0)(GIO.read[Int32](layout.ints, root)).otherwise(0) val newValue = oldValue + addValue - GIO.write[Int32](layout.intOut, mid, newValue) + GIO.write[Int32](layout.ints, mid, newValue) @annotation.tailrec - def upsweepPhases(exec: GExecution[Params, ScanLayout, ?], inSize: Int, intervalSize: Int): GExecution[Params, ScanLayout, ?] = + def upsweepPhases(exec: GExecution[ScanParams, ScanLayout, ?], inSize: Int, intervalSize: Int): GExecution[ScanParams, ScanLayout, ?] = if intervalSize >= inSize then exec else - val newExec = exec.addProgram(upsweep)(params => Params(inSize, intervalSize), layout => layout) + val newExec = exec.addProgram(upsweep)(params => ScanParams(inSize, intervalSize), layout => layout) upsweepPhases(newExec, inSize, intervalSize * 2) - val upsweepInitialExec = GExecution[Params, ScanLayout]() + val upsweepInitialExec = GExecution[ScanParams, ScanLayout]() val upsweepExec = upsweepPhases(upsweepInitialExec, 256, 2) @annotation.tailrec - def downsweepPhases(exec: GExecution[Params, ScanLayout, ?], inSize: Int, intervalSize: Int): GExecution[Params, ScanLayout, ?] = + def downsweepPhases(exec: GExecution[ScanParams, ScanLayout, ?], inSize: Int, intervalSize: Int): GExecution[ScanParams, ScanLayout, ?] = if intervalSize < 2 then exec else - val newExec = exec.addProgram(downsweep)(params => Params(inSize, intervalSize), layout => layout) + val newExec = exec.addProgram(downsweep)(params => ScanParams(inSize, intervalSize), layout => layout) downsweepPhases(newExec, inSize, intervalSize / 2) val downsweepExec = downsweepPhases(upsweepInitialExec, 256, 128) + val scanParams = ScanParams(256, 2) - val startParams = Params(256, 2) - // val region = GBufferRegion - // .allocate[ScanLayout] - // .map: region => - // upsweepExec.execute(startParams, region) + // Stream compaction + case class CompactLayout(in: GBuffer[C], intIn: GBuffer[Int32], out: GBuffer[C]) extends Layout + case class FilterResult(cOut: GBuffer[C]) extends Layout + // TODO implement compaction - ??? + // TODO: connect all the layouts/executions into one + case class PredScanCompactLayout() extends Layout + case class Result(out: GBuffer[C]) extends Layout - // legacy stuff working with GFunction - 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) + val region = GBufferRegion // TODO fix this! + .allocate[PredLayout] + .map: layout => + predExec.execute(predParams, layout) - 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) + val typeSize = typeStride(Tag.apply[C]) - 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) + // these are allocated once, reused for many chunks + val inBuf = BufferUtils.createByteBuffer(predParams.inSize * typeSize) + val outBuf = BufferUtils.createByteBuffer(predParams.inSize * typeSize) // TODO wrong size + + // stream + // .chunkMin(predParams.inSize) + // .flatMap: chunk => + // bridge.toByteBuffer(inBuf, chunk) + // region.runUnsafe(init = PredLayout(in = GBuffer[C](inBuf), out = GBuffer[Int32](outBuf)), onDone = layout => layout.out.read(outBuf)) + // val arr = bridge.fromByteBuffer(outBuf, new Array[S](params.inSize)) + // 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) From 24c333a7fe436ff7efc7f15434c28d66864144d1 Mon Sep 17 00:00:00 2001 From: spamegg Date: Mon, 18 Aug 2025 18:34:04 +0300 Subject: [PATCH 05/22] outline / sketch compaction and filter, add filter test --- .../cyfra/e2e/fs2interop/Fs2Tests.scala | 21 +++-- .../computenode/cyfra/fs2interop/GPipe.scala | 83 +++++++++---------- 2 files changed, 56 insertions(+), 48 deletions(-) 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 index 20c04b9d..daba84cd 100644 --- 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 @@ -2,7 +2,7 @@ package io.computenode.cyfra.e2e.fs2interop import io.computenode.cyfra.core.archive.*, mem.*, GMem.fRGBA import io.computenode.cyfra.dsl.{*, given}, algebra.VectorAlgebra -import io.computenode.cyfra.fs2interop.*, GPipe.*, Bridge.given +import io.computenode.cyfra.fs2interop.*, Bridge.given import io.computenode.cyfra.core.CyfraRuntime import io.computenode.cyfra.runtime.VkCyfraRuntime @@ -18,10 +18,10 @@ extension (f: fRGBA) class Fs2Tests extends munit.FunSuite: given cr: CyfraRuntime = VkCyfraRuntime() - test("fs2 through gPipeMap, just ints"): + test("fs2 through GPipe map, just ints"): val inSeq = (0 until 256).toSeq val stream = Stream.emits(inSeq) - val pipe = gPipeMap[Pure, Int32, Int](_ + 1) + val pipe = GPipe.map[Pure, Int32, Int](_ + 1) val result = stream.through(pipe).compile.toList val expected = inSeq.map(_ + 1) result @@ -29,10 +29,10 @@ class Fs2Tests extends munit.FunSuite: .foreach: (res, exp) => assert(res == exp, s"Expected $exp, got $res") - test("fs2 through gPipeMap, floats and vectors"): + test("fs2 through GPipe map, floats and vectors"): val inSeq = (0 to 255).map(_.toFloat).toSeq val stream = Stream.emits(inSeq) - val pipe = gPipeMap[Pure, Float32, Vec4[Float32], Float, fRGBA](f => (f, f + 1f, f + 2f, f + 3f)) + 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)) result @@ -40,6 +40,17 @@ class Fs2Tests extends munit.FunSuite: .foreach: (res, exp) => assert(res.close(exp)(0.001f), s"Expected $exp, got $res") + test("fs2 through GPipe filter, just ints"): + val inSeq = (0 until 256).toSeq + val stream = Stream.emits(inSeq) + val pipe = GPipe.filter[Pure, Int32, Int](_.mod(2) === 0) + val result = stream.through(pipe).compile.toList + val expected = inSeq.filter(_ % 2 == 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.* 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 index 439973d6..92452737 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -1,6 +1,5 @@ package io.computenode.cyfra.fs2interop -import io.computenode.cyfra.core.archive.*, mem.*, GMem.fRGBA import io.computenode.cyfra.core.{Allocation, layout}, layout.Layout import io.computenode.cyfra.core.{CyfraRuntime, GBufferRegion, GExecution, GProgram} import io.computenode.cyfra.dsl.{*, given}, gio.GIO, binding.{GBuffer, GUniform, GBinding} @@ -15,13 +14,12 @@ import izumi.reflect.Tag import scala.reflect.ClassTag object GPipe: - def gPipeMap[F[_], C1 <: Value: FromExpr: Tag, C2 <: Value: FromExpr: Tag, S1: ClassTag, S2: ClassTag]( + 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 - case class PResult(out: GBuffer[C2]) extends Layout val params = Params(inSize = 256) val inTypeSize = typeStride(Tag.apply[C1]) @@ -54,24 +52,12 @@ object GPipe: 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))) - // Syntax sugar for convenient single type version - def gPipeMap[F[_], C <: Value: FromExpr: Tag, S: ClassTag](f: C => C)(using CyfraRuntime, Bridge[C, S]): Pipe[F, S, S] = - gPipeMap[F, C, C, S, S](f) - - // https://developer.nvidia.com/gpugems/gpugems3/part-vi-gpu-computing/chapter-39-parallel-prefix-sum-scan-cuda - // Prefix Sum and Stream Compaction - // 11 - // 012345678901 index - // [abcdefghijkl] starting collection - // [tfftfftttftf] convert to booleans - // [100100111010] integer equivalent - // [111222345566] prefixsum, last number tells us the size of filtered collection - // [x..x..xxx.x.] take the ones that are 1 bigger than previous. - // [adhijl] compact the collection - // 012345 (prefixsum result - 1) are the new indices in compacted collection - def gPipeFilter[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]) => + // 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]) => // Predicate mapping case class PredParams(inSize: Int) case class PredLayout(in: GBuffer[C], out: GBuffer[Int32]) extends Layout @@ -85,9 +71,9 @@ object GPipe: val result = when(pred(element))(1: Int32).otherwise(0) GIO.write[Int32](layout.out, invocId, result) + val predParams = PredParams(256) val predExec = GExecution[PredParams, PredLayout]() .addProgram(predicateProgram)(params => params, layout => layout) - val predParams = PredParams(256) // Prefix sum (inclusive), upsweep/downsweep case class ScanParams(inSize: Int, intervalSize: Int) @@ -114,10 +100,10 @@ object GPipe: ): layout => val ScanArgs(size) = layout.intervalSize.read val invocId = GIO.invocationId - val root = invocId * size - 1 // if invocId = 0, this is -1 (out of bounds) - val mid = root + (size / 2) + 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(root > 0)(GIO.read[Int32](layout.ints, root)).otherwise(0) + val addValue = when(end > 0)(GIO.read[Int32](layout.ints, end)).otherwise(0) val newValue = oldValue + addValue GIO.write[Int32](layout.ints, mid, newValue) @@ -141,31 +127,42 @@ object GPipe: val downsweepExec = downsweepPhases(upsweepInitialExec, 256, 128) val scanParams = ScanParams(256, 2) - // Stream compaction - case class CompactLayout(in: GBuffer[C], intIn: GBuffer[Int32], out: GBuffer[C]) extends Layout - case class FilterResult(cOut: GBuffer[C]) extends Layout // TODO implement compaction + case class CompactParams() + case class CompactLayout(in: GBuffer[C], intIn: GBuffer[Int32], out: GBuffer[C]) extends Layout + val compactedStreamSize: Int = ??? // TODO how to get this out of prefix sum results + + val compactProgram = GProgram[CompactParams, CompactLayout](layout = params => ???, dispatch = (layout, params) => ???): compactLayout => + ??? + val compactExec = GExecution[CompactParams, CompactLayout]() // TODO: connect all the layouts/executions into one - case class PredScanCompactLayout() extends Layout - case class Result(out: GBuffer[C]) extends Layout + case class PredScanCompactParams(inSize: Int) + case class PredScanCompactLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C]) extends Layout + + val predScanCompactExec = GExecution[PredScanCompactParams, PredScanCompactLayout]() // TODO + val predScanCompactParams = PredScanCompactParams(256) // TODO - val region = GBufferRegion // TODO fix this! - .allocate[PredLayout] + val region = GBufferRegion + .allocate[PredScanCompactLayout] .map: layout => - predExec.execute(predParams, layout) + predScanCompactExec.execute(predScanCompactParams, layout) val typeSize = typeStride(Tag.apply[C]) + val intSize = typeStride(Tag.apply[Int32]) // these are allocated once, reused for many chunks - val inBuf = BufferUtils.createByteBuffer(predParams.inSize * typeSize) - val outBuf = BufferUtils.createByteBuffer(predParams.inSize * typeSize) // TODO wrong size - - // stream - // .chunkMin(predParams.inSize) - // .flatMap: chunk => - // bridge.toByteBuffer(inBuf, chunk) - // region.runUnsafe(init = PredLayout(in = GBuffer[C](inBuf), out = GBuffer[Int32](outBuf)), onDone = layout => layout.out.read(outBuf)) - // val arr = bridge.fromByteBuffer(outBuf, new Array[S](params.inSize)) - // Stream.emits(arr) - ??? + val predBuf = BufferUtils.createByteBuffer(predParams.inSize * typeSize) + val scanBuf = BufferUtils.createByteBuffer(predParams.inSize * intSize) + val compactBuf = BufferUtils.createByteBuffer(compactedStreamSize * typeSize) + + stream + .chunkMin(predScanCompactParams.inSize) + .flatMap: chunk => + bridge.toByteBuffer(predBuf, chunk) + region.runUnsafe( + init = PredScanCompactLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf)), + onDone = layout => layout.out.read(compactBuf), + ) + val arr = bridge.fromByteBuffer(compactBuf, new Array[S](compactedStreamSize)) + Stream.emits(arr) From 278bbb0c00f548952c2b2819da1548dd66f62a9c Mon Sep 17 00:00:00 2001 From: spamegg Date: Wed, 20 Aug 2025 11:17:05 +0300 Subject: [PATCH 06/22] some comments --- .../main/scala/io/computenode/cyfra/fs2interop/GPipe.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 index 92452737..8bfbcfa9 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -140,7 +140,11 @@ object GPipe: case class PredScanCompactParams(inSize: Int) case class PredScanCompactLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C]) extends Layout - val predScanCompactExec = GExecution[PredScanCompactParams, PredScanCompactLayout]() // TODO + // How to "connect" many GExecutions? with flatMap? + val predScanCompactExec = GExecution[PredScanCompactParams, PredScanCompactLayout]() + // predExec + // .flatMap(predLayout => ???) // add downsweepExec + // .flatMap(layout => ???) // add compactExec val predScanCompactParams = PredScanCompactParams(256) // TODO val region = GBufferRegion From c72e45cd56c2a6c56021ea39223538ed5bad91cf Mon Sep 17 00:00:00 2001 From: Szymon Date: Thu, 21 Aug 2025 00:52:56 +0200 Subject: [PATCH 07/22] GIO and Layouts WIP --- .../io/computenode/cyfra/spirv/Context.scala | 6 +- .../cyfra/spirv/compilers/DSLCompiler.scala | 12 +- .../spirv/compilers/ExpressionCompiler.scala | 19 +-- .../compilers/SpirvProgramCompiler.scala | 111 +++++++------ .../computenode/cyfra/core/GExecution.scala | 1 - .../cyfra/core/archive/GContext.scala | 1 - .../io/computenode/cyfra/dsl/Expression.scala | 3 +- .../cyfra/dsl/collections/GArray.scala | 14 -- .../cyfra/dsl/collections/GArray2D.scala | 12 -- .../cyfra/samples/TestingStuff.scala | 154 ------------------ .../cyfra/runtime/ExecutionHandler.scala | 2 +- 11 files changed, 77 insertions(+), 258 deletions(-) delete mode 100644 cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray.scala delete mode 100644 cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala 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..20ffd53a 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 @@ -16,11 +16,11 @@ private[cyfra] case class Context( voidTypeRef: Int = -1, voidFuncTypeRef: Int = -1, workerIndexRef: Int = -1, - uniformVarRef: Int = -1, + uniformVarRefs: Map[Int, 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: List[ArrayBufferBlock] = List(), nextResultId: Int = HEADER_REFS_TOP, nextBinding: Int = 0, exprNames: Map[Int, String] = Map(), 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..fc2b30bd 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 @@ -47,23 +47,23 @@ private[cyfra] object DSLCompiler: allScopesCache(root.treeid) = result result - def compile(tree: Value, inTypes: List[Tag[?]], outTypes: List[Tag[?]], uniformSchema: GStructSchema[?]): ByteBuffer = + def compile(tree: Value, buffers: List[Tag[?]], uniformSchemaIns: List[GStructSchema[?]]): ByteBuffer = val treeExpr = tree.tree val allExprs = getAllExprsFlattened(treeExpr, visitDetached = true) val typesInCode = allExprs.map(_.tag).distinct - val allTypes = (typesInCode ::: inTypes ::: outTypes).distinct + val allTypes = (typesInCode ::: buffers).distinct def scalarTypes = allTypes.filter(_.tag <:< summon[Tag[Scalar]].tag) val (typeDefs, typedContext) = defineScalarTypes(scalarTypes, Context.initialContext) val structsInCode = (allExprs.collect { case cs: ComposeStruct[?] => cs.resultSchema case gf: GetField[?, ?] => gf.resultSchema - } :+ uniformSchema).distinct + } ::: uniformSchemaIns).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) + val (decorations, uniformDefs, uniformContext) = initAndDecorateBuffers(buffers, structCtx) + val (uniformStructDecorations, uniformStructInsns, uniformStructContext) = createAndInitUniformBlocks(uniformSchemaIns, uniformContext) + val blockNames = getBlockNames(uniformContext, uniformSchemaIns) val (inputDefs, inputContext) = createInvocationId(uniformStructContext) val (constDefs, constCtx) = defineConstants(allExprs, inputContext) val (varDefs, varCtx) = defineVarNames(constCtx) 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..5d99da3a 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 @@ -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 @ WorkerIndex => + (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 @ Binding(id) => + (Nil, ctx.copy(exprRefs = ctx.exprRefs + (d.treeid -> ctx.uniformVarRefs(id)))) case c: ConvertExpression[?, ?] => compileConvertExpression(c, ctx) @@ -293,7 +289,7 @@ private[cyfra] object ExpressionCompiler: case fc: FunctionCall[?] => compileFunctionCall(fc, ctx) - case ga @ GArrayElem(index, i) => + case ReadBuffer(index, i) => val instructions = List( Instruction( Op.OpAccessChain, @@ -330,14 +326,15 @@ private[cyfra] object ExpressionCompiler: ) val updatedContext = ctx.copy(exprRefs = ctx.exprRefs + (cs.treeid -> ctx.nextResultId), nextResultId = ctx.nextResultId + 1) (insns, updatedContext) - case gf @ GetField(dynamic @ Dynamic(UniformStructRefTag), fieldIndex) => + + case gf @ GetField(binding @ Binding(bindingId), 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(bindingId)), ResultRef(ctx.constRefs((Int32Tag, gf.fieldIndex))), ), ), 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..1aea6fdb 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 @@ -101,13 +101,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,8 +119,14 @@ private[cyfra] object SpirvProgramCompiler: ), ) (definitionInstructions, context.copy(nextResultId = context.nextResultId + 3)) + def initAndDecorateBuffers(buffers: List[Tag[?]], 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) = + + def createAndInitBlocks(blocks: List[Tag[?]], 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) @@ -146,7 +146,7 @@ private[cyfra] object SpirvProgramCompiler: ) val contextWithBlock = - if in then ctx.copy(inBufferBlocks = block :: ctx.inBufferBlocks) else ctx.copy(outBufferBlocks = block :: ctx.outBufferBlocks) + ctx.copy(bufferBlocks = block :: ctx.bufferBlocks) ( decAcc ::: decorationInstructions, insnAcc ::: definitionInstructions, @@ -155,58 +155,61 @@ private[cyfra] object SpirvProgramCompiler: } (decoration, definition, newContext) - def getBlockNames(context: Context, uniformSchema: GStructSchema[?]): List[Words] = + def getBlockNames(context: Context, uniformSchemas: List[GStructSchema[?]]): 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))) + 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 createAndInitUniformBlocks(schemas: List[GStructSchema[?]], ctx: Context): (List[Words], List[Words], Context) = + schemas.foldLeft((List.empty[Words], List.empty[Words], ctx)) { case ((decorationsAcc, definitionsAcc, currentCtx), schema) => + val uniformStructTypeRef = currentCtx.valueTypeMap(schema.structTag.tag) + val currentBinding = currentCtx.nextBinding + + val (offsetDecorations, _) = 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) + + val uniformBlockDecoration = 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(currentBinding))) + + val newDecorations = decorationsAcc ::: offsetDecorations ::: List(uniformDecorateDescriptorSet, uniformDecorateBinding, uniformBlockDecoration) + val newDefinitions = definitionsAcc ::: List(uniformPointerUniform, uniformVar) + val newCtx = currentCtx.copy( + nextResultId = currentCtx.nextResultId + 2, + nextBinding = currentCtx.nextBinding + 1, + uniformVarRefs = currentCtx.uniformVarRefs + (currentBinding -> uniformVarRef), + uniformPointerMap = currentCtx.uniformPointerMap + (uniformStructTypeRef -> uniformPointerUniformRef), + bindingToStructType = currentCtx.bindingToStructType + (currentBinding -> uniformStructTypeRef) + ) - ( - 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), - ), - ) + (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/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/archive/GContext.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/archive/GContext.scala index 9f209ea4..59a04927 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 @@ -9,7 +9,6 @@ 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-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/collections/GArray.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray.scala deleted file mode 100644 index 6e9daf13..00000000 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray.scala +++ /dev/null @@ -1,14 +0,0 @@ -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.macros.Source -import io.computenode.cyfra.dsl.{Expression, Value} -import izumi.reflect.Tag - -case class GArray[T <: Value: {Tag, FromExpr}](index: Int): - def at(i: Int32)(using Source): T = - summon[FromExpr[T]].fromExpr(GArrayElem(index, i.tree)) - -object GArray: - case class GArrayElem[T <: Value: Tag](index: Int, i: Expression[Int32]) extends Expression[T] 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 deleted file mode 100644 index 70d6df19..00000000 --- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala +++ /dev/null @@ -1,12 +0,0 @@ -package io.computenode.cyfra.dsl.collections - -import io.computenode.cyfra.dsl.Value -import io.computenode.cyfra.dsl.Value.Int32 -import io.computenode.cyfra.dsl.algebra.ScalarAlgebra.{*, given} -import io.computenode.cyfra.dsl.macros.Source -import izumi.reflect.Tag -import io.computenode.cyfra.dsl.Value.FromExpr - -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-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala index 8b9a5014..cd2fc227 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 @@ -122,157 +122,3 @@ object TestingStuff: .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) - - 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))), - ) - - @main - def testAddProgram10Times = - 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 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 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)) - }, - ) - 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") 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..00855e4e 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 @@ -75,7 +75,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) From 27888dc6e97ae191c456d7c0fcaea0b94021a61e Mon Sep 17 00:00:00 2001 From: Szymon Date: Sat, 23 Aug 2025 15:50:08 +0200 Subject: [PATCH 08/22] WIP2 --- .../cyfra/spirv/compilers/GIOCompiler.scala | 4 + .../compilers/SpirvProgramCompiler.scala | 6 +- cyfra-e2e-test/src/test/resources/addOne.comp | 48 ++++ .../src/test/resources/compileAll.ps1 | 4 + .../src/test/resources/compileAll.sh | 7 + cyfra-e2e-test/src/test/resources/emit.comp | 23 ++ cyfra-e2e-test/src/test/resources/filter.comp | 20 ++ .../cyfra/e2e/RuntimeEnduranceTest.scala | 221 ++++++++++++++++++ 8 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GIOCompiler.scala create mode 100644 cyfra-e2e-test/src/test/resources/addOne.comp create mode 100644 cyfra-e2e-test/src/test/resources/compileAll.ps1 create mode 100644 cyfra-e2e-test/src/test/resources/compileAll.sh create mode 100644 cyfra-e2e-test/src/test/resources/emit.comp create mode 100644 cyfra-e2e-test/src/test/resources/filter.comp create mode 100644 cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/RuntimeEnduranceTest.scala 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..b291168a --- /dev/null +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GIOCompiler.scala @@ -0,0 +1,4 @@ +package io.computenode.cyfra.spirv.compilers + +object GIOCompiler: + \ No newline at end of file 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 1aea6fdb..e7330f66 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,7 @@ 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.gio.GIO import io.computenode.cyfra.dsl.struct.{GStructConstructor, GStructSchema} import io.computenode.cyfra.spirv.Context import io.computenode.cyfra.spirv.SpirvConstants.* @@ -18,7 +19,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(body: GIO[?], resultType: Tag[?], 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))), @@ -160,7 +161,8 @@ private[cyfra] object SpirvProgramCompiler: 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")) + //context.inBufferBlocks.flatMap(namesForBlock(_, "In")) ::: context.outBufferBlocks.flatMap(namesForBlock(_, "Out")) + List() def totalStride(gs: GStructSchema[?]): Int = gs.fields .map: 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..35ad5bb2 --- /dev/null +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/RuntimeEnduranceTest.scala @@ -0,0 +1,221 @@ +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.{*, given} +import io.computenode.cyfra.runtime.VkCyfraRuntime +import io.computenode.cyfra.utility.Logger.logger +import org.lwjgl.BufferUtils +import org.lwjgl.system.MemoryUtil + +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 () + + 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() + + 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") From 21494b8d4f9818310dcf3e7a7940f89bfc531188 Mon Sep 17 00:00:00 2001 From: Szymon Date: Sat, 23 Aug 2025 22:50:51 +0200 Subject: [PATCH 09/22] WIP3 --- .../io/computenode/cyfra/spirv/Context.scala | 3 +- .../cyfra/spirv/compilers/DSLCompiler.scala | 40 ++++++++++++--- .../spirv/compilers/ExpressionCompiler.scala | 10 ++-- .../cyfra/spirv/compilers/GIOCompiler.scala | 51 ++++++++++++++++++- .../compilers/SpirvProgramCompiler.scala | 39 +++++++------- .../io/computenode/cyfra/core/GProgram.scala | 1 - .../computenode/cyfra/core/GioProgram.scala | 7 +-- .../computenode/cyfra/core/SpirvProgram.scala | 16 +----- .../cyfra/core/archive/GContext.scala | 1 - .../cyfra/dsl/binding/GBinding.scala | 11 ++-- .../cyfra/dsl/binding/WriteBuffer.scala | 5 +- .../cyfra/dsl/binding/WriteUniform.scala | 5 +- .../cyfra/dsl/collections/GArray.scala | 13 +++++ .../cyfra/dsl/collections/GArray2D.scala | 13 +++++ .../io/computenode/cyfra/dsl/gio/GIO.scala | 28 +++++----- .../computenode/cyfra/runtime/VkShader.scala | 7 ++- 16 files changed, 170 insertions(+), 80 deletions(-) create mode 100644 cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray.scala create mode 100644 cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala 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 20ffd53a..35b39a46 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 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 @@ -20,7 +21,7 @@ private[cyfra] case class Context( bindingToStructType: Map[Int, Int] = Map.empty, constRefs: Map[(Tag[?], Any), Int] = Map(), exprRefs: Map[Int, Int] = Map(), - bufferBlocks: List[ArrayBufferBlock] = List(), + bufferBlocks: Map[GBuffer[?], ArrayBufferBlock] = Map(), nextResultId: Int = HEADER_REFS_TOP, nextBinding: Int = 0, exprNames: Map[Int, String] = Map(), 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 fc2b30bd..f15c9d54 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,25 @@ 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), 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) + // TODO: Not traverse same fn scopes for each fn call private def getAllExprsFlattened(root: E[?], visitDetached: Boolean): List[E[?]] = var blockI = 0 @@ -33,7 +54,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,18 +68,22 @@ private[cyfra] object DSLCompiler: allScopesCache(root.treeid) = result result - def compile(tree: Value, buffers: List[Tag[?]], uniformSchemaIns: List[GStructSchema[?]]): ByteBuffer = - val treeExpr = tree.tree - val allExprs = getAllExprsFlattened(treeExpr, visitDetached = true) + 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 ::: buffers).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 (buffers, uniforms) = bindings.partition: + case _: GBuffer[?] => true + case _: GUniform[?] => false + .asInstanceOf[(List[GBinding[?]], List[GUniform[?]])] + val uniformSchemas = uniforms.map(_.schema) val structsInCode = (allExprs.collect { case cs: ComposeStruct[?] => cs.resultSchema case gf: GetField[?, ?] => gf.resultSchema - } ::: uniformSchemaIns).distinct + } ::: uniformSchemas).distinct val (structDefs, structCtx) = defineStructTypes(structsInCode, typedContext) val structNames = getStructNames(structsInCode, structCtx) val (decorations, uniformDefs, uniformContext) = initAndDecorateBuffers(buffers, structCtx) @@ -67,8 +92,7 @@ private[cyfra] object DSLCompiler: 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) 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 5d99da3a..7e7cb849 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} @@ -289,19 +289,19 @@ private[cyfra] object ExpressionCompiler: case fc: FunctionCall[?] => compileFunctionCall(fc, ctx) - case ReadBuffer(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) 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 index b291168a..c64b9cae 100644 --- 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 @@ -1,4 +1,53 @@ 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.SpirvTypes.{Int32Tag, LInt32Tag} + object GIOCompiler: - \ No newline at end of file + + 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 insns = List(Instruction( + Op.OpAccessChain, + List( + ResultRef(ctx.uniformPointerMap(ctx.valueTypeMap(buffer.tag.tag))), + ResultRef(ctx.nextResultId), + ResultRef(ctx.bufferBlocks(buffer).blockVarRef), + ResultRef(ctx.constRefs((Int32Tag, 0))), + ResultRef(ctx.workerIndexRef), + ), + ), + Instruction(Op.OpStore, List(ResultRef(ctx.nextResultId), ResultRef(ctx.exprRefs(value.tree.treeid)))) + ) + val updatedCtx = ctx.copy(nextResultId = ctx.nextResultId + 1) + (acc ::: insns, updatedCtx) + + case GIO.Repeat(n, f) => + val (nInsts, ctxWithN) = ExpressionCompiler.compileBlock(n.tree, ctx) + val loopHeaderId = ctxWithN.nextResultId + val loopMergeId = ctxWithN.nextResultId + 1 + val loopContinueId = ctxWithN.nextResultId + 2 + val counterVarId = ctxWithN.nextResultId + 3 + val ctxWithCounter = ctxWithN.copy(exprRefs = ctxWithN.exprRefs + (CurrentRepeatIndex.treeid -> counterVarId), nextResultId = ctxWithN.nextResultId + 4) + val counterInit = Instruction(Op.OpVariable, List(ResultRef(ctxWithCounter.constRefs((Int32Tag, 0))), ResultRef(counterVarId), StorageClass.Function)) + val counterStore = Instruction(Op.OpStore, List(ResultRef(counterVarId), ResultRef(ctxWithCounter.constRefs((Int32Tag, 0))))) + val (body, afterBodyCtx) = compileGio(f, ctxWithCounter) + // todo + ??? + + + + + + + \ No newline at end of file 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 e7330f66..73ebf1bf 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,7 @@ 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 import io.computenode.cyfra.dsl.gio.GIO import io.computenode.cyfra.dsl.struct.{GStructConstructor, GStructSchema} import io.computenode.cyfra.spirv.Context @@ -19,7 +20,7 @@ private[cyfra] object SpirvProgramCompiler: case Instruction(Op.OpVariable, _) => true case _ => false - def compileMain(body: GIO[?], 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))), @@ -39,22 +40,23 @@ 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)))), +// TODO Remove - Write is a part of GIO now +// 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()), ) @@ -120,15 +122,14 @@ private[cyfra] object SpirvProgramCompiler: ), ) (definitionInstructions, context.copy(nextResultId = context.nextResultId + 3)) - def initAndDecorateBuffers(buffers: List[Tag[?]], context: Context): (List[Words], List[Words], Context) = + def initAndDecorateBuffers(buffers: List[GBuffer[?]], 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[?]], context: Context): (List[Words], List[Words], Context) = - val (decoration, definition, newContext) = blocks.foldLeft((List[Words](), List[Words](), context)) { case ((decAcc, insnAcc, ctx), tpe) => + def createAndInitBlocks(blocks: List[GBuffer[?]], context: Context): (List[Words], List[Words], Context) = + val (decoration, definition, newContext) = blocks.foldLeft((List[Words](), List[Words](), context)) { case ((decAcc, insnAcc, ctx), buff) => + val tpe = buff.tag val block = ArrayBufferBlock(ctx.nextResultId, ctx.nextResultId + 1, ctx.nextResultId + 2, ctx.nextResultId + 3, ctx.nextBinding) val decorationInstructions = List[Words]( @@ -147,7 +148,7 @@ private[cyfra] object SpirvProgramCompiler: ) val contextWithBlock = - ctx.copy(bufferBlocks = block :: ctx.bufferBlocks) + ctx.copy(bufferBlocks = ctx.bufferBlocks + (buff -> block)) ( decAcc ::: decorationInstructions, insnAcc ::: definitionInstructions, 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..d1a43226 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 @@ -16,7 +16,6 @@ trait GProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}] extends GExec 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: 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..b4bcbd85 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 @@ -26,7 +26,6 @@ case class SpirvProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}] priv code: ByteBuffer, entryPoint: String, shaderBindings: L => ShaderLayout, - cacheKey: String, ) extends GProgram[Params, L] object SpirvProgram: @@ -38,24 +37,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 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 59a04927..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,7 +5,6 @@ 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 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..8bad4bb8 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,14 +15,16 @@ 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[T]: {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: 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..a450a5e7 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 @@ -3,7 +3,8 @@ 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.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 <: Value: Tag](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 new file mode 100644 index 00000000..4ffc51c3 --- /dev/null +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray.scala @@ -0,0 +1,13 @@ +package io.computenode.cyfra.dsl.collections + +import io.computenode.cyfra.dsl.Value.* +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 + +// todo temporary +case class GArray[T <: Value: {Tag, FromExpr}](underlying: GBuffer[T]): + def at(i: Int32)(using Source): T = + summon[FromExpr[T]].fromExpr(ReadBuffer(underlying, i)) + diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala new file mode 100644 index 00000000..e532eea2 --- /dev/null +++ b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/collections/GArray2D.scala @@ -0,0 +1,13 @@ +package io.computenode.cyfra.dsl.collections + +import io.computenode.cyfra.dsl.Value +import io.computenode.cyfra.dsl.Value.Int32 +import io.computenode.cyfra.dsl.algebra.ScalarAlgebra.{*, given} +import io.computenode.cyfra.dsl.macros.Source +import izumi.reflect.Tag +import io.computenode.cyfra.dsl.Value.FromExpr + +// 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..7c1fa7f4 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,36 +6,40 @@ 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 izumi.reflect.Tag -trait GIO[T]: +trait GIO[T <: Value]: - def flatMap[U](f: T => GIO[U]): GIO[U] = FlatMap(this, f(this.underlying)) + def flatMap[U <: Value](f: T => GIO[U]): GIO[U] = FlatMap(this, f(this.underlying)) - def map[U](f: T => U): GIO[U] = flatMap(t => GIO.pure(f(t))) + def map[U <: Value](f: T => U): GIO[U] = flatMap(t => GIO.pure(f(t))) private[cyfra] def underlying: T object GIO: - case class Pure[T](value: T) extends GIO[T]: + case class Pure[T <: Value](value: T) extends GIO[T]: override def underlying: T = value - case class FlatMap[T, U](gio: GIO[T], next: GIO[U]) extends GIO[U]: + case class FlatMap[T <: Value, U <: Value](gio: GIO[T], next: GIO[U]) extends GIO[U]: override def underlying: U = next.underlying // TODO repeat that collects results - case class Repeat(n: Int32, f: Int32 => GIO[?]) extends GIO[Unit]: - override def underlying: Unit = () + case class Repeat(n: Int32, f: GIO[?]) extends GIO[Empty]: + override def underlying: Empty = Empty() - def pure[T](value: T): GIO[T] = Pure(value) + 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 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[Unit] = + def write[T <: Value](buffer: GBuffer[T], index: Int32, value: T): GIO[Empty] = WriteBuffer(buffer, index, value) def read[T <: Value: {FromExpr, Tag}](buffer: GBuffer[T], index: Int32): T = 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..3086b15c 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 @@ -36,7 +37,5 @@ object VkShader: 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) + val compiled = DSLCompiler.compile(program.body(summon[LayoutStruct[L]].layoutRef), ???) + SpirvProgram((il: InitProgramLayout) ?=> layout(il), dispatch, compiled) From 74d8d8b29fce13cb1a39aa0629c1a8de63df4154 Mon Sep 17 00:00:00 2001 From: Szymon Date: Sun, 24 Aug 2025 23:23:36 +0200 Subject: [PATCH 10/22] Works! --- .../io/computenode/cyfra/spirv/Context.scala | 4 +- .../cyfra/spirv/compilers/DSLCompiler.scala | 16 ++- .../spirv/compilers/ExpressionCompiler.scala | 12 +- .../cyfra/spirv/compilers/GIOCompiler.scala | 124 ++++++++++++++---- .../compilers/SpirvProgramCompiler.scala | 27 ++-- .../computenode/cyfra/core/Allocation.scala | 6 +- .../io/computenode/cyfra/core/GProgram.scala | 8 +- .../cyfra/core/binding/UniformRef.scala | 4 +- .../cyfra/core/layout/LayoutStruct.scala | 24 ++-- .../cyfra/dsl/binding/GBinding.scala | 6 +- .../cyfra/dsl/binding/ReadUniform.scala | 3 +- .../cyfra/dsl/binding/WriteUniform.scala | 4 +- .../cyfra/dsl/struct/GStruct.scala | 4 +- .../cyfra/dsl/struct/GStructSchema.scala | 4 +- .../cyfra/samples/TestingStuff.scala | 50 ++++++- .../cyfra/runtime/ExecutionHandler.scala | 22 ++-- .../cyfra/runtime/VkAllocation.scala | 10 +- .../cyfra/runtime/VkCyfraRuntime.scala | 25 +++- .../computenode/cyfra/runtime/VkShader.scala | 11 +- .../computenode/cyfra/runtime/VkUniform.scala | 5 +- .../cyfra/spirvtools/SpirvCross.scala | 2 +- .../cyfra/spirvtools/SpirvDisassembler.scala | 2 +- .../cyfra/spirvtools/SpirvOptimizer.scala | 2 +- .../cyfra/spirvtools/SpirvTool.scala | 19 ++- .../cyfra/spirvtools/SpirvToolsRunner.scala | 2 +- 25 files changed, 270 insertions(+), 126 deletions(-) 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 35b39a46..8e8f5f96 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,6 +1,6 @@ package io.computenode.cyfra.spirv -import io.computenode.cyfra.dsl.binding.GBuffer +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 @@ -17,7 +17,7 @@ private[cyfra] case class Context( voidTypeRef: Int = -1, voidFuncTypeRef: Int = -1, workerIndexRef: Int = -1, - uniformVarRefs: Map[Int, Int] = Map.empty, + uniformVarRefs: Map[GUniform[?], Int] = Map.empty, bindingToStructType: Map[Int, Int] = Map.empty, constRefs: Map[(Tag[?], Any), Int] = Map(), exprRefs: Map[Int, Int] = Map(), 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 f15c9d54..09dd9999 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 @@ -74,10 +74,12 @@ private[cyfra] object DSLCompiler: val allTypes = (typesInCode ::: bindings.map(_.tag)).distinct def scalarTypes = allTypes.filter(_.tag <:< summon[Tag[Scalar]].tag) val (typeDefs, typedContext) = defineScalarTypes(scalarTypes, Context.initialContext) - val (buffers, uniforms) = bindings.partition: - case _: GBuffer[?] => true - case _: GUniform[?] => false - .asInstanceOf[(List[GBinding[?]], List[GUniform[?]])] + 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 buffers = buffersWithIndices.map(_._1) val uniformSchemas = uniforms.map(_.schema) val structsInCode = (allExprs.collect { @@ -86,9 +88,9 @@ private[cyfra] object DSLCompiler: } ::: uniformSchemas).distinct val (structDefs, structCtx) = defineStructTypes(structsInCode, typedContext) val structNames = getStructNames(structsInCode, structCtx) - val (decorations, uniformDefs, uniformContext) = initAndDecorateBuffers(buffers, structCtx) - val (uniformStructDecorations, uniformStructInsns, uniformStructContext) = createAndInitUniformBlocks(uniformSchemaIns, uniformContext) - val blockNames = getBlockNames(uniformContext, uniformSchemaIns) + val (decorations, uniformDefs, uniformContext) = initAndDecorateBuffers(buffersWithIndices, structCtx) + 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) 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 7e7cb849..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 @@ -106,11 +106,11 @@ private[cyfra] object ExpressionCompiler: val updatedContext = ctx.copy(exprRefs = ctx.exprRefs + (c.treeid -> constRef)) (List(), updatedContext) - case w @ WorkerIndex => + case w @ InvocationId => (Nil, ctx.copy(exprRefs = ctx.exprRefs + (w.treeid -> ctx.workerIndexRef))) - case d @ Binding(id) => - (Nil, ctx.copy(exprRefs = ctx.exprRefs + (d.treeid -> ctx.uniformVarRefs(id)))) + case d @ ReadUniform(u) => + (Nil, ctx.copy(exprRefs = ctx.exprRefs + (d.treeid -> ctx.uniformVarRefs(u)))) case c: ConvertExpression[?, ?] => compileConvertExpression(c, ctx) @@ -306,6 +306,7 @@ private[cyfra] object ExpressionCompiler: val updatedContext = ctx.copy(exprRefs = ctx.exprRefs + (expr.treeid -> (ctx.nextResultId + 1)), nextResultId = ctx.nextResultId + 2) (instructions, updatedContext) + case when: WhenExpr[?] => compileWhen(when, ctx) @@ -327,14 +328,14 @@ private[cyfra] object ExpressionCompiler: val updatedContext = ctx.copy(exprRefs = ctx.exprRefs + (cs.treeid -> ctx.nextResultId), nextResultId = ctx.nextResultId + 1) (insns, updatedContext) - case gf @ GetField(binding @ Binding(bindingId), 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.uniformVarRefs(bindingId)), + ResultRef(ctx.uniformVarRefs(uf)), ResultRef(ctx.constRefs((Int32Tag, gf.fieldIndex))), ), ), @@ -342,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 index c64b9cae..a4a55212 100644 --- 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 @@ -5,7 +5,7 @@ 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.SpirvTypes.{Int32Tag, LInt32Tag} +import io.computenode.cyfra.spirv.SpirvTypes.{GBooleanTag, Int32Tag, LInt32Tag} object GIOCompiler: @@ -17,37 +17,113 @@ object GIOCompiler: (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(ctx.uniformPointerMap(ctx.valueTypeMap(buffer.tag.tag))), - ResultRef(ctx.nextResultId), - ResultRef(ctx.bufferBlocks(buffer).blockVarRef), - ResultRef(ctx.constRefs((Int32Tag, 0))), - ResultRef(ctx.workerIndexRef), + 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(ctx.nextResultId), ResultRef(ctx.exprRefs(value.tree.treeid)))) + Instruction(Op.OpStore, List(ResultRef(ctxWithIndex.nextResultId), ResultRef(ctxWithIndex.exprRefs(value.tree.treeid)))) ) - val updatedCtx = ctx.copy(nextResultId = ctx.nextResultId + 1) - (acc ::: insns, updatedCtx) - + val updatedCtx = ctxWithIndex.copy(nextResultId = ctxWithIndex.nextResultId + 1) + (acc ::: indexInsts ::: valueInsts ::: insns, updatedCtx) + 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) - val loopHeaderId = ctxWithN.nextResultId - val loopMergeId = ctxWithN.nextResultId + 1 - val loopContinueId = ctxWithN.nextResultId + 2 - val counterVarId = ctxWithN.nextResultId + 3 - val ctxWithCounter = ctxWithN.copy(exprRefs = ctxWithN.exprRefs + (CurrentRepeatIndex.treeid -> counterVarId), nextResultId = ctxWithN.nextResultId + 4) - val counterInit = Instruction(Op.OpVariable, List(ResultRef(ctxWithCounter.constRefs((Int32Tag, 0))), ResultRef(counterVarId), StorageClass.Function)) - val counterStore = Instruction(Op.OpStore, List(ResultRef(counterVarId), ResultRef(ctxWithCounter.constRefs((Int32Tag, 0))))) - val (body, afterBodyCtx) = compileGio(f, ctxWithCounter) - // todo - ??? - - - - + + // 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) + + // 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))) + ) + + val finalNextId = math.max(ctxAfterBody.nextResultId, addId + 1) + val finalCtx = ctxAfterBody.copy( + nextResultId = finalNextId, + exprRefs = ctxAfterBody.exprRefs - CurrentRepeatIndex.treeid + ) + + (acc ::: nInsts ::: preheader ::: header ::: bodyBlk ::: contBlk ::: mergeBlk, finalCtx) + + + + \ No newline at end of file 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 73ebf1bf..997c93b2 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,7 +4,7 @@ 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 +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 @@ -122,15 +122,15 @@ private[cyfra] object SpirvProgramCompiler: ), ) (definitionInstructions, context.copy(nextResultId = context.nextResultId + 3)) - def initAndDecorateBuffers(buffers: List[GBuffer[?]], context: Context): (List[Words], List[Words], Context) = + 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[GBuffer[?]], context: Context): (List[Words], List[Words], Context) = - val (decoration, definition, newContext) = blocks.foldLeft((List[Words](), List[Words](), context)) { case ((decAcc, insnAcc, ctx), buff) => + def createAndInitBlocks(blocks: List[(GBuffer[?], Int)], context: Context): (List[Words], List[Words], Context) = + 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, ctx.nextBinding) + 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)] @@ -152,12 +152,12 @@ private[cyfra] object SpirvProgramCompiler: ( 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, uniformSchemas: List[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 @@ -174,10 +174,10 @@ private[cyfra] object SpirvProgramCompiler: typeStride(t) .sum - def createAndInitUniformBlocks(schemas: List[GStructSchema[?]], ctx: Context): (List[Words], List[Words], Context) = - schemas.foldLeft((List.empty[Words], List.empty[Words], ctx)) { case ((decorationsAcc, definitionsAcc, currentCtx), schema) => + def createAndInitUniformBlocks(schemas: List[(GUniform[?], Int)], ctx: Context): (List[Words], List[Words], Context) = + 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 currentBinding = currentCtx.nextBinding val (offsetDecorations, _) = schema.fields.zipWithIndex.foldLeft[(List[Words], Int)](List.empty[Words], 0): case ((acc, offset), ((name, fromExpr, tag), idx)) => @@ -199,16 +199,15 @@ private[cyfra] object SpirvProgramCompiler: 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(currentBinding))) + val uniformDecorateBinding = Instruction(Op.OpDecorate, List(ResultRef(uniformVarRef), Decoration.Binding, IntWord(binding))) val newDecorations = decorationsAcc ::: offsetDecorations ::: List(uniformDecorateDescriptorSet, uniformDecorateBinding, uniformBlockDecoration) val newDefinitions = definitionsAcc ::: List(uniformPointerUniform, uniformVar) val newCtx = currentCtx.copy( nextResultId = currentCtx.nextResultId + 2, - nextBinding = currentCtx.nextBinding + 1, - uniformVarRefs = currentCtx.uniformVarRefs + (currentBinding -> uniformVarRef), + uniformVarRefs = currentCtx.uniformVarRefs + (uniform -> uniformVarRef), uniformPointerMap = currentCtx.uniformPointerMap + (uniformStructTypeRef -> uniformPointerUniformRef), - bindingToStructType = currentCtx.bindingToStructType + (currentBinding -> uniformStructTypeRef) + bindingToStructType = currentCtx.bindingToStructType + (binding -> uniformStructTypeRef) ) (newDecorations, newDefinitions, newCtx) 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/GProgram.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GProgram.scala index d1a43226..f1cef442 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,7 +8,7 @@ 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 @@ -34,7 +34,7 @@ object GProgram: 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) @@ -42,6 +42,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/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..0703bbbb 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 summon[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 summon[Tag[t]], ${ fromExpr.asExprOf[FromExpr[t]] }, ${ structSchema }) + } val constructor = sym.primaryConstructor 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 8bad4bb8..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 @@ -19,7 +19,7 @@ trait GBuffer[T <: Value: {FromExpr, Tag}] extends GBinding[T]: object GBuffer -trait GUniform[T <: GStruct[T]: {Tag, FromExpr, GStructSchema}] extends GBinding[T]: +trait GUniform[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}] extends GBinding[T]: def read: T = fromExprEval(ReadUniform(this)) def write(value: T): GIO[Empty] = WriteUniform(this, value) @@ -28,6 +28,6 @@ trait GUniform[T <: GStruct[T]: {Tag, FromExpr, GStructSchema}] extends GBinding 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/WriteUniform.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/WriteUniform.scala index a450a5e7..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,9 +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[Empty]: +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/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-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala index cd2fc227..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 @@ -88,9 +91,52 @@ object TestingStuff: layout => FilterProgramLayout(in = layout.emitBuffer, out = layout.filterBuffer), ) + @main + def testEmit = + given runtime: VkCyfraRuntime = VkCyfraRuntime( + spirvToolsRunner = SpirvToolsRunner( + crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl"))) + ) + ) + + val emitParams = EmitProgramParams(inSize = 1024, emitN = 2) + + val region = GBufferRegion + .allocate[EmitProgramLayout] + .map: region => + emitProgram.execute(emitParams, region) + + val data = (0 until 1024).toArray + val buffer = BufferUtils.createByteBuffer(data.length * 4) + buffer.asIntBuffer().put(data).flip() + + val result = BufferUtils.createIntBuffer(data.length * 2) + val rbb = MemoryUtil.memByteBuffer(result) + region.runUnsafe( + init = EmitProgramLayout( + in = GBuffer[Int32](buffer), + out = GBuffer[Int32](data.length * 2), + ), + onDone = layout => layout.out.read(rbb), + ) + runtime.close() + + 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") + @main def test = - given runtime: VkCyfraRuntime = VkCyfraRuntime() + given runtime: VkCyfraRuntime = VkCyfraRuntime( + spirvToolsRunner = SpirvToolsRunner( + crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl"))), + validator = SpirvValidator.Disable + ) + ) val emitFilterParams = EmitFilterParams(inSize = 1024, emitN = 2, filterValue = 42) @@ -115,7 +161,7 @@ object TestingStuff: ) runtime.close() - val actual = (0 until 2 * 1024).map(i => result.get(i * 1) != 0) + 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) 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 00855e4e..d0ad3a5a 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 @@ -248,11 +241,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..350f0369 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 @@ -57,22 +57,22 @@ class VkAllocation(commandPool: CommandPool, executionHandler: ExecutionHandler) GBuffer[T](length).tap(_.write(buff)) extension (buffers: GUniform.type) - def apply[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GUniform[T] = + 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..c6383665 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,34 @@ 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 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 shaderCache = mutable.Map[GProgram[?, ?], VkShader[?]]() + private[cyfra] def getOrLoadProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}](program: GProgram[Params, L]): VkShader[L] = synchronized: + val spirvProgram = program match + case p: GioProgram[?, ?] => compile(p) + case p: SpirvProgram[?, ?] => p + case _ => throw new IllegalArgumentException(s"Unsupported program type: ${program.getClass.getName}") + + shaderCache.getOrElseUpdate(program, 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 3086b15c..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 @@ -16,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 => @@ -35,7 +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 compiled = DSLCompiler.compile(program.body(summon[LayoutStruct[L]].layoutRef), ???) - SpirvProgram((il: InitProgramLayout) ?=> layout(il), dispatch, compiled) 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 => From dbf17349127767c9368f85d7595bab07dd1f5f16 Mon Sep 17 00:00:00 2001 From: Szymon Date: Sun, 24 Aug 2025 23:29:17 +0200 Subject: [PATCH 11/22] FlatMap --- .../io/computenode/cyfra/spirv/compilers/GIOCompiler.scala | 6 +++++- .../io/computenode/cyfra/e2e/RuntimeEnduranceTest.scala | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) 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 index a4a55212..dd1d853c 100644 --- 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 @@ -11,7 +11,7 @@ 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) @@ -35,6 +35,10 @@ object GIOCompiler: 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) 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 index 35ad5bb2..4d002c19 100644 --- 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 @@ -7,6 +7,7 @@ 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.utility.Logger.logger @@ -153,7 +154,7 @@ class RuntimeEnduranceTest extends munit.FunSuite: _ <- 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 () + yield Empty() def swap(l: AddProgramLayout): AddProgramLayout = val AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5, u1, u2) = l From b2d53f87c8d19ba2b0eba7111fd414386787ea49 Mon Sep 17 00:00:00 2001 From: spamegg Date: Tue, 26 Aug 2025 15:48:56 +0300 Subject: [PATCH 12/22] added stream compaction program --- .../computenode/cyfra/fs2interop/GPipe.scala | 108 +++++++++++------- 1 file changed, 69 insertions(+), 39 deletions(-) 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 index 8bfbcfa9..85346448 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -58,11 +58,12 @@ object GPipe: 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]) => + case class Params(inSize: Int) // used commonly by many program layouts + // Predicate mapping - case class PredParams(inSize: Int) case class PredLayout(in: GBuffer[C], out: GBuffer[Int32]) extends Layout - val predicateProgram = GProgram[PredParams, PredLayout]( + val predicateProgram = GProgram[Params, PredLayout]( layout = params => PredLayout(in = GBuffer[C](params.inSize), out = GBuffer[Int32](params.inSize)), dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize, 1, 1)), ): layout => @@ -71,10 +72,6 @@ object GPipe: val result = when(pred(element))(1: Int32).otherwise(0) GIO.write[Int32](layout.out, invocId, result) - val predParams = PredParams(256) - val predExec = GExecution[PredParams, PredLayout]() - .addProgram(predicateProgram)(params => params, layout => layout) - // Prefix sum (inclusive), upsweep/downsweep case class ScanParams(inSize: Int, intervalSize: Int) case class ScanArgs(intervalSize: Int32) extends GStruct[ScanArgs] @@ -107,66 +104,99 @@ object GPipe: val newValue = oldValue + addValue GIO.write[Int32](layout.ints, mid, newValue) + // Stitch together many upsweep / downsweep program phases recursively @annotation.tailrec - def upsweepPhases(exec: GExecution[ScanParams, ScanLayout, ?], inSize: Int, intervalSize: Int): GExecution[ScanParams, ScanLayout, ?] = + 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) - val upsweepInitialExec = GExecution[ScanParams, ScanLayout]() - val upsweepExec = upsweepPhases(upsweepInitialExec, 256, 2) - @annotation.tailrec - def downsweepPhases(exec: GExecution[ScanParams, ScanLayout, ?], inSize: Int, intervalSize: Int): GExecution[ScanParams, ScanLayout, ?] = + 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 downsweepExec = downsweepPhases(upsweepInitialExec, 256, 128) - val scanParams = ScanParams(256, 2) - - // TODO implement compaction - case class CompactParams() - case class CompactLayout(in: GBuffer[C], intIn: GBuffer[Int32], out: GBuffer[C]) extends Layout - val compactedStreamSize: Int = ??? // TODO how to get this out of prefix sum results - - val compactProgram = GProgram[CompactParams, CompactLayout](layout = params => ???, dispatch = (layout, params) => ???): compactLayout => - ??? - val compactExec = GExecution[CompactParams, CompactLayout]() + 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 - // TODO: connect all the layouts/executions into one - case class PredScanCompactParams(inSize: Int) - case class PredScanCompactLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C]) extends Layout + // Stream compaction + case class CompactLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C]) extends Layout - // How to "connect" many GExecutions? with flatMap? - val predScanCompactExec = GExecution[PredScanCompactParams, PredScanCompactLayout]() - // predExec - // .flatMap(predLayout => ???) // add downsweepExec - // .flatMap(layout => ???) // add compactExec - val predScanCompactParams = PredScanCompactParams(256) // TODO + val compactProgram = GProgram[Params, CompactLayout]( + layout = params => CompactLayout(in = GBuffer[C](params.inSize), scan = GBuffer[Int32](params.inSize), out = GBuffer[C](params.inSize)), + dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize, 1, 1)), + ): layout => + val invocId = GIO.invocationId + val element = GIO.read[C](layout.in, invocId) + val prefixSum = GIO.read[Int32](layout.scan, invocId) + val prevScan = when(invocId > 0)(GIO.read[Int32](layout.scan, invocId - 1)).otherwise(prefixSum) + val condt = when(invocId > 0)(when(prevScan < prefixSum)(1: Int32).otherwise(0)).otherwise(when(prefixSum > 0)(1: Int32).otherwise(0)) + val index = when(invocId > 0)(when(prevScan < prefixSum)(prevScan).otherwise(0)).otherwise(0) + GIO.repeat(condt): _ => + GIO.write[C](layout.out, index, element) + + // connect all the layouts/executions into one + case class FilterLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C]) extends Layout + + val filterExec = GExecution[Params, FilterLayout]() + // val filterExec = predicateProgram + // .flatMap[ScanLayout, ScanParams]: predLayout => + // scanExec + // .contramap[ScanLayout](layout => ScanLayout(???, ???)) + // .contramapParams[ScanParams](params => ScanParams(???, ???)) + // .map(_ => predLayout) + // .flatMap[FilterLayout, Params]: scanLayout => + // compactProgram + // .contramap[FilterLayout](layout => CompactLayout(???, ???, ???)) + // .contramapParams[Params](params => params) + // .map(_ => scanLayout) + + val filterParams = Params(256) // TODO + + // case class FilterParams(intervalSize: ScanArgs()) + // case class FilterLayout(inBuf: GBuffer[C], scanBuf: GBuffer[Int32]) + // val filterExec = GExecution[FilterParams, FilterLayout]() + // .addProgram(predicateProgram)(params => PredParams(params.inSize), layout => PredLayout(layout.in, layout.pred)) + // .flatMap[FilterLayout, FilterParams](l => + // downsweepExec + // .contramap[FilterLayout](layout => ScanLayout(layout.pred, layout.intervalSize)) + // .contramapParams[FilterParams](p => ScanParams(p.inSize, 2)) + // .map(_ => l) + // ) val region = GBufferRegion - .allocate[PredScanCompactLayout] + .allocate[FilterLayout] .map: layout => - predScanCompactExec.execute(predScanCompactParams, layout) + filterExec.execute(filterParams, layout) val typeSize = typeStride(Tag.apply[C]) val intSize = typeStride(Tag.apply[Int32]) + val params = Params(256) // these are allocated once, reused for many chunks - val predBuf = BufferUtils.createByteBuffer(predParams.inSize * typeSize) - val scanBuf = BufferUtils.createByteBuffer(predParams.inSize * intSize) - val compactBuf = BufferUtils.createByteBuffer(compactedStreamSize * typeSize) + val predBuf = BufferUtils.createByteBuffer(params.inSize * typeSize) + val scanBuf = BufferUtils.createByteBuffer(params.inSize * intSize) + val compactBuf = BufferUtils.createByteBuffer(256 * typeSize) stream - .chunkMin(predScanCompactParams.inSize) + .chunkMin(filterParams.inSize) .flatMap: chunk => bridge.toByteBuffer(predBuf, chunk) region.runUnsafe( - init = PredScanCompactLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf)), + init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf)), onDone = layout => layout.out.read(compactBuf), ) - val arr = bridge.fromByteBuffer(compactBuf, new Array[S](compactedStreamSize)) + val arr = bridge.fromByteBuffer(compactBuf, new Array[S](256)) Stream.emits(arr) From 77deb47c1c70caa87b6ae3b4c47ab70bbfdb2d11 Mon Sep 17 00:00:00 2001 From: spamegg Date: Tue, 26 Aug 2025 17:22:00 +0300 Subject: [PATCH 13/22] finish filter --- .../computenode/cyfra/fs2interop/GPipe.scala | 72 +++++++++---------- 1 file changed, 33 insertions(+), 39 deletions(-) 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 index 85346448..ddab40c1 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -58,12 +58,11 @@ object GPipe: 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]) => - case class Params(inSize: Int) // used commonly by many program layouts - // Predicate mapping + case class PredParams(inSize: Int) case class PredLayout(in: GBuffer[C], out: GBuffer[Int32]) extends Layout - val predicateProgram = GProgram[Params, PredLayout]( + val predicateProgram = GProgram[PredParams, PredLayout]( layout = params => PredLayout(in = GBuffer[C](params.inSize), out = GBuffer[Int32](params.inSize)), dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize, 1, 1)), ): layout => @@ -132,9 +131,10 @@ object GPipe: 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[Params, CompactLayout]( + 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((params.inSize, 1, 1)), ): layout => @@ -148,54 +148,48 @@ object GPipe: GIO.write[C](layout.out, index, element) // connect all the layouts/executions into one - case class FilterLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C]) extends Layout - - val filterExec = GExecution[Params, FilterLayout]() - // val filterExec = predicateProgram - // .flatMap[ScanLayout, ScanParams]: predLayout => - // scanExec - // .contramap[ScanLayout](layout => ScanLayout(???, ???)) - // .contramapParams[ScanParams](params => ScanParams(???, ???)) - // .map(_ => predLayout) - // .flatMap[FilterLayout, Params]: scanLayout => - // compactProgram - // .contramap[FilterLayout](layout => CompactLayout(???, ???, ???)) - // .contramapParams[Params](params => params) - // .map(_ => scanLayout) - - val filterParams = Params(256) // TODO - - // case class FilterParams(intervalSize: ScanArgs()) - // case class FilterLayout(inBuf: GBuffer[C], scanBuf: GBuffer[Int32]) - // val filterExec = GExecution[FilterParams, FilterLayout]() - // .addProgram(predicateProgram)(params => PredParams(params.inSize), layout => PredLayout(layout.in, layout.pred)) - // .flatMap[FilterLayout, FilterParams](l => - // downsweepExec - // .contramap[FilterLayout](layout => ScanLayout(layout.pred, layout.intervalSize)) - // .contramapParams[FilterParams](p => ScanParams(p.inSize, 2)) - // .map(_ => l) - // ) - + case class FilterParams(inSize: Int, intervalSize: Int) + case class FilterLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C], intervalSize: GUniform[ScanArgs]) 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, filterLayout.intervalSize) + .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(256, 2) val region = GBufferRegion .allocate[FilterLayout] - .map: layout => - filterExec.execute(filterParams, layout) + .map: filterLayout => + filterExec.execute(filterParams, filterLayout) val typeSize = typeStride(Tag.apply[C]) val intSize = typeStride(Tag.apply[Int32]) - val params = Params(256) // these are allocated once, reused for many chunks - val predBuf = BufferUtils.createByteBuffer(params.inSize * typeSize) - val scanBuf = BufferUtils.createByteBuffer(params.inSize * intSize) - val compactBuf = BufferUtils.createByteBuffer(256 * typeSize) + val predBuf = BufferUtils.createByteBuffer(filterParams.inSize * typeSize) + val scanBuf = BufferUtils.createByteBuffer(filterParams.inSize * intSize) + val compactBuf = BufferUtils.createByteBuffer(filterParams.inSize * typeSize) stream .chunkMin(filterParams.inSize) .flatMap: chunk => bridge.toByteBuffer(predBuf, chunk) region.runUnsafe( - init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf)), + init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf), intervalSize = ???), onDone = layout => layout.out.read(compactBuf), ) val arr = bridge.fromByteBuffer(compactBuf, new Array[S](256)) From dcfe4f9a9ca711553244a2d256fa950b34a946ed Mon Sep 17 00:00:00 2001 From: spamegg Date: Tue, 26 Aug 2025 17:23:41 +0300 Subject: [PATCH 14/22] small fix --- .../src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ddab40c1..d3aa531e 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -189,7 +189,7 @@ object GPipe: .flatMap: chunk => bridge.toByteBuffer(predBuf, chunk) region.runUnsafe( - init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf), intervalSize = ???), + init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf), intervalSize = GUniform()), onDone = layout => layout.out.read(compactBuf), ) val arr = bridge.fromByteBuffer(compactBuf, new Array[S](256)) From 0f92e5d4360a95f86348f277d42fff274e972ba5 Mon Sep 17 00:00:00 2001 From: spamegg Date: Wed, 27 Aug 2025 14:26:32 +0300 Subject: [PATCH 15/22] implement GIO.when and use for-expression in compactProgram --- .../scala/io/computenode/cyfra/dsl/gio/GIO.scala | 6 ++++++ .../io/computenode/cyfra/fs2interop/GPipe.scala | 13 +++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) 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..cbac2470 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,6 +6,7 @@ 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.control.When import izumi.reflect.Tag trait GIO[T]: @@ -35,6 +36,11 @@ object GIO: def repeat(n: Int32)(f: Int32 => GIO[?]): GIO[Unit] = Repeat(n, f) + def when(cond: GBoolean)(thenCode: GIO[?]): GIO[Unit] = + val n = When.when(cond)(1: Int32).otherwise(0) + repeat(n): _ => + thenCode + def write[T <: Value](buffer: GBuffer[T], index: Int32, value: T): GIO[Unit] = WriteBuffer(buffer, index, value) 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 index d3aa531e..00553c01 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -142,10 +142,15 @@ object GPipe: val element = GIO.read[C](layout.in, invocId) val prefixSum = GIO.read[Int32](layout.scan, invocId) val prevScan = when(invocId > 0)(GIO.read[Int32](layout.scan, invocId - 1)).otherwise(prefixSum) - val condt = when(invocId > 0)(when(prevScan < prefixSum)(1: Int32).otherwise(0)).otherwise(when(prefixSum > 0)(1: Int32).otherwise(0)) - val index = when(invocId > 0)(when(prevScan < prefixSum)(prevScan).otherwise(0)).otherwise(0) - GIO.repeat(condt): _ => - GIO.write[C](layout.out, index, element) + for + _ <- GIO.when(invocId > 0): + val prevScan = GIO.read[Int32](layout.scan, invocId - 1) + GIO.when(prevScan < prefixSum): + GIO.write(layout.out, prevScan, element) + _ <- GIO.when(invocId === 0): + GIO.when(prefixSum > 0): + GIO.write(layout.out, invocId, element) + yield () // connect all the layouts/executions into one case class FilterParams(inSize: Int, intervalSize: Int) From 2840c28e873c835777c5a83dbf2f9ecfb95058a5 Mon Sep 17 00:00:00 2001 From: spamegg Date: Wed, 27 Aug 2025 14:40:31 +0300 Subject: [PATCH 16/22] small cleanup --- .../src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala | 1 - 1 file changed, 1 deletion(-) 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 index 00553c01..cd61a423 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -141,7 +141,6 @@ object GPipe: val invocId = GIO.invocationId val element = GIO.read[C](layout.in, invocId) val prefixSum = GIO.read[Int32](layout.scan, invocId) - val prevScan = when(invocId > 0)(GIO.read[Int32](layout.scan, invocId - 1)).otherwise(prefixSum) for _ <- GIO.when(invocId > 0): val prevScan = GIO.read[Int32](layout.scan, invocId - 1) From 1a01f99d2f8d596e5ee7220f7563bd4b0da6d12e Mon Sep 17 00:00:00 2001 From: Szymon Date: Sat, 6 Sep 2025 22:33:41 +0200 Subject: [PATCH 17/22] No warnings now --- .../io/computenode/cyfra/spirv/Context.scala | 2 +- .../cyfra/spirv/compilers/DSLCompiler.scala | 5 +- .../spirv/compilers/GStructCompiler.scala | 14 +++- .../compilers/SpirvProgramCompiler.scala | 81 +++++++++++-------- .../cyfra/e2e/RuntimeEnduranceTest.scala | 8 +- 5 files changed, 69 insertions(+), 41 deletions(-) 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 8e8f5f96..62daf3d1 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 @@ -25,7 +25,7 @@ private[cyfra] case class Context( 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(), ): def joinNested(ctx: Context): Context = 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 09dd9999..84252ca7 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 @@ -79,7 +79,6 @@ private[cyfra] object DSLCompiler: case (_: GUniform[?], _) => false .asInstanceOf[(List[(GBuffer[?], Int)], List[(GUniform[?], Int)])] val uniforms = uniformsWithIndices.map(_._1) - val buffers = buffersWithIndices.map(_._1) val uniformSchemas = uniforms.map(_.schema) val structsInCode = (allExprs.collect { @@ -87,8 +86,8 @@ private[cyfra] object DSLCompiler: case gf: GetField[?, ?] => gf.resultSchema } ::: uniformSchemas).distinct val (structDefs, structCtx) = defineStructTypes(structsInCode, typedContext) - val structNames = getStructNames(structsInCode, structCtx) - val (decorations, uniformDefs, uniformContext) = initAndDecorateBuffers(buffersWithIndices, structCtx) + 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) 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 997c93b2..f9f86447 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 @@ -45,18 +45,6 @@ private[cyfra] object SpirvProgramCompiler: val (vars, nonVarsBody) = bubbleUpVars(body) val end = List( -// TODO Remove - Write is a part of GIO now -// 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()), ) @@ -128,21 +116,45 @@ private[cyfra] object SpirvProgramCompiler: (blockDecor, voidsDef ::: blockDef, voidCtx) 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 ) @@ -174,22 +186,26 @@ private[cyfra] object SpirvProgramCompiler: typeStride(t) .sum - def createAndInitUniformBlocks(schemas: List[(GUniform[?], Int)], ctx: Context): (List[Words], List[Words], Context) = + 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 (offsetDecorations, _) = 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) - - val uniformBlockDecoration = Instruction(Op.OpDecorate, List(ResultRef(uniformStructTypeRef), Decoration.Block)) + 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 = @@ -201,7 +217,7 @@ private[cyfra] object SpirvProgramCompiler: 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 ::: offsetDecorations ::: List(uniformDecorateDescriptorSet, uniformDecorateBinding, uniformBlockDecoration) + val newDecorations = decorationsAcc ::: structDecorations ::: List(uniformDecorateDescriptorSet, uniformDecorateBinding) val newDefinitions = definitionsAcc ::: List(uniformPointerUniform, uniformVar) val newCtx = currentCtx.copy( nextResultId = currentCtx.nextResultId + 2, @@ -212,6 +228,7 @@ private[cyfra] object SpirvProgramCompiler: (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-e2e-test/src/test/scala/io/computenode/cyfra/e2e/RuntimeEnduranceTest.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/RuntimeEnduranceTest.scala index 4d002c19..dec27507 100644 --- 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 @@ -10,10 +10,13 @@ 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} @@ -174,7 +177,10 @@ class RuntimeEnduranceTest extends munit.FunSuite: def runEnduranceTest(nRuns: Int): Unit = logger.info(s"Starting endurance test with ${nRuns} runs...") - given runtime: VkCyfraRuntime = VkCyfraRuntime() + 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) From c5d7da0cc84779108d324de177bc54730e286902 Mon Sep 17 00:00:00 2001 From: Szymon Date: Sun, 7 Sep 2025 18:47:49 +0200 Subject: [PATCH 18/22] Rebase & adjustments --- .../io/computenode/cyfra/spirv/Context.scala | 1 + .../cyfra/spirv/SpirvConstants.scala | 5 +- .../cyfra/spirv/compilers/DSLCompiler.scala | 23 +++++- .../cyfra/spirv/compilers/GIOCompiler.scala | 16 ++++ .../compilers/SpirvProgramCompiler.scala | 16 ++++ .../cyfra/core/layout/LayoutStruct.scala | 4 +- .../io/computenode/cyfra/dsl/gio/GIO.scala | 8 +- .../cyfra/e2e/fs2interop/Fs2Tests.scala | 23 ++++-- .../computenode/cyfra/fs2interop/GPipe.scala | 51 ++++++++----- .../cyfra/runtime/ExecutionHandler.scala | 69 ++++++++++++----- .../cyfra/runtime/VkAllocation.scala | 2 +- .../cyfra/vulkan/VulkanContext.scala | 2 +- .../cyfra/vulkan/core/Instance.scala | 76 +++++++++++++++++-- 13 files changed, 237 insertions(+), 59 deletions(-) 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 62daf3d1..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 @@ -27,6 +27,7 @@ private[cyfra] case class Context( exprNames: 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 84252ca7..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 @@ -31,7 +31,7 @@ private[cyfra] object DSLCompiler: pending match case Nil => acc case GIO.Pure(v) :: tail => - getAllExprsFlattened(tail, getAllExprsFlattened(v.tree, visitDetached), visitDetached) + 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 => @@ -44,6 +44,9 @@ private[cyfra] object DSLCompiler: 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[?]] = @@ -68,12 +71,26 @@ private[cyfra] object DSLCompiler: allScopesCache(root.treeid) = result result + // 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 ::: 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 @@ -85,7 +102,7 @@ private[cyfra] object DSLCompiler: case cs: ComposeStruct[?] => cs.resultSchema case gf: GetField[?, ?] => gf.resultSchema } ::: uniformSchemas).distinct - val (structDefs, structCtx) = defineStructTypes(structsInCode, typedContext) + 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) @@ -98,7 +115,7 @@ private[cyfra] object DSLCompiler: 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/GIOCompiler.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/GIOCompiler.scala index dd1d853c..231d8f97 100644 --- 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 @@ -5,6 +5,7 @@ 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: @@ -125,6 +126,21 @@ object GIOCompiler: (acc ::: nInsts ::: preheader ::: header ::: bodyBlk ::: contBlk ::: mergeBlk, finalCtx) + case GIO.Printf(format, args*) => + val (argsInsts, ctxAfterArgs) = args.foldLeft((List.empty[Words], ctx)) { case ((instsAcc, cAcc), arg) => + val (argInsts, cAfterArg) = ExpressionCompiler.compileBlock(arg.tree, cAcc) + (instsAcc ::: argInsts, cAfterArg) + } + val argResults = args.map(a => ResultRef(ctxAfterArgs.exprRefs(a.tree.treeid))).toList + val printf = Instruction(Op.OpExtInst, List( + ResultRef(TYPE_VOID_REF), + ResultRef(ctxAfterArgs.nextResultId), + ResultRef(DEBUG_PRINTF_REF), + IntWord(1), + ResultRef(ctx.stringLiterals(format)), + ) ::: argResults) + (acc ::: argsInsts ::: List(printf), ctxAfterArgs.copy(nextResultId = ctxAfterArgs.nextResultId + 1)) + diff --git a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/SpirvProgramCompiler.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/compilers/SpirvProgramCompiler.scala index f9f86447..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 @@ -74,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 @@ -185,6 +187,20 @@ private[cyfra] object SpirvProgramCompiler: 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]() 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 0703bbbb..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 @@ -59,14 +59,14 @@ object LayoutStruct: (tpe, ftype) match case ('[type t <: Value; t], '[type tg <: GBuffer[?]; tg]) => '{ - BufferRef[t](${ Expr(i) }, ${ tag.asExprOf[Tag[t]] })(using summon[Tag[t]], ${ fromExpr.asExprOf[FromExpr[t]] }) + 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 summon[Tag[t]], ${ fromExpr.asExprOf[FromExpr[t]] }, ${ structSchema }) + 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/gio/GIO.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/gio/GIO.scala index 9d5c5220..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 @@ -29,6 +29,9 @@ object GIO: // TODO repeat that collects results 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](value: T): GIO[T] = Pure(value) @@ -42,8 +45,11 @@ object GIO: 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[Unit] = + def when(cond: GBoolean)(thenCode: GIO[?]): GIO[Empty] = val n = When.when(cond)(1: Int32).otherwise(0) repeat(n): _ => thenCode 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 index daba84cd..941ba3cc 100644 --- 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 @@ -1,12 +1,19 @@ package io.computenode.cyfra.e2e.fs2interop -import io.computenode.cyfra.core.archive.*, mem.*, GMem.fRGBA -import io.computenode.cyfra.dsl.{*, given}, algebra.VectorAlgebra -import io.computenode.cyfra.fs2interop.*, Bridge.given +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) @@ -16,7 +23,13 @@ extension (f: fRGBA) 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: CyfraRuntime = VkCyfraRuntime() + given cr: CyfraRuntime = VkCyfraRuntime( + spirvToolsRunner = SpirvToolsRunner( + crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl"))), + disassembler = SpirvDisassembler.Enable(toolOutput = ToFile(Paths.get("output/disassembled.spv"))) + ) + ) + test("fs2 through GPipe map, just ints"): val inSeq = (0 until 256).toSeq 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 index cd61a423..77ceb323 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -1,11 +1,17 @@ package io.computenode.cyfra.fs2interop -import io.computenode.cyfra.core.{Allocation, layout}, layout.Layout +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}, gio.GIO, binding.{GBuffer, GUniform, GBinding} +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, GStruct.Empty, Empty.given - +import struct.GStruct +import GStruct.Empty +import Empty.given import fs2.* import java.nio.ByteBuffer import org.lwjgl.BufferUtils @@ -14,7 +20,7 @@ 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]( + 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]) => @@ -27,11 +33,16 @@ object GPipe: val gProg = GProgram[Params, PLayout]( layout = params => PLayout(in = GBuffer[C1](params.inSize), out = GBuffer[C2](params.inSize)), - dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize, 1, 1)), - ): layout => + dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / 256, 1, 1)), + )(layout => { val invocId = GIO.invocationId val element = GIO.read[C1](layout.in, invocId) - GIO.write[C2](layout.out, invocId, f(element)) + val res = f(element) + for + _ <- GIO.printf("Element %d -> %v4f", invocId, res) + _ <- 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)) @@ -64,21 +75,24 @@ object GPipe: val predicateProgram = GProgram[PredParams, PredLayout]( layout = params => PredLayout(in = GBuffer[C](params.inSize), out = GBuffer[Int32](params.inSize)), - dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize, 1, 1)), + dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / 256, 1, 1)), ): layout => val invocId = GIO.invocationId val element = GIO.read[C](layout.in, invocId) val result = when(pred(element))(1: Int32).otherwise(0) - GIO.write[Int32](layout.out, invocId, result) + for + _ <- GIO.printf("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]) extends Layout + 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((params.inSize / params.intervalSize, 1, 1)), + dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / params.intervalSize / 256, 1, 1)), ): layout => val ScanArgs(size) = layout.intervalSize.read val invocId = GIO.invocationId @@ -92,7 +106,7 @@ object GPipe: val downsweep = GProgram[ScanParams, ScanLayout]( layout = params => ScanLayout(ints = GBuffer[Int32](params.inSize), intervalSize = GUniform(ScanArgs(params.intervalSize))), - dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / params.intervalSize, 1, 1)), + dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / params.intervalSize / 256, 1, 1)), ): layout => val ScanArgs(size) = layout.intervalSize.read val invocId = GIO.invocationId @@ -136,12 +150,13 @@ object GPipe: 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((params.inSize, 1, 1)), + dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / 256, 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("Element %d, prefix sum %d", invocId, prefixSum) _ <- GIO.when(invocId > 0): val prevScan = GIO.read[Int32](layout.scan, invocId - 1) GIO.when(prevScan < prefixSum): @@ -149,11 +164,11 @@ object GPipe: _ <- GIO.when(invocId === 0): GIO.when(prefixSum > 0): GIO.write(layout.out, invocId, element) - yield () + 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], intervalSize: GUniform[ScanArgs]) extends Layout + case class FilterLayout(in: GBuffer[C], scan: GBuffer[Int32], out: GBuffer[C]) extends Layout val filterExec = GExecution[FilterParams, FilterLayout]() .addProgram(predicateProgram)( @@ -163,7 +178,7 @@ object GPipe: .flatMap[FilterLayout, FilterParams]: filterLayout => scanExec .contramap[FilterLayout]: filterLayout => - ScanLayout(filterLayout.scan, filterLayout.intervalSize) + ScanLayout(filterLayout.scan) .contramapParams[FilterParams](filterParams => ScanParams(filterParams.inSize, filterParams.intervalSize)) .map(scanLayout => filterLayout) .flatMap[FilterLayout, FilterParams]: filterLayout => @@ -193,7 +208,7 @@ object GPipe: .flatMap: chunk => bridge.toByteBuffer(predBuf, chunk) region.runUnsafe( - init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf), intervalSize = GUniform()), + init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf)), onDone = layout => layout.out.read(compactBuf), ) val arr = bridge.fromByteBuffer(compactBuf, new Array[S](256)) 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 d0ad3a5a..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 @@ -38,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) @@ -50,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[?]])): @@ -90,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 @@ -125,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[?] = @@ -227,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 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 350f0369..dc878c5b 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 @@ -56,7 +56,7 @@ class VkAllocation(commandPool: CommandPool, executionHandler: ExecutionHandler) if buff.remaining() % sizeOfT != 0 then ??? GBuffer[T](length).tap(_.write(buff)) - extension (buffers: GUniform.type) + extension (uniforms: GUniform.type) def apply[T <: GStruct[?]: {Tag, FromExpr, GStructSchema}](buff: ByteBuffer): GUniform[T] = GUniform[T]().tap(_.write(buff)) 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/core/Instance.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/Instance.scala index 43072840..812c14d6 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,69 @@ 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) + + layerSettingsCI.pNext(validationFeatures.address()) + pCreateInfo.pNext(layerSettingsCI.address()) val pInstance = stack.mallocPointer(1) check(vkCreateInstance(pCreateInfo, null, pInstance), "Failed to create VkInstance") new VkInstance(pInstance.get(0), pCreateInfo) + protected val debugMessenger: Option[LongBuffer] = + if enableValidationLayers then pushStack: stack => + Some: + val callback = 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 + + + val debugMessengerCreate = VkDebugUtilsMessengerCreateInfoEXT.calloc(stack) + .sType$Default() + .messageSeverity( + VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT + ) + .messageType( + VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT + ) + .pfnUserCallback(callback) + + val debugMessengerBuff = stack.callocLong(1) + check(vkCreateDebugUtilsMessengerEXT( + handle, + debugMessengerCreate, + null, + debugMessengerBuff + )) + debugMessengerBuff + else None + + lazy val enabledLayers: Seq[String] = List .empty[String] .pipe: x => @@ -93,8 +155,10 @@ 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) = val n = stack.callocInt(1) From aa91317208858b45f7638c65c57ce7cafbd9f5ce Mon Sep 17 00:00:00 2001 From: Szymon Date: Mon, 8 Sep 2025 00:48:20 +0200 Subject: [PATCH 19/22] More adjustments and printf --- .../cyfra/spirv/compilers/GIOCompiler.scala | 16 ++-- .../cyfra/e2e/fs2interop/Fs2Tests.scala | 9 +- .../computenode/cyfra/fs2interop/Bridge.scala | 30 ++++-- .../computenode/cyfra/fs2interop/GPipe.scala | 26 +++-- .../cyfra/runtime/VkAllocation.scala | 7 +- .../cyfra/vulkan/command/CommandPool.scala | 2 + .../cyfra/vulkan/core/Instance.scala | 95 +++++++++++-------- .../cyfra/vulkan/memory/Buffer.scala | 1 - 8 files changed, 113 insertions(+), 73 deletions(-) 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 index 231d8f97..5d54fc2b 100644 --- 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 @@ -39,9 +39,8 @@ object GIOCompiler: 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) @@ -68,7 +67,7 @@ object GIOCompiler: nextResultId = baseId + 8, exprRefs = ctxWithN.exprRefs + (CurrentRepeatIndex.treeid -> phiId) ) - val (bodyInsts, ctxAfterBody) = compileGio(f, bodyCtx) + 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( @@ -118,14 +117,13 @@ object GIOCompiler: Instruction(Op.OpLabel, List(ResultRef(mergeId))) ) - val finalNextId = math.max(ctxAfterBody.nextResultId, addId + 1) - val finalCtx = ctxAfterBody.copy( - nextResultId = finalNextId, - exprRefs = ctxAfterBody.exprRefs - CurrentRepeatIndex.treeid - ) + // 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) 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 index 941ba3cc..7c8414c9 100644 --- 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 @@ -23,13 +23,16 @@ extension (f: fRGBA) 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: CyfraRuntime = VkCyfraRuntime( + 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 @@ -43,11 +46,13 @@ class Fs2Tests extends munit.FunSuite: assert(res == exp, s"Expected $exp, got $res") test("fs2 through GPipe map, floats and vectors"): - val inSeq = (0 to 255).map(_.toFloat).toSeq + val n = 16 + val inSeq = (0 to (n * 256 - 1)).map(_.toFloat).toSeq 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) => 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 index 41d366c3..b148a53b 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/Bridge.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/Bridge.scala @@ -1,13 +1,12 @@ +// 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 -import org.lwjgl.BufferUtils -import izumi.reflect.Tag +import java.nio.{ByteBuffer, ByteOrder} +import izumi.reflect.Tag import scala.reflect.ClassTag trait Bridge[CyfraType <: Value: FromExpr: Tag, ScalaType: ClassTag]: @@ -17,22 +16,33 @@ trait Bridge[CyfraType <: Value: FromExpr: Tag, ScalaType: ClassTag]: object Bridge: given Bridge[Int32, Int]: def toByteBuffer(inBuf: ByteBuffer, chunk: Chunk[Int]): ByteBuffer = - inBuf.asIntBuffer().put(chunk.toArray[Int]).flip() + 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.asIntBuffer().get(arr).flip() + outBuf.order(ByteOrder.nativeOrder()) + outBuf.asIntBuffer().get(arr) + outBuf.rewind() arr given Bridge[Float32, Float]: def toByteBuffer(inBuf: ByteBuffer, chunk: Chunk[Float]): ByteBuffer = - inBuf.asFloatBuffer().put(chunk.toArray[Float]).flip() + 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.asFloatBuffer().get(arr).flip() + 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) => @@ -46,7 +56,7 @@ object Bridge: 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.flip() + outBuf.rewind() arr given Bridge[GBoolean, Boolean]: @@ -55,4 +65,4 @@ object Bridge: inBuf def fromByteBuffer(outBuf: ByteBuffer, arr: Array[Boolean]): Array[Boolean] = outBuf.get(arr.asInstanceOf[Array[Byte]]).flip() - arr + 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 index 77ceb323..95c86efc 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -38,8 +38,7 @@ object GPipe: val invocId = GIO.invocationId val element = GIO.read[C1](layout.in, invocId) val res = f(element) - for - _ <- GIO.printf("Element %d -> %v4f", invocId, res) + for _ <- GIO.write[C2](layout.out, invocId, res) yield Empty() }) @@ -57,10 +56,15 @@ object GPipe: val outBuf = BufferUtils.createByteBuffer(params.inSize * outTypeSize) stream - .chunkMin(params.inSize) + .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)) + 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 @@ -81,7 +85,7 @@ object GPipe: val element = GIO.read[C](layout.in, invocId) val result = when(pred(element))(1: Int32).otherwise(0) for - _ <- GIO.printf("Element %d -> %d", invocId, result) + _ <- GIO.printf("Pred: Element %d -> %d", invocId, result) _ <- GIO.write[Int32](layout.out, invocId, result) yield Empty() @@ -102,7 +106,10 @@ object GPipe: val oldValue = GIO.read[Int32](layout.ints, end) val addValue = GIO.read[Int32](layout.ints, mid) val newValue = oldValue + addValue - GIO.write[Int32](layout.ints, end, newValue) + for + _ <- GIO.printf("Upsweep: invocId %d, root %d, mid %d, end %d, oldValue %d, addValue %d, newValue %d", invocId, root, 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))), @@ -115,7 +122,10 @@ object GPipe: 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 - GIO.write[Int32](layout.ints, mid, newValue) + 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 @@ -204,7 +214,7 @@ object GPipe: val compactBuf = BufferUtils.createByteBuffer(filterParams.inSize * typeSize) stream - .chunkMin(filterParams.inSize) + .chunkN(filterParams.inSize) .flatMap: chunk => bridge.toByteBuffer(predBuf, chunk) region.runUnsafe( 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 dc878c5b..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 @@ -50,10 +50,11 @@ 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 (uniforms: GUniform.type) 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..fe0d0636 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 @@ -43,6 +43,7 @@ private[cyfra] abstract class CommandPool private (flags: Int, val queue: Queue) val commandBuffer = beginSingleTimeCommands() block(commandBuffer) endSingleTimeCommands(commandBuffer).block().destroy() + //vkDeviceWaitIdle(device.get) freeCommandBuffer(commandBuffer) private def beginSingleTimeCommands(): VkCommandBuffer = @@ -74,6 +75,7 @@ private[cyfra] abstract class CommandPool private (flags: Int, val queue: Queue) val pointerBuffer = stack.callocPointer(commandBuffer.length) commandBuffer.foreach(pointerBuffer.put) pointerBuffer.flip() + vkDeviceWaitIdle(device.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 812c14d6..a1680c0e 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 @@ -90,61 +90,77 @@ private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObj .pSettingName(stack.ASCII("printf_buffer_size")) .`type`(VK_LAYER_SETTING_TYPE_UINT32_EXT) .valueCount(1) - .pValues(MemoryUtil.memByteBuffer(stack.ints(1024 * 1024))) + .pValues(MemoryUtil.memByteBuffer(stack.ints(1024*1024))) val layerSettingsCI = VkLayerSettingsCreateInfoEXT.calloc(stack).sType$Default().pSettings(layerSettings) val validationFeatures = VkValidationFeaturesEXT.calloc(stack) .sType$Default() - .pEnabledValidationFeatures(stack.ints(VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT)) + .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 debugMessenger: Option[LongBuffer] = - if enableValidationLayers then pushStack: stack => - Some: - val callback = 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 - - - val debugMessengerCreate = VkDebugUtilsMessengerCreateInfoEXT.calloc(stack) - .sType$Default() - .messageSeverity( - VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT - ) - .messageType( - VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT - ) - .pfnUserCallback(callback) - - val debugMessengerBuff = stack.callocLong(1) - check(vkCreateDebugUtilsMessengerEXT( - handle, - debugMessengerCreate, - null, - debugMessengerBuff - )) - debugMessengerBuff + 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 = stack.callocLong(1) + check(vkCreateDebugUtilsMessengerEXT( + handle, + debugMessengerCreate.get(0), + null, + debugMessengerBuff + )) + debugMessengerBuff + lazy val enabledLayers: Seq[String] = List .empty[String] @@ -155,10 +171,9 @@ 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) = val n = stack.callocInt(1) diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/Buffer.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/Buffer.scala index 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) From 6354e4214ae56383f3ad3f64b210b3131da631f2 Mon Sep 17 00:00:00 2001 From: Szymon Date: Mon, 8 Sep 2025 01:06:50 +0200 Subject: [PATCH 20/22] Works!!! --- .../scala/io/computenode/cyfra/fs2interop/GPipe.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 index 95c86efc..492a4d7d 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -33,7 +33,7 @@ object GPipe: val gProg = GProgram[Params, PLayout]( layout = params => PLayout(in = GBuffer[C1](params.inSize), out = GBuffer[C2](params.inSize)), - dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / 256, 1, 1)), + 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) @@ -79,7 +79,7 @@ object GPipe: val predicateProgram = GProgram[PredParams, PredLayout]( layout = params => PredLayout(in = GBuffer[C](params.inSize), out = GBuffer[Int32](params.inSize)), - dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / 256, 1, 1)), + 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) @@ -96,7 +96,7 @@ object GPipe: val upsweep = GProgram[ScanParams, ScanLayout]( layout = params => ScanLayout(ints = GBuffer[Int32](params.inSize), intervalSize = GUniform(ScanArgs(params.intervalSize))), - dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / params.intervalSize / 256, 1, 1)), + dispatch = (layout, params) => GProgram.StaticDispatch((Math.ceil(params.inSize.toFloat / params.intervalSize / 256).toInt, 1, 1)), ): layout => val ScanArgs(size) = layout.intervalSize.read val invocId = GIO.invocationId @@ -113,7 +113,7 @@ object GPipe: val downsweep = GProgram[ScanParams, ScanLayout]( layout = params => ScanLayout(ints = GBuffer[Int32](params.inSize), intervalSize = GUniform(ScanArgs(params.intervalSize))), - dispatch = (layout, params) => GProgram.StaticDispatch((params.inSize / params.intervalSize / 256, 1, 1)), + dispatch = (layout, params) => GProgram.StaticDispatch((Math.ceil(params.inSize.toFloat / params.intervalSize / 256).toInt, 1, 1)), ): layout => val ScanArgs(size) = layout.intervalSize.read val invocId = GIO.invocationId @@ -160,7 +160,7 @@ object GPipe: 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((params.inSize / 256, 1, 1)), + 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) From a052ba2265802ea7b6cb8e4c07d16f3ca825bae3 Mon Sep 17 00:00:00 2001 From: Szymon Date: Mon, 8 Sep 2025 21:34:17 +0200 Subject: [PATCH 21/22] Improvements --- .../cyfra/e2e/fs2interop/Fs2Tests.scala | 9 +-- .../computenode/cyfra/fs2interop/GPipe.scala | 62 +++++++++++-------- .../cyfra/runtime/VkCyfraRuntime.scala | 15 +++-- .../cyfra/vulkan/command/CommandPool.scala | 4 +- 4 files changed, 51 insertions(+), 39 deletions(-) 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 index 7c8414c9..289426ec 100644 --- 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 @@ -47,7 +47,7 @@ class Fs2Tests extends munit.FunSuite: test("fs2 through GPipe map, floats and vectors"): val n = 16 - val inSeq = (0 to (n * 256 - 1)).map(_.toFloat).toSeq + 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 @@ -59,11 +59,12 @@ class Fs2Tests extends munit.FunSuite: assert(res.close(exp)(0.001f), s"Expected $exp, got $res") test("fs2 through GPipe filter, just ints"): - val inSeq = (0 until 256).toSeq + val n = 16 + val inSeq = (0 until n * 256) val stream = Stream.emits(inSeq) - val pipe = GPipe.filter[Pure, Int32, Int](_.mod(2) === 0) + val pipe = GPipe.filter[Pure, Int32, Int](_.mod(7) === 0) val result = stream.through(pipe).compile.toList - val expected = inSeq.filter(_ % 2 == 0) + val expected = inSeq.filter(_ % 7 == 0) result .zip(expected) .foreach: (res, exp) => 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 index 492a4d7d..c79946be 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -73,6 +73,8 @@ object GPipe: 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 @@ -99,33 +101,35 @@ object GPipe: dispatch = (layout, params) => GProgram.StaticDispatch((Math.ceil(params.inSize.toFloat / params.intervalSize / 256).toInt, 1, 1)), ): layout => val ScanArgs(size) = layout.intervalSize.read - 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, mid %d, end %d, oldValue %d, addValue %d, newValue %d", invocId, root, mid, end, oldValue, addValue, newValue) - _ <- GIO.write[Int32](layout.ints, end, newValue) - yield Empty() + 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 - 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() + 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 @@ -199,7 +203,7 @@ object GPipe: .map(compactLayout => filterLayout) // finally setup buffers, region, parameters, and run the program - val filterParams = FilterParams(256, 2) + val filterParams = FilterParams(chunkInSize, 2) val region = GBufferRegion .allocate[FilterLayout] .map: filterLayout => @@ -210,16 +214,20 @@ object GPipe: // these are allocated once, reused for many chunks val predBuf = BufferUtils.createByteBuffer(filterParams.inSize * typeSize) - val scanBuf = BufferUtils.createByteBuffer(filterParams.inSize * intSize) + val filteredCount = BufferUtils.createByteBuffer(intSize) val compactBuf = BufferUtils.createByteBuffer(filterParams.inSize * typeSize) stream - .chunkN(filterParams.inSize) + .chunkN(chunkInSize) .flatMap: chunk => bridge.toByteBuffer(predBuf, chunk) region.runUnsafe( - init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](scanBuf), out = GBuffer[C](compactBuf)), - onDone = layout => layout.out.read(compactBuf), + init = FilterLayout(in = GBuffer[C](predBuf), scan = GBuffer[Int32](filterParams.inSize), out = GBuffer[C](filterParams.inSize)), + onDone = layout => { + layout.scan.read(filteredCount, (filterParams.inSize - 2) * intSize) + layout.out.read(compactBuf) + } ) - val arr = bridge.fromByteBuffer(compactBuf, new Array[S](256)) + val filteredN = filteredCount.getInt(0) + val arr = bridge.fromByteBuffer(compactBuf, new Array[S](filteredN)) Stream.emits(arr) diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala index c6383665..e38648f4 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 @@ -16,14 +16,17 @@ class VkCyfraRuntime(spirvToolsRunner: SpirvToolsRunner = SpirvToolsRunner()) ex private val shaderCache = mutable.Map[GProgram[?, ?], VkShader[?]]() private[cyfra] def getOrLoadProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}](program: GProgram[Params, L]): VkShader[L] = synchronized: - val spirvProgram = program match - case p: GioProgram[?, ?] => compile(p) - case p: SpirvProgram[?, ?] => p - case _ => throw new IllegalArgumentException(s"Unsupported program type: ${program.getClass.getName}") + if (shaderCache.contains(program)) then + shaderCache(program).asInstanceOf[VkShader[L]] + else + val spirvProgram = program match + case p: GioProgram[?, ?] => compile(p) + case p: SpirvProgram[?, ?] => p + case _ => throw new IllegalArgumentException(s"Unsupported program type: ${program.getClass.getName}") - shaderCache.getOrElseUpdate(program, VkShader(spirvProgram)).asInstanceOf[VkShader[L]] + shaderCache.getOrElseUpdate(program, VkShader(spirvProgram)).asInstanceOf[VkShader[L]] - private def compile[Params, L <: Layout: {LayoutBinding as lbinding, LayoutStruct as lstruct}](program: GioProgram[Params, L]): SpirvProgram[Params, 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) 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 fe0d0636..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 @@ -43,7 +43,6 @@ private[cyfra] abstract class CommandPool private (flags: Int, val queue: Queue) val commandBuffer = beginSingleTimeCommands() block(commandBuffer) endSingleTimeCommands(commandBuffer).block().destroy() - //vkDeviceWaitIdle(device.get) freeCommandBuffer(commandBuffer) private def beginSingleTimeCommands(): VkCommandBuffer = @@ -75,7 +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() - vkDeviceWaitIdle(device.get) + // 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 = From 19c0bfec5a57b4fd8801130da5edd4166764eba1 Mon Sep 17 00:00:00 2001 From: Szymon Date: Wed, 10 Sep 2025 01:29:23 +0200 Subject: [PATCH 22/22] Various fixes and improvements --- .../io/computenode/cyfra/core/GProgram.scala | 17 ++ .../computenode/cyfra/core/SpirvProgram.scala | 25 ++- .../cyfra/e2e/SpirvRuntimeEnduranceTest.scala | 209 ++++++++++++++++++ .../e2e/{ => dsl}/ArithmeticsE2eTest.scala | 6 +- .../e2e/{ => dsl}/FunctionsE2eTest.scala | 7 +- .../cyfra/e2e/{ => dsl}/GStructE2eTest.scala | 6 +- .../cyfra/e2e/{ => dsl}/GseqE2eTest.scala | 6 +- .../cyfra/e2e/{ => dsl}/WhenE2eTest.scala | 6 +- .../computenode/cyfra/fs2interop/GPipe.scala | 7 +- .../cyfra/runtime/VkCyfraRuntime.scala | 23 +- .../cyfra/vulkan/core/Instance.scala | 2 +- 11 files changed, 283 insertions(+), 31 deletions(-) create mode 100644 cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/SpirvRuntimeEnduranceTest.scala rename cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/{ => dsl}/ArithmeticsE2eTest.scala (95%) rename cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/{ => dsl}/FunctionsE2eTest.scala (94%) rename cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/{ => dsl}/GStructE2eTest.scala (96%) rename cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/{ => dsl}/GseqE2eTest.scala (94%) rename cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/{ => dsl}/WhenE2eTest.scala (91%) 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 f1cef442..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 @@ -12,6 +12,10 @@ 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 @@ -32,6 +36,19 @@ 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, GStructSchema}]() extends GUniform[T] 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 b4bcbd85..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,7 +28,26 @@ case class SpirvProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}] priv code: ByteBuffer, entryPoint: String, shaderBindings: L => ShaderLayout, -) 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]] @@ -41,7 +62,7 @@ object SpirvProgram: dispatch: (L, Params) => ProgramDispatch, code: ByteBuffer ): SpirvProgram[Params, L] = - 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(_)) 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-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala index c79946be..0efefce1 100644 --- a/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala +++ b/cyfra-fs2/src/main/scala/io/computenode/cyfra/fs2interop/GPipe.scala @@ -138,7 +138,7 @@ object GPipe: inSize: Int, intervalSize: Int, ): GExecution[ScanParams, ScanLayout, ScanLayout] = - if intervalSize >= inSize then exec + if intervalSize > inSize then exec else val newExec = exec.addProgram(upsweep)(params => ScanParams(inSize, intervalSize), layout => layout) upsweepPhases(newExec, inSize, intervalSize * 2) @@ -170,7 +170,7 @@ object GPipe: val element = GIO.read[C](layout.in, invocId) val prefixSum = GIO.read[Int32](layout.scan, invocId) for - _ <- GIO.printf("Element %d, prefix sum %d", invocId, prefixSum) + _ <- 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): @@ -224,10 +224,11 @@ object GPipe: 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 - 2) * intSize) + 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-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala index e38648f4..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 @@ -8,23 +8,26 @@ 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(spirvToolsRunner: SpirvToolsRunner = SpirvToolsRunner()) extends CyfraRuntime: private val context = new VulkanContext() import context.given - private val shaderCache = mutable.Map[GProgram[?, ?], VkShader[?]]() + 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: - if (shaderCache.contains(program)) then - shaderCache(program).asInstanceOf[VkShader[L]] - else - val spirvProgram = program match - case p: GioProgram[?, ?] => compile(p) - case p: SpirvProgram[?, ?] => p - case _ => throw new IllegalArgumentException(s"Unsupported program type: ${program.getClass.getName}") - - shaderCache.getOrElseUpdate(program, VkShader(spirvProgram)).asInstanceOf[VkShader[L]] + + 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 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 a1680c0e..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 @@ -152,7 +152,7 @@ private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObj ) .pfnUserCallback(c) - val debugMessengerBuff = stack.callocLong(1) + val debugMessengerBuff = MemoryUtil.memAllocLong(1) check(vkCreateDebugUtilsMessengerEXT( handle, debugMessengerCreate.get(0),