changeset 1498:d3d667bbdda4 default tip

did: add support for per-frame-buffers, still need to limit this, to maybe only mapped buffers
author sam <sam@basx.dev>
date Thu, 25 Sep 2025 23:53:41 +0700
parents 56aa8fe70e9e
children
files semicongine/core/types.nim semicongine/rendering/memory.nim semicongine/rendering/renderer.nim semicongine/rendering/renderpasses.nim tests/test_rendering.nim
diffstat 5 files changed, 94 insertions(+), 87 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/core/types.nim	Wed Sep 24 23:55:36 2025 +0700
+++ b/semicongine/core/types.nim	Thu Sep 25 23:53:41 2025 +0700
@@ -140,13 +140,13 @@
     # TODO: when using mapped buffer memory, directly write values to mapped location
     # instead of using data as buffer
     data*: seq[T]
-    buffer*: Buffer
-    offset*: uint64
+    buffer*: array[INFLIGHTFRAMES.int, Buffer]
+    offset*: array[INFLIGHTFRAMES.int, uint64]
 
   GPUValue*[T: object, TBuffer: static BufferType] = object
     data*: T
-    buffer*: Buffer
-    offset*: uint64
+    buffer*: array[INFLIGHTFRAMES.int, Buffer]
+    offset*: array[INFLIGHTFRAMES.int, uint64]
 
   GPUData* = GPUArray | GPUValue
 
--- a/semicongine/rendering/memory.nim	Wed Sep 24 23:55:36 2025 +0700
+++ b/semicongine/rendering/memory.nim	Thu Sep 25 23:53:41 2025 +0700
@@ -121,8 +121,8 @@
   # santization checks
   for theName, value in descriptorSet.data[0].fieldPairs:
     when typeof(value) is GPUValue:
-      assert value.buffer.vk.Valid,
-        "Invalid buffer, did you call 'assignBuffers' for this buffer?"
+      for i in 0 ..< INFLIGHTFRAMES:
+        assert value.buffer[i].vk.Valid, "Invalid buffer, did you call 'assignBuffers' for this buffer?"
     elif typeof(value) is ImageObject:
       assert value.vk.Valid
       assert value.imageview.Valid
@@ -135,7 +135,8 @@
           assert t.sampler.Valid
       elif elementType(value) is GPUValue:
         for t in value.litems:
-          assert t.buffer.vk.Valid
+          for i in 0 ..< INFLIGHTFRAMES:
+            assert t.buffer[i].vk.Valid
       else:
         {.error: "Unsupported descriptor set field: '" & theName & "'".}
     else:
@@ -166,8 +167,8 @@
         const descriptorBindingNumber = getBindingNumber[typeof(descriptorSet.data[fI])](theFieldname)
         when typeof(fieldvalue) is GPUValue:
           bufferWrites.add VkDescriptorBufferInfo(
-            buffer: fieldvalue.buffer.vk,
-            offset: fieldvalue.offset,
+            buffer: fieldvalue.buffer[fi].vk,
+            offset: fieldvalue.offset[fi],
             range: fieldvalue.size,
           )
           descriptorSetWrites.add VkWriteDescriptorSet(
@@ -217,7 +218,7 @@
           elif elementType(fieldvalue) is GPUValue:
             for entry in fieldvalue.litems:
               bufferWrites.add VkDescriptorBufferInfo(
-                buffer: entry.buffer.vk, offset: entry.offset, range: entry.size
+                buffer: entry.buffer[fI].vk, offset: entry.offset[fI], range: entry.size
               )
             descriptorSetWrites.add VkWriteDescriptorSet(
               sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
@@ -267,20 +268,23 @@
 
 proc flushAllMemory*(renderData: RenderData) =
   var flushRegions = newSeq[VkMappedMemoryRange]()
+  # var mType = 0'u32;
   for memoryBlocks in renderData.memory.litems:
-    for memoryBlock in memoryBlocks:
-      if memoryBlock.rawPointer != nil and memoryBlock.offsetNextFree > 0:
-        flushRegions.add VkMappedMemoryRange(
-          sType: VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
-          memory: memoryBlock.vk,
-          size: alignedTo(
-            memoryBlock.offsetNextFree,
-            svkGetPhysicalDeviceProperties().limits.nonCoherentAtomSize,
-          ),
-        )
+    # if mType.isMappable() or true:
+      for memoryBlock in memoryBlocks:
+        if memoryBlock.rawPointer != nil and memoryBlock.offsetNextFree > 0:
+          flushRegions.add VkMappedMemoryRange(
+            sType: VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE,
+            memory: memoryBlock.vk,
+            size: alignedTo(
+              memoryBlock.offsetNextFree,
+              svkGetPhysicalDeviceProperties().limits.nonCoherentAtomSize,
+            ),
+          )
+    # mType.inc
   if flushRegions.len > 0:
     checkVkResult vkFlushMappedMemoryRanges(
-      engine().vulkan.device, flushRegions.len.uint32, flushRegions.ToCPointer()
+      engine().vulkan.device, flushRegions.len.uint32, addr flushRegions[0]
     )
 
 proc allocateNewBuffer(
@@ -317,7 +321,6 @@
   template selectedBlock(): untyped =
     renderData.memory[memoryType][selectedBlockI]
 
-  # let selectedBlock =
   renderData.memory[memoryType][selectedBlockI].offsetNextFree =
     alignedTo(selectedBlock.offsetNextFree, memoryRequirements.alignment)
   checkVkResult vkBindBufferMemory(
@@ -325,31 +328,30 @@
   )
   result.memory = selectedBlock.vk
   result.memoryOffset = selectedBlock.offsetNextFree
-  result.rawPointer =
-    selectedBlock.rawPointer.pointerAddOffset(selectedBlock.offsetNextFree)
+  result.rawPointer = selectedBlock.rawPointer.pointerAddOffset(selectedBlock.offsetNextFree)
   renderData.memory[memoryType][selectedBlockI].offsetNextFree += memoryRequirements.size
 
-proc updateGPUBuffer*(gpuData: GPUData, count = 0'u64, flush = false) =
+proc updateGPUBuffer*(gpuData: GPUData, frame: int, count = 0'u64, flush = false) =
   if gpuData.size() == 0:
     return
 
   when needsMapping(gpuData):
     when gpuData is GPUArray:
       copyMem(
-        pointerAddOffset(gpuData.buffer.rawPointer, gpuData.offset),
+        pointerAddOffset(gpuData.buffer[currentFiF()].rawPointer, gpuData.offset[currentFiF()]),
         gpuData.rawPointer,
         gpuData.size(count),
       )
     else:
       copyMem(
-        pointerAddOffset(gpuData.buffer.rawPointer, gpuData.offset),
+        pointerAddOffset(gpuData.buffer[currentFiF()].rawPointer, gpuData.offset[currentFiF()]),
         gpuData.rawPointer,
         gpuData.size(),
       )
     if flush:
-      flushBuffer(gpuData.buffer)
+      flushBuffer(gpuData.buffer[frame])
   else:
-    withStagingBuffer((gpuData.buffer.vk, gpuData.offset), gpuData.size, stagingPtr):
+    withStagingBuffer((gpuData.buffer[frame].vk, gpuData.offset[frame]), gpuData.size, stagingPtr):
       when gpuData is GPUArray:
         copyMem(stagingPtr, gpuData.rawPointer, gpuData.size(count))
       else:
@@ -358,11 +360,13 @@
 proc updateAllGPUBuffers*[T](value: T, flush = false) =
   for name, fieldvalue in value.fieldPairs():
     when typeof(fieldvalue) is GPUData:
-      updateGPUBuffer(fieldvalue, flush = flush)
+      for i in 0 ..< INFLIGHTFRAMES.int:
+        updateGPUBuffer(fieldvalue, frame=i, flush = flush)
     when typeof(fieldvalue) is array:
       when elementType(fieldvalue) is GPUData:
         for entry in fieldvalue.litems:
-          updateGPUBuffer(entry, flush = flush)
+          for i in 0 ..< INFLIGHTFRAMES.int:
+            updateGPUBuffer(entry, frame=i, flush = flush)
 
 proc allocateGPUData(
     renderdata: var RenderData, bufferType: BufferType, size: uint64
@@ -394,15 +398,16 @@
 proc assignBuffers*[T](renderdata: var RenderData, data: var T, uploadData = true) =
   for name, value in fieldPairs(data):
     when typeof(value) is GPUData:
-      (value.buffer, value.offset) =
-        allocateGPUData(renderdata, value.bufferType, value.size)
+      for i in 0 ..< INFLIGHTFRAMES.int:
+        (value.buffer[i], value.offset[i]) = allocateGPUData(renderdata, value.bufferType, value.size)
     elif typeof(value) is DescriptorSetData:
-      for i in 0 ..< INFLIGHTFRAMES:
+      for i in 0 ..< INFLIGHTFRAMES.int:
         assignBuffers(renderdata, value.data[i], uploadData = uploadData)
     elif typeof(value) is array:
       when elementType(value) is GPUValue:
         for v in value.mitems:
-          (v.buffer, v.offset) = allocateGPUData(renderdata, v.bufferType, v.size)
+          for i in 0 ..< INFLIGHTFRAMES.int:
+            (v.buffer[i], v.offset[i]) = allocateGPUData(renderdata, v.bufferType, v.size)
 
   if uploadData:
     updateAllGPUBuffers(data, flush = true)
--- a/semicongine/rendering/renderer.nim	Wed Sep 24 23:55:36 2025 +0700
+++ b/semicongine/rendering/renderer.nim	Thu Sep 25 23:53:41 2025 +0700
@@ -218,8 +218,7 @@
     index: static DescriptorSetIndex,
     layout: VkPipelineLayout,
 ) =
-  assert descriptorSet.vk[currentFiF()].Valid,
-    "DescriptorSetData not initialized, maybe forgot to call initDescriptorSet"
+  assert descriptorSet.vk[currentFiF()].Valid, "DescriptorSetData not initialized, maybe forgot to call initDescriptorSet"
   svkCmdBindDescriptorSet(commandBuffer, descriptorSet.vk[currentFiF()], index, layout)
 
 proc bindDescriptorSet*[TDescriptorSet, TShader](
@@ -286,10 +285,10 @@
       for meshName, meshValue in mesh.fieldPairs:
         when meshName == shaderAttributeName:
           debug("  vertex attr: ", shaderAttributeName)
-          assert meshValue.buffer.vk.Valid,
+          assert meshValue.buffer[currentFiF()].vk.Valid,
             "Mesh vertex-attribute '{TMesh}.{shaderAttributeName}' has no valid buffer (encountered while rendering with '{TShader}')"
-          vertexBuffers.add meshValue.buffer.vk
-          vertexBuffersOffsets.add meshValue.offset
+          vertexBuffers.add meshValue.buffer[currentFiF()].vk
+          vertexBuffersOffsets.add meshValue.offset[currentFiF()]
           if elementCount == 0:
             elementCount = meshValue.data.len.uint32
           else:
@@ -301,8 +300,8 @@
       for instanceName, instanceValue in instances.fieldPairs:
         when instanceName == shaderAttributeName:
           debug("  instnc attr: ", shaderAttributeName)
-          vertexBuffers.add instanceValue.buffer.vk
-          vertexBuffersOffsets.add instanceValue.offset
+          vertexBuffers.add instanceValue.buffer[currentFiF()].vk
+          vertexBuffersOffsets.add instanceValue.offset[currentFiF()]
           if instanceCount == 1:
             instanceCount = instanceValue.data.len.uint32
           else:
@@ -326,18 +325,18 @@
 
   for meshName, meshValue in mesh.fieldPairs:
     when typeof(meshValue) is GPUArray[uint8, IndexBuffer]:
-      indexBuffer = meshValue.buffer.vk
-      indexBufferOffset = meshValue.offset
+      indexBuffer = meshValue.buffer[currentFiF()].vk
+      indexBufferOffset = meshValue.offset[currentFiF()]
       indexType = VK_INDEX_TYPE_UINT8_EXT
       elementCount = meshValue.data.len.uint32
     elif typeof(meshValue) is GPUArray[uint16, IndexBuffer]:
-      indexBuffer = meshValue.buffer.vk
-      indexBufferOffset = meshValue.offset
+      indexBuffer = meshValue.buffer[currentFiF()].vk
+      indexBufferOffset = meshValue.offset[currentFiF()]
       indexType = VK_INDEX_TYPE_UINT16
       elementCount = meshValue.data.len.uint32
     elif typeof(meshValue) is GPUArray[uint32, IndexBuffer]:
-      indexBuffer = meshValue.buffer.vk
-      indexBufferOffset = meshValue.offset
+      indexBuffer = meshValue.buffer[currentFiF()].vk
+      indexBufferOffset = meshValue.offset[currentFiF()]
       indexType = VK_INDEX_TYPE_UINT32
       elementCount = meshValue.data.len.uint32
 
@@ -435,4 +434,6 @@
   render(commandBuffer, pipeline, mesh, EMPTY(), fixedVertexCount, fixedInstanceCount)
 
 proc asDescriptorSetData*[T](data: sink T): auto =
-  DescriptorSetData[T](data: data)
+  var data1: T
+  copyMem(addr(data1), addr(data), sizeof(T))
+  DescriptorSetData[T](data: [data, data1])
--- a/semicongine/rendering/renderpasses.nim	Wed Sep 24 23:55:36 2025 +0700
+++ b/semicongine/rendering/renderpasses.nim	Thu Sep 25 23:53:41 2025 +0700
@@ -4,7 +4,9 @@
 proc createDirectPresentationRenderPass*(
     depthBuffer: bool, samples = VK_SAMPLE_COUNT_1_BIT
 ): RenderPass =
-  result = RenderPass(depthBuffer: depthBuffer, samples: samples)
+  result = new(RenderPass)
+  result.depthBuffer = depthBuffer
+  result.samples = samples
 
   var attachments =
     @[
--- a/tests/test_rendering.nim	Wed Sep 24 23:55:36 2025 +0700
+++ b/tests/test_rendering.nim	Thu Sep 25 23:53:41 2025 +0700
@@ -10,6 +10,7 @@
 import ../semicongine/rendering
 import ../semicongine/input
 import ../semicongine/loaders
+import ../semicongine/images
 
 proc test_01_triangle(time: float32, renderPass: RenderPass) =
   var renderdata = initRenderData()
@@ -374,14 +375,12 @@
   )
   var otherset1 = asDescriptorSetData(
     OtherSet(
-      objectSettings:
-        asGPUValue(ObjectSettings(scale: 1.0, materialIndex: 0), UniformBufferMapped)
+      objectSettings: asGPUValue(ObjectSettings(scale: 1.0, materialIndex: 0), UniformBufferMapped)
     )
   )
   var otherset2 = asDescriptorSetData(
     OtherSet(
-      objectSettings:
-        asGPUValue(ObjectSettings(scale: 1.0, materialIndex: 1), UniformBufferMapped)
+      objectSettings: asGPUValue(ObjectSettings(scale: 1.0, materialIndex: 1), UniformBufferMapped)
     )
   )
 
@@ -400,8 +399,19 @@
   initDescriptorSet(renderdata, pipeline.descriptorSetLayouts[2], otherset1)
   initDescriptorSet(renderdata, pipeline.descriptorSetLayouts[2], otherset2)
 
+  updateGPUBuffer(otherset2.data[0].objectSettings, frame=0)
+  updateGPUBuffer(otherset2.data[1].objectSettings, frame=1)
+
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
+
+    mainset.data[currentFiF()].renderSettings.data.brigthness = ((getMonoTime() - start).inMilliseconds().int / 1000) / time
+    otherset1.data[currentFiF()].objectSettings.data.scale = 0.5 + ((getMonoTime() - start).inMilliseconds().int / 1000) / time
+    updateGPUBuffer(mainset.data[currentFiF()].renderSettings, frame=currentFiF())
+    updateGPUBuffer(otherset1.data[currentFiF()].objectSettings, frame=currentFiF())
+    updateGPUBuffer(otherset2.data[currentFiF()].objectSettings, frame=currentFiF())
+    renderdata.flushAllMemory()
+
     withNextFrame(framebuffer, commandbuffer):
       bindDescriptorSet(commandbuffer, constset, 0, pipeline)
       bindDescriptorSet(commandbuffer, mainset, 1, pipeline)
@@ -421,14 +431,6 @@
           bindDescriptorSet(commandbuffer, otherset2, 2, pipeline)
           render(commandbuffer = commandbuffer, pipeline = pipeline, mesh = quad)
 
-    mainset.data.renderSettings.data.brigthness =
-      ((getMonoTime() - start).inMilliseconds().int / 1000) / time
-    otherset1.data.objectSettings.data.scale =
-      0.5 + ((getMonoTime() - start).inMilliseconds().int / 1000) / time
-    updateGPUBuffer(mainset.data.renderSettings)
-    updateGPUBuffer(otherset1.data.objectSettings)
-    renderdata.flushAllMemory()
-
   # cleanup
   checkVkResult vkDeviceWaitIdle(engine().vulkan.device)
   destroyPipeline(pipeline)
@@ -548,13 +550,13 @@
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
     let tStartLoop = getMonoTime() - tStart
 
-    uniforms1.data.data.data.mvp = (
+    uniforms1.data[currentFiF()].data.data.mvp = (
       projection(-PI / 2, getAspectRatio(), 0.01, 100) * translate(0, 0, 2) *
       rotate(PI / 4, X) * rotate(
         PI * 0.1 * (tStartLoop.inMicroseconds() / 1_000_000), Y
       )
     )
-    updateGPUBuffer(uniforms1.data.data, flush = true)
+    updateGPUBuffer(uniforms1.data[currentFiF()].data, frame=currentFiF(), flush = true)
 
     withNextFrame(framebuffer, commandbuffer):
       withRenderPass(
@@ -831,7 +833,7 @@
   var (offscreenRP, presentRP) =
     createIndirectPresentationRenderPass(depthBuffer = depthBuffer, samples = samples)
 
-  setupSwapchain(renderpass = presentRP)
+  setupSwapchain(renderpass = presentRP, vSync = false, tripleBuffering = false)
 
   var renderdata = initRenderData()
 
@@ -886,19 +888,16 @@
       indices: GPUArray[uint16, IndexBuffer]
 
   var mesh = TriangleMesh(
-    position:
-      asGPUArray([vec3(-0.5, -0.5), vec3(0, 0.5), vec3(0.5, -0.5)], VertexBuffer),
-    color: asGPUArray([vec3(0, 0, 1), vec3(0, 1, 0), vec3(1, 0, 0)], VertexBuffer),
+    position: asGPUArray([vec3(-0.5, -0.5), vec3(0, 0.5), vec3(0.5, -0.5)], VertexBuffer),
+    color:    asGPUArray([vec3(0, 0, 1), vec3(0, 1, 0), vec3(1, 0, 0)], VertexBuffer),
   )
   var quad = QuadMesh(
-    position:
-      asGPUArray([vec2(-1, -1), vec2(-1, 1), vec2(1, 1), vec2(1, -1)], VertexBuffer),
-    indices: asGPUArray([0'u16, 1'u16, 2'u16, 2'u16, 3'u16, 0'u16], IndexBuffer),
+    position: asGPUArray([vec2(-1, -1), vec2(-1, 1), vec2(1, 1), vec2(1, -1)], VertexBuffer),
+    indices:  asGPUArray([0'u16, 1'u16, 2'u16, 2'u16, 3'u16, 0'u16], IndexBuffer),
   )
   var uniforms1 = asDescriptorSetData(
     Uniforms(
-      frameTexture:
-        Image[BGRA](width: frameWidth(), height: frameHeight(), isRenderTarget: true)
+      frameTexture: Image[BGRA](width: frameWidth(), height: frameHeight(), isRenderTarget: true)
     )
   )
   assignBuffers(renderdata, mesh)
@@ -934,7 +933,7 @@
       image = depthImage, format = DEPTH_FORMAT, aspect = VK_IMAGE_ASPECT_DEPTH_BIT
     )
 
-  # create msaa images (will not use the one in the swapchain
+  # create msaa images (will not use the one in the swapchain)
   var
     msaaImage: VkImage
     msaaImageView: VkImageView
@@ -948,26 +947,24 @@
       samples = offscreenRP.samples,
     )
     let requirements = svkGetImageMemoryRequirements(msaaImage)
-    msaaMemory = svkAllocateMemory(
-      requirements.size, bestMemory(mappable = false, filter = requirements.memoryTypes)
-    )
+    msaaMemory = svkAllocateMemory(requirements.size, bestMemory(mappable = false, filter = requirements.memoryTypes))
     checkVkResult vkBindImageMemory(engine().vulkan.device, msaaImage, msaaMemory, 0)
     msaaImageView = svkCreate2DImageView(image = msaaImage, format = SURFACE_FORMAT)
 
   var attachments: seq[VkImageView]
   if offscreenRP.samples == VK_SAMPLE_COUNT_1_BIT:
     if offscreenRP.depthBuffer:
-      attachments = @[uniforms1.data.frameTexture.imageview, depthImageView]
+      attachments = @[uniforms1.data[currentFiF()].frameTexture.imageview, depthImageView]
     else:
-      attachments = @[uniforms1.data.frameTexture.imageview]
+      attachments = @[uniforms1.data[currentFiF()].frameTexture.imageview]
   else:
     if offscreenRP.depthBuffer:
-      attachments =
-        @[msaaImageView, depthImageView, uniforms1.data.frameTexture.imageview]
+      attachments = @[msaaImageView, depthImageView, uniforms1.data[currentFiF()].frameTexture.imageview]
     else:
-      attachments = @[msaaImageView, uniforms1.data.frameTexture.imageview]
-  var offscreenFB =
-    svkCreateFramebuffer(offscreenRP.vk, frameWidth(), frameHeight(), attachments)
+      attachments = @[msaaImageView, uniforms1.data[currentFiF()].frameTexture.imageview]
+  var offscreenFB = svkCreateFramebuffer(offscreenRP.vk, frameWidth(), frameHeight(), attachments)
+
+  renderdata.flushAllMemory()
 
   var start = getMonoTime()
   while ((getMonoTime() - start).inMilliseconds().int / 1000) < time:
@@ -1024,11 +1021,12 @@
     (depthBuffer: true, samples: VK_SAMPLE_COUNT_4_BIT),
   ]
 
+  #[
   # test normal
+  var renderpass: RenderPass
   for i, (depthBuffer, samples) in renderPasses:
-    var renderpass =
-      createDirectPresentationRenderPass(depthBuffer = depthBuffer, samples = samples)
-    setupSwapchain(renderpass = renderpass)
+    renderpass = createDirectPresentationRenderPass(depthBuffer = depthBuffer, samples = samples)
+    setupSwapchain(renderpass = renderpass, vSync = false, tripleBuffering = false)
 
     # tests a simple triangle with minimalistic shader and vertex format
     test_01_triangle(time, renderpass)
@@ -1055,6 +1053,7 @@
     checkVkResult vkDeviceWaitIdle(engine().vulkan.device)
     destroyRenderPass(renderpass)
     clearSwapchain()
+  ]#
 
   # test multiple render passes
   for i, (depthBuffer, samples) in renderPasses: