diff --git a/README.md b/README.md
index c4488e56..e59b4dcb 100644
--- a/README.md
+++ b/README.md
@@ -3,8 +3,10 @@
Library provides a way to compile Scala 3 DSL to SPIR-V and to run it with Vulkan runtime on GPUs.
It is multiplatform. It works on:
- - Linux, Windows, and Mac (for Mac requires installation of moltenvk).
- - Any dedicated or integrated GPUs that support Vulkan. In practice, it means almost all moderately modern devices from most manufacturers including Nvidia, AMD, Intel, Apple.
+
+- Linux, Windows, and Mac (for Mac requires installation of moltenvk).
+- Any dedicated or integrated GPUs that support Vulkan. In practice, it means almost all moderately modern devices from
+ most manufacturers including Nvidia, AMD, Intel, Apple.
Library is in an early stage - alpha release and proper documentation are coming.
@@ -15,12 +17,15 @@ Included Foton library provides a clean and fun way to animate functions and ray
## Examples
### Ray traced animation
+

[code](https://github.com/ComputeNode/cyfra/blob/50aecea/cyfra-examples/src/main/scala/io/computenode/samples/cyfra/foton/AnimatedRaytrace.scala)
-(this is API usage, to see ray tracing implementation look at [RtRenderer](https://github.com/ComputeNode/cyfra/blob/50aecea132188776021afe0b407817665676a021/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/RtRenderer.scala))
+(this is API usage, to see ray tracing implementation look
+at [RtRenderer](https://github.com/ComputeNode/cyfra/blob/50aecea132188776021afe0b407817665676a021/cyfra-foton/src/main/scala/io/computenode/cyfra/foton/rt/RtRenderer.scala))
### Animated Julia set
+
[code](https://github.com/ComputeNode/cyfra/blob/50aecea132188776021afe0b407817665676a021/cyfra-examples/src/main/scala/io/computenode/samples/cyfra/foton/AnimatedJulia.scala)
@@ -28,9 +33,11 @@ Included Foton library provides a clean and fun way to animate functions and ray
## Animation features examples
### Custom animated functions
+
### Animated ray traced scene
+
## Coding features examples
@@ -48,9 +55,15 @@ Included Foton library provides a clean and fun way to animate functions and ray
## Development
-To enable validation layers for vulkan, you need to install vulkan SKD. After installing, set the following VM options:
+To enable validation layers for vulkan, you need to install vulkan SKD. After installing, set the following VM option:
+
```
--Dorg.lwjgl.vulkan.libname=libvulkan.1.dylib
-Dio.computenode.cyfra.vulkan.validation=true
+```
+
+If you are on MacOs, then also add:
+
+```
+-Dorg.lwjgl.vulkan.libname=libvulkan.1.dylib
-Djava.library.path=$VULKAN_SDK/lib
```
\ No newline at end of file
diff --git a/build.sbt b/build.sbt
index 44acec5f..35a46c2c 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,8 +1,8 @@
ThisBuild / organization := "com.computenode.cyfra"
ThisBuild / scalaVersion := "3.6.4"
-ThisBuild / version := "0.1.0-SNAPSHOT"
+ThisBuild / version := "0.2.0-SNAPSHOT"
-val lwjglVersion = "3.3.6"
+val lwjglVersion = "3.4.0-SNAPSHOT"
val jomlVersion = "1.10.0"
lazy val osName = System.getProperty("os.name").toLowerCase
@@ -37,8 +37,9 @@ lazy val vulkanNatives =
lazy val commonSettings = Seq(
scalacOptions ++= Seq("-feature", "-deprecation", "-unchecked", "-language:implicitConversions"),
+ resolvers += "maven snapshots" at "https://central.sonatype.com/repository/maven-snapshots/",
libraryDependencies ++= Seq(
- "dev.zio" % "izumi-reflect_3" % "2.3.10",
+ "dev.zio" % "izumi-reflect_3" % "3.0.5",
"com.lihaoyi" % "pprint_3" % "0.9.0",
"com.diogonunes" % "JColor" % "5.5.1",
"org.lwjgl" % "lwjgl" % lwjglVersion,
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 194b3a2f..783efaff 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
@@ -1,6 +1,6 @@
package io.computenode.cyfra.core
-import io.computenode.cyfra.core.layout.{Layout, LayoutStruct}
+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}
@@ -11,14 +11,15 @@ import java.nio.ByteBuffer
trait Allocation:
extension (buffer: GBinding[?])
- def read(bb: ByteBuffer, offset: Int = 0, length: Int = -1): Unit
+ def read(bb: ByteBuffer, offset: Int = 0, size: Int = -1): Unit
- def write(bb: ByteBuffer, offset: Int = 0, length: Int = -1): Unit
+ def write(bb: ByteBuffer, offset: Int = 0, size: Int = -1): Unit
- extension [Params, L <: Layout, RL <: Layout: LayoutStruct](execution: GExecution[Params, L, RL]) def execute(params: Params, layout: L): RL
+ extension [Params, EL <: Layout: LayoutBinding, RL <: Layout: LayoutBinding](execution: GExecution[Params, EL, RL])
+ def execute(params: Params, layout: EL): RL
extension (buffers: GBuffer.type)
- def apply[T <: Value: {Tag, FromExpr}](size: Int): GBuffer[T]
+ def apply[T <: Value: {Tag, FromExpr}](length: Int): GBuffer[T]
def apply[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GBuffer[T]
diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/CyfraRuntime.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/CyfraRuntime.scala
index a7c029e4..a38c620c 100644
--- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/CyfraRuntime.scala
+++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/CyfraRuntime.scala
@@ -4,4 +4,4 @@ import io.computenode.cyfra.core.Allocation
trait CyfraRuntime:
- def allocation(): Allocation
+ def withAllocation(f: Allocation => Unit): Unit
diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GBufferRegion.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GBufferRegion.scala
index 2aec9159..cfd3ad8d 100644
--- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GBufferRegion.scala
+++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GBufferRegion.scala
@@ -1,8 +1,8 @@
package io.computenode.cyfra.core
import io.computenode.cyfra.core.Allocation
-import io.computenode.cyfra.core.GProgram.BufferSizeSpec
-import io.computenode.cyfra.core.layout.{Layout, LayoutStruct}
+import io.computenode.cyfra.core.GProgram.BufferLengthSpec
+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.GBuffer
@@ -10,38 +10,34 @@ import izumi.reflect.Tag
import java.nio.ByteBuffer
-sealed trait GBufferRegion[ReqAlloc <: Layout: LayoutStruct, ResAlloc <: Layout: LayoutStruct]:
- val initAlloc: ReqAlloc
+sealed trait GBufferRegion[ReqAlloc <: Layout: LayoutBinding, ResAlloc <: Layout: LayoutBinding]
object GBufferRegion:
- def allocate[Alloc <: Layout: LayoutStruct]: GBufferRegion[Alloc, Alloc] =
- AllocRegion(summon[LayoutStruct[Alloc]].layoutRef)
+ def allocate[Alloc <: Layout: LayoutBinding]: GBufferRegion[Alloc, Alloc] = AllocRegion()
- case class AllocRegion[Alloc <: Layout: LayoutStruct](l: Alloc) extends GBufferRegion[Alloc, Alloc]:
- val initAlloc: Alloc = l
+ case class AllocRegion[Alloc <: Layout: LayoutBinding]() extends GBufferRegion[Alloc, Alloc]
- case class MapRegion[ReqAlloc <: Layout: LayoutStruct, BodyAlloc <: Layout: LayoutStruct, ResAlloc <: Layout: LayoutStruct](
+ case class MapRegion[ReqAlloc <: Layout: LayoutBinding, BodyAlloc <: Layout: LayoutBinding, ResAlloc <: Layout: LayoutBinding](
reqRegion: GBufferRegion[ReqAlloc, BodyAlloc],
f: Allocation => BodyAlloc => ResAlloc,
- ) extends GBufferRegion[ReqAlloc, ResAlloc]:
- val initAlloc: ReqAlloc = reqRegion.initAlloc
+ ) extends GBufferRegion[ReqAlloc, ResAlloc]
- extension [ReqAlloc <: Layout: LayoutStruct, ResAlloc <: Layout: LayoutStruct](region: GBufferRegion[ReqAlloc, ResAlloc])
- def map[NewAlloc <: Layout: LayoutStruct](f: Allocation ?=> ResAlloc => NewAlloc): GBufferRegion[ReqAlloc, NewAlloc] =
+ extension [ReqAlloc <: Layout: LayoutBinding, ResAlloc <: Layout: LayoutBinding](region: GBufferRegion[ReqAlloc, ResAlloc])
+ def map[NewAlloc <: Layout: LayoutBinding](f: Allocation ?=> ResAlloc => NewAlloc): GBufferRegion[ReqAlloc, NewAlloc] =
MapRegion(region, (alloc: Allocation) => (resAlloc: ResAlloc) => f(using alloc)(resAlloc))
def runUnsafe(init: Allocation ?=> ReqAlloc, onDone: Allocation ?=> ResAlloc => Unit)(using cyfraRuntime: CyfraRuntime): Unit =
- val allocation = cyfraRuntime.allocation()
- init(using allocation)
+ cyfraRuntime.withAllocation: allocation =>
- // noinspection ScalaRedundantCast
- val steps: Seq[Allocation => Layout => Layout] = Seq.unfold(region: GBufferRegion[?, ?]):
- case _: AllocRegion[?] => None
- case MapRegion(req, f) =>
- Some((f.asInstanceOf[Allocation => Layout => Layout], req))
+ // noinspection ScalaRedundantCast
+ val steps: Seq[Allocation => Layout => Layout] = Seq.unfold(region: GBufferRegion[?, ?]):
+ case _: AllocRegion[?] => None
+ case MapRegion(req, f) =>
+ Some((f.asInstanceOf[Allocation => Layout => Layout], req))
- val bodyAlloc = steps.foldLeft[Layout](region.initAlloc): (acc, step) =>
- step(allocation)(acc)
+ val initAlloc = init(using allocation)
+ val bodyAlloc = steps.foldLeft[Layout](initAlloc): (acc, step) =>
+ step(allocation)(acc)
- onDone(using allocation)(bodyAlloc.asInstanceOf[ResAlloc])
+ onDone(using allocation)(bodyAlloc.asInstanceOf[ResAlloc])
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 0caabec6..22418ead 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
@@ -10,15 +10,18 @@ import io.computenode.cyfra.spirv.compilers.ExpressionCompiler.UniformStructRef
import izumi.reflect.Tag
import GExecution.*
-trait GExecution[-Params, ExecLayout <: Layout, +ResLayout <: Layout]:
+trait GExecution[-Params, ExecLayout <: Layout: LayoutBinding, ResLayout <: Layout: LayoutBinding]:
- def flatMap[NRL <: Layout, NP <: Params](f: ResLayout => GExecution[NP, ExecLayout, NRL]): GExecution[NP, ExecLayout, NRL] =
+ def layoutBinding: LayoutBinding[ExecLayout] = summon[LayoutBinding[ExecLayout]]
+ def resLayoutBinding: LayoutBinding[ResLayout] = summon[LayoutBinding[ResLayout]]
+
+ def flatMap[NRL <: Layout: LayoutBinding, NP <: Params](f: ResLayout => GExecution[NP, ExecLayout, NRL]): GExecution[NP, ExecLayout, NRL] =
FlatMap(this, (p, r) => f(r))
- def map[NRL <: Layout](f: ResLayout => NRL): GExecution[Params, ExecLayout, NRL] =
+ def map[NRL <: Layout: LayoutBinding](f: ResLayout => NRL): GExecution[Params, ExecLayout, NRL] =
Map(this, f, identity, identity)
- def contramap[NL <: Layout](f: NL => ExecLayout): GExecution[Params, NL, ResLayout] =
+ def contramap[NEL <: Layout: LayoutBinding](f: NEL => ExecLayout): GExecution[Params, NEL, ResLayout] =
Map(this, identity, f, identity)
def contramapParams[NP](f: NP => Params): GExecution[NP, ExecLayout, ResLayout] =
@@ -32,31 +35,33 @@ trait GExecution[-Params, ExecLayout <: Layout, +ResLayout <: Layout]:
object GExecution:
- def apply[Params, L <: Layout]() =
- Pure[Params, L, L]()
+ def apply[Params, L <: Layout: LayoutBinding]() =
+ Pure[Params, L]()
- def forParams[Params, L <: Layout, RL <: Layout](f: Params => GExecution[Params, L, RL]): GExecution[Params, L, RL] =
- FlatMap[Params, L, RL, RL](Pure[Params, L, RL](), (params: Params, _: RL) => f(params))
+ def forParams[Params, EL <: Layout: LayoutBinding, RL <: Layout: LayoutBinding](
+ f: Params => GExecution[Params, EL, RL],
+ ): GExecution[Params, EL, RL] =
+ FlatMap[Params, EL, EL, RL](Pure[Params, EL](), (params: Params, _: EL) => f(params))
- case class Pure[Params, L <: Layout, RL <: Layout]() extends GExecution[Params, L, RL]
+ case class Pure[Params, L <: Layout: LayoutBinding]() extends GExecution[Params, L, L]
- case class FlatMap[Params, L <: Layout, RL <: Layout, NRL <: Layout](
- execution: GExecution[Params, L, RL],
- f: (Params, RL) => GExecution[Params, L, NRL],
- ) extends GExecution[Params, L, NRL]
+ case class FlatMap[Params, EL <: Layout: LayoutBinding, RL <: Layout: LayoutBinding, NRL <: Layout: LayoutBinding](
+ execution: GExecution[Params, EL, RL],
+ f: (Params, RL) => GExecution[Params, EL, NRL],
+ ) extends GExecution[Params, EL, NRL]
- case class Map[P, NP, L <: Layout, NL <: Layout, RL <: Layout, NRL <: Layout](
- execution: GExecution[P, L, RL],
+ case class Map[P, NP, EL <: Layout: LayoutBinding, NEL <: Layout: LayoutBinding, RL <: Layout: LayoutBinding, NRL <: Layout: LayoutBinding](
+ execution: GExecution[P, EL, RL],
mapResult: RL => NRL,
- contramapLayout: NL => L,
+ contramapLayout: NEL => EL,
contramapParams: NP => P,
- ) extends GExecution[NP, NL, NRL]:
+ ) extends GExecution[NP, NEL, NRL]:
- override def map[NNRL <: Layout](f: NRL => NNRL): GExecution[NP, NL, NNRL] =
+ override def map[NNRL <: Layout: LayoutBinding](f: NRL => NNRL): GExecution[NP, NEL, NNRL] =
Map(execution, mapResult andThen f, contramapLayout, contramapParams)
- override def contramapParams[NNP](f: NNP => NP): GExecution[NNP, NL, NRL] =
+ override def contramapParams[NNP](f: NNP => NP): GExecution[NNP, NEL, NRL] =
Map(execution, mapResult, contramapLayout, f andThen contramapParams)
- override def contramap[NNL <: Layout](f: NNL => NL): GExecution[NP, NNL, NRL] =
+ override def contramap[NNL <: Layout: LayoutBinding](f: NNL => NEL): GExecution[NP, NNL, NRL] =
Map(execution, mapResult, f andThen contramapLayout, contramapParams)
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 77586f5e..da5407df 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
@@ -1,70 +1,48 @@
package io.computenode.cyfra.core
-import io.computenode.cyfra.core.layout.LayoutStruct
+import io.computenode.cyfra.core.layout.{Layout, LayoutBinding, LayoutStruct}
import io.computenode.cyfra.dsl.gio.GIO
-import io.computenode.cyfra.core.layout.Layout
import java.nio.ByteBuffer
import GProgram.*
import io.computenode.cyfra.dsl.{Expression, Value}
-import io.computenode.cyfra.dsl.Value.{FromExpr, Int32}
+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.Empty
import izumi.reflect.Tag
-sealed trait GProgram[Params, L <: Layout: LayoutStruct] extends GExecution[Params, L, L]:
+trait GProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}] extends GExecution[Params, L, L]:
val layout: InitProgramLayout => Params => L
val dispatch: (L, Params) => ProgramDispatch
val workgroupSize: WorkDimensions
+ private[cyfra] def cacheKey: String // TODO better type
+ def layoutStruct = summon[LayoutStruct[L]]
object GProgram:
-
- class GioProgram[Params, L <: Layout: LayoutStruct](
- val body: L => GIO[?],
- val layout: InitProgramLayout => Params => L,
- val dispatch: (L, Params) => ProgramDispatch,
- val workgroupSize: WorkDimensions,
- ) extends GProgram[Params, L]:
- private[cyfra] def layoutStruct: LayoutStruct[L] = summon[LayoutStruct[L]]
-
- class SpirvProgram[Params, L <: Layout: LayoutStruct](
- val code: ByteBuffer,
- val layout: InitProgramLayout => Params => L,
- val dispatch: (L, Params) => ProgramDispatch,
- val workgroupSize: WorkDimensions,
- ) extends GProgram[Params, L]:
- private[cyfra] def layoutStruct: LayoutStruct[L] = summon[LayoutStruct[L]]
-
type WorkDimensions = (Int, Int, Int)
sealed trait ProgramDispatch
-
case class DynamicDispatch[L <: Layout](buffer: GBinding[?], offset: Int) extends ProgramDispatch
-
case class StaticDispatch(size: WorkDimensions) extends ProgramDispatch
- private[cyfra] case class BufferSizeSpec[T <: Value: {Tag, FromExpr}](size: Int) extends GBuffer[T]
-
- private[cyfra] case class ParamUniform[T <: GStruct[T]: {Tag, FromExpr}](value: T) extends GUniform[T]
+ def apply[Params, L <: Layout: {LayoutBinding, LayoutStruct}](
+ layout: InitProgramLayout ?=> Params => L,
+ dispatch: (L, Params) => ProgramDispatch,
+ workgroupSize: WorkDimensions = (128, 1, 1),
+ )(body: L => GIO[?]): GProgram[Params, L] =
+ new GioProgram[Params, L](body, s => layout(using s), dispatch, workgroupSize)
- private[cyfra] case class DynamicUniform[T <: GStruct[T]: {Tag, FromExpr}]() extends GUniform[T]
+ 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]
trait InitProgramLayout:
- extension (buffers: GBuffer.type)
- def apply[T <: Value: {Tag, FromExpr}](size: Int): GBuffer[T] =
- BufferSizeSpec[T](size)
-
- extension (uniforms: GUniform.type)
- def apply[T <: GStruct[T]: {Tag, FromExpr}](value: T): GUniform[T] =
- ParamUniform[T](value)
+ extension (_buffers: GBuffer.type)
+ def apply[T <: Value: {Tag, FromExpr}](length: Int): GBuffer[T] =
+ BufferLengthSpec[T](length)
+ extension (_uniforms: GUniform.type)
def apply[T <: GStruct[T]: {Tag, FromExpr}](): GUniform[T] =
DynamicUniform[T]()
-
- def apply[Params, L <: Layout: LayoutStruct](
- layout: InitProgramLayout ?=> Params => L,
- dispatch: (L, Params) => ProgramDispatch,
- workgroupSize: WorkDimensions = (128, 1, 1),
- )(body: L => GIO[?]): GProgram[Params, L] =
- new GioProgram[Params, L](body, s => layout(using s), dispatch, workgroupSize)
+ def apply[T <: GStruct[T]: {Tag, FromExpr}](value: T): GUniform[T]
diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/GioProgram.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GioProgram.scala
new file mode 100644
index 00000000..d97f97b2
--- /dev/null
+++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/GioProgram.scala
@@ -0,0 +1,19 @@
+package io.computenode.cyfra.core
+
+import io.computenode.cyfra.core.GProgram.*
+import io.computenode.cyfra.core.layout.*
+import io.computenode.cyfra.dsl.Value.GBoolean
+import io.computenode.cyfra.dsl.gio.GIO
+import izumi.reflect.Tag
+
+case class GioProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}](
+ body: L => GIO[?],
+ 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 _ => ???
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
new file mode 100644
index 00000000..c7b9f29a
--- /dev/null
+++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/SpirvProgram.scala
@@ -0,0 +1,61 @@
+package io.computenode.cyfra.core
+
+import io.computenode.cyfra.core.layout.{Layout, LayoutBinding, LayoutStruct}
+import io.computenode.cyfra.core.GProgram.{InitProgramLayout, ProgramDispatch, WorkDimensions}
+import io.computenode.cyfra.core.SpirvProgram.Operation.ReadWrite
+import io.computenode.cyfra.core.SpirvProgram.{Binding, ShaderLayout}
+import io.computenode.cyfra.dsl.Value
+import io.computenode.cyfra.dsl.Value.{FromExpr, GBoolean}
+import io.computenode.cyfra.dsl.binding.GBinding
+import io.computenode.cyfra.dsl.gio.GIO
+import izumi.reflect.Tag
+
+import java.io.File
+import java.io.FileInputStream
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
+import java.util.Objects
+import scala.util.Try
+import scala.util.Using
+import scala.util.chaining.*
+
+case class SpirvProgram[Params, L <: Layout: {LayoutBinding, LayoutStruct}] private (
+ layout: InitProgramLayout => Params => L,
+ dispatch: (L, Params) => ProgramDispatch,
+ workgroupSize: WorkDimensions,
+ code: ByteBuffer,
+ entryPoint: String,
+ shaderBindings: L => ShaderLayout,
+ cacheKey: String,
+) extends GProgram[Params, L]
+
+object SpirvProgram:
+ type ShaderLayout = Seq[Seq[Binding]]
+ case class Binding(binding: GBinding[?], operation: Operation)
+ enum Operation:
+ case Read
+ case Write
+ case ReadWrite
+
+ def apply[Params, L <: Layout: {LayoutBinding, LayoutStruct}](
+ path: String,
+ layout: InitProgramLayout ?=> Params => L,
+ dispatch: (L, Params) => ProgramDispatch,
+ ): 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())
diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/Layout.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/Layout.scala
index d03d858f..37f369e8 100644
--- a/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/Layout.scala
+++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/Layout.scala
@@ -1,6 +1,3 @@
package io.computenode.cyfra.core.layout
-import io.computenode.cyfra.dsl.Value
-import io.computenode.cyfra.dsl.binding.GBuffer
-
trait Layout
diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/LayoutBinding.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/LayoutBinding.scala
new file mode 100644
index 00000000..5a7eaa52
--- /dev/null
+++ b/cyfra-core/src/main/scala/io/computenode/cyfra/core/layout/LayoutBinding.scala
@@ -0,0 +1,34 @@
+package io.computenode.cyfra.core.layout
+
+import io.computenode.cyfra.dsl.binding.GBinding
+
+import scala.Tuple.*
+import scala.compiletime.{constValue, erasedValue, error}
+import scala.deriving.Mirror
+
+trait LayoutBinding[L <: Layout]:
+ def fromBindings(bindings: Seq[GBinding[?]]): L
+ def toBindings(layout: L): Seq[GBinding[?]]
+
+object LayoutBinding:
+ inline given derived[L <: Layout](using m: Mirror.ProductOf[L]): LayoutBinding[L] =
+ allElementsAreBindings[m.MirroredElemTypes, m.MirroredElemLabels]()
+ val size = constValue[Size[m.MirroredElemTypes]]
+ val constructor = m.fromProduct
+ new DerivedLayoutBinding[L](size, constructor)
+
+ // noinspection NoTailRecursionAnnotation
+ private inline def allElementsAreBindings[Types <: Tuple, Names <: Tuple](): Unit =
+ inline erasedValue[Types] match
+ case _: EmptyTuple => ()
+ case _: (GBinding[?] *: t) => allElementsAreBindings[t, Tail[Names]]()
+ case _ =>
+ val name = constValue[Head[Names]]
+ error(s"$name is not a GBinding, all elements of a Layout must be GBindings")
+
+ class DerivedLayoutBinding[L <: Layout](size: Int, constructor: Product => L) extends LayoutBinding[L]:
+ override def fromBindings(bindings: Seq[GBinding[?]]): L =
+ assert(bindings.size == size, s"Expected $size) bindings, got ${bindings.size}")
+ constructor(Tuple.fromArray(bindings.toArray))
+ override def toBindings(layout: L): Seq[GBinding[?]] =
+ layout.asInstanceOf[Product].productIterator.map(_.asInstanceOf[GBinding[?]]).toSeq
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 d9006d69..60c53cac 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
@@ -1,7 +1,30 @@
package io.computenode.cyfra.dsl.binding
import io.computenode.cyfra.dsl.Value
-import io.computenode.cyfra.dsl.Value.FromExpr
+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 izumi.reflect.Tag
-trait GBinding[T <: Value: {Tag, FromExpr}]
+sealed trait GBinding[T <: Value: {Tag, FromExpr}]:
+ def tag = summon[Tag[T]]
+ def fromExpr = summon[FromExpr[T]]
+
+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)
+
+object GBuffer
+
+trait GUniform[T <: Value: {Tag, FromExpr}] extends GBinding[T]:
+ def read: T = fromExprEval(ReadUniform(this))
+
+ def write(value: T): GIO[Unit] = WriteUniform(this, value)
+
+object GUniform:
+
+ class ParamUniform[T <: GStruct[T]: {Tag, FromExpr}]() extends GUniform[T]
+
+ def fromParams[T <: GStruct[T]: {Tag, FromExpr}] = ParamUniform[T]()
diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GBuffer.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GBuffer.scala
deleted file mode 100644
index b9f849cf..00000000
--- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GBuffer.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-package io.computenode.cyfra.dsl.binding
-
-import io.computenode.cyfra.dsl.Value
-import io.computenode.cyfra.dsl.Value.{FromExpr, Int32}
-import io.computenode.cyfra.dsl.gio.GIO
-import izumi.reflect.Tag
-
-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)
-
-object GBuffer
diff --git a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GUniform.scala b/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GUniform.scala
deleted file mode 100644
index 9e753e77..00000000
--- a/cyfra-dsl/src/main/scala/io/computenode/cyfra/dsl/binding/GUniform.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package io.computenode.cyfra.dsl.binding
-
-import io.computenode.cyfra.dsl.Value
-import io.computenode.cyfra.dsl.Value.FromExpr
-import io.computenode.cyfra.dsl.Value.FromExpr.fromExpr
-import io.computenode.cyfra.dsl.gio.GIO
-import io.computenode.cyfra.dsl.struct.GStruct
-import izumi.reflect.Tag
-
-trait GUniform[T <: Value: {Tag, FromExpr}] extends GBinding[T]:
- def read: T = fromExpr(ReadUniform(this))
-
- def write(value: T): GIO[Unit] = WriteUniform(this, value)
-
-object GUniform:
-
- case class ParamUniform[T <: GStruct[T]: {Tag, FromExpr}]() extends GUniform[T]
-
- def fromParams[T <: GStruct[T]: {Tag, FromExpr}] = ParamUniform[T]()
diff --git a/cyfra-examples/src/main/resources/addOne.comp b/cyfra-examples/src/main/resources/addOne.comp
new file mode 100644
index 00000000..091de31f
--- /dev/null
+++ b/cyfra-examples/src/main/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-vulkan/src/test/resources/compileAll.ps1 b/cyfra-examples/src/main/resources/compileAll.ps1
similarity index 100%
rename from cyfra-vulkan/src/test/resources/compileAll.ps1
rename to cyfra-examples/src/main/resources/compileAll.ps1
diff --git a/cyfra-vulkan/src/test/resources/compileAll.sh b/cyfra-examples/src/main/resources/compileAll.sh
similarity index 85%
rename from cyfra-vulkan/src/test/resources/compileAll.sh
rename to cyfra-examples/src/main/resources/compileAll.sh
index fdd4be8c..e4f70140 100644
--- a/cyfra-vulkan/src/test/resources/compileAll.sh
+++ b/cyfra-examples/src/main/resources/compileAll.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
for f in *.comp
do
diff --git a/cyfra-examples/src/main/resources/emit.comp b/cyfra-examples/src/main/resources/emit.comp
new file mode 100644
index 00000000..5789c424
--- /dev/null
+++ b/cyfra-examples/src/main/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-examples/src/main/resources/filter.comp b/cyfra-examples/src/main/resources/filter.comp
new file mode 100644
index 00000000..37beef64
--- /dev/null
+++ b/cyfra-examples/src/main/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-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala
index d94c895b..e604bde3 100644
--- a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala
+++ b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala
@@ -1,15 +1,16 @@
package io.computenode.cyfra.samples
import io.computenode.cyfra.core.archive.GContext
-import io.computenode.cyfra.core.{GBufferRegion, GExecution, GProgram}
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 org.lwjgl.BufferUtils
import io.computenode.cyfra.runtime.VkCyfraRuntime
+import org.lwjgl.BufferUtils
+import org.lwjgl.system.MemoryUtil
object TestingStuff:
given GContext = GContext()
@@ -33,7 +34,7 @@ object TestingStuff:
out = GBuffer[Int32](params.inSize * params.emitN),
args = GUniform(EmitProgramUniform(params.emitN)),
),
- dispatch = (layout, args) => GProgram.StaticDispatch((args.inSize, 1, 1)),
+ dispatch = (_, args) => GProgram.StaticDispatch((args.inSize / 128, 1, 1)),
): layout =>
val EmitProgramUniform(emitN) = layout.args.read
val invocId = GIO.invocationId
@@ -58,7 +59,7 @@ object TestingStuff:
out = GBuffer[GBoolean](params.inSize),
params = GUniform(FilterProgramUniform(params.filterValue)),
),
- dispatch = (layout, args) => GProgram.StaticDispatch((args.inSize, 1, 1)),
+ dispatch = (_, args) => GProgram.StaticDispatch((args.inSize / 128, 1, 1)),
): layout =>
val invocId = GIO.invocationId
val element = GIO.read(layout.in, invocId)
@@ -79,13 +80,13 @@ object TestingStuff:
layout => EmitProgramLayout(in = layout.inBuffer, out = layout.emitBuffer),
)
.addProgram(filterProgram)(
- params => FilterProgramParams(inSize = params.inSize, filterValue = params.filterValue),
+ params => FilterProgramParams(inSize = 2 * params.inSize, filterValue = params.filterValue),
layout => FilterProgramLayout(in = layout.emitBuffer, out = layout.filterBuffer),
)
@main
def test =
- given VkCyfraRuntime = VkCyfraRuntime()
+ given runtime: VkCyfraRuntime = VkCyfraRuntime()
val emitFilterParams = EmitFilterParams(inSize = 1024, emitN = 2, filterValue = 42)
@@ -94,16 +95,136 @@ object TestingStuff:
.map: region =>
emitFilterExecution.execute(emitFilterParams, region)
- val data = (0 to 1024).toArray
+ val data = (0 until 1024).toArray
val buffer = BufferUtils.createByteBuffer(data.length * 4)
buffer.asIntBuffer().put(data).flip()
- val result = BufferUtils.createByteBuffer(data.length * 2)
+ val result = BufferUtils.createIntBuffer(data.length * 2)
+ val rbb = MemoryUtil.memByteBuffer(result)
region.runUnsafe(
init = EmitFilterLayout(
inBuffer = GBuffer[Int32](buffer),
emitBuffer = GBuffer[Int32](data.length * 2),
filterBuffer = GBuffer[GBoolean](data.length * 2),
),
- onDone = layout => layout.filterBuffer.read(result),
+ onDone = layout => layout.filterBuffer.read(rbb),
+ )
+ runtime.close()
+
+ val actual = (0 until 2 * 1024).map(i => result.get(i * 1) != 0)
+ val expected = (0 until 1024).flatMap(x => Seq.fill(emitFilterParams.emitN)(x)).map(_ == emitFilterParams.filterValue)
+ expected
+ .zip(actual)
+ .zipWithIndex
+ .foreach:
+ case ((e, a), i) => assert(e == a, s"Mismatch at index $i: expected $e, got $a")
+
+// Test case: Use one program 10 times, copying values from five input buffers to five output buffers and adding values from two uniforms
+ case class AddProgramParams(bufferSize: Int, addA: Int, addB: Int)
+ case class AddProgramUniform(a: Int32) extends GStruct[AddProgramUniform]
+ case class AddProgramLayout(
+ in1: GBuffer[Int32],
+ in2: GBuffer[Int32],
+ in3: GBuffer[Int32],
+ in4: GBuffer[Int32],
+ in5: GBuffer[Int32],
+ out1: GBuffer[Int32],
+ out2: GBuffer[Int32],
+ out3: GBuffer[Int32],
+ out4: GBuffer[Int32],
+ out5: GBuffer[Int32],
+ u1: GUniform[AddProgramUniform] = GUniform.fromParams,
+ u2: GUniform[AddProgramUniform] = GUniform.fromParams,
+ ) extends Layout
+
+ case class AddProgramExecLayout(
+ in1: GBuffer[Int32],
+ in2: GBuffer[Int32],
+ in3: GBuffer[Int32],
+ in4: GBuffer[Int32],
+ in5: GBuffer[Int32],
+ out1: GBuffer[Int32],
+ out2: GBuffer[Int32],
+ out3: GBuffer[Int32],
+ out4: GBuffer[Int32],
+ out5: GBuffer[Int32],
+ ) extends Layout
+
+ val addProgram: GProgram[AddProgramParams, AddProgramLayout] = GProgram[AddProgramParams, AddProgramLayout](
+ layout = params =>
+ AddProgramLayout(
+ in1 = GBuffer[Int32](params.bufferSize),
+ in2 = GBuffer[Int32](params.bufferSize),
+ in3 = GBuffer[Int32](params.bufferSize),
+ in4 = GBuffer[Int32](params.bufferSize),
+ in5 = GBuffer[Int32](params.bufferSize),
+ out1 = GBuffer[Int32](params.bufferSize),
+ out2 = GBuffer[Int32](params.bufferSize),
+ out3 = GBuffer[Int32](params.bufferSize),
+ out4 = GBuffer[Int32](params.bufferSize),
+ out5 = GBuffer[Int32](params.bufferSize),
+ u1 = GUniform(AddProgramUniform(params.addA)),
+ u2 = GUniform(AddProgramUniform(params.addB)),
+ ),
+ dispatch = (layout, args) => GProgram.StaticDispatch((args.bufferSize / 128, 1, 1)),
+ )(_ => ???)
+ def swap(l: AddProgramLayout): AddProgramLayout =
+ val AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5, u1, u2) = l
+ AddProgramLayout(out1, out2, out3, out4, out5, in1, in2, in3, in4, in5, u1, u2)
+
+ def fromExecLayout(l: AddProgramExecLayout): AddProgramLayout =
+ val AddProgramExecLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5) = l
+ AddProgramLayout(in1, in2, in3, in4, in5, out1, out2, out3, out4, out5)
+
+ 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)}")
+ }
+ }
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 df0b470e..91b445a9 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
@@ -1,11 +1,258 @@
package io.computenode.cyfra.runtime
+import io.computenode.cyfra.core.GProgram.InitProgramLayout
+import io.computenode.cyfra.core.SpirvProgram.*
+import io.computenode.cyfra.core.binding.{BufferRef, UniformRef}
import io.computenode.cyfra.core.{GExecution, GProgram}
-import io.computenode.cyfra.core.layout.Layout
+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.runtime.ExecutionHandler.DispatchType.*
+import io.computenode.cyfra.runtime.ExecutionHandler.ExecutionBinding.{BufferBinding, UniformBinding}
+import io.computenode.cyfra.utility.Utility.timed
+import io.computenode.cyfra.vulkan.command.{CommandPool, Fence}
+import io.computenode.cyfra.vulkan.compute.ComputePipeline
+import io.computenode.cyfra.vulkan.core.Queue
+import io.computenode.cyfra.vulkan.memory.{DescriptorPool, DescriptorSet}
+import io.computenode.cyfra.vulkan.util.Util.{check, pushStack}
+import izumi.reflect.Tag
+import org.lwjgl.vulkan.VK10.*
+import org.lwjgl.vulkan.VK13.{VK_ACCESS_2_SHADER_READ_BIT, VK_ACCESS_2_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, vkCmdPipelineBarrier2}
+import org.lwjgl.vulkan.{VkCommandBuffer, VkCommandBufferBeginInfo, VkDependencyInfo, VkMemoryBarrier2, VkSubmitInfo}
+
+import scala.collection.mutable
+
+class ExecutionHandler(runtime: VkCyfraRuntime):
+ private val context = runtime.context
+ import context.given
+
+ private val queue: Queue = context.computeQueue // TODO multiple queues - multithreading support
+ private val descriptorPool: DescriptorPool = context.descriptorPool // TODO descriptor pool manager - descriptor allocation and reclamation support
+ private val commandPool: CommandPool = context.commandPool // TODO multiple command pools - different command pools for different workloads
+
+ def handle[Params, EL <: Layout: LayoutBinding, RL <: Layout: LayoutBinding](execution: GExecution[Params, EL, RL], params: Params, layout: EL)(
+ using VkAllocation,
+ ): RL =
+ val (result, shaderCalls) = interpret(execution, params, layout)
+
+ val descriptorSets = shaderCalls.map { case ShaderCall(pipeline, layout, _) =>
+ pipeline.pipelineLayout.sets.map(descriptorPool.allocate).zip(layout).map { case (set, bindings) =>
+ set.update(bindings.map(x => VkAllocation.getUnderlying(x.binding)))
+ set
+ }
+ }
+
+ val dispatches: Seq[Dispatch] = shaderCalls
+ .zip(descriptorSets)
+ .map:
+ case (ShaderCall(pipeline, layout, dispatch), sets) =>
+ Dispatch(pipeline, layout, sets, dispatch)
+
+ val (executeSteps, _) = dispatches.foldLeft((Seq.empty[ExecutionStep], Set.empty[GBinding[?]])):
+ case ((steps, dirty), step) =>
+ val bindings = step.layout.flatten.map(_.binding)
+ if bindings.exists(dirty.contains) then (steps.appendedAll(Seq(PipelineBarrier, step)), bindings.toSet)
+ else (steps.appended(step), dirty ++ bindings)
+
+ val commandBuffer = recordCommandBuffer(executeSteps)
+ pushStack: stack =>
+ val pCommandBuffer = stack.callocPointer(1).put(0, commandBuffer)
+ val submitInfo = VkSubmitInfo
+ .calloc(stack)
+ .sType$Default()
+ .pCommandBuffers(pCommandBuffer)
+
+ val fence = new Fence()
+ timed("Vulkan render command"):
+ check(vkQueueSubmit(queue.get, submitInfo, fence.get), "Failed to submit command buffer to queue")
+ fence.block().destroy()
+ commandPool.freeCommandBuffer(commandBuffer)
+ result
+
+ private def interpret[Params, EL <: Layout: LayoutBinding, RL <: Layout: LayoutBinding](
+ execution: GExecution[Params, EL, RL],
+ params: Params,
+ layout: EL,
+ )(using VkAllocation): (RL, Seq[ShaderCall]) =
+ val bindingsAcc: mutable.Map[GBinding[?], mutable.Buffer[GBinding[?]]] = mutable.Map.empty
+
+ def mockBindings[L <: Layout: LayoutBinding](layout: L): L =
+ val mapper = summon[LayoutBinding[L]]
+ val res = mapper
+ .toBindings(layout)
+ .map:
+ case x: ExecutionBinding[?] => x
+ case x: GBinding[?] =>
+ val e = ExecutionBinding(x)(using x.fromExpr, x.tag)
+ bindingsAcc.put(e, mutable.Buffer(x))
+ e
+
+ mapper.fromBindings(res)
+
+ // noinspection TypeParameterShadow
+ def interpretImpl[Params, EL <: Layout: LayoutBinding, RL <: Layout: LayoutBinding](
+ execution: GExecution[Params, EL, RL],
+ params: Params,
+ layout: EL,
+ ): (RL, Seq[ShaderCall]) =
+ execution match
+ case GExecution.Pure() => (layout, Seq.empty)
+ case GExecution.Map(innerExec, map, cmap, cmapP) =>
+ val pel = innerExec.layoutBinding
+ val prl = innerExec.resLayoutBinding
+ val cParams = cmapP(params)
+ val cLayout = mockBindings(cmap(layout))(using pel)
+ val (prevRl, calls) = interpretImpl(innerExec, cParams, cLayout)(using pel, prl)
+ val nextRl = mockBindings(map(prevRl))
+ (nextRl, calls)
+ case GExecution.FlatMap(execution, f) =>
+ val el = execution.layoutBinding
+ val (rl, calls) = interpretImpl(execution, params, layout)(using el, execution.resLayoutBinding)
+ val nextExecution = f(params, rl)
+ val (rl2, calls2) = interpretImpl(nextExecution, params, layout)(using el, nextExecution.resLayoutBinding)
+ (rl2, calls ++ calls2)
+ case program: GProgram[Params, EL] =>
+ given lb: LayoutBinding[EL] = program.layoutBinding
+ given LayoutStruct[EL] = program.layoutStruct
+ val shader =
+ runtime.getOrLoadProgram(program)
+ 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 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)))
+ case _ => ???
+
+ val (rl, steps) = interpretImpl(execution, params, mockBindings(layout))
+ val bingingToVk = bindingsAcc.map(x => (x._1, interpretBinding(x._1, x._2.toSeq)))
+
+ val nextSteps = steps.map:
+ case ShaderCall(pipeline, layout, dispatch) =>
+ val nextLayout = layout.map:
+ _.map:
+ case Binding(binding, operation) => Binding(bingingToVk(binding), operation)
+ val nextDispatch = dispatch match
+ case x: Direct => x
+ case Indirect(buffer, offset) => Indirect(bingingToVk(buffer), offset)
+ ShaderCall(pipeline, nextLayout, nextDispatch)
+
+ val mapper = summon[LayoutBinding[RL]]
+ val res = mapper.fromBindings(mapper.toBindings(rl).map(bingingToVk.apply))
+ (res, nextSteps)
+
+ private def interpretBinding(binding: GBinding[?], bindings: Seq[GBinding[?]])(using VkAllocation): GBinding[?] =
+ binding match
+ case _: BufferBinding[?] =>
+ val (allocations, sizeSpec) = bindings.partitionMap:
+ case x: VkBuffer[?] => Left(x)
+ case x: GProgram.BufferLengthSpec[?] => Right(x)
+ case x => throw BindingLogicError(x, "Unsupported buffer type")
+ if allocations.size > 1 then throw BindingLogicError(allocations, "Multiple allocations for buffer")
+ val alloc = allocations.headOption
+
+ val lengths = sizeSpec.distinctBy(_.length)
+ if lengths.size > 1 then throw BindingLogicError(lengths, "Multiple conflicting lengths for buffer")
+ val length = lengths.headOption
+
+ (alloc, length) match
+ case (Some(buffer), Some(sizeSpec)) =>
+ if buffer.length != sizeSpec.length then
+ throw BindingLogicError(Seq(buffer, sizeSpec), s"Buffer length mismatch, ${buffer.length} != ${sizeSpec.length}")
+ buffer
+ case (Some(buffer), None) => buffer
+ case (None, Some(length)) => length.materialise()
+ case (None, None) => throw new IllegalStateException("Cannot create buffer without size or allocation")
+
+ case _: UniformBinding[?] =>
+ val allocations = bindings.filter:
+ case _: VkUniform[?] => true
+ case _: GProgram.DynamicUniform[?] => false
+ case _: GUniform.ParamUniform[?] => false
+ case x => throw BindingLogicError(x, "Unsupported binding type")
+ if allocations.size > 1 then throw BindingLogicError(allocations, "Multiple allocations for uniform")
+ allocations.headOption.getOrElse(throw new IllegalStateException("Uniform never allocated"))
+ case x => throw new IllegalArgumentException(s"Binding of type ${x.getClass.getName} should not be here")
+
+ private def recordCommandBuffer(steps: Seq[ExecutionStep]): VkCommandBuffer = pushStack: stack =>
+ val commandBuffer = commandPool.createCommandBuffer()
+ val commandBufferBeginInfo = VkCommandBufferBeginInfo
+ .calloc(stack)
+ .sType$Default()
+ .flags(0)
+
+ check(vkBeginCommandBuffer(commandBuffer, commandBufferBeginInfo), "Failed to begin recording command buffer")
+
+ steps.foreach:
+ case PipelineBarrier =>
+ val memoryBarrier = VkMemoryBarrier2 // TODO don't synchronise everything
+ .calloc(1, stack)
+ .sType$Default()
+ .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
+ .srcAccessMask(VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT)
+ .dstStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
+ .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT)
+
+ val dependencyInfo = VkDependencyInfo
+ .calloc(stack)
+ .sType$Default()
+ .pMemoryBarriers(memoryBarrier)
+
+ vkCmdPipelineBarrier2(commandBuffer, dependencyInfo)
+
+ case Dispatch(pipeline, layout, descriptorSets, dispatch) =>
+ vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.get)
+
+ val pDescriptorSets = stack.longs(descriptorSets.map(_.get)*)
+ vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.pipelineLayout.id, 0, pDescriptorSets, null)
+
+ dispatch match
+ case Direct(x, y, z) => vkCmdDispatch(commandBuffer, x, y, z)
+ case Indirect(buffer, offset) => vkCmdDispatchIndirect(commandBuffer, VkAllocation.getUnderlying(buffer).get, offset)
+
+ check(vkEndCommandBuffer(commandBuffer), "Failed to finish recording command buffer")
+ commandBuffer
object ExecutionHandler:
+ case class ShaderCall(pipeline: ComputePipeline, layout: ShaderLayout, dispatch: DispatchType)
+
+ sealed trait 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
+ case class Indirect(buffer: GBinding[?], offset: Int) extends DispatchType
+
+ sealed trait ExecutionBinding[T <: Value: {FromExpr, Tag}]
+ object ExecutionBinding:
+ class UniformBinding[T <: Value: {FromExpr, Tag}] extends ExecutionBinding[T] with GUniform[T]
+ class BufferBinding[T <: Value: {FromExpr, Tag}] extends ExecutionBinding[T] with GBuffer[T]
- def handle[Params, L <: Layout, RL <: Layout](execution: GExecution[Params, L, RL], params: Params): List[BoundProgram[?, ?, ?]] =
- ???
+ def apply[T <: Value: {FromExpr, Tag}](binding: GBinding[T]): ExecutionBinding[T] & GBinding[T] = binding match
+ case _: GUniform[T] => new UniformBinding()
+ case _: GBuffer[T] => new BufferBinding()
- case class BoundProgram[LParams, Params, L <: Layout](layout: L, paramsMapping: LParams => Params, program: GProgram[Params, L])
+ case class BindingLogicError(bindings: Seq[GBinding[?]], message: String) extends RuntimeException(s"Error in binding logic for $bindings: $message")
+ object BindingLogicError:
+ def apply(binding: GBinding[?], message: String): BindingLogicError =
+ new BindingLogicError(Seq(binding), 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 d1474976..59142676 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
@@ -1,29 +1,105 @@
package io.computenode.cyfra.runtime
-import io.computenode.cyfra.core.layout.{Layout, LayoutStruct}
-import io.computenode.cyfra.core.{Allocation, GExecution}
+import io.computenode.cyfra.core.layout.{Layout, LayoutBinding}
+import io.computenode.cyfra.core.{Allocation, GExecution, GProgram}
+import io.computenode.cyfra.core.SpirvProgram
+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.runtime.VkAllocation.getUnderlying
+import io.computenode.cyfra.spirv.SpirvTypes.typeStride
+import io.computenode.cyfra.vulkan.command.CommandPool
+import io.computenode.cyfra.vulkan.memory.{Allocator, Buffer}
+import io.computenode.cyfra.vulkan.util.Util.pushStack
+import io.computenode.cyfra.dsl.Value.Int32
import izumi.reflect.Tag
+import org.lwjgl.BufferUtils
+import org.lwjgl.system.MemoryUtil
+import org.lwjgl.vulkan.VK10
+import org.lwjgl.vulkan.VK10.{VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_BUFFER_USAGE_TRANSFER_SRC_BIT}
import java.nio.ByteBuffer
+import scala.collection.mutable
+import scala.util.chaining.*
+
+class VkAllocation(commandPool: CommandPool, executionHandler: ExecutionHandler)(using Allocator) extends Allocation:
+ given VkAllocation = this
-class VkAllocation extends Allocation:
extension (buffer: GBinding[?])
- def read(bb: ByteBuffer, offset: Int = 0, length: Int = -1): Unit = ???
+ def read(bb: ByteBuffer, offset: Int = 0, size: Int = -1): Unit =
+ val buf = getUnderlying(buffer)
+ val s = if size < 0 then buf.size - offset else size
+
+ buf match
+ case buffer: Buffer.HostBuffer => Buffer.copyBuffer(buffer, bb, offset, 0, s)
+ case buffer: Buffer.DeviceBuffer =>
+ val stagingBuffer = getStagingBuffer(s)
+ Buffer.copyBuffer(buffer, stagingBuffer, offset, 0, s, commandPool).block().destroy()
+ Buffer.copyBuffer(stagingBuffer, bb, 0, 0, s)
- def write(bb: ByteBuffer, offset: Int = 0, length: Int = -1): Unit = ???
+ def write(bb: ByteBuffer, offset: Int = 0, size: Int = -1): Unit =
+ val buf = getUnderlying(buffer)
+ val s = if size < 0 then bb.remaining() else size
- extension [Params, L <: Layout, RL <: Layout: LayoutStruct](execution: GExecution[Params, L, RL]) def execute(params: Params, layout: L): RL = ???
+ buf match
+ case buffer: Buffer.HostBuffer => Buffer.copyBuffer(bb, buffer, offset, 0, s)
+ case buffer: Buffer.DeviceBuffer =>
+ val stagingBuffer = getStagingBuffer(s)
+ Buffer.copyBuffer(bb, stagingBuffer, 0, 0, s)
+ Buffer.copyBuffer(stagingBuffer, buffer, 0, offset, s, commandPool).block().destroy()
extension (buffers: GBuffer.type)
- def apply[T <: Value: {Tag, FromExpr}](size: Int): GBuffer[T] = ???
+ 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 ???
+ GBuffer[T](length).tap(_.write(buff))
extension (buffers: GUniform.type)
- def apply[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GUniform[T] = ???
+ def apply[T <: Value: {Tag, FromExpr}](buff: ByteBuffer): GUniform[T] =
+ GUniform[T]().tap(_.write(buff))
+
+ def apply[T <: Value: {Tag, FromExpr}](): 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] =
+ 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 =>
+ val bb = value.productElement(0) match
+ case Int32(tree: ConstInt32) => MemoryUtil.memByteBuffer(stack.ints(tree.value))
+ case _ => ???
+ direct(bb)
+
+ private val bindings = mutable.Buffer[VkUniform[?] | VkBuffer[?]]()
+ private[cyfra] def close(): Unit =
+ bindings.map(getUnderlying).foreach(_.destroy())
+ stagingBuffer.foreach(_.destroy())
+
+ private var stagingBuffer: Option[Buffer.HostBuffer] = None
+ private def getStagingBuffer(size: Int): Buffer.HostBuffer =
+ stagingBuffer match
+ case Some(buffer) if buffer.size >= size => buffer
+ case _ =>
+ stagingBuffer.foreach(_.destroy())
+ val newBuffer = Buffer.HostBuffer(size, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT)
+ stagingBuffer = Some(newBuffer)
+ newBuffer
- def apply[T <: Value: {Tag, FromExpr}](): GUniform[T] = ???
+object VkAllocation:
+ private[runtime] def getUnderlying(buffer: GBinding[?]): Buffer =
+ buffer match
+ case buffer: VkBuffer[?] => buffer.underlying
+ case uniform: VkUniform[?] => uniform.underlying
+ case _ => throw new IllegalArgumentException(s"Tried to get underlying of non-VkBinding $buffer")
diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBuffer.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBuffer.scala
new file mode 100644
index 00000000..cda73868
--- /dev/null
+++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBuffer.scala
@@ -0,0 +1,23 @@
+package io.computenode.cyfra.runtime
+
+import io.computenode.cyfra.dsl.Value
+import io.computenode.cyfra.dsl.Value.FromExpr
+import io.computenode.cyfra.dsl.binding.{GBinding, GBuffer}
+import io.computenode.cyfra.vulkan.memory.{Allocator, Buffer}
+import izumi.reflect.Tag
+import io.computenode.cyfra.spirv.SpirvTypes.typeStride
+import org.lwjgl.vulkan.VK10
+import org.lwjgl.vulkan.VK10.{VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_BUFFER_USAGE_TRANSFER_SRC_BIT}
+
+class VkBuffer[T <: Value: {Tag, FromExpr}] private (var length: Int, val underlying: Buffer) extends GBuffer[T]:
+ val sizeOfT: Int = typeStride(summon[Tag[T]])
+
+object VkBuffer:
+ private final val Padding = 64
+ private final val UsageFlags = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT
+
+ def apply[T <: Value: {Tag, FromExpr}](length: Int)(using Allocator): VkBuffer[T] =
+ val sizeOfT = typeStride(summon[Tag[T]])
+ val size = (length * sizeOfT + Padding - 1) / Padding * Padding
+ val buffer = new Buffer.DeviceBuffer(size, UsageFlags)
+ new VkBuffer[T](length, buffer)
diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkCyfraRuntime.scala
index 87dbd5ab..f28cfd65 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,8 +1,27 @@
package io.computenode.cyfra.runtime
-import io.computenode.cyfra.core.Allocation
-import io.computenode.cyfra.core.{Allocation, CyfraRuntime}
+import io.computenode.cyfra.core.layout.{Layout, LayoutBinding, LayoutStruct}
+import io.computenode.cyfra.core.{Allocation, CyfraRuntime, GExecution, GProgram, SpirvProgram}
+import io.computenode.cyfra.vulkan.VulkanContext
+import io.computenode.cyfra.vulkan.compute.ComputePipeline
+
+import scala.collection.mutable
class VkCyfraRuntime extends CyfraRuntime:
- override def allocation(): Allocation =
- new VkAllocation()
+ val context = new VulkanContext()
+ import context.given
+
+ private val executionHandler = new ExecutionHandler(this)
+
+ 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]]
+
+ override def withAllocation(f: Allocation => Unit): Unit =
+ val allocation = new VkAllocation(context.commandPool, executionHandler)
+ f(allocation)
+ allocation.close()
+
+ def close(): Unit =
+ shaderCache.values.foreach(_.underlying.destroy())
+ context.destroy()
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
new file mode 100644
index 00000000..4544553e
--- /dev/null
+++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkShader.scala
@@ -0,0 +1,42 @@
+package io.computenode.cyfra.runtime
+
+import io.computenode.cyfra.core.{GProgram, GioProgram, SpirvProgram}
+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.vulkan.compute.ComputePipeline
+import io.computenode.cyfra.vulkan.compute.ComputePipeline.*
+import io.computenode.cyfra.vulkan.core.Device
+import izumi.reflect.Tag
+
+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}")
+
+ val shaderLayout = shaderBindings(summon[LayoutStruct[L]].layoutRef)
+ val sets = shaderLayout.map: set =>
+ val descriptors = set.map { case Binding(binding, op) =>
+ val kind = binding match
+ case buffer: GBuffer[?] => BindingType.StorageBuffer
+ case uniform: GUniform[?] => BindingType.Uniform
+ DescriptorInfo(kind)
+ }
+ DescriptorSetInfo(descriptors)
+
+ val pipeline = ComputePipeline(code, entryPoint, LayoutInfo(sets))
+ VkShader(pipeline, shaderBindings)
+
+ def compile[Params, L <: Layout: {LayoutBinding, LayoutStruct}](program: GioProgram[Params, L]): SpirvProgram[Params, L] =
+ val GioProgram(_, layout, dispatch, _) = program
+ val name = program.cacheKey + ".spv"
+ loadShader(name) match
+ case Failure(_) => ???
+ case Success(_) => SpirvProgram(name, (il: InitProgramLayout) ?=> layout(il), dispatch)
diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkUniform.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkUniform.scala
new file mode 100644
index 00000000..f8c75da7
--- /dev/null
+++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkUniform.scala
@@ -0,0 +1,21 @@
+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.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]:
+ 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] =
+ val sizeOfT = 4 // typeStride(summon[Tag[T]])
+ val buffer = new Buffer.DeviceBuffer(sizeOfT, UsageFlags)
+ new VkUniform[T](buffer)
diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/compute/ComputePipeline.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/compute/ComputePipeline.scala
index c63d2210..2fe2c35d 100644
--- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/compute/ComputePipeline.scala
+++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/compute/ComputePipeline.scala
@@ -4,6 +4,7 @@ import io.computenode.cyfra.vulkan.VulkanContext
import io.computenode.cyfra.vulkan.core.Device
import io.computenode.cyfra.vulkan.util.Util.{check, pushStack}
import io.computenode.cyfra.vulkan.util.VulkanObjectHandle
+import io.computenode.cyfra.vulkan.compute.ComputePipeline.*
import org.lwjgl.vulkan.*
import org.lwjgl.vulkan.VK10.*
@@ -16,7 +17,7 @@ import scala.util.{Try, Using}
/** @author
* MarconZet Created 14.04.2020
*/
-private[cyfra] class ComputePipeline(shaderCode: ByteBuffer, functionName: String, val layoutInfo: LayoutInfo)(using device: Device)
+private[cyfra] class ComputePipeline(shaderCode: ByteBuffer, functionName: String, layoutInfo: LayoutInfo)(using device: Device)
extends VulkanObjectHandle:
private val shader: Long = pushStack: stack => // TODO khr_maintenance5
@@ -31,9 +32,9 @@ private[cyfra] class ComputePipeline(shaderCode: ByteBuffer, functionName: Strin
check(vkCreateShaderModule(device.get, shaderModuleCreateInfo, null, pShaderModule), "Failed to create shader module")
pShaderModule.get()
- val descriptorSetLayouts: Seq[(Long, LayoutSet)] = layoutInfo.sets.map(x => (createDescriptorSetLayout(x), x))
+ val pipelineLayout: PipelineLayout = pushStack: stack =>
+ val descriptorSetLayouts: Seq[DescriptorSetLayout] = layoutInfo.sets.map(x => DescriptorSetLayout(createDescriptorSetLayout(x), x))
- val pipelineLayout: Long = pushStack: stack =>
val pipelineLayoutCreateInfo = VkPipelineLayoutCreateInfo
.calloc(stack)
.sType$Default()
@@ -44,7 +45,8 @@ private[cyfra] class ComputePipeline(shaderCode: ByteBuffer, functionName: Strin
val pPipelineLayout = stack.callocLong(1)
check(vkCreatePipelineLayout(device.get, pipelineLayoutCreateInfo, null, pPipelineLayout), "Failed to create pipeline layout")
- pPipelineLayout.get(0)
+ val layout = pPipelineLayout.get(0)
+ PipelineLayout(layout, descriptorSetLayouts)
protected val handle: Long = pushStack: stack =>
val pipelineShaderStageCreateInfo = VkPipelineShaderStageCreateInfo
@@ -63,7 +65,7 @@ private[cyfra] class ComputePipeline(shaderCode: ByteBuffer, functionName: Strin
.pNext(0)
.flags(0)
.stage(pipelineShaderStageCreateInfo)
- .layout(pipelineLayout)
+ .layout(pipelineLayout.id)
.basePipelineHandle(0)
.basePipelineIndex(0)
@@ -73,19 +75,19 @@ private[cyfra] class ComputePipeline(shaderCode: ByteBuffer, functionName: Strin
protected def close(): Unit =
vkDestroyPipeline(device.get, handle, null)
- vkDestroyPipelineLayout(device.get, pipelineLayout, null)
- descriptorSetLayouts.map(_._1).foreach(vkDestroyDescriptorSetLayout(device.get, _, null))
- vkDestroyShaderModule(device.get, handle, null)
+ vkDestroyPipelineLayout(device.get, pipelineLayout.id, null)
+ pipelineLayout.sets.map(_.id).foreach(vkDestroyDescriptorSetLayout(device.get, _, null))
+ vkDestroyShaderModule(device.get, shader, null)
- private def createDescriptorSetLayout(set: LayoutSet): Long = pushStack: stack =>
- val descriptorSetLayoutBindings = VkDescriptorSetLayoutBinding.calloc(set.bindings.length, stack)
- set.bindings.foreach: binding =>
+ private def createDescriptorSetLayout(set: DescriptorSetInfo): Long = pushStack: stack =>
+ val descriptorSetLayoutBindings = VkDescriptorSetLayoutBinding.calloc(set.descriptors.length, stack)
+ set.descriptors.zipWithIndex.foreach: binding =>
descriptorSetLayoutBindings
.get()
- .binding(binding.id)
- .descriptorType(binding.size match
- case InputBufferSize(_) => VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
- case UniformSize(_) => VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
+ .binding(binding._2)
+ .descriptorType(binding._1.kind match
+ case BindingType.StorageBuffer => VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
+ case BindingType.Uniform => VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
.descriptorCount(1)
.stageFlags(VK_SHADER_STAGE_COMPUTE_BIT)
.pImmutableSamplers(null)
@@ -104,12 +106,13 @@ private[cyfra] class ComputePipeline(shaderCode: ByteBuffer, functionName: Strin
pDescriptorSetLayout.get(0)
object ComputePipeline:
- def loadShader(path: String): Try[ByteBuffer] =
- loadShader(path, getClass.getClassLoader)
-
- private def loadShader(path: String, classLoader: ClassLoader): 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())
+ private[cyfra] case class PipelineLayout(id: Long, sets: Seq[DescriptorSetLayout])
+ private[cyfra] case class DescriptorSetLayout(id: Long, set: DescriptorSetInfo)
+
+ private[cyfra] case class LayoutInfo(sets: Seq[DescriptorSetInfo])
+ private[cyfra] case class DescriptorSetInfo(descriptors: Seq[DescriptorInfo])
+ private[cyfra] case class DescriptorInfo(kind: BindingType)
+
+ private[cyfra] enum BindingType:
+ case StorageBuffer
+ case Uniform
diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/compute/LayoutInfo.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/compute/LayoutInfo.scala
deleted file mode 100644
index 1edbcea8..00000000
--- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/compute/LayoutInfo.scala
+++ /dev/null
@@ -1,12 +0,0 @@
-package io.computenode.cyfra.vulkan.compute
-
-/** @author
- * MarconZet Created 25.04.2020
- */
-private[cyfra] case class LayoutInfo(sets: Seq[LayoutSet])
-private[cyfra] case class LayoutSet(id: Int, bindings: Seq[Binding])
-private[cyfra] case class Binding(id: Int, size: LayoutElementSize)
-
-private[cyfra] sealed trait LayoutElementSize
-private[cyfra] case class InputBufferSize(elemSize: Int) extends LayoutElementSize
-private[cyfra] case class UniformSize(size: Int) extends LayoutElementSize
diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugCallback.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugCallback.scala
index f3e83712..3721e9b3 100644
--- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugCallback.scala
+++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/core/DebugCallback.scala
@@ -35,7 +35,7 @@ private[cyfra] class DebugCallback(instance: Instance) extends VulkanObjectHandl
case VK_DEBUG_REPORT_DEBUG_BIT_EXT =>
logger.debug(decodedMessage)
case VK_DEBUG_REPORT_ERROR_BIT_EXT =>
- logger.error(decodedMessage, new RuntimeException())
+ logger.error(decodedMessage)
case VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT =>
logger.warn(decodedMessage)
case VK_DEBUG_REPORT_INFORMATION_BIT_EXT =>
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 dfb585d1..7452ec0a 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
@@ -4,10 +4,11 @@ import io.computenode.cyfra.utility.Logger.logger
import io.computenode.cyfra.vulkan.VulkanContext.ValidationLayer
import io.computenode.cyfra.vulkan.util.Util.{check, pushStack}
import io.computenode.cyfra.vulkan.util.VulkanObject
-import org.lwjgl.system.MemoryStack
+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.KHRPortabilityEnumeration.{VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME}
import org.lwjgl.vulkan.VK10.*
@@ -67,6 +68,19 @@ private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObj
.pApplicationInfo(appInfo)
.ppEnabledExtensionNames(ppEnabledExtensionNames)
.ppEnabledLayerNames(ppEnabledLayerNames)
+
+ if enableValidationLayers then
+ val layerSettings = VkLayerSettingEXT.calloc(1, stack)
+ layerSettings
+ .get(0)
+ .pLayerName(stack.ASCII(ValidationLayer))
+ .pSettingName(stack.ASCII("validate_sync"))
+ .`type`(VK_LAYER_SETTING_TYPE_BOOL32_EXT)
+ .valueCount(1)
+ .pValues(MemoryUtil.memByteBuffer(stack.ints(1)))
+ val layerSettingsCI = VkLayerSettingsCreateInfoEXT.calloc(stack).sType$Default().pSettings(layerSettings)
+ pCreateInfo.pNext(layerSettingsCI)
+
val pInstance = stack.mallocPointer(1)
check(vkCreateInstance(pCreateInfo, null, pInstance), "Failed to create VkInstance")
new VkInstance(pInstance.get(0), pCreateInfo)
@@ -103,7 +117,7 @@ private[cyfra] class Instance(enableValidationLayers: Boolean) extends VulkanObj
if enableValidationLayers then extensions.addAll(Instance.ValidationLayersExtensions)
val filteredExtensions = extensions.filter(ext =>
- availableExtensions.contains(ext).tap { x =>
+ availableExtensions.contains(ext).tap { x => // TODO detect when this extension is needed
if !x then logger.warn(s"Requested Vulkan instance extension '$ext' is not available")
},
)
diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/executor/BufferAction.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/executor/BufferAction.scala
deleted file mode 100644
index 32b5ba49..00000000
--- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/executor/BufferAction.scala
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.computenode.cyfra.vulkan.executor
-
-import org.lwjgl.vulkan.VK10.{VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_BUFFER_USAGE_TRANSFER_SRC_BIT}
-
-enum BufferAction(val action: Int):
- case DoNothing extends BufferAction(0)
- case LoadTo extends BufferAction(VK_BUFFER_USAGE_TRANSFER_DST_BIT)
- case LoadFrom extends BufferAction(VK_BUFFER_USAGE_TRANSFER_SRC_BIT)
- case LoadFromTo extends BufferAction(VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT)
-
- private def findAction(action: Int): BufferAction = action match
- case VK_BUFFER_USAGE_TRANSFER_DST_BIT => LoadTo
- case VK_BUFFER_USAGE_TRANSFER_SRC_BIT => LoadFrom
- case 3 => LoadFromTo
- case _ => DoNothing
-
- def |(other: BufferAction): BufferAction = findAction(this.action | other.action)
diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/executor/SequenceExecutor.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/executor/SequenceExecutor.scala
deleted file mode 100644
index 63c61bc2..00000000
--- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/executor/SequenceExecutor.scala
+++ /dev/null
@@ -1,206 +0,0 @@
-package io.computenode.cyfra.vulkan.executor
-
-import io.computenode.cyfra.utility.Utility.timed
-import io.computenode.cyfra.vulkan.VulkanContext
-import io.computenode.cyfra.vulkan.command.*
-import io.computenode.cyfra.vulkan.compute.*
-import io.computenode.cyfra.vulkan.core.*
-import io.computenode.cyfra.vulkan.executor.SequenceExecutor.*
-import io.computenode.cyfra.vulkan.memory.*
-import io.computenode.cyfra.vulkan.util.Util.*
-import org.lwjgl.BufferUtils
-import org.lwjgl.util.vma.Vma.*
-import org.lwjgl.vulkan.*
-import org.lwjgl.vulkan.KHRSynchronization2.vkCmdPipelineBarrier2KHR
-import org.lwjgl.vulkan.VK10.*
-import org.lwjgl.vulkan.VK13.*
-
-import java.nio.ByteBuffer
-
-/** @author
- * MarconZet Created 15.04.2020
- */
-private[cyfra] class SequenceExecutor(computeSequence: ComputationSequence, context: VulkanContext):
- import context.given
-
- private val queue: Queue = context.computeQueue
- private val descriptorPool: DescriptorPool = context.descriptorPool
- private val commandPool: CommandPool = context.commandPool
-
- private val pipelineToDescriptorSets: Map[ComputePipeline, Seq[DescriptorSet]] = pushStack: stack =>
- val pipelines = computeSequence.sequence.collect:
- case Compute(pipeline, _) => pipeline
-
- val rawSets = pipelines.map(_.layoutInfo.sets)
- val numbered = rawSets.flatten.zipWithIndex
- val numberedSets = rawSets
- .foldLeft((numbered, Seq.empty[Seq[(LayoutSet, Int)]])) { case ((remaining, acc), sequence) =>
- val (current, rest) = remaining.splitAt(sequence.length)
- (rest, acc :+ current)
- }
- ._2
-
- val pipelineToIndex = pipelines.zipWithIndex.toMap
- val dependencies = computeSequence.dependencies.map { case Dependency(from, fromSet, to, toSet) =>
- (pipelineToIndex(from), fromSet, pipelineToIndex(to), toSet)
- }
- val resolvedSets = dependencies
- .foldLeft(numberedSets.map(_.toArray)) { case (sets, (from, fromSet, to, toSet)) =>
- val a = sets(from)(fromSet)
- val b = sets(to)(toSet)
- assert(a._1.bindings == b._1.bindings)
- val nextIndex = a._2 min b._2
- sets(from).update(fromSet, (a._1, nextIndex))
- sets(to).update(toSet, (b._1, nextIndex))
- sets
- }
- .map(_.toSeq.map(_._2))
-
- val descriptorSetMap = resolvedSets
- .zip(pipelines.map(_.descriptorSetLayouts))
- .flatMap { case (sets, layouts) =>
- sets.zip(layouts)
- }
- .distinctBy(_._1)
- .map { case (set, (id, layout)) =>
- (set, new DescriptorSet(id, layout.bindings, descriptorPool))
- }
- .toMap
-
- pipelines.zip(resolvedSets.map(_.map(descriptorSetMap(_)))).toMap
-
- private val descriptorSets = pipelineToDescriptorSets.toSeq.flatMap(_._2).distinctBy(_.get)
-
- private def recordCommandBuffer() = pushStack: stack =>
- val pipelinesHasDependencies = computeSequence.dependencies.map(_.to).toSet
- val commandBuffer = commandPool.createCommandBuffer()
-
- val commandBufferBeginInfo = VkCommandBufferBeginInfo
- .calloc(stack)
- .sType$Default()
- .flags(0)
-
- check(vkBeginCommandBuffer(commandBuffer, commandBufferBeginInfo), "Failed to begin recording command buffer")
-
- computeSequence.sequence.foreach { case Compute(pipeline, _) =>
- if pipelinesHasDependencies(pipeline) then
- val memoryBarrier = VkMemoryBarrier2
- .calloc(1, stack)
- .sType$Default()
- .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
- .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
- .dstStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
- .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT)
-
- val dependencyInfo = VkDependencyInfo
- .calloc(stack)
- .sType$Default()
- .pMemoryBarriers(memoryBarrier)
-
- vkCmdPipelineBarrier2KHR(commandBuffer, dependencyInfo)
-
- vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.get)
-
- val pDescriptorSets = stack.longs(pipelineToDescriptorSets(pipeline).map(_.get)*)
- vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline.pipelineLayout, 0, pDescriptorSets, null)
-
- vkCmdDispatch(commandBuffer, 8, 1, 1)
- }
-
- check(vkEndCommandBuffer(commandBuffer), "Failed to finish recording command buffer")
- commandBuffer
-
- private def createBuffers(): Map[DescriptorSet, Seq[Buffer]] =
-
- val setToActions = computeSequence.sequence
- .collect { case Compute(pipeline, bufferActions) =>
- pipelineToDescriptorSets(pipeline).zipWithIndex.map { case (descriptorSet, i) =>
- val descriptorBufferActions = descriptorSet.bindings
- .map(_.id)
- .map(LayoutLocation(i, _))
- .map(bufferActions.getOrElse(_, BufferAction.DoNothing))
- (descriptorSet, descriptorBufferActions)
- }
- }
- .flatten
- .groupMapReduce(_._1)(_._2)((a, b) => a.zip(b).map(x => x._1 | x._2))
-
- val setToBuffers = descriptorSets
- .map(set =>
- val actions = setToActions(set)
- val buffers = set.bindings.zip(actions).map { case (binding, action) =>
- binding.size match
- case InputBufferSize(elemSize) =>
- new Buffer.DeviceBuffer(elemSize * 1024, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | action.action)
- case UniformSize(size) =>
- new Buffer.DeviceBuffer(size, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | action.action)
- }
- set.update(buffers)
- (set, buffers),
- )
- .toMap
-
- setToBuffers
-
- def execute(inputs: Seq[ByteBuffer]): Seq[ByteBuffer] = pushStack: stack =>
- timed("Vulkan full execute"):
- val setToBuffers = createBuffers()
-
- def buffersWithAction(bufferAction: BufferAction): Seq[Buffer] =
- computeSequence.sequence.collect { case x: Compute =>
- pipelineToDescriptorSets(x.pipeline)
- .map(setToBuffers)
- .zip(x.pumpLayoutLocations)
- .flatMap(x => x._1.zip(x._2))
- .collect:
- case (buffer, action) if (action.action & bufferAction.action) != 0 => buffer
- }.flatten
-
- val stagingBuffer =
- new Buffer.HostBuffer(inputs.map(_.remaining()).max, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT)
-
- buffersWithAction(BufferAction.LoadTo).zipWithIndex.foreach { case (buffer, i) =>
- Buffer.copyBuffer(inputs(i), stagingBuffer, buffer.size)
- Buffer.copyBuffer(stagingBuffer, buffer, buffer.size, commandPool).block().destroy()
- }
-
- val fence = new Fence()
- val commandBuffer = recordCommandBuffer()
- val pCommandBuffer = stack.callocPointer(1).put(0, commandBuffer)
- val submitInfo = VkSubmitInfo
- .calloc(stack)
- .sType$Default()
- .pCommandBuffers(pCommandBuffer)
-
- timed("Vulkan render command"):
- check(vkQueueSubmit(queue.get, submitInfo, fence.get), "Failed to submit command buffer to queue")
- fence.block().destroy()
-
- val output = buffersWithAction(BufferAction.LoadFrom).map { buffer =>
- Buffer.copyBuffer(buffer, stagingBuffer, buffer.size, commandPool).block().destroy()
- val out = BufferUtils.createByteBuffer(buffer.size)
- Buffer.copyBuffer(stagingBuffer, out, buffer.size)
- out
- }
-
- stagingBuffer.destroy()
- commandPool.freeCommandBuffer(commandBuffer)
- setToBuffers.flatMap(_._2).foreach(_.destroy())
-
- output
-
- def destroy(): Unit =
- descriptorSets.foreach(_.destroy())
-
-object SequenceExecutor:
- private[cyfra] case class ComputationSequence(sequence: Seq[ComputationStep], dependencies: Seq[Dependency])
-
- private[cyfra] sealed trait ComputationStep
- case class Compute(pipeline: ComputePipeline, bufferActions: Map[LayoutLocation, BufferAction]) extends ComputationStep:
- def pumpLayoutLocations: Seq[Seq[BufferAction]] =
- pipeline.layoutInfo.sets
- .map(x => x.bindings.map(y => (x.id, y.id)).map(x => bufferActions.getOrElse(LayoutLocation.apply.tupled(x), BufferAction.DoNothing)))
-
- case class LayoutLocation(set: Int, binding: Int)
-
- case class Dependency(from: ComputePipeline, fromSet: Int, to: ComputePipeline, toSet: Int)
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 60ecb48a..f8926e3d 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
@@ -15,7 +15,7 @@ import java.nio.ByteBuffer
* MarconZet Created 11.05.2019
*/
-private[cyfra] sealed class Buffer private (val size: Int, usage: Int, flags: Int)(using allocator: Allocator) extends VulkanObjectHandle:
+private[cyfra] sealed abstract class Buffer private (val size: Int, usage: Int, flags: Int)(using allocator: Allocator) extends VulkanObjectHandle:
val (handle, allocation) = pushStack: stack =>
val bufferInfo = VkBufferCreateInfo
.calloc(stack)
@@ -58,20 +58,20 @@ object Buffer:
if flush then vmaFlushAllocation(this.allocator.get, this.allocation, 0, size)
vmaUnmapMemory(this.allocator.get, this.allocation)
- def copyBuffer(src: ByteBuffer, dst: HostBuffer, bytes: Long): Unit =
+ def copyBuffer(src: ByteBuffer, dst: HostBuffer, srcOffset: Int, dstOffset: Int, bytes: Int): Unit =
dst.mapped: destination =>
- memCopy(memAddress(src), memAddress(destination), bytes)
+ memCopy(memAddress(src) + srcOffset, memAddress(destination) + dstOffset, bytes)
- def copyBuffer(src: HostBuffer, dst: ByteBuffer, bytes: Long): Unit =
+ def copyBuffer(src: HostBuffer, dst: ByteBuffer, srcOffset: Int, dstOffset: Int, bytes: Int): Unit =
src.mappedNoFlush: source =>
- memCopy(memAddress(source), memAddress(dst), bytes)
+ memCopy(memAddress(source) + srcOffset, memAddress(dst) + dstOffset, bytes)
- def copyBuffer(src: Buffer, dst: Buffer, bytes: Long, commandPool: CommandPool): Fence =
+ def copyBuffer(src: Buffer, dst: Buffer, srcOffset: Int, dstOffset: Int, bytes: Int, commandPool: CommandPool): Fence =
commandPool.executeCommand: commandBuffer =>
pushStack: stack =>
val copyRegion = VkBufferCopy
.calloc(1, stack)
- .srcOffset(0)
- .dstOffset(0)
+ .srcOffset(srcOffset)
+ .dstOffset(dstOffset)
.size(bytes)
vkCmdCopyBuffer(commandBuffer, src.get, dst.get, copyRegion)
diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/DescriptorPool.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/DescriptorPool.scala
index 4cb57928..093fb350 100644
--- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/DescriptorPool.scala
+++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/DescriptorPool.scala
@@ -1,5 +1,6 @@
package io.computenode.cyfra.vulkan.memory
+import io.computenode.cyfra.vulkan.compute.ComputePipeline.DescriptorSetLayout
import io.computenode.cyfra.vulkan.core.Device
import io.computenode.cyfra.vulkan.memory.DescriptorPool.MAX_SETS
import io.computenode.cyfra.vulkan.util.Util.{check, pushStack}
@@ -14,12 +15,18 @@ object DescriptorPool:
val MAX_SETS = 100
private[cyfra] class DescriptorPool(using device: Device) extends VulkanObjectHandle:
protected val handle: Long = pushStack: stack =>
- val descriptorPoolSize = VkDescriptorPoolSize.calloc(1, stack)
+ val descriptorPoolSize = VkDescriptorPoolSize.calloc(2, stack)
descriptorPoolSize
- .get(0)
- .`type`(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) // TODO this is sus when using with uniform buffers
+ .get()
+ .`type`(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
.descriptorCount(2 * MAX_SETS)
+ descriptorPoolSize
+ .get()
+ .`type`(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
+ .descriptorCount(2 * MAX_SETS)
+ descriptorPoolSize.rewind()
+
val descriptorPoolCreateInfo = VkDescriptorPoolCreateInfo
.calloc(stack)
.sType$Default()
@@ -31,5 +38,7 @@ private[cyfra] class DescriptorPool(using device: Device) extends VulkanObjectHa
check(vkCreateDescriptorPool(device.get, descriptorPoolCreateInfo, null, pDescriptorPool), "Failed to create descriptor pool")
pDescriptorPool.get()
+ def allocate(descriptorSetLayout: DescriptorSetLayout): DescriptorSet = DescriptorSet(descriptorSetLayout, this)
+
override protected def close(): Unit =
vkDestroyDescriptorPool(device.get, handle, null)
diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/DescriptorSet.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/DescriptorSet.scala
index 52e001a0..bd33df63 100644
--- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/DescriptorSet.scala
+++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/DescriptorSet.scala
@@ -1,6 +1,6 @@
package io.computenode.cyfra.vulkan.memory
-import io.computenode.cyfra.vulkan.compute.{Binding, InputBufferSize, LayoutSet, UniformSize}
+import io.computenode.cyfra.vulkan.compute.ComputePipeline.{BindingType, DescriptorSetInfo, DescriptorSetLayout}
import io.computenode.cyfra.vulkan.core.Device
import io.computenode.cyfra.vulkan.util.Util.{check, pushStack}
import io.computenode.cyfra.vulkan.util.VulkanObjectHandle
@@ -10,11 +10,11 @@ import org.lwjgl.vulkan.{VkDescriptorBufferInfo, VkDescriptorSetAllocateInfo, Vk
/** @author
* MarconZet Created 15.04.2020
*/
-private[cyfra] class DescriptorSet(descriptorSetLayout: Long, val bindings: Seq[Binding], descriptorPool: DescriptorPool)(using device: Device)
+private[cyfra] class DescriptorSet(descriptorSetLayout: DescriptorSetLayout, descriptorPool: DescriptorPool)(using device: Device)
extends VulkanObjectHandle:
protected val handle: Long = pushStack: stack =>
- val pSetLayout = stack.callocLong(1).put(0, descriptorSetLayout)
+ val pSetLayout = stack.callocLong(1).put(0, descriptorSetLayout.id)
val descriptorSetAllocateInfo = VkDescriptorSetAllocateInfo
.calloc(stack)
.sType$Default()
@@ -26,22 +26,23 @@ private[cyfra] class DescriptorSet(descriptorSetLayout: Long, val bindings: Seq[
pDescriptorSet.get()
def update(buffers: Seq[Buffer]): Unit = pushStack: stack =>
+ val bindings = descriptorSetLayout.set.descriptors
assert(buffers.length == bindings.length, s"Number of buffers (${buffers.length}) does not match number of bindings (${bindings.length})")
val writeDescriptorSet = VkWriteDescriptorSet.calloc(buffers.length, stack)
- buffers.zip(bindings).foreach { case (buffer, binding) =>
+ buffers.zip(bindings).zipWithIndex.foreach { case ((buffer, binding), idx) =>
val descriptorBufferInfo = VkDescriptorBufferInfo
.calloc(1, stack)
.buffer(buffer.get)
.offset(0)
.range(VK_WHOLE_SIZE)
- val descriptorType = binding.size match
- case InputBufferSize(elemSize) => VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
- case UniformSize(size) => VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
+ val descriptorType = binding.kind match
+ case BindingType.StorageBuffer => VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
+ case BindingType.Uniform => VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
writeDescriptorSet
.get()
.sType$Default()
.dstSet(handle)
- .dstBinding(binding.id)
+ .dstBinding(idx)
.descriptorCount(1)
.descriptorType(descriptorType)
.pBufferInfo(descriptorBufferInfo)
diff --git a/cyfra-vulkan/src/test/resources/copy_test.comp b/cyfra-vulkan/src/test/resources/copy_test.comp
deleted file mode 100644
index 13f55532..00000000
--- a/cyfra-vulkan/src/test/resources/copy_test.comp
+++ /dev/null
@@ -1,15 +0,0 @@
-#version 450
-
-layout (local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
-
-layout (binding = 0, set = 0) buffer InputBuffer {
- int inArray[];
-};
-layout (binding = 0, set = 1) buffer OutputBuffer {
- int outArray[];
-};
-
-void main(void){
- uint index = gl_GlobalInvocationID.x;
- outArray[index] = inArray[index] + 10000;
-}
diff --git a/cyfra-vulkan/src/test/scala/io/computenode/cyfra/vulkan/SequenceExecutorTest.scala b/cyfra-vulkan/src/test/scala/io/computenode/cyfra/vulkan/SequenceExecutorTest.scala
deleted file mode 100644
index 14ab3e46..00000000
--- a/cyfra-vulkan/src/test/scala/io/computenode/cyfra/vulkan/SequenceExecutorTest.scala
+++ /dev/null
@@ -1,35 +0,0 @@
-package io.computenode.cyfra.vulkan
-
-import io.computenode.cyfra.vulkan.VulkanContext
-import io.computenode.cyfra.vulkan.compute.*
-import io.computenode.cyfra.vulkan.core.Device
-import io.computenode.cyfra.vulkan.executor.BufferAction.{LoadFrom, LoadTo}
-import io.computenode.cyfra.vulkan.executor.SequenceExecutor
-import io.computenode.cyfra.vulkan.executor.SequenceExecutor.{ComputationSequence, Compute, Dependency, LayoutLocation}
-import munit.FunSuite
-import org.lwjgl.BufferUtils
-
-class SequenceExecutorTest extends FunSuite:
- val vulkanContext = VulkanContext()
- import vulkanContext.given
-
- test("Memory barrier"):
- val code = ComputePipeline.loadShader("copy_test.spv").get
- val layout = LayoutInfo(Seq(LayoutSet(0, Seq(Binding(0, InputBufferSize(4)))), LayoutSet(1, Seq(Binding(0, InputBufferSize(4))))))
- val copy1 = new ComputePipeline(code, "main", layout)
- val copy2 = new ComputePipeline(code, "main", layout)
-
- val sequence =
- ComputationSequence(
- Seq(Compute(copy1, Map(LayoutLocation(0, 0) -> LoadTo)), Compute(copy2, Map(LayoutLocation(1, 0) -> LoadFrom))),
- Seq(Dependency(copy1, 1, copy2, 0)),
- )
- val sequenceExecutor = new SequenceExecutor(sequence, vulkanContext)
- val input = 0 until 1024
- val buffer = BufferUtils.createByteBuffer(input.length * 4)
- input.foreach(buffer.putInt)
- buffer.flip()
- val res = sequenceExecutor.execute(Seq(buffer))
- val output = input.map(_ => res.head.getInt)
-
- assertEquals(input.map(_ + 20000).toList, output.toList)
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 00000000..2468070f
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,61 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1735563628,
+ "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-24.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 00000000..b3691241
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,33 @@
+{
+ description = "Dev shell for Vulkan + Java 21";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
+ flake-utils.url = "github:numtide/flake-utils";
+ };
+
+ outputs = { self, nixpkgs, flake-utils }:
+ flake-utils.lib.eachDefaultSystem (system:
+ let
+ pkgs = import nixpkgs { inherit system; };
+ jdk = pkgs.jdk21;
+ in {
+ devShells.default = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ jdk
+ sbt
+ vulkan-tools
+ vulkan-loader
+ vulkan-validation-layers
+ glslang
+ pkg-config
+ ];
+
+ JAVA_HOME = jdk;
+ VK_LAYER_PATH = "${pkgs.vulkan-validation-layers}/share/vulkan/explicit_layer.d";
+ LD_LIBRARY_PATH="${pkgs.vulkan-loader}/lib:${pkgs.vulkan-validation-layers}/lib:$LD_LIBRARY_PATH";
+
+ };
+ });
+}
+