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 + ![output](https://github.com/user-attachments/assets/3eac9f7f-72df-4a5d-b768-9117d651c78d) [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"; + + }; + }); +} +