diff --git a/build.sbt b/build.sbt index c369050c..2fef2351 100644 --- a/build.sbt +++ b/build.sbt @@ -47,11 +47,10 @@ lazy val commonSettings = Seq( "org.lwjgl" % "lwjgl-vma" % lwjglVersion classifier lwjglNatives, "org.joml" % "joml" % jomlVersion, "commons-io" % "commons-io" % "2.16.1", - "org.slf4j" % "slf4j-api" % "1.7.30", - "org.slf4j" % "slf4j-simple" % "1.7.30" % Test, "org.scalameta" % "munit_3" % "1.0.0" % Test, "com.lihaoyi" %% "sourcecode" % "0.4.3-M5", "org.slf4j" % "slf4j-api" % "2.0.17", + "org.apache.logging.log4j" % "log4j-slf4j2-impl" % "2.24.3" % Test, ) ++ vulkanNatives, ) @@ -60,6 +59,10 @@ lazy val runnerSettings = Seq(libraryDependencies += "org.apache.logging.log4j" lazy val utility = (project in file("cyfra-utility")) .settings(commonSettings) +lazy val spirvTools = (project in file("cyfra-spirv-tools")) + .settings(commonSettings) + .dependsOn(utility) + lazy val vulkan = (project in file("cyfra-vulkan")) .settings(commonSettings) .dependsOn(utility) @@ -74,7 +77,7 @@ lazy val compiler = (project in file("cyfra-compiler")) lazy val runtime = (project in file("cyfra-runtime")) .settings(commonSettings) - .dependsOn(compiler, dsl, vulkan, utility) + .dependsOn(compiler, dsl, vulkan, utility, spirvTools) lazy val foton = (project in file("cyfra-foton")) .settings(commonSettings) diff --git a/cyfra-e2e-test/src/test/resources/io/computenode/cyfra/e2e/juliaset/julia.png b/cyfra-e2e-test/src/test/resources/julia.png similarity index 100% rename from cyfra-e2e-test/src/test/resources/io/computenode/cyfra/e2e/juliaset/julia.png rename to cyfra-e2e-test/src/test/resources/julia.png diff --git a/cyfra-e2e-test/src/test/resources/julia_O_optimized.png b/cyfra-e2e-test/src/test/resources/julia_O_optimized.png new file mode 100644 index 00000000..a1549b0a Binary files /dev/null and b/cyfra-e2e-test/src/test/resources/julia_O_optimized.png differ diff --git a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/juliaset/JuliaSet.scala b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/juliaset/JuliaSet.scala index 0726b3cb..ec607279 100644 --- a/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/juliaset/JuliaSet.scala +++ b/cyfra-e2e-test/src/test/scala/io/computenode/cyfra/e2e/juliaset/JuliaSet.scala @@ -5,25 +5,23 @@ import io.computenode.cyfra.* import io.computenode.cyfra.dsl.collections.GSeq import io.computenode.cyfra.dsl.control.Pure.pure import io.computenode.cyfra.dsl.struct.GStruct.Empty -import io.computenode.cyfra.runtime.{GContext, GFunction} -import org.apache.commons.io.IOUtils -import org.junit.runner.RunWith +import io.computenode.cyfra.e2e.ImageTests import io.computenode.cyfra.runtime.mem.Vec4FloatMem +import io.computenode.cyfra.runtime.{GContext, GFunction} +import io.computenode.cyfra.spirvtools.* +import io.computenode.cyfra.spirvtools.SpirvTool.{Param, ToFile} import io.computenode.cyfra.utility.ImageUtility import munit.FunSuite import java.io.File -import java.nio.file.Files +import java.nio.file.Paths +import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContext.Implicits -import scala.concurrent.duration.DurationInt -import scala.concurrent.{Await, ExecutionContext} -import io.computenode.cyfra.e2e.ImageTests class JuliaSet extends FunSuite: - given GContext = new GContext() given ExecutionContext = Implicits.global - test("Render julia set"): + def runJuliaSet(referenceImgName: String)(using GContext): Unit = { val dim = 4096 val max = 1 val RECURSION_LIMIT = 1000 @@ -70,5 +68,22 @@ class JuliaSet extends FunSuite: val r = Vec4FloatMem(dim * dim).map(function).asInstanceOf[Vec4FloatMem].toArray val outputTemp = File.createTempFile("julia", ".png") ImageUtility.renderToImage(r, dim, outputTemp.toPath) - val referenceImage = getClass.getResource("julia.png") + val referenceImage = getClass.getResource(referenceImgName) ImageTests.assertImagesEquals(outputTemp, new File(referenceImage.getPath)) + } + + test("Render julia set"): + given GContext = new GContext + runJuliaSet("/julia.png") + + test("Render julia set optimized"): + given GContext = new GContext( + SpirvToolsRunner( + validator = SpirvValidator.Enable(throwOnFail = true), + optimizer = SpirvOptimizer.Enable(toolOutput = ToFile(Paths.get("output/optimized.spv")), settings = Seq(Param("-O"))), + disassembler = SpirvDisassembler.Enable(toolOutput = ToFile(Paths.get("output/optimized.spvasm")), throwOnFail = true), + crossCompilation = SpirvCross.Enable(toolOutput = ToFile(Paths.get("output/optimized.glsl")), throwOnFail = true), + originalSpirvOutput = ToFile(Paths.get("output/original.spv")), + ), + ) + runJuliaSet("/julia_O_optimized.png") diff --git a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/GContext.scala b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/GContext.scala index 422370ee..90f0f91c 100644 --- a/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/GContext.scala +++ b/cyfra-runtime/src/main/scala/io/computenode/cyfra/runtime/GContext.scala @@ -1,30 +1,26 @@ package io.computenode.cyfra.runtime -import io.computenode.cyfra.dsl.{*, given} -import Value.{Float32, Int32, Vec4} -import io.computenode.cyfra.vulkan.VulkanContext -import io.computenode.cyfra.vulkan.compute.{Binding, ComputePipeline, InputBufferSize, LayoutInfo, LayoutSet, Shader, UniformSize} -import io.computenode.cyfra.vulkan.executor.{BufferAction, SequenceExecutor} -import SequenceExecutor.* +import io.computenode.cyfra.dsl.Value +import io.computenode.cyfra.dsl.Value.{Float32, FromExpr, Int32, Vec4} import io.computenode.cyfra.dsl.collections.GArray import io.computenode.cyfra.dsl.struct.* -import io.computenode.cyfra.dsl.struct.GStruct.* import io.computenode.cyfra.runtime.mem.GMem.totalStride +import io.computenode.cyfra.runtime.mem.{FloatMem, GMem, IntMem, Vec4FloatMem} import io.computenode.cyfra.spirv.SpirvTypes.typeStride import io.computenode.cyfra.spirv.compilers.DSLCompiler import io.computenode.cyfra.spirv.compilers.ExpressionCompiler.{UniformStructRef, WorkerIndex} -import mem.{FloatMem, GMem, IntMem, Vec4FloatMem} -import org.lwjgl.system.{Configuration, MemoryUtil} +import io.computenode.cyfra.spirvtools.SpirvToolsRunner +import io.computenode.cyfra.vulkan.VulkanContext +import io.computenode.cyfra.vulkan.compute.* +import io.computenode.cyfra.vulkan.executor.SequenceExecutor.* +import io.computenode.cyfra.vulkan.executor.{BufferAction, SequenceExecutor} import izumi.reflect.Tag +import org.lwjgl.system.Configuration -import java.io.FileOutputStream -import java.nio.ByteBuffer -import java.nio.channels.FileChannel import java.util.concurrent.Executors import scala.concurrent.{ExecutionContext, ExecutionContextExecutor} -class GContext: - +class GContext(spirvToolsRunner: SpirvToolsRunner = SpirvToolsRunner()): Configuration.STACK_SIZE.set(1024) // fix lwjgl stack size val vkContext = new VulkanContext() @@ -38,20 +34,17 @@ class GContext: val uniformStruct = uniformStructSchema.fromTree(UniformStructRef) val tree = function.fn .apply(uniformStruct, WorkerIndex, GArray[H](0)) - val shaderCode = DSLCompiler.compile(tree, function.arrayInputs, function.arrayOutputs, uniformStructSchema) - dumpSpvToFile(shaderCode, "program.spv") // TODO remove before release + + val optimizedShaderCode = + spirvToolsRunner.processShaderCodeWithSpirvTools(DSLCompiler.compile(tree, function.arrayInputs, function.arrayOutputs, uniformStructSchema)) + val inOut = 0 to 1 map (Binding(_, InputBufferSize(typeStride(summon[Tag[H]])))) val uniform = Option.when(uniformStructSchema.fields.nonEmpty)(Binding(2, UniformSize(totalStride(uniformStructSchema)))) val layoutInfo = LayoutInfo(Seq(LayoutSet(0, inOut ++ uniform))) - val shader = new Shader(shaderCode, new org.joml.Vector3i(256, 1, 1), layoutInfo, "main", vkContext.device) - new ComputePipeline(shader, vkContext) - } - private def dumpSpvToFile(code: ByteBuffer, path: String): Unit = - val fc: FileChannel = new FileOutputStream("program.spv").getChannel - fc.write(code) - fc.close() - code.rewind() + val shader = Shader(optimizedShaderCode, org.joml.Vector3i(256, 1, 1), layoutInfo, "main", vkContext.device) + ComputePipeline(shader, vkContext) + } def execute[G <: GStruct[G]: Tag: GStructSchema, H <: Value, R <: Value](mem: GMem[H], fn: GFunction[G, H, R])(using uniformContext: UniformContext[G], diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvCross.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvCross.scala new file mode 100644 index 00000000..73304350 --- /dev/null +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvCross.scala @@ -0,0 +1,56 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.spirvtools.SpirvDisassembler.executeSpirvCmd +import io.computenode.cyfra.spirvtools.SpirvTool.{Ignore, Param, ToFile, ToLogger} +import io.computenode.cyfra.utility.Logger.logger + +import java.nio.ByteBuffer + +object SpirvCross extends SpirvTool("spirv-cross"): + + def crossCompileSpirv(shaderCode: ByteBuffer, crossCompilation: CrossCompilation): Option[String] = + crossCompilation match + case Enable(throwOnFail, toolOutput, params) => + val crossCompilationRes = tryCrossCompileSpirv(shaderCode, params) + crossCompilationRes match + case Left(err) if throwOnFail => throw err + case Left(err) => + logger.warn(err.message) + None + case Right(crossCompiledCode) => + toolOutput match + case Ignore => + case toFile @ SpirvTool.ToFile(_) => + toFile.write(crossCompiledCode) + logger.debug(s"Saved cross compiled shader code in ${toFile.filePath}.") + case ToLogger => logger.debug(s"SPIR-V Cross Compilation result:\n$crossCompiledCode") + Some(crossCompiledCode) + case Disable => + logger.debug("SPIR-V cross compilation is disabled.") + None + + private def tryCrossCompileSpirv(shaderCode: ByteBuffer, params: Seq[Param]): Either[SpirvToolError, String] = + val cmd = Seq(toolName) ++ Seq("-") ++ params.flatMap(_.asStringParam.split(" ")) + for + (stdout, stderr, exitCode) <- executeSpirvCmd(shaderCode, cmd) + result <- Either.cond( + exitCode == 0, { + logger.debug("SPIR-V cross compilation succeeded.") + stdout.toString + }, + SpirvToolCrossCompilationFailed(exitCode, stderr.toString), + ) + yield result + + sealed trait CrossCompilation + + case class Enable(throwOnFail: Boolean = false, toolOutput: ToFile | Ignore.type | ToLogger.type = ToLogger, settings: Seq[Param] = Seq.empty) + extends CrossCompilation + + final case class SpirvToolCrossCompilationFailed(exitCode: Int, stderr: String) extends SpirvToolError: + def message: String = + s"""SPIR-V cross compilation failed with exit code $exitCode. + |Cross errors: + |$stderr""".stripMargin + + case object Disable extends CrossCompilation diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvDisassembler.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvDisassembler.scala new file mode 100644 index 00000000..e845adde --- /dev/null +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvDisassembler.scala @@ -0,0 +1,57 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.spirvtools.SpirvTool.{Ignore, Param, ToFile, ToLogger} +import io.computenode.cyfra.utility.Logger.logger + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.nio.file.Files + +object SpirvDisassembler extends SpirvTool("spirv-dis"): + + def disassembleSpirv(shaderCode: ByteBuffer, disassembly: Disassembly): Option[String] = + disassembly match + case Enable(throwOnFail, toolOutput, params) => + val disassemblyResult = tryGetDisassembleSpirv(shaderCode, params) + disassemblyResult match + case Left(err) if throwOnFail => throw err + case Left(err) => + logger.warn(err.message) + None + case Right(disassembledShader) => + toolOutput match + case Ignore => + case toFile @ SpirvTool.ToFile(_) => + toFile.write(disassembledShader) + logger.debug(s"Saved disassembled shader code in ${toFile.filePath}.") + case ToLogger => logger.debug(s"SPIR-V Assembly:\n$disassembledShader") + Some(disassembledShader) + case Disable => + logger.debug("SPIR-V disassembly is disabled.") + None + + private def tryGetDisassembleSpirv(shaderCode: ByteBuffer, params: Seq[Param]): Either[SpirvToolError, String] = + val cmd = Seq(toolName) ++ params.flatMap(_.asStringParam.split(" ")) ++ Seq("-") + for + (stdout, stderr, exitCode) <- executeSpirvCmd(shaderCode, cmd) + result <- Either.cond( + exitCode == 0, { + logger.debug("SPIR-V disassembly succeeded.") + stdout.toString + }, + SpirvToolDisassemblyFailed(exitCode, stderr.toString), + ) + yield result + + sealed trait Disassembly + + final case class SpirvToolDisassemblyFailed(exitCode: Int, stderr: String) extends SpirvToolError: + def message: String = + s"""SPIR-V disassembly failed with exit code $exitCode. + |Disassembly errors: + |$stderr""".stripMargin + + case class Enable(throwOnFail: Boolean = false, toolOutput: ToFile | Ignore.type | ToLogger.type = ToLogger, settings: Seq[Param] = Seq.empty) + extends Disassembly + + case object Disable extends Disassembly diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvOptimizer.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvOptimizer.scala new file mode 100644 index 00000000..b42c0651 --- /dev/null +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvOptimizer.scala @@ -0,0 +1,61 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.spirvtools.SpirvDisassembler.executeSpirvCmd +import io.computenode.cyfra.spirvtools.SpirvTool.{Ignore, Param, ToFile} +import io.computenode.cyfra.utility.Logger.logger + +import java.nio.ByteBuffer + +object SpirvOptimizer extends SpirvTool("spirv-opt"): + + def optimizeSpirv(shaderCode: ByteBuffer, optimization: Optimization): Option[ByteBuffer] = + optimization match + case Enable(throwOnFail, toolOutput, params) => + val optimizationRes = tryGetOptimizeSpirv(shaderCode, params) + optimizationRes match + case Left(err) if throwOnFail => throw err + case Left(err) => + logger.warn(err.message) + None + case Right(optimizedShaderCode) => + toolOutput match + case SpirvTool.Ignore => + case toFile @ SpirvTool.ToFile(_) => + toFile.write(optimizedShaderCode) + logger.debug(s"Saved optimized shader code in ${toFile.filePath}.") + Some(optimizedShaderCode) + case Disable => + logger.debug("SPIR-V optimization is disabled.") + None + + private def tryGetOptimizeSpirv(shaderCode: ByteBuffer, params: Seq[Param]): Either[SpirvToolError, ByteBuffer] = + val cmd = Seq(toolName) ++ params.flatMap(_.asStringParam.split(" ")) ++ Seq("-", "-o", "-") + for + (stdout, stderr, exitCode) <- executeSpirvCmd(shaderCode, cmd) + result <- Either.cond( + exitCode == 0, { + logger.debug("SPIR-V optimization succeeded.") + val optimized = toDirectBuffer(ByteBuffer.wrap(stdout.toByteArray)) + optimized + }, + SpirvToolOptimizationFailed(exitCode, stderr.toString), + ) + yield result + + private def toDirectBuffer(buf: ByteBuffer): ByteBuffer = + val direct = ByteBuffer.allocateDirect(buf.remaining()) + direct.put(buf) + direct.flip() + direct + + sealed trait Optimization + + case class Enable(throwOnFail: Boolean = false, toolOutput: ToFile | Ignore.type = Ignore, settings: Seq[Param] = Seq.empty) extends Optimization + + final case class SpirvToolOptimizationFailed(exitCode: Int, stderr: String) extends SpirvToolError: + def message: String = + s"""SPIR-V optimization failed with exit code $exitCode. + |Optimizer errors: + |$stderr""".stripMargin + + case object Disable extends Optimization diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvTool.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvTool.scala new file mode 100644 index 00000000..729c0efd --- /dev/null +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvTool.scala @@ -0,0 +1,119 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.utility.Logger.logger + +import java.io.* +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Path} +import scala.annotation.tailrec +import scala.sys.process.{ProcessIO, stringSeqToProcess} +import scala.util.{Try, Using} + +abstract class SpirvTool(protected val toolName: String): + + protected def executeSpirvCmd( + shaderCode: ByteBuffer, + cmd: Seq[String], + ): Either[SpirvToolError, (ByteArrayOutputStream, ByteArrayOutputStream, Int)] = + logger.debug(s"SPIR-V cmd $cmd.") + val inputBytes = + val arr = new Array[Byte](shaderCode.remaining()) + shaderCode.get(arr) + shaderCode.rewind() + arr + val inputStream = new ByteArrayInputStream(inputBytes) + val outputStream = new ByteArrayOutputStream() + val errorStream = new ByteArrayOutputStream() + + def safeIOCopy(inStream: InputStream, outStream: OutputStream, description: String): Either[SpirvToolIOError, Unit] = + @tailrec + def loopOverBuffer(buf: Array[Byte]): Unit = + val len = inStream.read(buf) + if len == -1 then () + else + outStream.write(buf, 0, len) + loopOverBuffer(buf) + + Using + .Manager { use => + val in = use(inStream) + val out = use(outStream) + val buf = new Array[Byte](1024) + loopOverBuffer(buf) + out.flush() + } + .toEither + .left + .map(e => SpirvToolIOError(s"$description failed: ${e.getMessage}")) + + def createProcessIO(): Either[SpirvToolError, ProcessIO] = + val inHandler: OutputStream => Unit = + in => + safeIOCopy(inputStream, in, "Writing to stdin") match + case Left(err) => SpirvToolIOError(s"Failed to create ProcessIO: ${err.getMessage}") + case Right(_) => () + + val outHandler: InputStream => Unit = + out => + safeIOCopy(out, outputStream, "Reading stdout") match + case Left(err) => SpirvToolIOError(s"Failed to create ProcessIO: ${err.getMessage}") + case Right(_) => () + + val errHandler: InputStream => Unit = + err => + safeIOCopy(err, errorStream, "Reading stderr") match + case Left(err) => SpirvToolIOError(s"Failed to create ProcessIO: ${err.getMessage}") + case Right(_) => () + + Try { + new ProcessIO(inHandler, outHandler, errHandler) + }.toEither.left.map(e => SpirvToolIOError(s"Failed to create ProcessIO: ${e.getMessage}")) + + for + processIO <- createProcessIO() + process <- Try(cmd.run(processIO)).toEither.left.map(ex => SpirvToolCommandExecutionFailed(s"Failed to execute SPIR-V command: ${ex.getMessage}")) + yield (outputStream, errorStream, process.exitValue()) + + trait SpirvToolError extends RuntimeException: + def message: String + + override def getMessage: String = message + + final case class SpirvToolNotFound(toolName: String) extends SpirvToolError: + def message: String = s"Tool '$toolName' not found in PATH." + + final case class SpirvToolCommandExecutionFailed(details: String) extends SpirvToolError: + def message: String = s"SPIR-V command execution failed: $details" + + final case class SpirvToolIOError(details: String) extends SpirvToolError: + def message: String = s"SPIR-V command encountered IO error: $details" + +object SpirvTool: + sealed trait ToolOutput + + case class Param(value: String): + def asStringParam: String = value + + case class ToFile(filePath: Path) extends ToolOutput: + require(filePath != null, "filePath must not be null") + + def write(outputToSave: String | ByteBuffer): Unit = + Option(filePath.getParent).foreach { dir => + if !Files.exists(dir) then + Files.createDirectories(dir) + logger.debug(s"Created output directory: $dir") + outputToSave match + case stringOutput: String => Files.write(filePath, stringOutput.getBytes(StandardCharsets.UTF_8)) + case byteBuffer: ByteBuffer => dumpByteBufferToFile(byteBuffer, filePath) + } + + private def dumpByteBufferToFile(code: ByteBuffer, path: Path): Unit = + Using.resource(new FileOutputStream(path.toAbsolutePath.toString).getChannel) { fc => + fc.write(code) + } + code.rewind() + + case object ToLogger extends ToolOutput + + case object Ignore extends ToolOutput diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvToolsRunner.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvToolsRunner.scala new file mode 100644 index 00000000..234fca7b --- /dev/null +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvToolsRunner.scala @@ -0,0 +1,35 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.spirvtools.SpirvTool.{Ignore, ToFile} +import io.computenode.cyfra.utility.Logger.logger + +import java.nio.ByteBuffer + +class SpirvToolsRunner( + val validator: SpirvValidator.Validation = SpirvValidator.Enable(), + val optimizer: SpirvOptimizer.Optimization = SpirvOptimizer.Disable, + val disassembler: SpirvDisassembler.Disassembly = SpirvDisassembler.Disable, + val crossCompilation: SpirvCross.CrossCompilation = SpirvCross.Disable, + val originalSpirvOutput: ToFile | Ignore.type = Ignore, +): + + def processShaderCodeWithSpirvTools(shaderCode: ByteBuffer): ByteBuffer = + def runTools(code: ByteBuffer): Unit = + SpirvDisassembler.disassembleSpirv(code, disassembler) + SpirvCross.crossCompileSpirv(code, crossCompilation) + SpirvValidator.validateSpirv(code, validator) + + originalSpirvOutput match + case toFile @ SpirvTool.ToFile(_) => + toFile.write(shaderCode) + logger.debug(s"Saved original shader code in ${toFile.filePath}.") + case Ignore => + + val optimized = SpirvOptimizer.optimizeSpirv(shaderCode, optimizer) + optimized match + case Some(optimizedCode) => + runTools(optimizedCode) + optimizedCode + case None => + runTools(shaderCode) + shaderCode diff --git a/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvValidator.scala b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvValidator.scala new file mode 100644 index 00000000..d1f0b3c4 --- /dev/null +++ b/cyfra-spirv-tools/src/main/scala/io/computenode/cyfra/spirvtools/SpirvValidator.scala @@ -0,0 +1,38 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.spirvtools.SpirvDisassembler.executeSpirvCmd +import io.computenode.cyfra.spirvtools.SpirvTool.Param +import io.computenode.cyfra.utility.Logger.logger + +import java.nio.ByteBuffer + +object SpirvValidator extends SpirvTool("spirv-val"): + + def validateSpirv(shaderCode: ByteBuffer, validation: Validation): Unit = + validation match + case Enable(throwOnFail, params) => + val validationRes = tryValidateSpirv(shaderCode, params) + validationRes match + case Left(err) if throwOnFail => throw err + case Left(err) => logger.warn(err.message) + case Right(_) => () + case Disable => logger.debug("SPIR-V validation is disabled.") + + private def tryValidateSpirv(shaderCode: ByteBuffer, params: Seq[Param]): Either[SpirvToolError, Unit] = + val cmd = Seq(toolName) ++ params.flatMap(_.asStringParam.split(" ")) ++ Seq("-") + for + (stdout, stderr, exitCode) <- executeSpirvCmd(shaderCode, cmd) + _ <- Either.cond(exitCode == 0, logger.debug("SPIR-V validation succeeded."), SpirvToolValidationFailed(exitCode, stderr.toString())) + yield () + + sealed trait Validation + + case class Enable(throwOnFail: Boolean = false, settings: Seq[Param] = Seq.empty) extends Validation + + final case class SpirvToolValidationFailed(exitCode: Int, stderr: String) extends SpirvToolError: + def message: String = + s"""SPIR-V validation failed with exit code $exitCode. + |Validation errors: + |$stderr""".stripMargin + + case object Disable extends Validation diff --git a/cyfra-spirv-tools/src/test/resources/optimized.glsl b/cyfra-spirv-tools/src/test/resources/optimized.glsl new file mode 100644 index 00000000..5c37820b --- /dev/null +++ b/cyfra-spirv-tools/src/test/resources/optimized.glsl @@ -0,0 +1,53 @@ +#version 450 +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +layout(binding = 1, std430) buffer BufferOut +{ + vec4 _m0[]; +} dataOut; + +void main() +{ + vec2 rotatedUv = vec2(float((int(gl_GlobalInvocationID.x) % 4096) - 2048) * 0.000732421875, float((int(gl_GlobalInvocationID.x) / 4096) - 2048) * 0.000732421875); + int _309; + vec2 _312; + _312 = vec2(dot(rotatedUv, vec2(0.4999999701976776123046875, 0.866025447845458984375)), dot(rotatedUv, vec2(-vec2(0.4999999701976776123046875, 0.866025447845458984375).y, vec2(0.4999999701976776123046875, 0.866025447845458984375).x))) * 0.89999997615814208984375; + _309 = 0; + bool _263; + vec2 _281; + int _283; + int _315; + bool _307 = true; + int _308 = 0; + for (; _307 && (_308 < 1000); _312 = _281, _309 = _315, _308 = _283, _307 = _263) + { + _263 = length(_312) < 2.0; + if (_263) + { + _315 = _309 + 1; + } + else + { + _315 = _309; + } + float _268 = _312.x; + float _269 = _312.y; + _281 = vec2((_268 * _268) - (_269 * _269), (2.0 * _268) * _269) + vec2(0.3549999892711639404296875); + _283 = _308 + 1; + } + vec4 _311; + if (_309 > 20) + { + float f = float(_309) * 0.00999999977648258209228515625; + float _336 = (f > 1.0) ? 1.0 : f; + float _289 = 1.0 - _336; + vec3 _306 = ((vec3(0.0313725508749485015869140625, 0.086274512112140655517578125, 0.407843172550201416015625) * (_289 * _289)) + (vec3(0.2431372702121734619140625, 0.3215686380863189697265625, 0.780392229557037353515625) * ((2.0 * _336) * _289))) + (vec3(0.866666734218597412109375, 0.913725554943084716796875, 1.0) * (_336 * _336)); + _311 = vec4(_306.x, _306.y, _306.z, 1.0); + } + else + { + _311 = vec4(0.0313725508749485015869140625, 0.086274512112140655517578125, 0.4078431427478790283203125, 1.0); + } + dataOut._m0[int(gl_GlobalInvocationID.x)] = _311; +} + diff --git a/cyfra-spirv-tools/src/test/resources/optimized.spv b/cyfra-spirv-tools/src/test/resources/optimized.spv new file mode 100644 index 00000000..63ab9b72 Binary files /dev/null and b/cyfra-spirv-tools/src/test/resources/optimized.spv differ diff --git a/cyfra-spirv-tools/src/test/resources/optimized.spvasm b/cyfra-spirv-tools/src/test/resources/optimized.spvasm new file mode 100644 index 00000000..dd916bfe --- /dev/null +++ b/cyfra-spirv-tools/src/test/resources/optimized.spvasm @@ -0,0 +1,179 @@ +; SPIR-V +; Version: 1.0 +; Generator: LunarG; 44 +; Bound: 337 +; Schema: 0 + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %4 "main" %gl_GlobalInvocationID + OpExecutionMode %4 LocalSize 256 1 1 + OpSource GLSL 450 + OpName %BufferOut "BufferOut" + OpName %dataOut "dataOut" + OpName %ff "ff" + OpName %y "y" + OpName %function "function" + OpName %x "x" + OpName %function_0 "function" + OpName %function_1 "function" + OpName %y_0 "y" + OpName %f "f" + OpName %rotatedUv "rotatedUv" + OpName %function_2 "function" + OpName %function_3 "function" + OpName %x_0 "x" + OpName %y_1 "y" + OpName %rotatedUv_0 "rotatedUv" + OpName %x_1 "x" + OpName %function_4 "function" + OpName %y_2 "y" + OpName %ff_0 "ff" + OpName %y_3 "y" + OpDecorate %gl_GlobalInvocationID BuiltIn GlobalInvocationId + OpDecorate %gl_WorkGroupSize BuiltIn WorkgroupSize + OpDecorate %_runtimearr_v4float ArrayStride 16 + OpMemberDecorate %BufferOut 0 Offset 0 + OpDecorate %BufferOut BufferBlock + OpDecorate %dataOut DescriptorSet 0 + OpDecorate %dataOut Binding 1 + %bool = OpTypeBool + %uint = OpTypeInt 32 0 + %v3uint = OpTypeVector %uint 3 + %float = OpTypeFloat 32 + %v2float = OpTypeVector %float 2 + %v3float = OpTypeVector %float 3 + %v4float = OpTypeVector %float 4 +%_ptr_Uniform_v4float = OpTypePointer Uniform %v4float + %int = OpTypeInt 32 1 +%_ptr_Input_int = OpTypePointer Input %int + %v3int = OpTypeVector %int 3 +%_ptr_Input_v3int = OpTypePointer Input %v3int + %void = OpTypeVoid + %3 = OpTypeFunction %void +%_runtimearr_v4float = OpTypeRuntimeArray %v4float + %BufferOut = OpTypeStruct %_runtimearr_v4float +%_ptr_Uniform_BufferOut = OpTypePointer Uniform %BufferOut + %dataOut = OpVariable %_ptr_Uniform_BufferOut Uniform + %uint_256 = OpConstant %uint 256 + %uint_1 = OpConstant %uint 1 + %uint_1_0 = OpConstant %uint 1 +%gl_WorkGroupSize = OpConstantComposite %v3uint %uint_256 %uint_1 %uint_1_0 + %int_1 = OpConstant %int 1 + %int_4096 = OpConstant %int 4096 + %float_1 = OpConstant %float 1 + %float_2 = OpConstant %float 2 + %int_0 = OpConstant %int 0 + %int_2048 = OpConstant %int 2048 +%float_0_354999989 = OpConstant %float 0.354999989 +%float_0_899999976 = OpConstant %float 0.899999976 + %int_1000 = OpConstant %int 1000 + %int_2 = OpConstant %int 2 +%float_0_0313725509 = OpConstant %float 0.0313725509 +%float_0_0862745121 = OpConstant %float 0.0862745121 +%float_0_407843143 = OpConstant %float 0.407843143 + %int_20 = OpConstant %int 20 + %true = OpConstantTrue %bool +%gl_GlobalInvocationID = OpVariable %_ptr_Input_v3int Input +%float_0_866025448 = OpConstant %float 0.866025448 +%float_0_49999997 = OpConstant %float 0.49999997 + %318 = OpConstantComposite %v2float %float_0_49999997 %float_0_866025448 + %319 = OpConstantComposite %v2float %float_0_354999989 %float_0_354999989 + %320 = OpConstantComposite %v4float %float_0_0313725509 %float_0_0862745121 %float_0_407843143 %float_1 +%float_0_24313727 = OpConstant %float 0.24313727 +%float_0_321568638 = OpConstant %float 0.321568638 +%float_0_78039223 = OpConstant %float 0.78039223 + %327 = OpConstantComposite %v3float %float_0_24313727 %float_0_321568638 %float_0_78039223 +%float_0_407843173 = OpConstant %float 0.407843173 + %329 = OpConstantComposite %v3float %float_0_0313725509 %float_0_0862745121 %float_0_407843173 +%float_0_866666734 = OpConstant %float 0.866666734 +%float_0_913725555 = OpConstant %float 0.913725555 + %332 = OpConstantComposite %v3float %float_0_866666734 %float_0_913725555 %float_1 +%float_0_000732421875 = OpConstant %float 0.000732421875 +%float_0_00999999978 = OpConstant %float 0.00999999978 + %4 = OpFunction %void None %3 + %115 = OpLabel + %116 = OpAccessChain %_ptr_Input_int %gl_GlobalInvocationID %int_0 + %y_0 = OpLoad %int %116 + %x_0 = OpSDiv %int %y_0 %int_4096 + %x_1 = OpSMod %int %y_0 %int_4096 + %y = OpISub %int %x_0 %int_2048 + %x = OpISub %int %x_1 %int_2048 + %y_3 = OpConvertSToF %float %y + %y_1 = OpConvertSToF %float %x + %y_2 = OpFMul %float %y_3 %float_0_000732421875 +%rotatedUv_0 = OpFMul %float %y_1 %float_0_000732421875 + %rotatedUv = OpCompositeConstruct %v2float %rotatedUv_0 %y_2 + %239 = OpVectorExtractDynamic %float %318 %int_1 + %240 = OpVectorExtractDynamic %float %318 %int_0 + %241 = OpFNegate %float %239 + %242 = OpCompositeConstruct %v2float %241 %240 + %244 = OpDot %float %rotatedUv %242 + %245 = OpDot %float %rotatedUv %318 + %246 = OpCompositeConstruct %v2float %245 %244 + %247 = OpVectorTimesScalar %v2float %246 %float_0_899999976 + OpBranch %254 + %254 = OpLabel + %312 = OpPhi %v2float %247 %115 %281 %284 + %309 = OpPhi %int %int_0 %115 %315 %284 + %308 = OpPhi %int %int_0 %115 %283 %284 + %307 = OpPhi %bool %true %115 %263 %284 + %258 = OpSLessThan %bool %308 %int_1000 + %259 = OpLogicalAnd %bool %307 %258 + OpLoopMerge %285 %284 None + OpBranchConditional %259 %260 %285 + %260 = OpLabel + %262 = OpExtInst %float %1 Length %312 + %263 = OpFOrdLessThan %bool %262 %float_2 + OpSelectionMerge %267 None + OpBranchConditional %263 %264 %267 + %264 = OpLabel + %266 = OpIAdd %int %309 %int_1 + OpBranch %267 + %267 = OpLabel + %315 = OpPhi %int %309 %260 %266 %264 + %268 = OpVectorExtractDynamic %float %312 %int_0 + %269 = OpVectorExtractDynamic %float %312 %int_1 + %274 = OpFMul %float %float_2 %268 + %275 = OpFMul %float %269 %269 + %276 = OpFMul %float %268 %268 + %277 = OpFMul %float %274 %269 + %278 = OpFSub %float %276 %275 + %280 = OpCompositeConstruct %v2float %278 %277 + %281 = OpFAdd %v2float %280 %319 + %283 = OpIAdd %int %308 %int_1 + OpBranch %284 + %284 = OpLabel + OpBranch %254 + %285 = OpLabel + %function_1 = OpSGreaterThan %bool %309 %int_20 + OpSelectionMerge %150 None + OpBranchConditional %function_1 %151 %function + %151 = OpLabel + %ff_0 = OpConvertSToF %float %309 + %f = OpFMul %float %ff_0 %float_0_00999999978 + %ff = OpFOrdGreaterThan %bool %f %float_1 + %336 = OpSelect %float %ff %float_1 %f + %289 = OpFSub %float %float_1 %336 + %290 = OpFMul %float %float_2 %336 + %296 = OpFMul %float %290 %289 + %298 = OpFMul %float %289 %289 + %300 = OpFMul %float %336 %336 + %302 = OpVectorTimesScalar %v3float %327 %296 + %303 = OpVectorTimesScalar %v3float %329 %298 + %304 = OpVectorTimesScalar %v3float %332 %300 + %305 = OpFAdd %v3float %303 %302 + %306 = OpFAdd %v3float %305 %304 + %function_4 = OpVectorExtractDynamic %float %306 %int_2 + %function_0 = OpVectorExtractDynamic %float %306 %int_1 + %function_2 = OpVectorExtractDynamic %float %306 %int_0 + %function_3 = OpCompositeConstruct %v4float %function_2 %function_0 %function_4 %float_1 + OpBranch %150 + %function = OpLabel + OpBranch %150 + %150 = OpLabel + %311 = OpPhi %v4float %function_3 %151 %320 %function + %154 = OpAccessChain %_ptr_Uniform_v4float %dataOut %int_0 %y_0 + OpStore %154 %311 + OpReturn + OpFunctionEnd diff --git a/cyfra-spirv-tools/src/test/resources/original.spv b/cyfra-spirv-tools/src/test/resources/original.spv new file mode 100644 index 00000000..d11e1e51 Binary files /dev/null and b/cyfra-spirv-tools/src/test/resources/original.spv differ diff --git a/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvCrossTest.scala b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvCrossTest.scala new file mode 100644 index 00000000..6a21f550 --- /dev/null +++ b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvCrossTest.scala @@ -0,0 +1,19 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.spirvtools.SpirvCross.Enable +import munit.FunSuite + +class SpirvCrossTest extends FunSuite { + + test("SPIR-V cross compilation succeeded") { + val shaderCode = SpirvTestUtils.loadShaderFromResources("optimized.spv") + val glslShader = SpirvCross.crossCompileSpirv(shaderCode, crossCompilation = Enable(throwOnFail = true)) match { + case None => fail("Failed to disassemble shader.") + case Some(assembly) => assembly + } + + val referenceGlsl = SpirvTestUtils.loadResourceAsString("optimized.glsl") + + assertEquals(glslShader, referenceGlsl) + } +} diff --git a/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvDisassemblerTest.scala b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvDisassemblerTest.scala new file mode 100644 index 00000000..0138b0cd --- /dev/null +++ b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvDisassemblerTest.scala @@ -0,0 +1,19 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.spirvtools.SpirvDisassembler.Enable +import munit.FunSuite + +class SpirvDisassemblerTest extends FunSuite { + + test("SPIR-V disassembly succeeded") { + val shaderCode = SpirvTestUtils.loadShaderFromResources("optimized.spv") + val assembly = SpirvDisassembler.disassembleSpirv(shaderCode, disassembly = Enable(throwOnFail = true)) match { + case None => fail("Failed to disassemble shader.") + case Some(assembly) => assembly + } + + val referenceAssembly = SpirvTestUtils.loadResourceAsString("optimized.spvasm") + + assertEquals(assembly, referenceAssembly) + } +} diff --git a/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvOptimizerTest.scala b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvOptimizerTest.scala new file mode 100644 index 00000000..5557cfe0 --- /dev/null +++ b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvOptimizerTest.scala @@ -0,0 +1,24 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.spirvtools.SpirvDisassembler.Enable +import io.computenode.cyfra.spirvtools.SpirvTool.Param +import munit.FunSuite + +import java.nio.ByteBuffer + +class SpirvOptimizerTest extends FunSuite { + + test("SPIR-V optimization succeeded") { + val shaderCode = SpirvTestUtils.loadShaderFromResources("original.spv") + val optimizedShaderCode = SpirvOptimizer.optimizeSpirv(shaderCode, SpirvOptimizer.Enable(throwOnFail = true, settings = Seq(Param("-O")))) match { + case None => fail("Failed to optimize shader code.") + case Some(optimizedShaderCode) => optimizedShaderCode + } + val optimizedAssembly = SpirvDisassembler.disassembleSpirv(optimizedShaderCode, disassembly = Enable(throwOnFail = true)) + + val referenceOptimizedShaderCode = SpirvTestUtils.loadShaderFromResources("optimized.spv") + val referenceAssembly = SpirvDisassembler.disassembleSpirv(referenceOptimizedShaderCode, disassembly = Enable(throwOnFail = true)) + + assertEquals(optimizedAssembly, referenceAssembly) + } +} diff --git a/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvTestUtils.scala b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvTestUtils.scala new file mode 100644 index 00000000..ccb74760 --- /dev/null +++ b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvTestUtils.scala @@ -0,0 +1,31 @@ +package io.computenode.cyfra.spirvtools + +import java.nio.ByteBuffer +import java.nio.file.{Files, Paths} +import scala.io.Source + +object SpirvTestUtils { + def loadShaderFromResources(path: String): ByteBuffer = { + val resourceUrl = getClass.getClassLoader.getResource(path) + require(resourceUrl != null, s"Resource not found: $path") + val bytes = Files.readAllBytes(Paths.get(resourceUrl.toURI)) + ByteBuffer.wrap(bytes) + } + + def loadResourceAsString(path: String): String = { + val source = Source.fromResource(path) + try source.mkString + finally source.close() + } + + def corruptMagicNumber(original: ByteBuffer): ByteBuffer = { + val corrupted = ByteBuffer.allocate(original.capacity()) + original.rewind() + corrupted.put(original) + corrupted.rewind() + corrupted.put(0, 0.toByte) + + corrupted.rewind() + corrupted + } +} diff --git a/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvToolTest.scala b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvToolTest.scala new file mode 100644 index 00000000..b819aec0 --- /dev/null +++ b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvToolTest.scala @@ -0,0 +1,71 @@ +package io.computenode.cyfra.spirvtools + +import munit.FunSuite + +import java.io.{ByteArrayOutputStream, File} +import java.nio.ByteBuffer +import java.nio.file.Files + +class SpirvToolTest extends FunSuite { + private def isWindows: Boolean = + System.getProperty("os.name").toLowerCase.contains("win") + + class TestSpirvTool(toolName: String) extends SpirvTool(toolName) { + def runExecuteCmd(input: ByteBuffer, cmd: Seq[String]): Either[SpirvToolError, (ByteArrayOutputStream, ByteArrayOutputStream, Int)] = + executeSpirvCmd(input, cmd) + } + + if !isWindows then + test("executeSpirvCmd returns exit code and output streams on valid command") { + val tool = new TestSpirvTool("cat") + + val inputBytes = "hello SPIR-V".getBytes("UTF-8") + val byteBuffer = ByteBuffer.wrap(inputBytes) + + val cmd = Seq("cat") + + val result = tool.runExecuteCmd(byteBuffer, cmd) + assert(result.isRight) + + val (outStream, errStream, exitCode) = result.getOrElse(fail("Execution failed")) + val outputString = outStream.toString("UTF-8") + + assertEquals(exitCode, 0) + assert(outputString == "hello SPIR-V") + assertEquals(errStream.size(), 0) + } + + test("executeSpirvCmd returns non-zero exit code on invalid command") { + val tool = new TestSpirvTool("invalid-cmd") + + val byteBuffer = ByteBuffer.wrap("".getBytes("UTF-8")) + val cmd = Seq("invalid-cmd") + + val result = tool.runExecuteCmd(byteBuffer, cmd) + assert(result.isLeft) + val error = result.left.getOrElse(fail("Should have error")) + assert(error.getMessage.contains("Failed to execute SPIR-V command")) + } + + test("dumpSpvToFile writes ByteBuffer content to file") { + val tmpFile = Files.createTempFile("spirv-dump-test", ".spv") + + val data = "SPIRV binary data".getBytes("UTF-8") + val buffer = ByteBuffer.wrap(data) + + val tmp = SpirvTool.ToFile(tmpFile) + tmp.write(buffer) + + val fileBytes = Files.readAllBytes(tmpFile) + assert(java.util.Arrays.equals(data, fileBytes)) + + assert(buffer.position() == 0) + + Files.deleteIfExists(tmpFile) + } + + test("Param.asStringParam returns correct string") { + val param = SpirvTool.Param("test-value") + assertEquals(param.asStringParam, "test-value") + } +} diff --git a/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvValidatorTest.scala b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvValidatorTest.scala new file mode 100644 index 00000000..df49b5c3 --- /dev/null +++ b/cyfra-spirv-tools/src/test/scala/io/computenode/cyfra/spirvtools/SpirvValidatorTest.scala @@ -0,0 +1,33 @@ +package io.computenode.cyfra.spirvtools + +import io.computenode.cyfra.spirvtools.SpirvValidator.Enable +import munit.FunSuite + +class SpirvValidatorTest extends FunSuite { + + test("SPIR-V validation succeeded") { + val shaderCode = SpirvTestUtils.loadShaderFromResources("optimized.spv") + + try { + SpirvValidator.validateSpirv(shaderCode, validation = Enable(throwOnFail = true)) + assert(true) + } catch { + case e: Throwable => + fail(s"Validation unexpectedly failed: ${e.getMessage}") + } + } + + test("SPIR-V validation fail") { + val shaderCode = SpirvTestUtils.loadShaderFromResources("optimized.spv") + val corruptedShaderCode = SpirvTestUtils.corruptMagicNumber(shaderCode) + + try { + SpirvValidator.validateSpirv(corruptedShaderCode, validation = Enable(throwOnFail = true)) + fail(s"Validation was supposed to fail.") + } catch { + case e: Throwable => + val result = e.getMessage + assertEquals(result, "SPIR-V validation failed with exit code 1.\nValidation errors:\nerror: line 0: Invalid SPIR-V magic number.\n") + } + } +} diff --git a/cyfra-utility/src/main/scala/io/computenode/cyfra/utility/Logger.scala b/cyfra-utility/src/main/scala/io/computenode/cyfra/utility/Logger.scala index 68cae972..296d9882 100644 --- a/cyfra-utility/src/main/scala/io/computenode/cyfra/utility/Logger.scala +++ b/cyfra-utility/src/main/scala/io/computenode/cyfra/utility/Logger.scala @@ -1,6 +1,6 @@ package io.computenode.cyfra.utility -import org.slf4j.LoggerFactory +import org.slf4j.{Logger, LoggerFactory} object Logger: - val logger = LoggerFactory.getLogger("Cyfra") + val logger: Logger = LoggerFactory.getLogger("Cyfra")