changeset 1128:ae54dbb08e53

merge into git mirror
author sam <sam@basx.dev>
date Thu, 23 May 2024 01:44:10 +0700
parents 073ce95ae5c7 (current diff) 0811ddd7326d (diff)
children 7f2c477ae1f4
files .gitignore
diffstat 22 files changed, 622 insertions(+), 561 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgtags	Thu May 23 01:44:10 2024 +0700
@@ -0,0 +1,1 @@
+2ba3f18e7cad4339c3404d2c49580bde83b285f6 hg
--- a/examples/E01_hello_triangle.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/examples/E01_hello_triangle.nim	Thu May 23 01:44:10 2024 +0700
@@ -5,6 +5,7 @@
 # shader setup
 const
   shaderConfiguration = createShaderConfiguration(
+    name = "default shader",
     inputs = [
       attr[Vec3f]("position"),
       attr[Vec4f]("color"),
@@ -29,7 +30,7 @@
 myengine.initRenderer({VERTEX_COLORED_MATERIAL: shaderConfiguration}, inFlightFrames = 2)
 myengine.loadScene(scene)
 
-while myengine.updateInputs() == Running and not myengine.keyWasPressed(Escape):
+while myengine.UpdateInputs() and not KeyWasPressed(Escape):
   transform[Vec3f](scene.meshes[0][], "position", scale(1.001, 1.001))
   myengine.renderScene(scene)
 
--- a/examples/E02_squares.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/examples/E02_squares.nim	Thu May 23 01:44:10 2024 +0700
@@ -2,7 +2,7 @@
 import std/tables
 import std/random
 
-import ../src/semicongine
+import ../semicongine
 
 
 when isMainModule:
@@ -44,15 +44,16 @@
 
   const
     shaderConfiguration = createShaderConfiguration(
-      inputs=[
+      name = "default shader",
+      inputs = [
         attr[Vec3f]("position"),
-        attr[Vec4f]("color", memoryPerformanceHint=PreferFastWrite),
+        attr[Vec4f]("color", memoryPerformanceHint = PreferFastWrite),
         attr[uint32]("index"),
       ],
-      intermediates=[attr[Vec4f]("outcolor")],
-      uniforms=[attr[float32]("time")],
-      outputs=[attr[Vec4f]("color")],
-      vertexCode="""
+      intermediates = [attr[Vec4f]("outcolor")],
+      uniforms = [attr[float32]("time")],
+      outputs = [attr[Vec4f]("color")],
+      vertexCode = """
 float pos_weight = index / 100.0; // add some gamma correction?
 float t = sin(Uniforms.time * 0.5) * 0.5 + 0.5;
 float v = min(1, max(0, pow(pos_weight - t, 2)));
@@ -60,23 +61,28 @@
 outcolor = vec4(color.r, color.g, v * 0.5, 1);
 gl_Position = vec4(position, 1.0);
 """,
-      fragmentCode="color = outcolor;",
+      fragmentCode = "color = outcolor;",
     )
+  let matDef = MaterialType(name: "default", vertexAttributes: {
+    "position": Vec3F32,
+    "color": Vec4F32,
+    "index": UInt32,
+  }.toTable)
   var squaremesh = newMesh(
-    positions=vertices,
-    indices=indices,
-    colors=colors,
-    material=Material(name: "default")
+    positions = vertices,
+    indices = indices,
+    colors = colors,
   )
   squaremesh[].initVertexAttribute("index", iValues.toSeq)
+  squaremesh.material = matDef.initMaterialData(name = "default")
 
   var myengine = initEngine("Squares")
-  myengine.initRenderer({"default": shaderConfiguration}.toTable)
+  myengine.initRenderer({matDef: shaderConfiguration})
 
   var scene = Scene(name: "scene", meshes: @[squaremesh])
   scene.addShaderGlobal("time", 0.0'f32)
-  myengine.addScene(scene)
-  while myengine.updateInputs() == Running and not myengine.keyWasPressed(Escape):
+  myengine.loadScene(scene)
+  while myengine.UpdateInputs() and not KeyWasPressed(Escape):
     scene.setShaderGlobal("time", getShaderGlobal[float32](scene, "time") + 0.0005'f)
     myengine.renderScene(scene)
 
--- a/examples/E03_hello_cube.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/examples/E03_hello_cube.nim	Thu May 23 01:44:10 2024 +0700
@@ -1,17 +1,7 @@
-#
-#   TODO: Needs Depth-Buffer first!
-#
-#
-#
-#
-#
-#
-
-
 import std/tables
 import std/times
 
-import ../src/semicongine
+import ../semicongine
 
 const
   TopLeftFront = newVec3f(-0.5'f32, -0.5'f32, -0.5'f32)
@@ -24,11 +14,11 @@
   BottomLeftBack = newVec3f(0.5'f32, 0.5'f32, 0.5'f32)
 const
   cube_pos = @[
-    TopLeftFront, TopRightFront, BottomRightFront, BottomLeftFront, # front
-    TopLeftBack, TopRightBack, BottomRightBack, BottomLeftBack, # back
-    TopLeftBack, TopLeftFront, BottomLeftFront, BottomLeftBack, # left
-    TopRightBack, TopRightFront, BottomRightFront, BottomRightBack, # right
-    TopLeftBack, TopRightBack, TopRightFront, TopLeftFront, # top
+    TopLeftFront, TopRightFront, BottomRightFront, BottomLeftFront,     # front
+    TopLeftBack, TopRightBack, BottomRightBack, BottomLeftBack,         # back
+    TopLeftBack, TopLeftFront, BottomLeftFront, BottomLeftBack,         # left
+    TopRightBack, TopRightFront, BottomRightFront, BottomRightBack,     # right
+    TopLeftBack, TopRightBack, TopRightFront, TopLeftFront,             # top
     BottomLeftFront, BottomRightFront, BottomRightBack, BottomLeftBack, # bottom
   ]
   R = newVec4f(1, 0, 0, 1)
@@ -54,34 +44,36 @@
 
   const
     shaderConfiguration = createShaderConfiguration(
-      inputs=[
+      name = "default shader",
+      inputs = [
         attr[Vec3f]("position"),
-        attr[Vec4f]("color", memoryPerformanceHint=PreferFastWrite),
+        attr[Vec4f]("color", memoryPerformanceHint = PreferFastWrite),
       ],
-      intermediates=[attr[Vec4f]("outcolor")],
-      uniforms=[
+      intermediates = [attr[Vec4f]("outcolor")],
+      uniforms = [
         attr[Mat4]("projection"),
         attr[Mat4]("view"),
         attr[Mat4]("model"),
       ],
-      outputs=[attr[Vec4f]("color")],
-      vertexCode="""outcolor = color; gl_Position = (Uniforms.projection * Uniforms.view * Uniforms.model) * vec4(position, 1);""",
-      fragmentCode="color = outcolor;",
+      outputs = [attr[Vec4f]("color")],
+      vertexCode = """outcolor = color; gl_Position = (Uniforms.projection * Uniforms.view * Uniforms.model) * vec4(position, 1);""",
+      fragmentCode = "color = outcolor;",
     )
-  myengine.initRenderer({"default": shaderConfiguration}.toTable)
-  var cube = Scene(name: "scene", meshes: @[newMesh(positions=cube_pos, indices=tris, colors=cube_color, material=Material(name: "default"))])
+  var matDef = MaterialType(name: "default material", vertexAttributes: {"position": Vec3F32, "color": Vec4F32}.toTable)
+  var cube = Scene(name: "scene", meshes: @[newMesh(positions = cube_pos, indices = tris, colors = cube_color, material = matDef.initMaterialData(name = "default"))])
   cube.addShaderGlobal("projection", Unit4f32)
   cube.addShaderGlobal("view", Unit4f32)
   cube.addShaderGlobal("model", Unit4f32)
-  myengine.addScene(cube)
+  myengine.initRenderer({matDef: shaderConfiguration})
+  myengine.loadScene(cube)
 
   var t: float32 = cpuTime()
-  while myengine.updateInputs() == Running and not myengine.keyWasPressed(Escape):
+  while myengine.UpdateInputs() and not KeyWasPressed(Escape):
     setShaderGlobal(cube, "model", translate(0'f32, 0'f32, 10'f32) * rotate(t, Yf32))
     setShaderGlobal(cube, "projection",
       perspective(
         float32(PI / 4),
-        float32(myengine.getWindow().size[0]) / float32(myengine.getWindow().size[0]),
+        float32(myengine.GetWindow().size[0]) / float32(myengine.GetWindow().size[1]),
         0.1'f32,
         100'f32
       )
--- a/examples/E04_input.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/examples/E04_input.nim	Thu May 23 01:44:10 2024 +0700
@@ -3,7 +3,7 @@
 import std/typetraits
 import std/math
 
-import ../src/semicongine
+import ../semicongine
 
 const
   arrow = @[
@@ -108,33 +108,36 @@
 
   # define mesh objects
   var
-    material = Material(name: "default")
+    matDef = MaterialType(name: "default", vertexAttributes: {
+      "position": Vec3F32,
+      "color": Vec4F32,
+    }.toTable)
     cursormesh = newMesh(
-      positions=positions,
-      colors=arrow_colors,
-      material=material,
+      positions = positions,
+      colors = arrow_colors,
+      material = matDef.initMaterialData(),
     )
     keyboardmesh = newMesh(
-      positions=keyvertexpos,
-      colors=keyvertexcolor,
-      indices=keymeshindices,
-      material=material,
+      positions = keyvertexpos,
+      colors = keyvertexcolor,
+      indices = keymeshindices,
+      material = matDef.initMaterialData(),
     )
     backgroundmesh = newMesh(
-      positions= @[
+      positions = @[
         newVec3f(0'f32, 0'f32),
         newVec3f(1'f32, 0'f32),
         newVec3f(1'f32, 1'f32),
         newVec3f(0'f32, 1'f32),
       ],
-      colors= @[
+      colors = @[
         backgroundColor,
         backgroundColor,
         backgroundColor,
         backgroundColor,
       ],
-      indices= @[[0'u16, 1'u16, 2'u16], [2'u16, 3'u16, 0'u16]],
-      material=material,
+      indices = @[[0'u16, 1'u16, 2'u16], [2'u16, 3'u16, 0'u16]],
+      material = matDef.initMaterialData(),
     )
 
   # define mesh objects
@@ -148,51 +151,52 @@
   # shaders
   const
     shaderConfiguration = createShaderConfiguration(
-      inputs=[
+      name = "default shader",
+      inputs = [
         attr[Vec3f]("position"),
-        attr[Vec4f]("color", memoryPerformanceHint=PreferFastWrite),
-        attr[Mat4]("transform", memoryPerformanceHint=PreferFastWrite, perInstance=true),
+        attr[Vec4f]("color", memoryPerformanceHint = PreferFastWrite),
+        attr[Mat4]("transform", memoryPerformanceHint = PreferFastWrite, perInstance = true),
       ],
-      intermediates=[attr[Vec4f]("outcolor")],
-      uniforms=[attr[Mat4]("projection")],
-      outputs=[attr[Vec4f]("color")],
-      vertexCode="""outcolor = color; gl_Position = vec4(position, 1) * (transform * Uniforms.projection);""",
-      fragmentCode="color = outcolor;",
+      intermediates = [attr[Vec4f]("outcolor")],
+      uniforms = [attr[Mat4]("projection")],
+      outputs = [attr[Vec4f]("color")],
+      vertexCode = """outcolor = color; gl_Position = vec4(position, 1) * (transform * Uniforms.projection);""",
+      fragmentCode = "color = outcolor;",
     )
 
   # set up rendering
-  myengine.initRenderer({"default": shaderConfiguration}.toTable)
+  myengine.initRenderer({matDef: shaderConfiguration})
   scene.addShaderGlobal("projection", Unit4f32)
-  myengine.addScene(scene)
-  myengine.hideSystemCursor()
+  myengine.loadScene(scene)
+  myengine.HideSystemCursor()
 
   # mainloop
-  while myengine.updateInputs() == Running:
-    if myengine.windowWasResized():
+  while myengine.UpdateInputs():
+    if WindowWasResized():
       scene.setShaderGlobal("projection",
         ortho(
-          0, float32(myengine.getWindow().size[0]),
-          0, float32(myengine.getWindow().size[1]),
+          0, float32(myengine.GetWindow().size[0]),
+          0, float32(myengine.GetWindow().size[1]),
           0, 1,
         )
       )
       let
-        winsize = myengine.getWindow().size
+        winsize = myengine.GetWindow().size
         center = translate(float32(winsize[0]) / 2'f32, float32(winsize[1]) / 2'f32, 0.1'f32)
       keyboardmesh.transform = keyboard_center * center
       backgroundmesh.transform = scale(float32(winsize[0]), float32(winsize[1]), 1'f32)
 
-    let mousePos = translate(myengine.mousePosition().x + 20, myengine.mousePosition().y + 20, 0'f32)
+    let mousePos = translate(MousePosition().x + 20, MousePosition().y + 20, 0'f32)
     cursormesh.transform = mousePos
 
     for (index, key) in enumerate(keyIndices):
-      if myengine.keyWasPressed(key):
+      if KeyWasPressed(key):
         let baseIndex = index * 4
         keyboardmesh["color", baseIndex + 0] = activeColor
         keyboardmesh["color", baseIndex + 1] = activeColor
         keyboardmesh["color", baseIndex + 2] = activeColor
         keyboardmesh["color", baseIndex + 3] = activeColor
-      if myengine.keyWasReleased(key):
+      if KeyWasReleased(key):
         let baseIndex = index * 4
         keyboardmesh["color", baseIndex + 0] = baseColor
         keyboardmesh["color", baseIndex + 1] = baseColor
--- a/examples/E10_pong.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/examples/E10_pong.nim	Thu May 23 01:44:10 2024 +0700
@@ -1,16 +1,19 @@
 import std/times
 import std/tables
 
-import ../src/semicongine
+import ../semicongine
 
 let
-  barcolor = hexToColorAlpha("5A3F00").gamma(2.2).colorToHex()
+  barcolor = toRGBA("5A3F00").toSRGB().colorToHex()
   barSize = 0.1'f
   barWidth = 0.01'f
-  ballcolor = hexToColorAlpha("B17F08").gamma(2.2).colorToHex()
+  ballcolor = toRGBA("B17F08").toSRGB().colorToHex()
   ballSize = 0.01'f
   ballSpeed = 60'f
-  material = Material(name: "default")
+  matDef = MaterialType(name: "default", vertexAttributes: {
+    "position": Vec3F32,
+    "color": Vec4F32,
+  }.toTable)
 
 var
   level: Scene
@@ -19,59 +22,60 @@
 when isMainModule:
   var myengine = initEngine("Pong")
 
-  var player = rect(color=barcolor, width=barWidth, height=barSize)
-  player.material = material
-  var ball = circle(color=ballcolor)
-  ball.material = material
+  var player = rect(color = barcolor, width = barWidth, height = barSize)
+  player.material = matDef.initMaterialData(name = "player material")
+  var ball = circle(color = ballcolor)
+  ball.material = matDef.initMaterialData(name = "player material")
   level = Scene(name: "scene", meshes: @[ball, player])
 
   const
     shaderConfiguration = createShaderConfiguration(
-      inputs=[
+      name = "default shader",
+      inputs = [
         attr[Vec3f]("position"),
-        attr[Vec4f]("color", memoryPerformanceHint=PreferFastWrite),
-        attr[Mat4]("transform", memoryPerformanceHint=PreferFastWrite, perInstance=true),
+        attr[Vec4f]("color", memoryPerformanceHint = PreferFastWrite),
+        attr[Mat4]("transform", memoryPerformanceHint = PreferFastWrite, perInstance = true),
       ],
-      intermediates=[attr[Vec4f]("outcolor")],
-      uniforms=[attr[Mat4]("projection")],
-      outputs=[attr[Vec4f]("color")],
-      vertexCode="""outcolor = color; gl_Position = vec4(position, 1) * (transform * Uniforms.projection);""",
-      fragmentCode="color = outcolor;",
+      intermediates = [attr[Vec4f]("outcolor")],
+      uniforms = [attr[Mat4]("projection")],
+      outputs = [attr[Vec4f]("color")],
+      vertexCode = """outcolor = color; gl_Position = vec4(position, 1) * (transform * Uniforms.projection);""",
+      fragmentCode = "color = outcolor;",
     )
 
   # set up rendering
-  myengine.initRenderer({"default": shaderConfiguration}.toTable)
+  myengine.initRenderer({matDef: shaderConfiguration})
   level.addShaderGlobal("projection", Unit4f32)
-  myengine.addScene(level)
+  myengine.loadScene(level)
 
   var
-    winsize = myengine.getWindow().size
+    winsize = myengine.GetWindow().size
     height = float32(winsize[1]) / float32(winsize[0])
     width = 1'f
     currentTime = cpuTime()
     showSystemCursor = true
     fullscreen = false
-  while myengine.updateInputs() == Running and not myengine.keyIsDown(Escape):
-    if myengine.keyWasPressed(C):
+  while myengine.UpdateInputs() and not KeyIsDown(Escape):
+    if KeyWasPressed(C):
       if showSystemCursor:
-        myengine.hideSystemCursor()
+        myengine.HideSystemCursor()
       else:
-        myengine.showSystemCursor()
+        myengine.ShowSystemCursor()
       showSystemCursor = not showSystemCursor
-    if myengine.keyWasPressed(F):
+    if KeyWasPressed(F):
       fullscreen = not fullscreen
-      myengine.fullscreen(fullscreen)
+      myengine.Fullscreen = fullscreen
 
     let dt: float32 = cpuTime() - currentTime
     currentTime = cpuTime()
-    if myengine.windowWasResized():
-      winsize = myengine.getWindow().size
+    if WindowWasResized():
+      winsize = myengine.GetWindow().size
       height = float32(winsize[1]) / float32(winsize[0])
       width = 1'f
       setShaderGlobal(level, "projection", ortho(0, width, 0, height, 0, 1))
-    if myengine.keyIsDown(Down) and (player.transform.col(3).y + barSize/2) < height:
+    if KeyIsDown(Down) and (player.transform.col(3).y + barSize/2) < height:
       player.transform = player.transform * translate(0'f, 1'f * dt, 0'f)
-    if myengine.keyIsDown(Up) and (player.transform.col(3).y - barSize/2) > 0:
+    if KeyIsDown(Up) and (player.transform.col(3).y - barSize/2) > 0:
       player.transform = player.transform * translate(0'f, -1'f * dt, 0'f)
 
     # bounce level
--- a/semicongine.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/semicongine.nim	Thu May 23 01:44:10 2024 +0700
@@ -10,6 +10,7 @@
 import semicongine/collision
 import semicongine/scene
 import semicongine/events
+import semicongine/input
 import semicongine/material
 import semicongine/mesh
 import semicongine/noise
@@ -29,6 +30,7 @@
 export collision
 export scene
 export events
+export input
 export material
 export mesh
 export noise
--- a/semicongine/audio.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/semicongine/audio.nim	Thu May 23 01:44:10 2024 +0700
@@ -1,4 +1,5 @@
 import std/monotimes
+import std/strformat
 import std/times
 import std/tables
 import std/locks
@@ -43,7 +44,7 @@
     currentBuffer: int
     lastUpdate: MonoTime
 
-proc initMixer*(): Mixer =
+proc initMixer(): Mixer =
   result = Mixer(
     tracks: {"": Track(level: 1'f)}.toTable,
     level: 1'f,
@@ -59,25 +60,25 @@
     bufferaddresses.add (addr mixer.buffers[i])
   mixer.device = openSoundDevice(AUDIO_SAMPLE_RATE, bufferaddresses)
 
-proc loadSound*(mixer: var Mixer, name: string, resource: string) =
+proc LoadSound*(mixer: var Mixer, name: string, resource: string) =
   assert not (name in mixer.sounds)
   mixer.sounds[name] = loadAudio(resource)
 
-proc addSound*(mixer: var Mixer, name: string, sound: Sound) =
+proc AddSound*(mixer: var Mixer, name: string, sound: Sound) =
   assert not (name in mixer.sounds)
   mixer.sounds[name] = sound
 
-proc replaceSound*(mixer: var Mixer, name: string, sound: Sound) =
+proc ReplaceSound*(mixer: var Mixer, name: string, sound: Sound) =
   assert (name in mixer.sounds)
   mixer.sounds[name] = sound
 
-proc addTrack*(mixer: var Mixer, name: string, level: Level = 1'f) =
+proc AddTrack*(mixer: var Mixer, name: string, level: Level = 1'f) =
   assert not (name in mixer.tracks)
   mixer.lock.withLock():
     mixer.tracks[name] = Track(level: level)
 
-proc play*(mixer: var Mixer, soundName: string, track = "", stopOtherSounds = false, loop = false, levelLeft, levelRight: Level): uint64 =
-  assert track in mixer.tracks
+proc Play*(mixer: var Mixer, soundName: string, track = "", stopOtherSounds = false, loop = false, levelLeft, levelRight: Level): uint64 =
+  assert track in mixer.tracks, &"Track '{track}' does not exists"
   assert soundName in mixer.sounds, soundName & " not loaded"
   mixer.lock.withLock():
     if stopOtherSounds:
@@ -93,8 +94,8 @@
   result = mixer.playbackCounter
   inc mixer.playbackCounter
 
-proc play*(mixer: var Mixer, soundName: string, track = "", stopOtherSounds = false, loop = false, level: Level = 1'f): uint64 =
-  play(
+proc Play*(mixer: var Mixer, soundName: string, track = "", stopOtherSounds = false, loop = false, level: Level = 1'f): uint64 =
+  Play(
     mixer = mixer,
     soundName = soundName,
     track = track,
@@ -104,73 +105,73 @@
     levelRight = level
   )
 
-proc stop*(mixer: var Mixer) =
+proc Stop*(mixer: var Mixer) =
   mixer.lock.withLock():
     for track in mixer.tracks.mvalues:
       track.playing.clear()
 
-proc getLevel*(mixer: var Mixer): Level = mixer.level
-proc getLevel*(mixer: var Mixer, track: string): Level = mixer.tracks[track].level
-proc getLevel*(mixer: var Mixer, playbackId: uint64): (Level, Level) =
+proc GetLevel*(mixer: var Mixer): Level = mixer.level
+proc GetLevel*(mixer: var Mixer, track: string): Level = mixer.tracks[track].level
+proc GetLevel*(mixer: var Mixer, playbackId: uint64): (Level, Level) =
   for track in mixer.tracks.mvalues:
     if playbackId in track.playing:
       return (track.playing[playbackId].levelLeft, track.playing[playbackId].levelRight)
 
-proc setLevel*(mixer: var Mixer, level: Level) = mixer.level = level
-proc setLevel*(mixer: var Mixer, track: string, level: Level) =
+proc SetLevel*(mixer: var Mixer, level: Level) = mixer.level = level
+proc SetLevel*(mixer: var Mixer, track: string, level: Level) =
   mixer.lock.withLock():
     mixer.tracks[track].level = level
-proc setLevel*(mixer: var Mixer, playbackId: uint64, levelLeft, levelRight: Level) =
+proc SetLevel*(mixer: var Mixer, playbackId: uint64, levelLeft, levelRight: Level) =
   mixer.lock.withLock():
     for track in mixer.tracks.mvalues:
       if playbackId in track.playing:
         track.playing[playbackId].levelLeft = levelLeft
         track.playing[playbackId].levelRight = levelRight
-proc setLevel*(mixer: var Mixer, playbackId: uint64, level: Level) =
-  setLevel(mixer, playbackId, level, level)
+proc SetLevel*(mixer: var Mixer, playbackId: uint64, level: Level) =
+  SetLevel(mixer, playbackId, level, level)
 
-proc stop*(mixer: var Mixer, track: string) =
+proc Stop*(mixer: var Mixer, track: string) =
   assert track in mixer.tracks
   mixer.lock.withLock():
     mixer.tracks[track].playing.clear()
 
-proc stop*(mixer: var Mixer, playbackId: uint64) =
+proc Stop*(mixer: var Mixer, playbackId: uint64) =
   mixer.lock.withLock():
     for track in mixer.tracks.mvalues:
       if playbackId in track.playing:
         track.playing.del(playbackId)
         break
 
-proc pause*(mixer: var Mixer, value: bool) =
+proc Pause*(mixer: var Mixer, value: bool) =
   mixer.lock.withLock():
     for track in mixer.tracks.mvalues:
       for playback in track.playing.mvalues:
         playback.paused = value
 
-proc pause*(mixer: var Mixer, track: string, value: bool) =
+proc Pause*(mixer: var Mixer, track: string, value: bool) =
   mixer.lock.withLock():
     for playback in mixer.tracks[track].playing.mvalues:
       playback.paused = value
 
-proc pause*(mixer: var Mixer, playbackId: uint64, value: bool) =
+proc Pause*(mixer: var Mixer, playbackId: uint64, value: bool) =
   mixer.lock.withLock():
     for track in mixer.tracks.mvalues:
       if playbackId in track.playing:
         track.playing[playbackId].paused = value
 
-proc pause*(mixer: var Mixer) = mixer.pause(true)
-proc pause*(mixer: var Mixer, track: string) = mixer.pause(track, true)
-proc pause*(mixer: var Mixer, playbackId: uint64) = mixer.pause(playbackId, true)
-proc unpause*(mixer: var Mixer) = mixer.pause(false)
-proc unpause*(mixer: var Mixer, track: string) = mixer.pause(track, false)
-proc unpause*(mixer: var Mixer, playbackId: uint64) = mixer.pause(playbackId, false)
+proc Pause*(mixer: var Mixer) = mixer.Pause(true)
+proc Pause*(mixer: var Mixer, track: string) = mixer.Pause(track, true)
+proc Pause*(mixer: var Mixer, playbackId: uint64) = mixer.Pause(playbackId, true)
+proc Unpause*(mixer: var Mixer) = mixer.Pause(false)
+proc Unpause*(mixer: var Mixer, track: string) = mixer.Pause(track, false)
+proc Unpause*(mixer: var Mixer, playbackId: uint64) = mixer.Pause(playbackId, false)
 
-proc fadeTo*(mixer: var Mixer, track: string, level: Level, time: float) =
+proc FadeTo*(mixer: var Mixer, track: string, level: Level, time: float) =
   mixer.tracks[track].targetLevel = level
   mixer.tracks[track].fadeTime = time
   mixer.tracks[track].fadeStep = level.float - mixer.tracks[track].level.float / time
 
-proc isPlaying*(mixer: var Mixer): bool =
+proc IsPlaying*(mixer: var Mixer): bool =
   mixer.lock.withLock():
     for track in mixer.tracks.mvalues:
       for playback in track.playing.values:
@@ -178,7 +179,7 @@
           return true
   return false
 
-proc isPlaying*(mixer: var Mixer, track: string): bool =
+proc IsPlaying*(mixer: var Mixer, track: string): bool =
   mixer.lock.withLock():
     if mixer.tracks.contains(track):
       for playback in mixer.tracks[track].playing.values:
@@ -240,7 +241,10 @@
   mixer.currentBuffer = (mixer.currentBuffer + 1) mod mixer.buffers.len
 
 # DSP functions
+# TODO: finish implementation, one day
 
+#[
+#
 proc lowPassFilter(data: var SoundData, cutoff: int) =
   let alpha = float(cutoff) / AUDIO_SAMPLE_RATE
   var value = data[0]
@@ -249,29 +253,31 @@
     value[1] += int16(alpha * float(data[i][1] - value[1]))
     data[i] = value
 
-proc downsample(data: var SoundData, n: int) =
-  let newLen = (data.len - 1) div n + 1
-  for i in 0 ..< newLen:
-    data[i] = data[i * n]
-  data.setLen(newLen)
+  proc downsample(data: var SoundData, n: int) =
+    let newLen = (data.len - 1) div n + 1
+    for i in 0 ..< newLen:
+      data[i] = data[i * n]
+    data.setLen(newLen)
 
-proc upsample(data: var SoundData, m: int) =
-  data.setLen(data.len * m)
-  var i = data.len - 1
-  while i < 0:
-    if i mod m == 0:
-      data[i] = data[i div m]
-    else:
-      data[i] = [0, 0]
-    i.dec
+  proc upsample(data: var SoundData, m: int) =
+    data.setLen(data.len * m)
+    var i = data.len - 1
+    while i < 0:
+      if i mod m == 0:
+        data[i] = data[i div m]
+      else:
+        data[i] = [0, 0]
+      i.dec
 
-proc slowdown(data: var SoundData, m, n: int) =
-  data.upsample(m)
-  # TODO
-  # data.lowPassFilter(m)
-  data.downsample(n)
+    proc slowdown(data: var SoundData, m, n: int) =
+      data.upsample(m)
+      # TODO
+      # data.lowPassFilter(m)
+      data.downsample(n)
 
-proc destroy*(mixer: var Mixer) =
+    ]#
+
+proc destroy(mixer: var Mixer) =
   mixer.lock.deinitLock()
   mixer.device.closeSoundDevice()
 
@@ -287,7 +293,7 @@
   while true:
     mixer[].updateSoundBuffer()
 
-proc startMixerThread*() =
+proc StartMixerThread*() =
   mixer[] = initMixer()
   audiothread.createThread(audioWorker)
   debug "Created audio thread"
--- a/semicongine/core/buildconfig.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/semicongine/core/buildconfig.nim	Thu May 23 01:44:10 2024 +0700
@@ -29,7 +29,7 @@
 
 # root of where settings files will be searched
 # must be relative (to the directory of the binary)
-const DEBUG* = not defined(release)
+const DEBUG* {.booldefine.} = not defined(release)
 const CONFIGROOT* {.strdefine.}: string = "."
 assert not isAbsolute(CONFIGROOT)
 
--- a/semicongine/engine.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/semicongine/engine.nim	Thu May 23 01:44:10 2024 +0700
@@ -18,8 +18,8 @@
 import ./scene
 import ./material
 import ./renderer
-import ./events
 import ./audio
+import ./input
 import ./text
 import ./panel
 
@@ -32,38 +32,20 @@
     Starting
     Running
     Shutdown
-  Input = object
-    keyIsDown: set[Key]
-    keyWasPressed: set[Key]
-    keyWasReleased: set[Key]
-    mouseIsDown: set[MouseButton]
-    mouseWasPressed: set[MouseButton]
-    mouseWasReleased: set[MouseButton]
-    mousePosition: Vec2f
-    mouseMove: Vec2f
-    eventsProcessed: uint64
-    windowWasResized: bool
-    mouseWheel: float32
   Engine* = object
     applicationName: string
-    debug: bool
     showFps: bool
-    state*: EngineState
     device: Device
     debugger: Debugger
     instance: Instance
     window: NativeWindow
     renderer: Option[Renderer]
-    input: Input
-    exitHandler: proc(engine: var Engine)
-    resizeHandler: proc(engine: var Engine)
-    eventHandler: proc(engine: var Engine, event: Event)
     fullscreen: bool
     lastNRenderTimes: array[COUNT_N_RENDERTIMES, int64]
     currentRenderTimeI: int = 0
 
 # forward declarations
-func getAspectRatio*(engine: Engine): float32
+func GetAspectRatio*(engine: Engine): float32
 
 proc destroy*(engine: var Engine) =
   checkVkResult engine.device.vk.vkDeviceWaitIdle()
@@ -80,11 +62,7 @@
 
 proc initEngine*(
   applicationName = querySetting(projectName),
-  debug = DEBUG,
   showFps = DEBUG,
-  exitHandler: proc(engine: var Engine) = nil,
-  resizeHandler: proc(engine: var Engine) = nil,
-  eventHandler: proc(engine: var Engine, event: Event) = nil,
   vulkanVersion = VK_MAKE_API_VERSION(0, 1, 3, 0),
   vulkanLayers: openArray[string] = [],
 ): Engine =
@@ -97,12 +75,7 @@
   else:
     echo "Starting without Steam"
 
-  result.state = Starting
-  result.exitHandler = exitHandler
-  result.resizeHandler = resizeHandler
-  result.eventHandler = eventHandler
   result.applicationName = applicationName
-  result.debug = debug
   result.showFps = showFps
   result.window = createWindow(result.applicationName)
 
@@ -110,7 +83,7 @@
     layers = @vulkanLayers
     instanceExtensions: seq[string]
 
-  if result.debug:
+  if DEBUG:
     instanceExtensions.add "VK_EXT_debug_utils"
     layers.add "VK_LAYER_KHRONOS_validation"
     # This stuff might be usefull if we one day to smart GPU memory allocation,
@@ -123,7 +96,7 @@
     instanceExtensions = instanceExtensions,
     layers = layers.deduplicate(),
   )
-  if result.debug:
+  if DEBUG:
     result.debugger = result.instance.createDebugMessenger()
   # create devices
   let selectedPhysicalDevice = result.instance.getPhysicalDevices().filterBestGraphics()
@@ -132,7 +105,7 @@
     enabledExtensions = @[],
     selectedPhysicalDevice.filterForGraphicsPresentationQueues()
   )
-  startMixerThread()
+  StartMixerThread()
 
 proc initRenderer*(
   engine: var Engine,
@@ -169,7 +142,7 @@
   assert engine.renderer.isSome
   assert not scene.loaded
   checkVkResult engine.device.vk.vkDeviceWaitIdle()
-  scene.addShaderGlobal(ASPECT_RATIO_ATTRIBUTE, engine.getAspectRatio)
+  scene.addShaderGlobal(ASPECT_RATIO_ATTRIBUTE, engine.GetAspectRatio)
   engine.renderer.get.setupDrawableBuffers(scene)
   engine.renderer.get.updateMeshData(scene, forceAll = true)
   engine.renderer.get.updateUniformData(scene, forceAll = true)
@@ -181,13 +154,12 @@
   engine.renderer.get.destroy(scene)
 
 proc renderScene*(engine: var Engine, scene: var Scene) =
-  assert engine.state == Running
   assert engine.renderer.isSome, "Renderer has not yet been initialized, call 'engine.initRenderer' first"
   assert engine.renderer.get.hasScene(scene), &"Scene '{scene.name}' has not been loaded yet"
   let t0 = getMonoTime()
 
   engine.renderer.get.startNewFrame()
-  scene.setShaderGlobal(ASPECT_RATIO_ATTRIBUTE, engine.getAspectRatio)
+  scene.setShaderGlobal(ASPECT_RATIO_ATTRIBUTE, engine.GetAspectRatio)
   engine.renderer.get.updateMeshData(scene)
   engine.renderer.get.updateUniformData(scene)
   engine.renderer.get.render(scene)
@@ -206,94 +178,26 @@
       engine.window.setTitle(&"{engine.applicationName} ({min:.2}, {median:.2}, {max:.2})")
 
 
-proc updateInputs*(engine: var Engine): EngineState =
-  assert engine.state in [Starting, Running]
-
-  # reset input states
-  engine.input.keyWasPressed = {}
-  engine.input.keyWasReleased = {}
-  engine.input.mouseWasPressed = {}
-  engine.input.mouseWasReleased = {}
-  engine.input.mouseWheel = 0
-  engine.input.mouseMove = newVec2f()
-  engine.input.windowWasResized = false
-
-  if engine.state == Starting:
-    engine.input.windowWasResized = true
-    var mpos = engine.window.getMousePosition()
-    if mpos.isSome:
-      engine.input.mousePosition = mpos.get
-
-  var killed = false
-  for event in engine.window.pendingEvents():
-    inc engine.input.eventsProcessed
-    if engine.eventHandler != nil:
-      engine.eventHandler(engine, event)
-    case event.eventType:
-      of Quit:
-        killed = true
-      of ResizedWindow:
-        engine.input.windowWasResized = true
-      of KeyPressed:
-        engine.input.keyWasPressed.incl event.key
-        engine.input.keyIsDown.incl event.key
-      of KeyReleased:
-        engine.input.keyWasReleased.incl event.key
-        engine.input.keyIsDown.excl event.key
-      of MousePressed:
-        engine.input.mouseWasPressed.incl event.button
-        engine.input.mouseIsDown.incl event.button
-      of MouseReleased:
-        engine.input.mouseWasReleased.incl event.button
-        engine.input.mouseIsDown.excl event.button
-      of MouseMoved:
-        let newPos = newVec2(float32(event.x), float32(event.y))
-        engine.input.mouseMove = newPos - engine.input.mousePosition
-        engine.input.mousePosition = newPos
-      of MouseWheel:
-        engine.input.mouseWheel = event.amount
-  if engine.state == Starting:
-    engine.state = Running
-  if killed:
-    engine.state = Shutdown
-    if engine.exitHandler != nil:
-      engine.exitHandler(engine)
-  if engine.input.windowWasResized and engine.resizeHandler != nil:
-    engine.resizeHandler(engine)
-  return engine.state
-
 # wrappers for internal things
-func keyIsDown*(engine: Engine, key: Key): auto = key in engine.input.keyIsDown
-func keyWasPressed*(engine: Engine, key: Key): auto = key in engine.input.keyWasPressed
-func keyWasPressed*(engine: Engine): auto = engine.input.keyWasPressed.len > 0
-func keyWasReleased*(engine: Engine, key: Key): auto = key in engine.input.keyWasReleased
-func mouseIsDown*(engine: Engine, button: MouseButton): auto = button in engine.input.mouseIsDown
-func mouseWasPressed*(engine: Engine, button: MouseButton): auto = button in engine.input.mouseWasPressed
-func mouseWasReleased*(engine: Engine, button: MouseButton): auto = button in engine.input.mouseWasReleased
-func mousePosition*(engine: Engine): auto = engine.input.mousePosition
-func mousePositionNormalized*(engine: Engine): Vec2f =
-  result.x = (engine.input.mousePosition.x / float32(engine.window.size[0])) * 2.0 - 1.0
-  result.y = (engine.input.mousePosition.y / float32(engine.window.size[1])) * 2.0 - 1.0
-func mouseMove*(engine: Engine): auto = engine.input.mouseMove
-func mouseWheel*(engine: Engine): auto = engine.input.mouseWheel
-func eventsProcessed*(engine: Engine): auto = engine.input.eventsProcessed
-func gpuDevice*(engine: Engine): Device = engine.device
-func getWindow*(engine: Engine): auto = engine.window
-func getAspectRatio*(engine: Engine): float32 = engine.getWindow().size[0] / engine.getWindow().size[1]
-func windowWasResized*(engine: Engine): auto = engine.input.windowWasResized
-func showSystemCursor*(engine: Engine) = engine.window.showSystemCursor()
-func hideSystemCursor*(engine: Engine) = engine.window.hideSystemCursor()
-func fullscreen*(engine: Engine): bool = engine.fullscreen
-proc `fullscreen=`*(engine: var Engine, enable: bool) =
+func GpuDevice*(engine: Engine): Device = engine.device
+func GetWindow*(engine: Engine): auto = engine.window
+func GetAspectRatio*(engine: Engine): float32 = engine.GetWindow().size[0] / engine.GetWindow().size[1]
+func ShowSystemCursor*(engine: Engine) = engine.window.showSystemCursor()
+func HideSystemCursor*(engine: Engine) = engine.window.hideSystemCursor()
+func Fullscreen*(engine: Engine): bool = engine.fullscreen
+proc `Fullscreen=`*(engine: var Engine, enable: bool) =
   if enable != engine.fullscreen:
     engine.fullscreen = enable
     engine.window.fullscreen(engine.fullscreen)
 
-func limits*(engine: Engine): VkPhysicalDeviceLimits =
-  engine.gpuDevice().physicalDevice.properties.limits
+func Limits*(engine: Engine): VkPhysicalDeviceLimits =
+  engine.device.physicalDevice.properties.limits
 
-proc processEvents*(engine: Engine, panel: var Panel) =
-  let hasMouseNow = panel.contains(engine.mousePositionNormalized, engine.getAspectRatio)
+proc UpdateInputs*(engine: Engine): bool =
+  UpdateInputs(engine.window.pendingEvents())
+
+proc ProcessEvents*(engine: Engine, panel: var Panel) =
+  let hasMouseNow = panel.contains(MousePositionNormalized(engine.window.size), engine.GetAspectRatio)
 
   # enter/leave events
   if hasMouseNow:
@@ -307,9 +211,9 @@
 
   # button events
   if hasMouseNow:
-    if engine.input.mouseWasPressed.len > 0:
-      if panel.onMouseDown != nil: panel.onMouseDown(panel, engine.input.mouseWasPressed)
-    if engine.input.mouseWasReleased.len > 0:
-      if panel.onMouseUp != nil: panel.onMouseUp(panel, engine.input.mouseWasReleased)
+    if MouseWasPressed():
+      if panel.onMouseDown != nil: panel.onMouseDown(panel, MousePressedButtons())
+    if MouseWasReleased():
+      if panel.onMouseUp != nil: panel.onMouseUp(panel, MouseReleasedButtons())
 
   panel.hasMouse = hasMouseNow
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/semicongine/input.nim	Thu May 23 01:44:10 2024 +0700
@@ -0,0 +1,176 @@
+# Linux joystick: https://www.kernel.org/doc/Documentation/input/joystick-api.txt
+# Windows joystick: https://learn.microsoft.com/en-us/windows/win32/xinput/getting-started-with-xinput
+
+
+import std/tables
+import std/strutils
+
+import ./core/vector
+import ./events
+import ./storage
+
+type
+  Input = object
+    keyIsDown: set[Key]
+    keyWasPressed: set[Key]
+    keyWasReleased: set[Key]
+    mouseIsDown: set[MouseButton]
+    mouseWasPressed: set[MouseButton]
+    mouseWasReleased: set[MouseButton]
+    mousePosition: Vec2f
+    mouseMove: Vec2f
+    mouseWheel: float32
+    windowWasResized: bool = true
+
+# warning, shit is not thread safe
+var input: Input
+
+proc UpdateInputs*(events: seq[Event]): bool =
+  # reset input states
+  input.keyWasPressed = {}
+  input.keyWasReleased = {}
+  input.mouseWasPressed = {}
+  input.mouseWasReleased = {}
+  input.mouseWheel = 0
+  input.mouseMove = newVec2f()
+  input.windowWasResized = false
+
+  var killed = false
+  for event in events:
+    case event.eventType:
+      of Quit:
+        killed = true
+      of ResizedWindow:
+        input.windowWasResized = true
+      of KeyPressed:
+        input.keyWasPressed.incl event.key
+        input.keyIsDown.incl event.key
+      of KeyReleased:
+        input.keyWasReleased.incl event.key
+        input.keyIsDown.excl event.key
+      of MousePressed:
+        input.mouseWasPressed.incl event.button
+        input.mouseIsDown.incl event.button
+      of MouseReleased:
+        input.mouseWasReleased.incl event.button
+        input.mouseIsDown.excl event.button
+      of MouseMoved:
+        let newPos = newVec2(float32(event.x), float32(event.y))
+        input.mouseMove = newPos - input.mousePosition
+        input.mousePosition = newPos
+      of MouseWheel:
+        input.mouseWheel = event.amount
+  return not killed
+
+proc KeyIsDown*(key: Key): bool = key in input.keyIsDown
+proc KeyWasPressed*(key: Key): bool = key in input.keyWasPressed
+proc KeyWasPressed*(): bool = input.keyWasPressed.len > 0
+proc KeyWasReleased*(key: Key): bool = key in input.keyWasReleased
+proc MouseIsDown*(button: MouseButton): bool = button in input.mouseIsDown
+proc MouseWasPressed*(): bool = input.mouseWasPressed.len > 0
+proc MouseWasPressed*(button: MouseButton): bool = button in input.mouseWasPressed
+proc MousePressedButtons*(): set[MouseButton] = input.mouseWasPressed
+proc MouseWasReleased*(): bool = input.mouseWasReleased.len > 0
+proc MouseWasReleased*(button: MouseButton): bool = button in input.mouseWasReleased
+proc MouseReleasedButtons*(): set[MouseButton] = input.mouseWasReleased
+proc MousePosition*(): Vec2f = input.mousePosition
+proc MousePositionNormalized*(size: (int, int)): Vec2f =
+  result.x = (input.mousePosition.x / float32(size[0])) * 2.0 - 1.0
+  result.y = (input.mousePosition.y / float32(size[1])) * 2.0 - 1.0
+proc MouseMove*(): auto = input.mouseMove
+proc MouseWheel*(): auto = input.mouseWheel
+proc WindowWasResized*(): auto = input.windowWasResized
+
+# actions as a slight abstraction over raw input
+
+type
+  ActionMap = object
+    keyActions: Table[string, set[Key]]
+    mouseActions: Table[string, set[MouseButton]]
+
+# warning, shit is not thread safe
+var actionMap: ActionMap
+
+proc MapAction*[T: enum](action: T, key: Key) =
+  if not actionMap.keyActions.contains($action):
+    actionMap.keyActions[$action] = {}
+  actionMap.keyActions[$action].incl key
+
+proc MapAction*[T: enum](action: T, button: MouseButton) =
+  if not actionMap.mouseActions.contains($action):
+    actionMap.mouseActions[$action] = {}
+  actionMap.mouseActions[$action].incl button
+
+proc MapAction*[T: enum](action: T, keys: openArray[Key|MouseButton]) =
+  for key in keys:
+    MapAction(action, key)
+
+proc UnmapAction*[T: enum](action: T, key: Key) =
+  if actionMap.keyActions.contains($action):
+    actionMap.keyActions[$action].excl(key)
+
+proc UnmapAction*[T: enum](action: T, button: MouseButton) =
+  if actionMap.mouseActions.contains($action):
+    actionMap.mouseActions[$action].excl(button)
+
+proc UnmapAction*[T: enum](action: T) =
+  if actionMap.keyActions.contains($action):
+    actionMap.keyActions[$action] = {}
+  if actionMap.mouseActions.contains($action):
+    actionMap.mouseActions[$action] = {}
+
+proc SaveCurrentActionMapping*() =
+  for name, keys in actionMap.keyActions.pairs:
+    SystemStorage.store(name, keys, table = "input_mapping_key")
+  for name, buttons in actionMap.mouseActions.pairs:
+    SystemStorage.store(name, buttons, table = "input_mapping_mouse")
+
+proc LoadActionMapping*[T]() =
+  reset(actionMap)
+  for name in SystemStorage.list(table = "input_mapping_key"):
+    let action = parseEnum[T](name)
+    let keys = SystemStorage.load(name, set[Key](), table = "input_mapping_key")
+    for key in keys:
+      MapAction(action, key)
+
+proc ActionDown*[T](action: T): bool =
+  if actionMap.keyActions.contains($action):
+    for key in actionMap.keyActions[$action]:
+      if key in input.keyIsDown:
+        return true
+    return false
+  if actionMap.mouseActions.contains($action):
+    for button in actionMap.mouseActions[$action]:
+      if button in input.mouseIsDown:
+        return true
+    return false
+
+proc ActionPressed*[T](action: T): bool =
+  if actionMap.keyActions.contains($action):
+    for key in actionMap.keyActions[$action]:
+      if key in input.keyWasPressed:
+        return true
+  elif actionMap.mouseActions.contains($action):
+    for button in actionMap.mouseActions[$action]:
+      if button in input.mouseWasPressed:
+        return true
+
+proc ActionReleased*[T](action: T): bool =
+  if actionMap.keyActions.contains($action):
+    for key in actionMap.keyActions[$action]:
+      if key in input.keyWasReleased:
+        return true
+  elif actionMap.mouseActions.contains($action):
+    for button in actionMap.mouseActions[$action]:
+      if button in input.mouseWasReleased:
+        return true
+
+proc ActionValue*[T](action: T): float32 =
+  if actionMap.keyActions.contains($action):
+    for key in actionMap.keyActions[$action]:
+      if key in input.keyIsDown:
+        return 1
+  elif actionMap.mouseActions.contains($action):
+    for button in actionMap.mouseActions[$action]:
+      if button in input.mouseIsDown:
+        return 1
--- a/semicongine/mesh.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/semicongine/mesh.nim	Thu May 23 01:44:10 2024 +0700
@@ -382,6 +382,13 @@
 proc clearDirtyAttributes*(mesh: var MeshObject) =
   mesh.dirtyAttributes.reset
 
+proc setShaderMaterialIndices*(mesh: var MeshObject, shadername: string, values: seq[uint16], attributeName = MATERIALINDEX_ATTRIBUTE) =
+  let indexAttribute = shadername & "_" & attributeName
+  assert values.len == mesh.instanceCount, &"Mesh {mesh}: While trying to set shader material indices for shader '{shadername}': indices have len {values.len}, but instance count is {mesh.instanceCount}"
+  mesh[indexAttribute] = values
+
+# MESH-TOOLS
+
 proc transform*[T: GPUType](mesh: var MeshObject, attribute: string, transform: Mat4) =
   if mesh.vertexData.contains(attribute):
     for i in 0 ..< mesh.vertexData[attribute].len:
@@ -393,17 +400,12 @@
     raise newException(Exception, &"Attribute {attribute} is not defined for mesh {mesh}")
   mesh.dirtyAttributes.add attribute
 
-proc applyTransformToVertices*(mesh: var MeshObject, positionAttribute = DEFAULT_POSITION_ATTRIBUTE) =
-  for i in 0 ..< mesh.vertexData[positionAttribute].len:
-    mesh.vertexData[positionAttribute][i] = mesh.transform * mesh.vertexData[positionAttribute][i, Vec3f]
-  mesh.transform = Unit4
-
-func getCollisionPoints*(mesh: MeshObject, positionAttribute = DEFAULT_POSITION_ATTRIBUTE): seq[Vec3f] =
+func getMeshPoints*(mesh: MeshObject, positionAttribute = DEFAULT_POSITION_ATTRIBUTE): seq[Vec3f] =
   for p in mesh[positionAttribute, Vec3f][]:
     result.add mesh.transform * p
 
 func getCollider*(mesh: MeshObject, positionAttribute = DEFAULT_POSITION_ATTRIBUTE): Collider =
-  return mesh.getCollisionPoints(positionAttribute).calculateCollider(Points)
+  return mesh.getMeshPoints(positionAttribute).calculateCollider(Points)
 
 proc asNonIndexedMesh*(mesh: MeshObject): MeshObject =
   if mesh.indexType == None:
--- a/semicongine/renderer.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/semicongine/renderer.nim	Thu May 23 01:44:10 2024 +0700
@@ -25,17 +25,20 @@
 const VERTEX_ATTRIB_ALIGNMENT = 4 # used for buffer alignment
 
 type
+  ShaderData = ref object
+    descriptorPool: DescriptorPool
+    descriptorSets: seq[DescriptorSet] # len = n swapchain images
+    uniformBuffers: seq[Buffer]
+    textures: Table[string, seq[VulkanTexture]]
+
   SceneData = ref object
-    drawables*: seq[tuple[drawable: Drawable, mesh: Mesh]]
-    vertexBuffers*: Table[MemoryPerformanceHint, Buffer]
-    indexBuffer*: Buffer
-    uniformBuffers*: Table[VkPipeline, seq[Buffer]]                 # one per frame-in-flight
-    textures*: Table[VkPipeline, Table[string, seq[VulkanTexture]]] # per frame-in-flight
-    attributeLocation*: Table[string, MemoryPerformanceHint]
-    vertexBufferOffsets*: Table[(Mesh, string), uint64]
-    descriptorPools*: Table[VkPipeline, DescriptorPool]
-    descriptorSets*: Table[VkPipeline, seq[DescriptorSet]]
+    drawables: seq[tuple[drawable: Drawable, mesh: Mesh]]
+    vertexBuffers: Table[MemoryPerformanceHint, Buffer]
+    indexBuffer: Buffer
+    attributeLocation: Table[string, MemoryPerformanceHint]
+    vertexBufferOffsets: Table[(Mesh, string), uint64]
     materials: Table[MaterialType, seq[MaterialData]]
+    shaderData: Table[VkPipeline, ShaderData]
   Renderer* = object
     device: Device
     renderPass: RenderPass
@@ -78,78 +81,23 @@
   result.swapchain = swapchain.get()
   result.emptyTexture = device.uploadTexture(result.queue, EMPTY_TEXTURE)
 
-func inputs(renderer: Renderer, scene: Scene): seq[ShaderAttribute] =
-  var found: Table[string, ShaderAttribute]
+func shadersForScene(renderer: Renderer, scene: Scene): seq[(MaterialType, ShaderPipeline)] =
   for (materialType, shaderPipeline) in renderer.renderPass.shaderPipelines:
     if scene.usesMaterial(materialType):
-      for input in shaderPipeline.inputs:
-        if found.contains(input.name):
-          assert input.name == found[input.name].name, &"{input.name}: {input.name} != {found[input.name].name}"
-          assert input.theType == found[input.name].theType, &"{input.name}: {input.theType} != {found[input.name].theType}"
-          assert input.arrayCount == found[input.name].arrayCount, &"{input.name}: {input.arrayCount} != {found[input.name].arrayCount}"
-          assert input.memoryPerformanceHint == found[input.name].memoryPerformanceHint, &"{input.name}: {input.memoryPerformanceHint} != {found[input.name].memoryPerformanceHint}"
-        else:
-          result.add input
-          found[input.name] = input
-
-proc materialCompatibleWithPipeline(scene: Scene, materialType: MaterialType, shaderPipeline: ShaderPipeline): (bool, string) =
-  for uniform in shaderPipeline.uniforms:
-    if scene.shaderGlobals.contains(uniform.name):
-      if scene.shaderGlobals[uniform.name].theType != uniform.theType:
-        return (true, &"shader uniform needs type {uniform.theType} but scene global is of type {scene.shaderGlobals[uniform.name].theType}")
-    else:
-      if not materialType.hasMatchingAttribute(uniform):
-        return (true, &"shader uniform '{uniform.name}' was not found in scene globals or scene materials")
-  for texture in shaderPipeline.samplers:
-    if scene.shaderGlobals.contains(texture.name):
-      if scene.shaderGlobals[texture.name].theType != texture.theType:
-        return (true, &"shader texture '{texture.name}' needs type {texture.theType} but scene global is of type {scene.shaderGlobals[texture.name].theType}")
-    else:
-      if not materialType.hasMatchingAttribute(texture):
-        return (true, &"Required texture for shader texture '{texture.name}' was not found in scene materials")
-
-  return (false, "")
+      result.add (materialType, shaderPipeline)
 
-proc meshCompatibleWithPipeline(scene: Scene, mesh: Mesh, shaderPipeline: ShaderPipeline): (bool, string) =
-  for input in shaderPipeline.inputs:
-    if input.name in [TRANSFORM_ATTRIB, MATERIALINDEX_ATTRIBUTE]: # will be populated automatically
-      assert input.perInstance == true, &"Currently the {input.name} attribute must be a per instance attribute"
-      continue
-    if not (input.name in mesh[].attributes):
-      return (true, &"Shader input '{input.name}' is not available for mesh")
-    if input.theType != mesh[].attributeType(input.name):
-      return (true, &"Shader input '{input.name}' expects type {input.theType}, but mesh has {mesh[].attributeType(input.name)}")
-    if not input.perInstance and not mesh[].vertexAttributes.contains(input.name):
-      return (true, &"Shader input '{input.name}' expected to be vertex attribute, but mesh has no such vertex attribute (available are: {mesh[].vertexAttributes})")
-    if input.perInstance and not mesh[].instanceAttributes.contains(input.name):
-      return (true, &"Shader input '{input.name}' expected to be per instance attribute, but mesh has no such instance attribute (available are: {mesh[].instanceAttributes})")
-
-  let pipelineCompatability = scene.materialCompatibleWithPipeline(mesh.material.theType, shaderPipeline)
-  if pipelineCompatability[0]:
-    return (true, pipelineCompatability[1])
-  return (false, "")
-
-proc checkSceneIntegrity(renderer: Renderer, scene: Scene) =
-  # TODO: this and the sub-functions can likely be simplified a ton
-  if scene.meshes.len == 0:
-    return
-
-  var foundRenderableObject = false
-  var materialTypes: seq[MaterialType]
-  for (materialType, shaderPipeline) in renderer.renderPass.shaderPipelines:
-    materialTypes.add materialType
-    for mesh in scene.meshes:
-      if mesh.material.theType == materialType:
-        foundRenderableObject = true
-        let (error, message) = scene.meshCompatibleWithPipeline(mesh, shaderPipeline)
-        assert not error, &"Mesh '{mesh}' not compatible with assigned shaderPipeline ({materialType}) because: {message}"
-
-  if not foundRenderableObject:
-    var matTypes: Table[string, MaterialType]
-    for mesh in scene.meshes:
-      if not matTypes.contains(mesh.material.name):
-        matTypes[mesh.material.name] = mesh.material.theType
-    assert false, &"Scene '{scene.name}' has been added but materials are not compatible with any registered shader: Materials in scene: {matTypes}, registered shader-materialtypes: {materialTypes}"
+func vertexInputsForScene(renderer: Renderer, scene: Scene): seq[ShaderAttribute] =
+  var found: Table[string, ShaderAttribute]
+  for (materialType, shaderPipeline) in renderer.shadersForScene(scene):
+    for input in shaderPipeline.inputs:
+      if found.contains(input.name):
+        assert input.name == found[input.name].name, &"{input.name}: {input.name} != {found[input.name].name}"
+        assert input.theType == found[input.name].theType, &"{input.name}: {input.theType} != {found[input.name].theType}"
+        assert input.arrayCount == found[input.name].arrayCount, &"{input.name}: {input.arrayCount} != {found[input.name].arrayCount}"
+        assert input.memoryPerformanceHint == found[input.name].memoryPerformanceHint, &"{input.name}: {input.memoryPerformanceHint} != {found[input.name].memoryPerformanceHint}"
+      else:
+        result.add input
+        found[input.name] = input
 
 proc setupDrawableBuffers*(renderer: var Renderer, scene: var Scene) =
   assert not (scene in renderer.scenedata)
@@ -171,8 +119,6 @@
     if not (MATERIALINDEX_ATTRIBUTE in mesh[].attributes):
       mesh[].initInstanceAttribute(MATERIALINDEX_ATTRIBUTE, uint16(scenedata.materials[mesh.material.theType].find(mesh.material)))
 
-  # renderer.checkSceneIntegrity(scene)
-
   # create index buffer if necessary
   var indicesBufferSize = 0'u64
   for mesh in scene.meshes:
@@ -200,17 +146,21 @@
   for hint in MemoryPerformanceHint:
     perLocationSizes[hint] = 0
 
-  let inputs = renderer.inputs(scene)
+  let sceneVertexInputs = renderer.vertexInputsForScene(scene)
+  let sceneShaders = renderer.shadersForScene(scene)
 
-  for attribute in inputs:
-    scenedata.attributeLocation[attribute.name] = attribute.memoryPerformanceHint
-    # setup one buffer per attribute-location-type
+  for (materialType, shaderPipeline) in sceneShaders:
+    scenedata.shaderData[shaderPipeline.vk] = ShaderData()
+
+  for vertexAttribute in sceneVertexInputs:
+    scenedata.attributeLocation[vertexAttribute.name] = vertexAttribute.memoryPerformanceHint
+    # setup one buffer per vertexAttribute-location-type
     for mesh in scene.meshes:
       # align size to VERTEX_ATTRIB_ALIGNMENT bytes (the important thing is the correct alignment of the offsets, but
       # we need to expand the buffer size as well, therefore considering alignment already here as well
-      if perLocationSizes[attribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT != 0:
-        perLocationSizes[attribute.memoryPerformanceHint] += VERTEX_ATTRIB_ALIGNMENT - (perLocationSizes[attribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT)
-      perLocationSizes[attribute.memoryPerformanceHint] += mesh[].attributeSize(attribute.name)
+      if perLocationSizes[vertexAttribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT != 0:
+        perLocationSizes[vertexAttribute.memoryPerformanceHint] += VERTEX_ATTRIB_ALIGNMENT - (perLocationSizes[vertexAttribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT)
+      perLocationSizes[vertexAttribute.memoryPerformanceHint] += mesh[].attributeSize(vertexAttribute.name)
 
   # create vertex buffers
   for memoryPerformanceHint, bufferSize in perLocationSizes.pairs:
@@ -229,7 +179,7 @@
     perLocationOffsets[hint] = 0
 
   for mesh in scene.meshes:
-    for attribute in inputs:
+    for attribute in sceneVertexInputs:
       scenedata.vertexBufferOffsets[(mesh, attribute.name)] = perLocationOffsets[attribute.memoryPerformanceHint]
       if mesh[].attributes.contains(attribute.name):
         perLocationOffsets[attribute.memoryPerformanceHint] += mesh[].attributeSize(attribute.name)
@@ -238,11 +188,10 @@
 
     # fill offsets per shaderPipeline (as sequence corresponds to shader input binding)
     var offsets: Table[VkPipeline, seq[(string, MemoryPerformanceHint, uint64)]]
-    for (materialType, shaderPipeline) in renderer.renderPass.shaderPipelines:
-      if scene.usesMaterial(materialType):
-        offsets[shaderPipeline.vk] = newSeq[(string, MemoryPerformanceHint, uint64)]()
-        for attribute in shaderPipeline.inputs:
-          offsets[shaderPipeline.vk].add (attribute.name, attribute.memoryPerformanceHint, scenedata.vertexBufferOffsets[(mesh, attribute.name)])
+    for (materialType, shaderPipeline) in sceneShaders:
+      offsets[shaderPipeline.vk] = newSeq[(string, MemoryPerformanceHint, uint64)]()
+      for attribute in shaderPipeline.inputs:
+        offsets[shaderPipeline.vk].add (attribute.name, attribute.memoryPerformanceHint, scenedata.vertexBufferOffsets[(mesh, attribute.name)])
 
     # create drawables
     let indexed = mesh.indexType != MeshIndexType.None
@@ -271,66 +220,63 @@
 
   # setup uniforms and textures (anything descriptor)
   var uploadedTextures: Table[Texture, VulkanTexture]
-  for (materialType, shaderPipeline) in renderer.renderPass.shaderPipelines:
-    if scene.usesMaterial(materialType):
-      # gather textures
-      scenedata.textures[shaderPipeline.vk] = initTable[string, seq[VulkanTexture]]()
-      for texture in shaderPipeline.samplers:
-        scenedata.textures[shaderPipeline.vk][texture.name] = newSeq[VulkanTexture]()
-        if scene.shaderGlobals.contains(texture.name):
-          for textureValue in scene.shaderGlobals[texture.name][Texture][]:
-            if not uploadedTextures.contains(textureValue):
-              uploadedTextures[textureValue] = renderer.device.uploadTexture(renderer.queue, textureValue)
-            scenedata.textures[shaderPipeline.vk][texture.name].add uploadedTextures[textureValue]
-        else:
-          var foundTexture = false
-          for material in scene.getMaterials(materialType):
-            if material.hasMatchingAttribute(texture):
-              foundTexture = true
-              let value = material[texture.name, Texture][]
-              assert value.len == 1, &"Mesh material attribute '{texture.name}' has texture-array, but only single textures are allowed"
-              if not uploadedTextures.contains(value[0]):
-                uploadedTextures[value[0]] = renderer.device.uploadTexture(renderer.queue, value[0])
-              scenedata.textures[shaderPipeline.vk][texture.name].add uploadedTextures[value[0]]
-          assert foundTexture, &"No texture found in shaderGlobals or materials for '{texture.name}'"
-        let nTextures = scenedata.textures[shaderPipeline.vk][texture.name].len.uint32
-        assert (texture.arrayCount == 0 and nTextures == 1) or texture.arrayCount >= nTextures, &"Shader assigned to render '{materialType}' expected {texture.arrayCount} textures for '{texture.name}' but got {nTextures}"
-        if texture.arrayCount < nTextures:
-          warn &"Shader assigned to render '{materialType}' expected {texture.arrayCount} textures for '{texture.name}' but got {nTextures}"
+  for (materialType, shaderPipeline) in sceneShaders:
+    # gather textures
+    for textureAttribute in shaderPipeline.samplers:
+      scenedata.shaderData[shaderPipeline.vk].textures[textureAttribute.name] = newSeq[VulkanTexture]()
+      if scene.shaderGlobals.contains(textureAttribute.name):
+        for textureValue in scene.shaderGlobals[textureAttribute.name][Texture][]:
+          if not uploadedTextures.contains(textureValue):
+            uploadedTextures[textureValue] = renderer.device.uploadTexture(renderer.queue, textureValue)
+          scenedata.shaderData[shaderPipeline.vk].textures[textureAttribute.name].add uploadedTextures[textureValue]
+      else:
+        var foundTexture = false
+        for material in scene.getMaterials(materialType):
+          if material.hasMatchingAttribute(textureAttribute):
+            foundTexture = true
+            let value = material[textureAttribute.name, Texture][]
+            assert value.len == 1, &"Mesh material attribute '{textureAttribute.name}' has texture-array, but only single textures are allowed"
+            if not uploadedTextures.contains(value[0]):
+              uploadedTextures[value[0]] = renderer.device.uploadTexture(renderer.queue, value[0])
+            scenedata.shaderData[shaderPipeline.vk].textures[textureAttribute.name].add uploadedTextures[value[0]]
+        assert foundTexture, &"No texture found in shaderGlobals or materials for '{textureAttribute.name}'"
+      let nTextures = scenedata.shaderData[shaderPipeline.vk].textures[textureAttribute.name].len.uint32
+      assert (textureAttribute.arrayCount == 0 and nTextures == 1) or textureAttribute.arrayCount >= nTextures, &"Shader assigned to render '{materialType}' expected {textureAttribute.arrayCount} textures for '{textureAttribute.name}' but got {nTextures}"
+      if textureAttribute.arrayCount < nTextures:
+        warn &"Shader assigned to render '{materialType}' expected {textureAttribute.arrayCount} textures for '{textureAttribute.name}' but got {nTextures}"
 
-      # gather uniform sizes
-      var uniformBufferSize = 0'u64
-      for uniform in shaderPipeline.uniforms:
-        uniformBufferSize += uniform.size
-      if uniformBufferSize > 0:
-        scenedata.uniformBuffers[shaderPipeline.vk] = newSeq[Buffer]()
-        for frame_i in 0 ..< renderer.swapchain.inFlightFrames:
-          scenedata.uniformBuffers[shaderPipeline.vk].add renderer.device.createBuffer(
-            size = uniformBufferSize,
-            usage = [VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT],
-            requireMappable = true,
-            preferVRAM = true,
-          )
+    # gather uniform sizes
+    var uniformBufferSize = 0'u64
+    for uniform in shaderPipeline.uniforms:
+      uniformBufferSize += uniform.size
+    if uniformBufferSize > 0:
+      for frame_i in 0 ..< renderer.swapchain.inFlightFrames:
+        scenedata.shaderData[shaderPipeline.vk].uniformBuffers.add renderer.device.createBuffer(
+          size = uniformBufferSize,
+          usage = [VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT],
+          requireMappable = true,
+          preferVRAM = true,
+        )
 
-      # TODO: rework the whole descriptor/pool/layout stuff, a bit unclear
-      var poolsizes = @[(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, renderer.swapchain.inFlightFrames.uint32)]
-      var nTextures = 0'u32
-      for descriptor in shaderPipeline.descriptorSetLayout.descriptors:
-        if descriptor.thetype == ImageSampler:
-          nTextures += descriptor.count
-      if nTextures > 0:
-        poolsizes.add (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, nTextures * renderer.swapchain.inFlightFrames.uint32)
-      scenedata.descriptorPools[shaderPipeline.vk] = renderer.device.createDescriptorSetPool(poolsizes)
+    # TODO: rework the whole descriptor/pool/layout stuff, a bit unclear
+    var poolsizes = @[(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, renderer.swapchain.inFlightFrames.uint32)]
+    var nTextures = 0'u32
+    for descriptor in shaderPipeline.descriptorSetLayout.descriptors:
+      if descriptor.thetype == ImageSampler:
+        nTextures += descriptor.count
+    if nTextures > 0:
+      poolsizes.add (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, nTextures * renderer.swapchain.inFlightFrames.uint32)
+    scenedata.shaderData[shaderPipeline.vk].descriptorPool = renderer.device.createDescriptorSetPool(poolsizes)
 
-      scenedata.descriptorSets[shaderPipeline.vk] = shaderPipeline.setupDescriptors(
-        scenedata.descriptorPools[shaderPipeline.vk],
-        scenedata.uniformBuffers.getOrDefault(shaderPipeline.vk, @[]),
-        scenedata.textures[shaderPipeline.vk],
-        inFlightFrames = renderer.swapchain.inFlightFrames,
-        emptyTexture = renderer.emptyTexture,
-      )
-      for frame_i in 0 ..< renderer.swapchain.inFlightFrames:
-        scenedata.descriptorSets[shaderPipeline.vk][frame_i].writeDescriptorSet()
+    scenedata.shaderData[shaderPipeline.vk].descriptorSets = shaderPipeline.setupDescriptors(
+      scenedata.shaderData[shaderPipeline.vk].descriptorPool,
+      scenedata.shaderData[shaderPipeline.vk].uniformBuffers,
+      scenedata.shaderData[shaderPipeline.vk].textures,
+      inFlightFrames = renderer.swapchain.inFlightFrames,
+      emptyTexture = renderer.emptyTexture,
+    )
+    for frame_i in 0 ..< renderer.swapchain.inFlightFrames:
+      scenedata.shaderData[shaderPipeline.vk].descriptorSets[frame_i].writeDescriptorSet()
 
   renderer.scenedata[scene] = scenedata
 
@@ -382,19 +328,15 @@
     debug &"Update uniforms because of dirty scene globals: {dirty}"
 
   # loop over all used shaders/pipelines
-  for (materialType, shaderPipeline) in renderer.renderPass.shaderPipelines:
-    if (
-      scene.usesMaterial(materialType) and
-      renderer.scenedata[scene].uniformBuffers.hasKey(shaderPipeline.vk) and
-      renderer.scenedata[scene].uniformBuffers[shaderPipeline.vk].len != 0
-    ):
+  for (materialType, shaderPipeline) in renderer.shadersForScene(scene):
+    if renderer.scenedata[scene].shaderData[shaderPipeline.vk].uniformBuffers.len > 0:
       var dirtyMaterialAttribs: seq[string]
       for material in renderer.scenedata[scene].materials[materialType].mitems:
         dirtyMaterialAttribs.add material.dirtyAttributes
         material.clearDirtyAttributes()
-      assert renderer.scenedata[scene].uniformBuffers[shaderPipeline.vk][renderer.swapchain.currentInFlight].vk.valid
+      assert renderer.scenedata[scene].shaderData[shaderPipeline.vk].uniformBuffers[renderer.swapchain.currentInFlight].vk.valid
       if forceAll:
-        for buffer in renderer.scenedata[scene].uniformBuffers[shaderPipeline.vk]:
+        for buffer in renderer.scenedata[scene].shaderData[shaderPipeline.vk].uniformBuffers:
           assert buffer.vk.valid
 
       var offset = 0'u64
@@ -422,7 +364,7 @@
           # TODO: technically we would only need to update the uniform buffer of the current
           # frameInFlight (I think), but we don't track for which frame the shaderglobals are no longer dirty
           # therefore we have to update the uniform values in all buffers, of all inFlightframes (usually 2)
-          for buffer in renderer.scenedata[scene].uniformBuffers[shaderPipeline.vk]:
+          for buffer in renderer.scenedata[scene].shaderData[shaderPipeline.vk].uniformBuffers:
             buffer.setData(renderer.queue, value.getPointer(), value.size, offset)
         offset += uniform.size
   scene.clearDirtyShaderGlobals()
@@ -458,7 +400,15 @@
     if scene.usesMaterial(materialType):
       debug &"Start shaderPipeline for '{materialType}'"
       renderer.currentFrameCommandBuffer.vkCmdBindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, shaderPipeline.vk)
-      renderer.currentFrameCommandBuffer.vkCmdBindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, shaderPipeline.layout, 0, 1, addr(renderer.scenedata[scene].descriptorSets[shaderPipeline.vk][renderer.swapchain.currentInFlight].vk), 0, nil)
+      renderer.currentFrameCommandBuffer.vkCmdBindDescriptorSets(
+        VK_PIPELINE_BIND_POINT_GRAPHICS,
+        shaderPipeline.layout,
+        0,
+        1,
+        addr(renderer.scenedata[scene].shaderData[shaderPipeline.vk].descriptorSets[renderer.swapchain.currentInFlight].vk),
+        0,
+        nil
+      )
       for (drawable, mesh) in renderer.scenedata[scene].drawables.filterIt(it[1].visible and it[1].material.theType == materialType):
         drawable.draw(renderer.currentFrameCommandBuffer, vertexBuffers = renderer.scenedata[scene].vertexBuffers, indexBuffer = renderer.scenedata[scene].indexBuffer, shaderPipeline.vk)
 
@@ -482,52 +432,37 @@
 proc destroy*(renderer: var Renderer, scene: Scene) =
   checkVkResult renderer.device.vk.vkDeviceWaitIdle()
   var scenedata = renderer.scenedata[scene]
+
   for buffer in scenedata.vertexBuffers.mvalues:
     assert buffer.vk.valid
     buffer.destroy()
+
   if scenedata.indexBuffer.vk.valid:
     assert scenedata.indexBuffer.vk.valid
     scenedata.indexBuffer.destroy()
-  for pipelineUniforms in scenedata.uniformBuffers.mvalues:
-    for buffer in pipelineUniforms.mitems:
+
+  var destroyedTextures: seq[VkImage]
+
+  for (vkPipeline, shaderData) in scenedata.shaderData.mpairs:
+
+    for buffer in shaderData.uniformBuffers.mitems:
       assert buffer.vk.valid
       buffer.destroy()
-  var destroyedTextures: seq[VkImage]
-  for pipelineTextures in scenedata.textures.mvalues:
-    for textures in pipelineTextures.mvalues:
+
+    for textures in shaderData.textures.mvalues:
       for texture in textures.mitems:
         if not destroyedTextures.contains(texture.image.vk):
           destroyedTextures.add texture.image.vk
           texture.destroy()
-  for descriptorPool in scenedata.descriptorPools.mvalues:
-    descriptorPool.destroy()
+
+    shaderData.descriptorPool.destroy()
+
   renderer.scenedata.del(scene)
 
 proc destroy*(renderer: var Renderer) =
-  checkVkResult renderer.device.vk.vkDeviceWaitIdle()
-
-  for scenedata in renderer.scenedata.mvalues:
-    for buffer in scenedata.vertexBuffers.mvalues:
-      assert buffer.vk.valid
-      buffer.destroy()
-    if scenedata.indexBuffer.vk.valid:
-      assert scenedata.indexBuffer.vk.valid
-      scenedata.indexBuffer.destroy()
-    for pipelineUniforms in scenedata.uniformBuffers.mvalues:
-      for buffer in pipelineUniforms.mitems:
-        assert buffer.vk.valid
-        buffer.destroy()
-    var destroyedTextures: seq[VkImage]
-    for pipelineTextures in scenedata.textures.mvalues:
-      for textures in pipelineTextures.mvalues:
-        for texture in textures.mitems:
-          if not destroyedTextures.contains(texture.image.vk):
-            destroyedTextures.add texture.image.vk
-            texture.destroy()
-    for descriptorPool in scenedata.descriptorPools.mvalues:
-      descriptorPool.destroy()
   for scene in renderer.scenedata.keys.toSeq:
-    renderer.scenedata.del(scene)
+    renderer.destroy(scene)
+  assert renderer.scenedata.len == 0
   renderer.emptyTexture.destroy()
   renderer.renderPass.destroy()
   renderer.commandBufferPool.destroy()
--- a/semicongine/scene.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/semicongine/scene.nim	Thu May 23 01:44:10 2024 +0700
@@ -51,11 +51,11 @@
 proc addShaderGlobal*[T](scene: var Scene, name: string, data: T) =
   scene.addShaderGlobalArray(name, [data])
 
-func getShaderGlobalArray*[T](scene: Scene, name: string): ref seq[T] =
+proc getShaderGlobalArray*[T](scene: Scene, name: string): ref seq[T] =
   scene.shaderGlobals[name][T]
 
-func getShaderGlobal*[T](scene: Scene, name: string): T =
-  scene.getShaderGlobalArray(name)[][0]
+proc getShaderGlobal*[T](scene: Scene, name: string): T =
+  getShaderGlobalArray[T](scene, name)[][0]
 
 proc setShaderGlobalArray*[T](scene: var Scene, name: string, value: openArray[T]) =
   if scene.shaderGlobals[name, T][] == @value:
--- a/semicongine/storage.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/semicongine/storage.nim	Thu May 23 01:44:10 2024 +0700
@@ -9,7 +9,7 @@
 import ./core
 
 const STORAGE_NAME = Path("storage.db")
-const KEY_VALUE_TABLE_NAME = "shelf"
+const DEFAULT_KEY_VALUE_TABLE_NAME = "shelf"
 
 type
   StorageType* = enum
@@ -27,29 +27,35 @@
       string(Path(getDataDir()) / Path(AppName())).createDir()
       Path(getDataDir()) / Path(AppName()) / STORAGE_NAME
 
-proc setup(storageType: StorageType) =
+proc ensureExists(storageType: StorageType) =
   if storageType in db:
     return
   db[storageType] = open(string(storageType.path), "", "", "")
-  db[storageType].exec(sql(&"""CREATE TABLE IF NOT EXISTS {KEY_VALUE_TABLE_NAME} (
+
+proc ensureExists(storageType: StorageType, table: string) =
+  storageType.ensureExists()
+  db[storageType].exec(sql(&"""CREATE TABLE IF NOT EXISTS {table} (
     key TEXT NOT NULL UNIQUE,
     value TEXT NOT NULL
   )"""))
 
-proc store*[T](storageType: StorageType, key: string, value: T) =
-  storageType.setup()
-  const KEY_VALUE_TABLE_NAME = "shelf"
-  db[storageType].exec(sql(&"""INSERT INTO {KEY_VALUE_TABLE_NAME} VALUES(?, ?)
+proc store*[T](storageType: StorageType, key: string, value: T, table = DEFAULT_KEY_VALUE_TABLE_NAME) =
+  storageType.ensureExists(table)
+  db[storageType].exec(sql(&"""INSERT INTO {table} VALUES(?, ?)
   ON CONFLICT(key) DO UPDATE SET value=excluded.value
   """), key, $$value)
 
-proc load*[T](storageType: StorageType, key: string, default: T): T =
-  storageType.setup()
-  const KEY_VALUE_TABLE_NAME = "shelf"
-  let dbResult = db[storageType].getValue(sql(&"""SELECT value FROM {KEY_VALUE_TABLE_NAME} WHERE key = ? """), key)
+proc load*[T](storageType: StorageType, key: string, default: T, table = DEFAULT_KEY_VALUE_TABLE_NAME): T =
+  storageType.ensureExists(table)
+  let dbResult = db[storageType].getValue(sql(&"""SELECT value FROM {table} WHERE key = ? """), key)
   if dbResult == "":
     return default
   return to[T](dbResult)
 
+proc list*[T](storageType: StorageType, table = DEFAULT_KEY_VALUE_TABLE_NAME): seq[string] =
+  storageType.ensureExists(table)
+  for row in db[storageType].fastRows(sql(&"""SELECT key FROM {table}""")):
+    result.add row[0]
+
 proc purge*(storageType: StorageType) =
   storageType.path().string.removeFile()
--- a/semicongine/text.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/semicongine/text.nim	Thu May 23 01:44:10 2024 +0700
@@ -13,7 +13,7 @@
   SPACE = Rune(' ')
 
   # font shader
-  MAX_TEXT_MATERIALS = 10
+  MAX_TEXT_MATERIALS = 100 # need for every different font AND color
   SHADER_ATTRIB_PREFIX = "semicon_text_"
   POSITION_ATTRIB = SHADER_ATTRIB_PREFIX & "position"
   UV_ATTRIB = SHADER_ATTRIB_PREFIX & "uv"
@@ -254,8 +254,7 @@
   result.mesh = newMesh(positions = positions, indices = indices, uvs = uvs, name = &"text-{instanceCounter}")
   result.mesh[].renameAttribute("position", POSITION_ATTRIB)
   result.mesh[].renameAttribute("uv", UV_ATTRIB)
-  result.mesh.material = initMaterialData(
-    theType = TEXT_MATERIAL_TYPE,
+  result.mesh.material = TEXT_MATERIAL_TYPE.initMaterialData(
     name = font.name & " text",
     attributes = {"fontAtlas": initDataList(@[font.fontAtlas]), "color": initDataList(@[color])},
   )
--- a/tests/test_collision.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/tests/test_collision.nim	Thu May 23 01:44:10 2024 +0700
@@ -3,9 +3,9 @@
 proc main() =
   var scene = Scene(name: "main")
 
-  scene.add rect(color="f00f")
+  scene.add rect(color = "f00f")
   scene.add rect()
-  scene.add circle(color="0f0f")
+  scene.add circle(color = "0f0f")
   scene.meshes[0].material = VERTEX_COLORED_MATERIAL.initMaterialData()
   scene.meshes[1].material = VERTEX_COLORED_MATERIAL.initMaterialData()
   scene.meshes[2].material = VERTEX_COLORED_MATERIAL.initMaterialData()
@@ -15,16 +15,17 @@
 
   const
     shaderConfiguration = createShaderConfiguration(
-      inputs=[
-        attr[Mat4]("transform", memoryPerformanceHint=PreferFastRead, perInstance=true),
-        attr[Vec3f]("position", memoryPerformanceHint=PreferFastRead),
-        attr[Vec4f]("color", memoryPerformanceHint=PreferFastRead),
+      name = "default shader",
+      inputs = [
+        attr[Mat4]("transform", memoryPerformanceHint = PreferFastRead, perInstance = true),
+        attr[Vec3f]("position", memoryPerformanceHint = PreferFastRead),
+        attr[Vec4f]("color", memoryPerformanceHint = PreferFastRead),
       ],
-      intermediates=[attr[Vec4f]("colorout")],
-      uniforms=[attr[Mat4]("perspective")],
-      outputs=[attr[Vec4f]("fragcolor")],
-      vertexCode="""gl_Position = vec4(position, 1.0) * (transform * Uniforms.perspective); colorout = color;""",
-      fragmentCode="""fragcolor = colorout;""",
+      intermediates = [attr[Vec4f]("colorout")],
+      uniforms = [attr[Mat4]("perspective")],
+      outputs = [attr[Vec4f]("fragcolor")],
+      vertexCode = """gl_Position = vec4(position, 1.0) * (transform * Uniforms.perspective); colorout = color;""",
+      fragmentCode = """fragcolor = colorout;""",
     )
 
   var engine = initEngine("Test collisions")
@@ -32,21 +33,21 @@
   engine.initRenderer({VERTEX_COLORED_MATERIAL: shaderConfiguration})
   engine.loadScene(scene)
 
-  while engine.updateInputs() == Running and not engine.keyIsDown(Escape):
-    if engine.windowWasResized():
+  while engine.updateInputs() and not keyIsDown(Escape):
+    if windowWasResized():
       var winSize = engine.getWindow().size
-      scene.setShaderGlobal("perspective", orthoWindowAspect(winSize[1] / winSize[0]))
-    if engine.keyIsDown(A): scene.meshes[0].transform = scene.meshes[0].transform * translate(-0.001,      0, 0)
-    if engine.keyIsDown(D): scene.meshes[0].transform = scene.meshes[0].transform * translate( 0.001,      0, 0)
-    if engine.keyIsDown(W): scene.meshes[0].transform = scene.meshes[0].transform * translate(     0, -0.001, 0)
-    if engine.keyIsDown(S): scene.meshes[0].transform = scene.meshes[0].transform * translate(     0,  0.001, 0)
-    if engine.keyIsDown(Q): scene.meshes[0].transform = scene.meshes[0].transform * rotate(-0.001, Z)
-    if engine.keyIsDown(Key.E): scene.meshes[0].transform = scene.meshes[0].transform * rotate( 0.001, Z)
+      scene.setShaderGlobal("perspective", orthoWindowAspect(winSize[0] / winSize[1]))
+    if keyIsDown(A): scene.meshes[0].transform = scene.meshes[0].transform * translate(-0.001, 0, 0)
+    if keyIsDown(D): scene.meshes[0].transform = scene.meshes[0].transform * translate(0.001, 0, 0)
+    if keyIsDown(W): scene.meshes[0].transform = scene.meshes[0].transform * translate(0, -0.001, 0)
+    if keyIsDown(S): scene.meshes[0].transform = scene.meshes[0].transform * translate(0, 0.001, 0)
+    if keyIsDown(Q): scene.meshes[0].transform = scene.meshes[0].transform * rotate(-0.001, Z)
+    if keyIsDown(Key.E): scene.meshes[0].transform = scene.meshes[0].transform * rotate(0.001, Z)
 
-    if engine.keyIsDown(Key.Z): scene.meshes[1].transform = scene.meshes[1].transform * rotate(-0.001, Z)
-    if engine.keyIsDown(Key.X): scene.meshes[1].transform = scene.meshes[1].transform * rotate( 0.001, Z)
-    if engine.keyIsDown(Key.C): scene.meshes[1].transform = scene.meshes[1].transform * translate(0, -0.001, 0)
-    if engine.keyIsDown(Key.V): scene.meshes[1].transform = scene.meshes[1].transform * translate(0,  0.001, 0)
+    if keyIsDown(Key.Z): scene.meshes[1].transform = scene.meshes[1].transform * rotate(-0.001, Z)
+    if keyIsDown(Key.X): scene.meshes[1].transform = scene.meshes[1].transform * rotate(0.001, Z)
+    if keyIsDown(Key.C): scene.meshes[1].transform = scene.meshes[1].transform * translate(0, -0.001, 0)
+    if keyIsDown(Key.V): scene.meshes[1].transform = scene.meshes[1].transform * translate(0, 0.001, 0)
     let hitbox = Collider(theType: Box, transform: scene.meshes[0].transform * translate(-0.5, -0.5))
     let hitsphere = Collider(theType: Sphere, transform: scene.meshes[2].transform, radius: 0.5)
     echo intersects(hitbox, hitsphere)
--- a/tests/test_font.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/tests/test_font.nim	Thu May 23 01:44:10 2024 +0700
@@ -31,10 +31,10 @@
   mixer[].loadSound("key", "key.ogg")
   mixer[].setLevel(0.5)
 
-  while engine.updateInputs() == Running and not engine.keyIsDown(Escape):
+  while engine.updateInputs() and not keyIsDown(Escape):
     var t = cpuTime()
     main_text.color = newVec4f(sin(t) * 0.5 + 0.5, 0.15, 0.15, 1)
-    if engine.windowWasResized():
+    if windowWasResized():
       var winSize = engine.getWindow().size
 
     # add character
@@ -42,31 +42,31 @@
       for c in [Key.A, Key.B, Key.C, Key.D, Key.E, Key.F, Key.G, Key.H, Key.I,
           Key.J, Key.K, Key.L, Key.M, Key.N, Key.O, Key.P, Key.Q, Key.R, Key.S,
           Key.T, Key.U, Key.V, Key.W, Key.X, Key.Y, Key.Z]:
-        if engine.keyWasPressed(c):
+        if keyWasPressed(c):
           discard mixer[].play("key")
-          if engine.keyIsDown(ShiftL) or engine.keyIsDown(ShiftR):
+          if keyIsDown(ShiftL) or keyIsDown(ShiftR):
             main_text.text = main_text.text & ($c).toRunes
           else:
             main_text.text = main_text.text & ($c).toRunes[0].toLower()
-      if engine.keyWasPressed(Enter):
+      if keyWasPressed(Enter):
         discard mixer[].play("key")
         main_text.text = main_text.text & Rune('\n')
-      if engine.keyWasPressed(Space):
+      if keyWasPressed(Space):
         discard mixer[].play("key")
         main_text.text = main_text.text & Rune(' ')
 
     # remove character
-    if engine.keyWasPressed(Backspace) and main_text.text.len > 0:
+    if keyWasPressed(Backspace) and main_text.text.len > 0:
       discard mixer[].play("key")
       main_text.text = main_text.text[0 ..< ^1]
 
     # alignemtn with F-keys
-    if engine.keyWasPressed(F1): main_text.horizontalAlignment = Left
-    elif engine.keyWasPressed(F2): main_text.horizontalAlignment = Center
-    elif engine.keyWasPressed(F3): main_text.horizontalAlignment = Right
-    elif engine.keyWasPressed(F4): main_text.verticalAlignment = Top
-    elif engine.keyWasPressed(F5): main_text.verticalAlignment = Center
-    elif engine.keyWasPressed(F6): main_text.verticalAlignment = Bottom
+    if keyWasPressed(F1): main_text.horizontalAlignment = Left
+    elif keyWasPressed(F2): main_text.horizontalAlignment = Center
+    elif keyWasPressed(F3): main_text.horizontalAlignment = Right
+    elif keyWasPressed(F4): main_text.verticalAlignment = Top
+    elif keyWasPressed(F5): main_text.verticalAlignment = Center
+    elif keyWasPressed(F6): main_text.verticalAlignment = Bottom
 
     origin.refresh()
     main_text.text = main_text.text & Rune('_')
--- a/tests/test_materials.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/tests/test_materials.nim	Thu May 23 01:44:10 2024 +0700
@@ -41,6 +41,7 @@
 
   const
     shaderConfiguration1 = createShaderConfiguration(
+      name = "shader 1",
       inputs = [
         attr[Vec3f]("position", memoryPerformanceHint = PreferFastRead),
         attr[Vec2f]("uv", memoryPerformanceHint = PreferFastRead),
@@ -67,7 +68,7 @@
   })
   engine.loadScene(scene)
   var t = cpuTime()
-  while engine.updateInputs() == Running and not engine.keyIsDown(Escape):
+  while engine.updateInputs() and not keyIsDown(Escape):
     var d = float32(cpuTime() - t)
     setShaderGlobalArray(scene, "test2", @[newVec4f(d), newVec4f(d * 2)])
     engine.renderScene(scene)
--- a/tests/test_mesh.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/tests/test_mesh.nim	Thu May 23 01:44:10 2024 +0700
@@ -1,14 +1,25 @@
 import std/strformat
 import semicongine
 
+const
+  MeshMaterial* = MaterialType(
+    name: "colored single texture material",
+    vertexAttributes: {
+      "position": Vec3F32,
+      "texcoord_0": Vec2F32,
+    }.toTable,
+    attributes: {"baseTexture": TextureType, "color": Vec4F32}.toTable
+  )
+
 proc main() =
   var scenes = [
-    Scene(name: "Donut", meshes: loadMeshes("donut.glb", COLORED_SINGLE_TEXTURE_MATERIAL)[0].toSeq),
+    Scene(name: "Donut", meshes: loadMeshes("donut.glb", MeshMaterial)[0].toSeq),
   ]
 
   var engine = initEngine("Test meshes")
   const
     shaderConfiguration = createShaderConfiguration(
+      name = "default shader",
       inputs = [
         attr[Vec3f]("position", memoryPerformanceHint = PreferFastRead),
         attr[uint16](MATERIALINDEX_ATTRIBUTE, memoryPerformanceHint = PreferFastRead, perInstance = true),
@@ -28,14 +39,14 @@
       ],
       samplers = [attr[Texture]("baseTexture", arrayCount = 4)],
       vertexCode = &"""
-  gl_Position =  vec4(position, 1.0) * (transform * Uniforms.view * Uniforms.projection);
+  gl_Position =  vec4(position, 1.0) * (transform * (Uniforms.view * Uniforms.projection));
   vertexColor = Uniforms.color[{MATERIALINDEX_ATTRIBUTE}];
   colorTexCoord = texcoord_0;
   materialIndexOut = {MATERIALINDEX_ATTRIBUTE};
   """,
       fragmentCode = "color = texture(baseTexture[materialIndexOut], colorTexCoord) * vertexColor;"
     )
-  engine.initRenderer({COLORED_SINGLE_TEXTURE_MATERIAL: shaderConfiguration})
+  engine.initRenderer({MeshMaterial: shaderConfiguration})
 
   for scene in scenes.mitems:
     scene.addShaderGlobal("projection", Unit4F32)
@@ -48,30 +59,30 @@
     azimut = 0'f32
     currentScene = 0
 
-  while engine.updateInputs() == Running and not engine.keyIsDown(Escape):
-    if engine.keyWasPressed(`1`):
+  while engine.updateInputs() and not keyIsDown(Escape):
+    if keyWasPressed(`1`):
       currentScene = 0
-    elif engine.keyWasPressed(`2`):
+    elif keyWasPressed(`2`):
       currentScene = 1
-    elif engine.keyWasPressed(`3`):
+    elif keyWasPressed(`3`):
       currentScene = 2
-    elif engine.keyWasPressed(`4`):
+    elif keyWasPressed(`4`):
       currentScene = 3
-    elif engine.keyWasPressed(`5`):
+    elif keyWasPressed(`5`):
       currentScene = 4
-    elif engine.keyWasPressed(`6`):
+    elif keyWasPressed(`6`):
       currentScene = 5
 
-    if engine.keyWasPressed(NumberRowExtra3):
+    if keyWasPressed(NumberRowExtra3):
       size = 0.3'f32
       elevation = 0'f32
       azimut = 0'f32
 
     let ratio = engine.getWindow().size[0] / engine.getWindow().size[1]
-    size *= 1'f32 + engine.mouseWheel() * 0.05
-    azimut += engine.mouseMove().x / 180'f32
-    elevation -= engine.mouseMove().y / 180'f32
-    scenes[currentScene].setShaderGlobal("projection", ortho(-ratio, ratio, -1, 1, -1, 1))
+    size *= 1'f32 + mouseWheel() * 0.05
+    azimut += mouseMove().x / 180'f32
+    elevation -= mouseMove().y / 180'f32
+    scenes[currentScene].setShaderGlobal("projection", perspective(PI / 2, ratio, -0.5, 1))
     scenes[currentScene].setShaderGlobal(
       "view",
        scale(size, size, size) * rotate(elevation, newVec3f(1, 0, 0)) * rotate(azimut, Yf32)
--- a/tests/test_panel.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/tests/test_panel.nim	Thu May 23 01:44:10 2024 +0700
@@ -64,23 +64,23 @@
   scene.add origin
   engine.loadScene(scene)
 
-  while engine.updateInputs() == Running and not engine.keyIsDown(Escape):
-    if engine.keyWasPressed(F1):
+  while engine.updateInputs() and not keyIsDown(Escape):
+    if keyWasPressed(F1):
       button.horizontalAlignment = Left
       counterText.horizontalAlignment = Left
-    elif engine.keyWasPressed(F2):
+    elif keyWasPressed(F2):
       button.horizontalAlignment = Center
       counterText.horizontalAlignment = Center
-    elif engine.keyWasPressed(F3):
+    elif keyWasPressed(F3):
       button.horizontalAlignment = Right
       counterText.horizontalAlignment = Right
-    elif engine.keyWasPressed(F4):
+    elif keyWasPressed(F4):
       button.verticalAlignment = Top
       counterText.verticalAlignment = Top
-    elif engine.keyWasPressed(F5):
+    elif keyWasPressed(F5):
       button.verticalAlignment = Center
       counterText.verticalAlignment = Center
-    elif engine.keyWasPressed(F6):
+    elif keyWasPressed(F6):
       button.verticalAlignment = Bottom
       counterText.verticalAlignment = Bottom
 
--- a/tests/test_vulkan_wrapper.nim	Mon Apr 29 02:37:42 2024 -0700
+++ b/tests/test_vulkan_wrapper.nim	Thu May 23 01:44:10 2024 +0700
@@ -11,7 +11,16 @@
     wrapModeT: VK_SAMPLER_ADDRESS_MODE_REPEAT,
   )
   (R, W) = ([255'u8, 0'u8, 0'u8, 255'u8], [255'u8, 255'u8, 255'u8, 255'u8])
-  mat = SINGLE_TEXTURE_MATERIAL.initMaterialData(
+  Mat1Type = MaterialType(
+    name: "single texture material 1",
+    vertexAttributes: {
+      "color": Vec4F32,
+      "position": Vec3F32,
+      "uv": Vec2F32,
+    }.toTable,
+    attributes: {"baseTexture": TextureType}.toTable
+  )
+  mat = Mat1Type.initMaterialData(
     name = "mat",
     attributes = {
       "baseTexture": initDataList(@[Texture(isGrayscale: false, colorImage: Image[RGBAPixel](width: 5, height: 5, imagedata: @[
@@ -26,6 +35,7 @@
   Mat2Type = MaterialType(
     name: "single texture material 2",
     vertexAttributes: {
+      "color": Vec4F32,
       "position": Vec3F32,
       "uv": Vec2F32,
     }.toTable,
@@ -178,6 +188,7 @@
   # INIT RENDERER:
   const
     shaderConfiguration1 = createShaderConfiguration(
+      name = "shader1",
       inputs = [
         attr[Vec3f]("position", memoryPerformanceHint = PreferFastRead),
         attr[Vec4f]("color", memoryPerformanceHint = PreferFastWrite),
@@ -187,13 +198,12 @@
         attr[Vec4f]("outcolor"),
       ],
       outputs = [attr[Vec4f]("color")],
-      samplers = [
-        attr[Texture]("baseTexture")
-      ],
+      samplers = [attr[Texture]("baseTexture")],
       vertexCode = """gl_Position = vec4(position, 1.0) * transform; outcolor = color;""",
       fragmentCode = "color = texture(baseTexture, outcolor.xy) * 0.5 + outcolor * 0.5;",
     )
     shaderConfiguration2 = createShaderConfiguration(
+      name = "shader2",
       inputs = [
         attr[Vec3f]("position", memoryPerformanceHint = PreferFastRead),
         attr[Mat4]("transform", perInstance = true),
@@ -205,8 +215,8 @@
       fragmentCode = "color = outcolor;",
     )
   engine.initRenderer({
-    SINGLE_TEXTURE_MATERIAL: shaderConfiguration1,
-    SINGLE_TEXTURE_MATERIAL: shaderConfiguration1,
+    Mat1Type: shaderConfiguration1,
+    Mat1Type: shaderConfiguration1,
     Mat2Type: shaderConfiguration1,
     SINGLE_COLOR_MATERIAL: shaderConfiguration2,
   })
@@ -229,7 +239,7 @@
     for scene in scenes.mitems:
       echo "rendering scene ", scene.name
       for i in 0 ..< 1000:
-        if engine.updateInputs() != Running or engine.keyIsDown(Escape):
+        if not engine.updateInputs() or keyIsDown(Escape):
           engine.destroy()
           return
         engine.renderScene(scene)