Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,21 @@ lazy val vscode = (project in file("cyfra-vscode"))
.settings(commonSettings)
.dependsOn(foton)

lazy val interpreter = (project in file("cyfra-interpreter"))
.settings(commonSettings)
.dependsOn(dsl, compiler)

lazy val fs2interop = (project in file("cyfra-fs2"))
.settings(commonSettings, fs2Settings)
.dependsOn(runtime)

lazy val e2eTest = (project in file("cyfra-e2e-test"))
.settings(commonSettings, runnerSettings)
.dependsOn(runtime, fs2interop)
.dependsOn(runtime, fs2interop, interpreter)

lazy val root = (project in file("."))
.settings(name := "Cyfra")
.aggregate(compiler, dsl, foton, core, runtime, vulkan, examples, fs2interop)
.aggregate(compiler, dsl, foton, core, runtime, vulkan, examples, fs2interop, interpreter)

e2eTest / Test / javaOptions ++= Seq("-Dorg.lwjgl.system.stackSize=1024", "-DuniqueLibraryNames=true")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.computenode.cyfra.e2e.interpreter

import io.computenode.cyfra.interpreter.*, Result.*
import io.computenode.cyfra.dsl.{*, given}
import binding.*, Value.*, gio.GIO, GIO.*
import FromExpr.fromExpr, control.Scope
import izumi.reflect.Tag

class InterpreterE2eTest extends munit.FunSuite:
test("interpret should not stack overflow".ignore):
val fakeContext = SimContext(Map(), Map(), SimData())
val n: Int32 = 0
val pure = Pure(n)
var gio = FlatMap(pure, pure)
for _ <- 0 until 1000000 do gio = FlatMap(pure, gio)
val result = Interpreter.interpret(gio, fakeContext)
println("all good, interpret did not stack overflow!")

test("interpret mixed arithmetic, buffer reads/writes, uniform reads/writes, and when"):
case class SimGBuffer[T <: Value: Tag: FromExpr]() extends GBuffer[T]
val buffer = SimGBuffer[Int32]()
val array = Array[Result](0, 1, 2)

case class SimGUniform[T <: Value: Tag: FromExpr]() extends GUniform[T]
val uniform = SimGUniform[Int32]()
val uniValue = 4

val data = SimData().addBuffer(buffer, array).addUniform(uniform, uniValue)
val startingRecords = Records(0 until 3) // running 3 invocations
val startingSc = SimContext(records = startingRecords, data = data)

val a = ReadUniform(uniform) // 4
val invocId = InvocationId // 0,1,2
val readExpr = ReadBuffer(buffer, fromExpr(invocId)) // 0,1,2

val expr1 = Mul(fromExpr(a), fromExpr(readExpr)) // 4*0 = 0, 4*1 = 4, 4*2 = 8
val expr2 = Sum(fromExpr(a), fromExpr(expr1)) // 4+0 = 4, 4+4 = 8, 4+8 = 12
val expr3 = Mod(fromExpr(expr2), 5) // 4%5 = 4, 8%5 = 3, 12%5 = 2

val cond1 = fromExpr(expr1) <= fromExpr(expr3) // 0 <= 4, 4 <= 3, 8 <= 2
val cond2 = Equal(fromExpr(expr3), fromExpr(readExpr)) // 4 == 0, 3 == 1, 2 == 2

// invoc 0 enters when, invoc2 enters elseWhen, invoc1 enters otherwise
val expr = WhenExpr(
when = cond1, // true false false
thenCode = Scope(expr1), // 0 _ _
otherConds = List(Scope(cond2)), // _ false true
otherCaseCodes = List(Scope(expr2)), // _ _ 12
otherwise = Scope(expr3), // _ 3 _
)

val writeBufGIO = WriteBuffer(buffer, fromExpr(invocId), fromExpr(expr))
val writeUniGIO = WriteUniform(uniform, fromExpr(expr))
val gio = FlatMap(writeBufGIO, writeUniGIO)

val sc = Interpreter.interpret(gio, startingSc)
println(sc) // TODO not sure what/how to test for now.
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package io.computenode.cyfra.e2e.interpreter

import io.computenode.cyfra.interpreter.*, Result.*
import io.computenode.cyfra.dsl.{*, given}, binding.{ReadBuffer, GBuffer}
import Value.FromExpr.fromExpr, control.Scope
import izumi.reflect.Tag

class SimulateE2eTest extends munit.FunSuite:
test("simulate binary operation arithmetic, record cache"):
val startingSc = SimContext(records = Map(0 -> Record())) // running with only 1 invocation

