diff --git a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/SpirvTypes.scala b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/SpirvTypes.scala index 9fe1b386..7adeb972 100644 --- a/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/SpirvTypes.scala +++ b/cyfra-compiler/src/main/scala/io/computenode/cyfra/spirv/SpirvTypes.scala @@ -54,6 +54,7 @@ private[cyfra] object SpirvTypes: case LGBooleanTag => 4 case v if v <:< LVecTag => vecSize(v) * typeStride(v.typeArgs.head) + case _ => 4 def typeStride(tag: Tag[?]): Int = typeStride(tag.tag) diff --git a/cyfra-core/src/main/scala/io/computenode/cyfra/core/Allocation.scala b/cyfra-core/src/main/scala/io/computenode/cyfra/core/Allocation.scala index bdc1d5a7..493b6a6e 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 @@ -10,6 +10,8 @@ import izumi.reflect.Tag import java.nio.ByteBuffer trait Allocation: + def submitLayout[L <: Layout: LayoutBinding](layout: L): Unit + extension (buffer: GBinding[?]) def read(bb: ByteBuffer, offset: Int = 0): 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 cfd3ad8d..cfc041cf 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,6 +1,7 @@ package io.computenode.cyfra.core import io.computenode.cyfra.core.Allocation +import io.computenode.cyfra.core.GBufferRegion.MapRegion import io.computenode.cyfra.core.GProgram.BufferLengthSpec import io.computenode.cyfra.core.layout.{Layout, LayoutBinding} import io.computenode.cyfra.dsl.Value @@ -8,9 +9,15 @@ import io.computenode.cyfra.dsl.Value.FromExpr import io.computenode.cyfra.dsl.binding.GBuffer import izumi.reflect.Tag +import scala.util.chaining.given import java.nio.ByteBuffer -sealed trait GBufferRegion[ReqAlloc <: Layout: LayoutBinding, ResAlloc <: Layout: LayoutBinding] +sealed trait GBufferRegion[ReqAlloc <: Layout: LayoutBinding, ResAlloc <: Layout: LayoutBinding]: + def reqAllocBinding: LayoutBinding[ReqAlloc] = summon[LayoutBinding[ReqAlloc]] + def resAllocBinding: LayoutBinding[ResAlloc] = summon[LayoutBinding[ResAlloc]] + + def map[NewAlloc <: Layout: LayoutBinding](f: Allocation ?=> ResAlloc => NewAlloc): GBufferRegion[ReqAlloc, NewAlloc] = + MapRegion(this, (alloc: Allocation) => (resAlloc: ResAlloc) => f(using alloc)(resAlloc)) object GBufferRegion: @@ -24,20 +31,17 @@ object GBufferRegion: ) extends GBufferRegion[ReqAlloc, ResAlloc] 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 = cyfraRuntime.withAllocation: allocation => // noinspection ScalaRedundantCast - val steps: Seq[Allocation => Layout => Layout] = Seq.unfold(region: GBufferRegion[?, ?]): - case _: AllocRegion[?] => None + val steps: Seq[(Allocation => Layout => Layout, LayoutBinding[Layout])] = Seq.unfold(region: GBufferRegion[?, ?]): + case AllocRegion() => None case MapRegion(req, f) => - Some((f.asInstanceOf[Allocation => Layout => Layout], req)) + Some(((f.asInstanceOf[Allocation => Layout => Layout], req.resAllocBinding.asInstanceOf[LayoutBinding[Layout]]), req)) - val initAlloc = init(using allocation) + val initAlloc = init(using allocation).tap(allocation.submitLayout) val bodyAlloc = steps.foldLeft[Layout](initAlloc): (acc, step) => - step(allocation)(acc) + step._1(allocation)(acc).tap(allocation.submitLayout(_)(using step._2)) onDone(using allocation)(bodyAlloc.asInstanceOf[ResAlloc]) diff --git a/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala b/cyfra-examples/src/main/scala/io/computenode/cyfra/samples/TestingStuff.scala index 8b9a5014..2f1b2799 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 @@ -12,9 +12,16 @@ import io.computenode.cyfra.runtime.VkCyfraRuntime import org.lwjgl.BufferUtils import org.lwjgl.system.MemoryUtil +import java.nio.ByteBuffer import java.util.concurrent.atomic.AtomicInteger import scala.collection.parallel.CollectionConverters.given +def printBuffer(bb: ByteBuffer): Unit = + val l = bb.asIntBuffer() + val a = new Array[Int](l.remaining()) + l.get(a) + println(a.mkString(" ")) + object TestingStuff: given GContext = GContext() @@ -115,6 +122,7 @@ object TestingStuff: ) runtime.close() + printBuffer(rbb) val actual = (0 until 2 * 1024).map(i => result.get(i * 1) != 0) val expected = (0 until 1024).flatMap(x => Seq.fill(emitFilterParams.emitN)(x)).map(_ == emitFilterParams.filterValue) expected @@ -191,7 +199,7 @@ object TestingStuff: def testAddProgram10Times = given runtime: VkCyfraRuntime = VkCyfraRuntime() val bufferSize = 1280 - val params = AddProgramParams(bufferSize, addA = 0, addB = 1) + val params = AddProgramParams(bufferSize, addA = 5, addB = 10) val region = GBufferRegion .allocate[AddProgramExecLayout] .map: region => @@ -226,6 +234,8 @@ object TestingStuff: }, ) runtime.close() + + printBuffer(rbbList(0)) val expected = inData.map(_ + 11 * (params.addA + params.addB)) outBuffers.foreach { buf => (0 until bufferSize).foreach { 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 782b2a85..9b9b385d 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 @@ -29,7 +29,7 @@ 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 org.lwjgl.vulkan.{VK13, VkCommandBuffer, VkCommandBufferBeginInfo, VkDependencyInfo, VkMemoryBarrier2, VkSubmitInfo} import scala.collection.mutable @@ -51,7 +51,7 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte .zip(layout) .map: case (set, bindings) => - set.update(bindings.map(x => VkAllocation.getUnderlying(x.binding))) + set.update(bindings.map(x => VkAllocation.getUnderlying(x.binding).buffer)) set val dispatches: Seq[Dispatch] = shaderCalls @@ -67,19 +67,15 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte 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(commandPool.queue.get, submitInfo, fence.get), "Failed to submit command buffer to queue") - fence.block().destroy() - commandPool.freeCommandBuffer(commandBuffer) - descriptorSets.flatten.foreach(dsManager.free) + val cleanup = () => + descriptorSets.flatten.foreach(dsManager.free) + commandPool.freeCommandBuffer(commandBuffer) + + val externalBindings = getAllBindings(executeSteps).map(VkAllocation.getUnderlying) + val deps = externalBindings.flatMap(_.execution.fold(Seq(_), _.toSeq)) + val pe = new PendingExecution(commandBuffer, deps, cleanup) + summon[VkAllocation].addExecution(pe) + externalBindings.foreach(_.execution = Left(pe)) // TODO we assume all accesses are read-write result private def interpret[Params, EL <: Layout: LayoutBinding, RL <: Layout: LayoutBinding]( @@ -202,7 +198,6 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte .flags(0) check(vkBeginCommandBuffer(commandBuffer, commandBufferBeginInfo), "Failed to begin recording command buffer") - steps.foreach: case PipelineBarrier => val memoryBarrier = VkMemoryBarrier2 // TODO don't synchronise everything @@ -228,11 +223,18 @@ class ExecutionHandler(runtime: VkCyfraRuntime, threadContext: VulkanThreadConte dispatch match case Direct(x, y, z) => vkCmdDispatch(commandBuffer, x, y, z) - case Indirect(buffer, offset) => vkCmdDispatchIndirect(commandBuffer, VkAllocation.getUnderlying(buffer).get, offset) + case Indirect(buffer, offset) => vkCmdDispatchIndirect(commandBuffer, VkAllocation.getUnderlying(buffer).buffer.get, offset) check(vkEndCommandBuffer(commandBuffer), "Failed to finish recording command buffer") commandBuffer + private def getAllBindings(steps: Seq[ExecutionStep]): Seq[GBinding[?]] = + steps + .flatMap: + case Dispatch(_, layout, _, _) => layout.flatten.map(_.binding) + case PipelineBarrier => Seq.empty + .distinct + object ExecutionHandler: case class ShaderCall(pipeline: ComputePipeline, layout: ShaderLayout, dispatch: DispatchType) diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/PendingExecution.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/PendingExecution.scala new file mode 100644 index 00000000..9ed42d7d --- /dev/null +++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/PendingExecution.scala @@ -0,0 +1,118 @@ +package io.computenode.cyfra.runtime + +import io.computenode.cyfra.vulkan.command.{CommandPool, Fence, Semaphore} +import io.computenode.cyfra.vulkan.core.{Device, Queue} +import io.computenode.cyfra.vulkan.util.Util.{check, pushStack} +import io.computenode.cyfra.vulkan.util.VulkanObject +import org.lwjgl.vulkan.VK10.VK_TRUE +import org.lwjgl.vulkan.VK13.{VK_PIPELINE_STAGE_2_COPY_BIT, vkQueueSubmit2} +import org.lwjgl.vulkan.{VK13, VkCommandBuffer, VkCommandBufferSubmitInfo, VkSemaphoreSubmitInfo, VkSubmitInfo2} + +import scala.collection.mutable + +/** A command buffer that is pending execution, along with its dependencies and cleanup actions. + * + * You can call `close()` only when `isFinished || isPending` is true + * + * You can call `destroy()` only when all dependants are `isClosed` + */ +class PendingExecution(protected val handle: VkCommandBuffer, val dependencies: Seq[PendingExecution], cleanup: () => Unit)(using Device): + private val semaphore: Semaphore = Semaphore() + private var fence: Option[Fence] = None + + def isPending: Boolean = fence.isEmpty + def isRunning: Boolean = fence.exists(f => f.isAlive && !f.isSignaled) + def isFinished: Boolean = fence.exists(f => !f.isAlive || f.isSignaled) + + def block(): Unit = fence.foreach(_.block()) + + private var closed = false + def isClosed: Boolean = closed + private def close(): Unit = + assert(isFinished || isPending, "Cannot close a PendingExecution that is not finished or pending") + if closed then return + cleanup() + closed = true + + private var destroyed = false + def destroy(): Unit = + if destroyed then return + close() + semaphore.destroy() + fence.foreach(x => if x.isAlive then x.destroy()) + destroyed = true + + /** Gathers all command buffers and their semaphores for submission to the queue, in the correct order. + * + * When you call this method, you are expected to submit the command buffers to the queue, and signal the provided fence when done. + * @param f + * The fence to signal when the command buffers are done executing. + * @return + * A sequence of tuples, each containing a command buffer, semaphore to signal, and a set of semaphores to wait on. + */ + private def gatherForSubmission(f: Fence): Seq[((VkCommandBuffer, Semaphore), Set[Semaphore])] = + if !isPending then return Seq.empty + val mySubmission = ((handle, semaphore), dependencies.map(_.semaphore).toSet) + fence = Some(f) + dependencies.flatMap(_.gatherForSubmission(f)).appended(mySubmission) + +object PendingExecution: + def executeAll(executions: Seq[PendingExecution], queue: Queue)(using Device): Fence = pushStack: stack => + assert(executions.forall(_.isPending), "All executions must be pending") + assert(executions.nonEmpty, "At least one execution must be provided") + + val fence = Fence() + + val exec: Seq[(Set[Semaphore], Set[(VkCommandBuffer, Semaphore)])] = + val gathered = executions.flatMap(_.gatherForSubmission(fence)) + val ordering = gathered.zipWithIndex.map(x => (x._1._1._1, x._2)).toMap + gathered.toSet.groupMap(_._2)(_._1).toSeq.sortBy(x => x._2.map(_._1).map(ordering).min) + + val submitInfos = VkSubmitInfo2.calloc(exec.size, stack) + exec.foreach: (semaphores, executions) => + val pCommandBuffersSI = VkCommandBufferSubmitInfo.calloc(executions.size, stack) + val signalSemaphoreSI = VkSemaphoreSubmitInfo.calloc(executions.size, stack) + executions.foreach: (cb, s) => + pCommandBuffersSI + .get() + .sType$Default() + .commandBuffer(cb) + .deviceMask(0) + signalSemaphoreSI + .get() + .sType$Default() + .semaphore(s.get) + .stageMask(VK13.VK_PIPELINE_STAGE_2_COPY_BIT | VK13.VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT) + + pCommandBuffersSI.flip() + signalSemaphoreSI.flip() + + val waitSemaphoreSI = VkSemaphoreSubmitInfo.calloc(semaphores.size, stack) + semaphores.foreach: s => + waitSemaphoreSI + .get() + .sType$Default() + .semaphore(s.get) + .stageMask(VK13.VK_PIPELINE_STAGE_2_COPY_BIT | VK13.VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT) + + waitSemaphoreSI.flip() + + submitInfos + .get() + .sType$Default() + .flags(0) + .pCommandBufferInfos(pCommandBuffersSI) + .pSignalSemaphoreInfos(signalSemaphoreSI) + .pWaitSemaphoreInfos(waitSemaphoreSI) + + submitInfos.flip() + + check(vkQueueSubmit2(queue.get, submitInfos, fence.get), "Failed to submit command buffer to queue") + fence + + def cleanupAll(executions: Seq[PendingExecution]): Unit = + def cleanupRec(ex: PendingExecution): Unit = + if !ex.isClosed then return + ex.close() + ex.dependencies.foreach(cleanupRec) + executions.foreach(cleanupRec) diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkAllocation.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkAllocation.scala index a038ac7b..ea80f7c6 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 @@ -14,37 +14,59 @@ 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 io.computenode.cyfra.vulkan.core.Device import izumi.reflect.Tag import org.lwjgl.BufferUtils import org.lwjgl.system.MemoryUtil import org.lwjgl.vulkan.VK10 +import org.lwjgl.vulkan.VK13.VK_PIPELINE_STAGE_2_COPY_BIT 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: +class VkAllocation(commandPool: CommandPool, executionHandler: ExecutionHandler)(using Allocator, Device) extends Allocation: given VkAllocation = this + override def submitLayout[L <: Layout: LayoutBinding](layout: L): Unit = + val executions = summon[LayoutBinding[L]] + .toBindings(layout) + .map(getUnderlying) + .flatMap(_.execution.fold(Seq(_), _.toSeq)) + .filter(_.isPending) + + PendingExecution.executeAll(executions, commandPool.queue) + extension (buffer: GBinding[?]) def read(bb: ByteBuffer, offset: Int = 0): Unit = val size = bb.remaining() - getUnderlying(buffer) match - case buffer: Buffer.HostBuffer => buffer.copyTo(bb, offset) - case buffer: Buffer.DeviceBuffer => + buffer match + case VkBinding(buffer: Buffer.HostBuffer) => buffer.copyTo(bb, offset) + case binding: VkBinding[?] => + binding.materialise(commandPool.queue) val stagingBuffer = getStagingBuffer(size) - Buffer.copyBuffer(buffer, stagingBuffer, offset, 0, size, commandPool) + Buffer.copyBuffer(binding.buffer, stagingBuffer, offset, 0, size, commandPool) stagingBuffer.copyTo(bb, 0) + stagingBuffer.destroy() + case _ => throw new IllegalArgumentException(s"Tried to read from non-VkBinding $buffer") def write(bb: ByteBuffer, offset: Int = 0): Unit = val size = bb.remaining() - getUnderlying(buffer) match - case buffer: Buffer.HostBuffer => buffer.copyFrom(bb, offset) - case buffer: Buffer.DeviceBuffer => + buffer match + case VkBinding(buffer: Buffer.HostBuffer) => buffer.copyFrom(bb, offset) + case binding: VkBinding[?] => + binding.materialise(commandPool.queue) val stagingBuffer = getStagingBuffer(size) - stagingBuffer.copyFrom(bb, offset) - Buffer.copyBuffer(stagingBuffer, buffer, 0, offset, size, commandPool) + stagingBuffer.copyFrom(bb, 0) + val cb = Buffer.copyBufferCommandBuffer(stagingBuffer, binding.buffer, 0, offset, size, commandPool) + val cleanup = () => + commandPool.freeCommandBuffer(cb) + stagingBuffer.destroy() + val pe = new PendingExecution(cb, binding.execution.fold(Seq(_), _.toSeq), cleanup) + addExecution(pe) + binding.execution = Left(pe) + case _ => throw new IllegalArgumentException(s"Tried to write to non-VkBinding $buffer") extension (buffers: GBuffer.type) def apply[T <: Value: {Tag, FromExpr}](length: Int): GBuffer[T] = @@ -78,24 +100,21 @@ class VkAllocation(commandPool: CommandPool, executionHandler: ExecutionHandler) case _ => ??? direct(bb) + private val executions = mutable.Buffer[PendingExecution]() + + def addExecution(pe: PendingExecution): Unit = + executions += pe + private val bindings = mutable.Buffer[VkUniform[?] | VkBuffer[?]]() private[cyfra] def close(): Unit = - bindings.map(getUnderlying).foreach(_.destroy()) - stagingBuffer.foreach(_.destroy()) + executions.foreach(_.destroy()) + bindings.map(getUnderlying).foreach(_.buffer.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 + Buffer.HostBuffer(size, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT) object VkAllocation: - private[runtime] def getUnderlying(buffer: GBinding[?]): Buffer = + private[runtime] def getUnderlying(buffer: GBinding[?]): VkBinding[?] = 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") + case buffer: VkBinding[?] => buffer + 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/VkBinding.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBinding.scala new file mode 100644 index 00000000..6283ad78 --- /dev/null +++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBinding.scala @@ -0,0 +1,72 @@ +package io.computenode.cyfra.runtime + +import io.computenode.cyfra.dsl.Value +import io.computenode.cyfra.dsl.Value.FromExpr +import io.computenode.cyfra.spirv.SpirvTypes.typeStride +import izumi.reflect.Tag +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 io.computenode.cyfra.vulkan.core.Queue +import io.computenode.cyfra.vulkan.core.Device +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} +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.* + +import scala.collection.mutable + +sealed abstract class VkBinding[T <: Value: {Tag, FromExpr}](val buffer: Buffer): + val sizeOfT: Int = typeStride(summon[Tag[T]]) + + /** Holds either: + * 1. a single execution that writes to this buffer + * 1. multiple executions that read from this buffer + */ + var execution: Either[PendingExecution, mutable.Buffer[PendingExecution]] = Right(mutable.Buffer.empty) + + def materialise(queue: Queue)(using Device): Unit = + val (pendingExecs, runningExecs) = execution.fold(Seq(_), _.toSeq).partition(_.isPending) // TODO better handle read only executions + if pendingExecs.nonEmpty then + val fence = PendingExecution.executeAll(pendingExecs, queue) + fence.block() + PendingExecution.cleanupAll(pendingExecs) + + runningExecs.foreach(_.block()) + PendingExecution.cleanupAll(runningExecs) + +object VkBinding: + def unapply(binding: GBinding[?]): Option[Buffer] = binding match + case b: VkBinding[?] => Some(b.buffer) + case _ => None + +class VkBuffer[T <: Value: {Tag, FromExpr}] private (val length: Int, underlying: Buffer) extends VkBinding(underlying) with GBuffer[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) + +class VkUniform[T <: Value: {Tag, FromExpr}] private (underlying: Buffer) extends VkBinding[T](underlying) with GUniform[T] + +object VkUniform: + private final val UsageFlags = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | + VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT + + def apply[T <: Value: {Tag, FromExpr}]()(using Allocator): VkUniform[T] = + val sizeOfT = typeStride(summon[Tag[T]]) + val buffer = new Buffer.DeviceBuffer(sizeOfT, UsageFlags) + new VkUniform[T](buffer) diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBuffer.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBuffer.scala deleted file mode 100644 index cda73868..00000000 --- a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkBuffer.scala +++ /dev/null @@ -1,23 +0,0 @@ -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/VkUniform.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkUniform.scala deleted file mode 100644 index f8c75da7..00000000 --- a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/VkUniform.scala +++ /dev/null @@ -1,21 +0,0 @@ -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/VulkanContext.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/VulkanContext.scala index 9c8c99c6..67d612fe 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/VulkanContext.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/VulkanContext.scala @@ -17,7 +17,6 @@ import scala.jdk.CollectionConverters.* private[cyfra] object VulkanContext: val ValidationLayer: String = "VK_LAYER_KHRONOS_validation" private val ValidationLayers: Boolean = System.getProperty("io.computenode.cyfra.vulkan.validation", "false").toBoolean - if Configuration.STACK_SIZE.get() < 100 then logger.warn(s"Small stack size. Increase with org.lwjgl.system.stackSize") private[cyfra] class VulkanContext: private val instance: Instance = new Instance(ValidationLayers) diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/CommandPool.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/CommandPool.scala index 3db65668..11d21e1a 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/CommandPool.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/CommandPool.scala @@ -39,35 +39,18 @@ private[cyfra] abstract class CommandPool private (flags: Int, val queue: Queue) check(vkAllocateCommandBuffers(device.get, allocateInfo, pointerBuffer), "Failed to allocate command buffers") 0 until n map (i => pointerBuffer.get(i)) map (new VkCommandBuffer(_, device.get)) - def executeCommand(block: VkCommandBuffer => Unit): Unit = - val commandBuffer = beginSingleTimeCommands() - block(commandBuffer) - endSingleTimeCommands(commandBuffer).block().destroy() - freeCommandBuffer(commandBuffer) - - private def beginSingleTimeCommands(): VkCommandBuffer = - pushStack: stack => - val commandBuffer = this.createCommandBuffer() - - val beginInfo = VkCommandBufferBeginInfo - .calloc(stack) - .sType$Default() - .flags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) + def recordSingleTimeCommand(block: VkCommandBuffer => Unit): VkCommandBuffer = pushStack: stack => + val commandBuffer = createCommandBuffer() - check(vkBeginCommandBuffer(commandBuffer, beginInfo), "Failed to begin single time command buffer") - commandBuffer + val beginInfo = VkCommandBufferBeginInfo + .calloc(stack) + .sType$Default() + .flags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) - private def endSingleTimeCommands(commandBuffer: VkCommandBuffer): Fence = - pushStack: stack => - vkEndCommandBuffer(commandBuffer) - val pointerBuffer = stack.callocPointer(1).put(0, commandBuffer) - val submitInfo = VkSubmitInfo - .calloc(stack) - .sType$Default() - .pCommandBuffers(pointerBuffer) - val fence = Fence() - check(vkQueueSubmit(queue.get, submitInfo, fence.get), "Failed to submit single time command buffer") - fence + check(vkBeginCommandBuffer(commandBuffer, beginInfo), "Failed to begin single time command buffer") + block(commandBuffer) + check(vkEndCommandBuffer(commandBuffer), "Failed to end single time command buffer") + commandBuffer def freeCommandBuffer(commandBuffer: VkCommandBuffer*): Unit = pushStack: stack => diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/Semaphore.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/Semaphore.scala index 04034b1c..2e86ef68 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/Semaphore.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/command/Semaphore.scala @@ -9,7 +9,7 @@ import org.lwjgl.vulkan.VkSemaphoreCreateInfo /** @author * MarconZet Created 30.10.2019 */ -private[cyfra] class Semaphore(using device: Device) extends VulkanObjectHandle: +private[cyfra] class Semaphore()(using device: Device) extends VulkanObjectHandle: protected val handle: Long = pushStack: stack => val semaphoreCreateInfo = VkSemaphoreCreateInfo .calloc(stack) diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/Buffer.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/memory/Buffer.scala index 963aa1cd..c1f34b40 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 @@ -1,13 +1,14 @@ package io.computenode.cyfra.vulkan.memory import io.computenode.cyfra.vulkan.command.{CommandPool, Fence} +import io.computenode.cyfra.vulkan.core.Device import io.computenode.cyfra.vulkan.util.Util.{check, pushStack} import io.computenode.cyfra.vulkan.util.VulkanObjectHandle import org.lwjgl.system.MemoryUtil.* import org.lwjgl.util.vma.Vma.* import org.lwjgl.util.vma.VmaAllocationCreateInfo import org.lwjgl.vulkan.VK10.* -import org.lwjgl.vulkan.{VkBufferCopy, VkBufferCreateInfo} +import org.lwjgl.vulkan.{VkBufferCopy, VkBufferCreateInfo, VkCommandBuffer, VkSubmitInfo} import java.nio.ByteBuffer @@ -61,8 +62,22 @@ object Buffer: def copyFrom(src: ByteBuffer, dstOffset: Int): Unit = pushStack: stack => vmaCopyMemoryToAllocation(allocator.get, src, allocation, dstOffset) - def copyBuffer(src: Buffer, dst: Buffer, srcOffset: Int, dstOffset: Int, bytes: Int, commandPool: CommandPool): Unit = - commandPool.executeCommand: commandBuffer => + def copyBuffer(src: Buffer, dst: Buffer, srcOffset: Int, dstOffset: Int, bytes: Int, commandPool: CommandPool)(using Device): Unit = pushStack: + stack => + val cb = copyBufferCommandBuffer(src, dst, srcOffset, dstOffset, bytes, commandPool) + + val pCB = stack.callocPointer(1).put(0, cb) + val submitInfo = VkSubmitInfo + .calloc(stack) + .sType$Default() + .pCommandBuffers(pCB) + + val fence = Fence() + check(vkQueueSubmit(commandPool.queue.get, submitInfo, fence.get), "Failed to submit single time command buffer") + fence.block().destroy() + + def copyBufferCommandBuffer(src: Buffer, dst: Buffer, srcOffset: Int, dstOffset: Int, bytes: Int, commandPool: CommandPool): VkCommandBuffer = + commandPool.recordSingleTimeCommand: commandBuffer => pushStack: stack => val copyRegion = VkBufferCopy .calloc(1, stack) diff --git a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/util/VulkanObject.scala b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/util/VulkanObject.scala index 3ec34726..50d3baf7 100644 --- a/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/util/VulkanObject.scala +++ b/cyfra-vulkan/src/main/scala/io/computenode/cyfra/vulkan/util/VulkanObject.scala @@ -6,6 +6,7 @@ package io.computenode.cyfra.vulkan.util private[cyfra] abstract class VulkanObject[T]: protected val handle: T private var alive: Boolean = true + def isAlive: Boolean = alive def get: T = if !alive then throw new IllegalStateException()