val a: Int32 = 1
val b: Int32 = 2
val c: Int32 = 3
val d: Int32 = 4
val e: Int32 = 5
val f: Int32 = 6
val e1 = Diff(a, b) // -1
val e2 = Sum(fromExpr(e1), c) // 2
val e3 = Mul(f, fromExpr(e2)) // 12
val e4 = Div(fromExpr(e3), d) // 3
val expr = Mod(e, fromExpr(e4)) // 5 % ((6 * ((1 - 2) + 3)) / 4)

val SimContext(results, records, _, _) = Simulate.sim(expr, startingSc)
val expected = 2
assert(results(0) == expected, s"Expected $expected, got $results")

// records cache should have kept track of intermediate expression results correctly
val exp = Map(
a.treeid -> 1,
b.treeid -> 2,
c.treeid -> 3,
d.treeid -> 4,
e.treeid -> 5,
f.treeid -> 6,
e1.treeid -> -1,
e2.treeid -> 2,
e3.treeid -> 12,
e4.treeid -> 3,
expr.treeid -> 2,
)
val res = records(0).cache
assert(res == exp, s"Expected $exp, got $res")

test("simulate Vec4, scalar, dot, extract scalar"):
val startingSc = SimContext(records = Map(0 -> Record())) // running with only 1 invocation

val v1 = ComposeVec4[Float32](1f, 2f, 3f, 4f)
val sc1 = Simulate.sim(v1, startingSc)
val exp1 = Vector(1f, 2f, 3f, 4f)
val res1 = sc1.results(0)
assert(res1 == exp1, s"Expected $exp1, got $res1")

val i: Int32 = 2
val expr = ExtractScalar(fromExpr(v1), i)
val sc2 = Simulate.sim(expr, sc1)
val exp2 = 3f
val res2 = sc2.results(0)
assert(res2 == exp2, s"Expected $exp2, got $res2")

val v2 = ScalarProd(fromExpr(v1), -1f)
val sc3 = Simulate.sim(v2, sc2)
val exp3 = Vector(-1f, -2f, -3f, -4f)
val res3 = sc3.results(0)
assert(res3 == exp3, s"Expected $exp3, got $res3")

val v3 = ComposeVec4[Float32](-4f, -3f, 2f, 1f)
val dot = DotProd(fromExpr(v1), fromExpr(v3))
val SimContext(results, _, _, _) = Simulate.sim(dot, sc3)
val exp4 = 0f
val res4 = results(0).asInstanceOf[Float]
assert(Math.abs(res4 - exp4) < 0.001f, s"Expected $exp4, got $res4")

test("simulate bitwise ops"):
val startingSc = SimContext(records = Map(0 -> Record())) // running with only 1 invocation

val a: Int32 = 5
val by: UInt32 = 3
val aNot = BitwiseNot(a)
val left = ShiftLeft(fromExpr(aNot), by)
val right = ShiftRight(fromExpr(aNot), by)
val and = BitwiseAnd(fromExpr(left), fromExpr(right))
val or = BitwiseOr(fromExpr(left), fromExpr(right))
val xor = BitwiseXor(fromExpr(and), fromExpr(or))

val SimContext(res, _, _, _) = Simulate.sim(xor, startingSc)
val exp = ((~5 << 3) & (~5 >> 3)) ^ ((~5 << 3) | (~5 >> 3))
assert(res(0) == exp, s"Expected $exp, got ${res(0)}")

test("simulate should not stack overflow"):
val startingSc = SimContext(records = Map(0 -> Record())) // running with only 1 invocation

val a: Int32 = 1
var sum = Sum(a, a) // 2
for _ <- 0 until 1000000 do sum = Sum(a, fromExpr(sum))
val SimContext(res, _, _, _) = Simulate.sim(sum, startingSc)
val exp = 1000002
assert(res(0) == exp, s"Expected $exp, got ${res(0)}")

test("simulate ReadBuffer"):
// We fake a GBuffer with an array
case class SimGBuffer[T <: Value: Tag: FromExpr]() extends GBuffer[T]
val buffer = SimGBuffer[Int32]()
val array = (0 until 1024).toArray[Result]

val data = SimData().addBuffer(buffer, array)
val startingSc = SimContext(records = Map(0 -> Record()), data = data) // running with only 1 invocation

val expr = ReadBuffer(buffer, 128)
val SimContext(res, records, _, _) = Simulate.sim(expr, startingSc)
val exp = 128
assert(res(0) == exp, s"Expected $exp, got $res")

// the records should keep track of the read
val read = ReadBuf(expr.treeid, buffer, 128, 128) // 128 has treeid 0, so expr has treeid 1
assert(records(0).reads.contains(read), "missing read")
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.computenode.cyfra.e2e.interpreter

import io.computenode.cyfra.interpreter.*, Result.*
import io.computenode.cyfra.dsl.{*, given}
import Value.FromExpr.fromExpr, control.Scope, binding.{GBuffer, ReadBuffer}
import izumi.reflect.Tag

class SimulateWhenE2eTest extends munit.FunSuite:
test("simulate when"):
val startingSc = SimContext(records = Map(0 -> Record())) // running with only 1 invocation

val expr = WhenExpr(
when = 2 >= 1, // true
thenCode = Scope(ConstInt32(1)),
otherConds = List(Scope(ConstGB(3 == 2)), Scope(ConstGB(1 <= 3))),
otherCaseCodes = List(Scope(ConstInt32(2)), Scope(ConstInt32(4))),
otherwise = Scope(ConstInt32(3)),
)
val SimContext(res, _, _, _) = Simulate.sim(expr, startingSc)
val exp = 1
assert(res(0) == exp, s"Expected $exp, got ${res(0)}")

test("simulate elseWhen first"):
val startingSc = SimContext(records = Map(0 -> Record())) // running with only 1 invocation

val expr = WhenExpr(
when = 2 <= 1, // false
thenCode = Scope(ConstInt32(1)),
otherConds = List(Scope(ConstGB(3 >= 2)) /*true*/, Scope(ConstGB(1 <= 3))),
otherCaseCodes = List(Scope(ConstInt32(2)), Scope(ConstInt32(4))),
otherwise = Scope(ConstInt32(3)),
)
val SimContext(res, _, _, _) = Simulate.sim(expr, startingSc)
val exp = 2
assert(res(0) == exp, s"Expected $exp, got ${res(0)}")

test("simulate elseWhen second"):
val startingSc = SimContext(records = Map(0 -> Record())) // running with only 1 invocation

val expr = WhenExpr(
when = 2 <= 1, // false
thenCode = Scope(ConstInt32(1)),
otherConds = List(Scope(ConstGB(3 == 2)) /*false*/, Scope(ConstGB(1 <= 3))), // true
otherCaseCodes = List(Scope(ConstInt32(2)), Scope(ConstInt32(4))),
otherwise = Scope(ConstInt32(3)),
)
val SimContext(res, _, _, _) = Simulate.sim(expr, startingSc)
val exp = 4
assert(res(0) == exp, s"Expected $exp, got $res")

test("simulate otherwise"):
val startingSc = SimContext(records = Map(0 -> Record())) // running with only 1 invocation

val expr = WhenExpr(
when = 2 <= 1, // false
thenCode = Scope(ConstInt32(1)),
otherConds = List(Scope(ConstGB(3 == 2)) /*false*/, Scope(ConstGB(1 >= 3))), // false
otherCaseCodes = List(Scope(ConstInt32(2)), Scope(ConstInt32(4))),
otherwise = Scope(ConstInt32(3)),
)
val SimContext(res, _, _, _) = Simulate.sim(expr, startingSc)
val exp = 3
assert(res(0) == exp, s"Expected $exp, got $res")

test("simulate mixed arithmetic, buffer reads and when"):
case class SimGBuffer[T <: Value: Tag: FromExpr]() extends GBuffer[T]
val buffer = SimGBuffer[Int32]()
val array = (0 until 3).toArray[Result]

val data = SimData().addBuffer(buffer, array)
val startingRecords = Map(0 -> Record(), 1 -> Record(), 2 -> Record()) // running 3 invocations
val startingSc = SimContext(records = startingRecords, data = data)

val a: Int32 = 4
val invocId = InvocationId
val readExpr = ReadBuffer(buffer, fromExpr(invocId)) // 0,1,2

val expr1 = Mul(a, fromExpr(readExpr)) // 4*0 = 0, 4*1 = 4, 4*2 = 8
val expr2 = Sum(a, fromExpr(expr1)) // 4+0 = 4, 4+4 = 8, 4+8 = 12
val expr3 = Mod(fromExpr(expr2), 5) // 4%5 = 4, 8%5 = 3, 12%5 = 2

val cond1 = fromExpr(expr1) <= fromExpr(expr3)
val cond2 = Equal(fromExpr(expr3), fromExpr(readExpr))

// invoc 0 enters when, invoc2 enters elseWhen, invoc1 enters otherwise
val expr = WhenExpr(
when = cond1, // true false false
thenCode = Scope(expr1), // 0 _ _
otherConds = List(Scope(cond2)), // _ false true
otherCaseCodes = List(Scope(expr2)), // _ _ 12
otherwise = Scope(expr3), // _ 3 _
)
val SimContext(res, _, _, _) = Simulate.sim(expr, startingSc)
val exp = Map(0 -> 0, 1 -> 3, 2 -> 12)
assert(res == exp, s"Expected $exp, got $res")
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.computenode.cyfra.interpreter

import io.computenode.cyfra.dsl.{*, given}
import binding.*, Value.*, gio.GIO, GIO.*
import izumi.reflect.Tag

object Interpreter:
private def interpretPure(gio: Pure[?], sc: SimContext): SimContext = gio match
// TODO needs fixing, throws ClassCastException, Pure[T] should be Pure[T <: Value]
case Pure(value) => Simulate.sim(value.asInstanceOf[Value], sc) // no writes here

private def interpretWriteBuffer(gio: WriteBuffer[?], sc: SimContext): SimContext = gio match
case WriteBuffer(buffer, index, value) =>
val indexSc = Simulate.sim(index, sc) // get the write index for each invocation
val SimContext(writeVals, records, data, profs) = Simulate.sim(value, indexSc) // get the values to be written

// write the values to the buffer, update records with writes
val indices = indexSc.results
val newData = data.writeToBuffer(buffer, indices, writeVals)
val writes = indices.map: (invocId, ind) =>
invocId -> WriteBuf(buffer, ind.asInstanceOf[Int], writeVals(invocId))
val newRecords = records.addWrites(writes)

// check if the write addresses coalesced or not
val addresses = indices.values.toSeq.map(_.asInstanceOf[Int])
val profile = WriteProfile(buffer, addresses)
val coalesceProfile = CoalesceProfile(addresses, profile)

SimContext(writeVals, newRecords, newData, coalesceProfile :: profs)

private def interpretWriteUniform(gio: WriteUniform[?], sc: SimContext): SimContext = gio match
case WriteUniform(uniform, value) =>
// get the uniform value to be written (same for all invocations)
val SimContext(writeVals, records, data, profs) = Simulate.sim(value, sc)

// write the (single) value to the uniform, update records with writes
val uniVal = writeVals.values.head
val writes = writeVals.map((invocId, res) => invocId -> WriteUni(uniform, res))
val newData = data.write(WriteUni(uniform, uniVal))
val newRecords = records.addWrites(writes)

SimContext(writeVals, newRecords, newData, profs)

private def interpretOne(gio: GIO[?], sc: SimContext): SimContext = gio match
case p: Pure[?] => interpretPure(p, sc)
case wb: WriteBuffer[?] => interpretWriteBuffer(wb, sc)
case wu: WriteUniform[?] => interpretWriteUniform(wu, sc)
case _ => throw IllegalArgumentException("interpretOne: invalid GIO")

@annotation.tailrec
private def interpretMany(gios: List[GIO[?]], sc: SimContext): SimContext = gios match
case FlatMap(gio, next) :: tail => interpretMany(gio :: next :: tail, sc)
case Repeat(n, f) :: tail =>
// does the value of n vary by invocation?
// can different invocations run different numbers of GIOs?
val newSc = Simulate.sim(n, sc)
val repeat = newSc.results.values.head.asInstanceOf[Int]
val newGios = (0 until repeat).map(i => f).toList
interpretMany(newGios ::: tail, newSc)
case head :: tail => interpretMany(tail, interpretOne(head, sc))
case Nil => sc

def interpret(gio: GIO[?], sc: SimContext): SimContext = interpretMany(List(gio), sc)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.computenode.cyfra.interpreter

import io.computenode.cyfra.dsl.{*, given}
import binding.{GBuffer, GUniform}

enum Read:
case ReadBuf(id: Int, buffer: GBuffer[?], index: Int, value: Result)
case ReadUni(id: Int, uniform: GUniform[?], value: Result)
export Read.*

enum Write:
case WriteBuf(buffer: GBuffer[?], index: Int, value: Result)
case WriteUni(uni: GUniform[?], value: Result)
export Write.*

enum Profile:
case ReadProfile(treeid: TreeId, addresses: Seq[Int])
case WriteProfile(buffer: GBuffer[?], addresses: Seq[Int])
export Profile.*

enum CoalesceProfile:
case RaceCondition(profile: Profile)
case Coalesced(startAddress: Int, endAddress: Int, profile: Profile)
case NotCoalesced(profile: Profile)
import CoalesceProfile.*

object CoalesceProfile:
def apply(addresses: Seq[Int], profile: Profile): CoalesceProfile =
val length = addresses.length
val distinct = addresses.distinct.length == length
if length == 0 then NotCoalesced(profile)
else if !distinct then RaceCondition(profile)
else
val (start, end) = (addresses.min, addresses.max)
val coalesced = end - start + 1 == length
if coalesced then Coalesced(start, end, profile)
else NotCoalesced(profile)
Loading