Mercurial > games > semicongine
view semicongine/renderer.nim @ 1139:114f395b9144
did: finish refactoring and updated all tests accordingly
author | sam <sam@basx.dev> |
---|---|
date | Sat, 08 Jun 2024 14:58:25 +0700 |
parents | 02e1d2658ff5 |
children | dd757eb5ca86 |
line wrap: on
line source
import std/options import std/tables import std/strformat import std/sequtils import std/strutils import std/logging import ./core import ./vulkan/commandbuffer import ./vulkan/buffer import ./vulkan/device import ./vulkan/drawable import ./vulkan/physicaldevice import ./vulkan/pipeline import ./vulkan/renderpass import ./vulkan/swapchain import ./vulkan/shader import ./vulkan/descriptor import ./vulkan/image import ./scene import ./mesh import ./material 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 attributeLocation: Table[string, MemoryPerformanceHint] vertexBufferOffsets: Table[(Mesh, string), uint64] materials: Table[MaterialType, seq[MaterialData]] shaderData: Table[VkPipeline, ShaderData] Renderer* = object device: Device renderPass: RenderPass swapchain: Swapchain scenedata: Table[Scene, SceneData] emptyTexture: VulkanTexture queue: Queue commandBufferPool: CommandBufferPool nextFrameReady: bool = false proc currentFrameCommandBuffer(renderer: Renderer): VkCommandBuffer = renderer.commandBufferPool.buffers[renderer.swapchain.currentInFlight] proc HasScene*(renderer: Renderer, scene: Scene): bool = scene in renderer.scenedata proc InitRenderer*( device: Device, shaders: openArray[(MaterialType, ShaderConfiguration)], clearColor = NewVec4f(0, 0, 0, 0), backFaceCulling = true, vSync = false, inFlightFrames = 2, ): Renderer = assert device.vk.Valid result.device = device result.renderPass = device.CreateRenderPass(shaders, clearColor = clearColor, backFaceCulling = backFaceCulling) let swapchain = device.CreateSwapchain( result.renderPass.vk, device.physicalDevice.GetSurfaceFormats().FilterSurfaceFormat(), vSync = vSync, inFlightFrames = inFlightFrames, ) if not swapchain.isSome: raise newException(Exception, "Unable to create swapchain") result.queue = device.FirstGraphicsQueue().get() result.commandBufferPool = device.CreateCommandBufferPool(result.queue.family, swapchain.get().inFlightFrames) result.swapchain = swapchain.get() result.emptyTexture = device.UploadTexture(result.queue, EMPTY_TEXTURE) func shadersForScene(renderer: Renderer, scene: Scene): seq[(MaterialType, ShaderPipeline)] = for (materialType, shaderPipeline) in renderer.renderPass.shaderPipelines: if scene.UsesMaterial(materialType): result.add (materialType, shaderPipeline) 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) var scenedata = SceneData() # find all material data and group it by material type for mesh in scene.meshes: assert mesh.material != nil, "Mesh {mesh} has no material assigned" if not scenedata.materials.contains(mesh.material.theType): scenedata.materials[mesh.material.theType] = @[] if not scenedata.materials[mesh.material.theType].contains(mesh.material): scenedata.materials[mesh.material.theType].add mesh.material # automatically populate material and tranform attributes for mesh in scene.meshes: if not (TRANSFORM_ATTRIB in mesh[].Attributes): mesh[].InitInstanceAttribute(TRANSFORM_ATTRIB, Unit4) if not (MATERIALINDEX_ATTRIBUTE in mesh[].Attributes): mesh[].InitInstanceAttribute(MATERIALINDEX_ATTRIBUTE, uint16(scenedata.materials[mesh.material.theType].find(mesh.material))) # create index buffer if necessary var indicesBufferSize = 0'u64 for mesh in scene.meshes: if mesh[].indexType != MeshIndexType.None: let indexAlignment = case mesh[].indexType of MeshIndexType.None: 0'u64 of Tiny: 1'u64 of Small: 2'u64 of Big: 4'u64 # index value alignment required by Vulkan if indicesBufferSize mod indexAlignment != 0: indicesBufferSize += indexAlignment - (indicesBufferSize mod indexAlignment) indicesBufferSize += mesh[].IndexSize if indicesBufferSize > 0: scenedata.indexBuffer = renderer.device.CreateBuffer( size = indicesBufferSize, usage = [VK_BUFFER_USAGE_INDEX_BUFFER_BIT], requireMappable = false, preferVRAM = true, ) # calculcate offsets for attributes in vertex buffers # trying to use one buffer per memory type var perLocationSizes: Table[MemoryPerformanceHint, uint64] for hint in MemoryPerformanceHint: perLocationSizes[hint] = 0 let sceneVertexInputs = renderer.vertexInputsForScene(scene) let sceneShaders = renderer.shadersForScene(scene) 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[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: if bufferSize > 0: scenedata.vertexBuffers[memoryPerformanceHint] = renderer.device.CreateBuffer( size = bufferSize, usage = [VK_BUFFER_USAGE_VERTEX_BUFFER_BIT], requireMappable = memoryPerformanceHint == PreferFastWrite, preferVRAM = true, ) # calculate offset of each attribute for all meshes var perLocationOffsets: Table[MemoryPerformanceHint, uint64] var indexBufferOffset = 0'u64 for hint in MemoryPerformanceHint: perLocationOffsets[hint] = 0 for mesh in scene.meshes: 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) if perLocationOffsets[attribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT != 0: perLocationOffsets[attribute.memoryPerformanceHint] += VERTEX_ATTRIB_ALIGNMENT - (perLocationOffsets[attribute.memoryPerformanceHint] mod VERTEX_ATTRIB_ALIGNMENT) # fill offsets per shaderPipeline (as sequence corresponds to shader input binding) var offsets: Table[VkPipeline, seq[(string, MemoryPerformanceHint, uint64)]] 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 var drawable = Drawable( name: mesh.name, elementCount: if indexed: mesh[].IndicesCount else: mesh[].vertexCount, bufferOffsets: offsets, instanceCount: mesh[].InstanceCount, indexed: indexed, ) if indexed: let indexAlignment = case mesh.indexType of MeshIndexType.None: 0'u64 of Tiny: 1'u64 of Small: 2'u64 of Big: 4'u64 # index value alignment required by Vulkan if indexBufferOffset mod indexAlignment != 0: indexBufferOffset += indexAlignment - (indexBufferOffset mod indexAlignment) drawable.indexBufferOffset = indexBufferOffset drawable.indexType = mesh.indexType var (pdata, size) = mesh[].GetRawIndexData() scenedata.indexBuffer.SetData(renderer.queue, pdata, size, indexBufferOffset) indexBufferOffset += size scenedata.drawables.add (drawable, mesh) # setup uniforms and textures (anything descriptor) var uploadedTextures: Table[Texture, VulkanTexture] 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: 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.shaderData[shaderPipeline.vk].descriptorPool = renderer.device.CreateDescriptorSetPool(poolsizes) 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 proc UpdateMeshData*(renderer: var Renderer, scene: var Scene, forceAll = false) = assert scene in renderer.scenedata var addedBarrier = false; for (drawable, mesh) in renderer.scenedata[scene].drawables.mitems: if mesh[].Attributes.contains(TRANSFORM_ATTRIB): mesh[].UpdateInstanceTransforms(TRANSFORM_ATTRIB) let attrs = (if forceAll: mesh[].Attributes else: mesh[].DirtyAttributes) for attribute in attrs: # ignore attributes that are not used in this scene if attribute in renderer.scenedata[scene].attributeLocation: debug &"Update mesh attribute {attribute}" let memoryPerformanceHint = renderer.scenedata[scene].attributeLocation[attribute] # if we have to do a vkCmdCopyBuffer (not buffer.canMap), then we want to added a barrier to # not infer with the current frame that is being renderer (relevant when we have multiple frames in flight) # (remark: ...I think..., I am pretty new to this sync stuff) if not renderer.scenedata[scene].vertexBuffers[memoryPerformanceHint].CanMap and not addedBarrier: WithSingleUseCommandBuffer(renderer.device, renderer.queue, commandBuffer): let barrier = VkMemoryBarrier( sType: VK_STRUCTURE_TYPE_MEMORY_BARRIER, srcAccessMask: [VK_ACCESS_MEMORY_READ_BIT].toBits, dstAccessMask: [VK_ACCESS_MEMORY_WRITE_BIT].toBits, ) commandBuffer.PipelineBarrier( srcStages = [VK_PIPELINE_STAGE_VERTEX_INPUT_BIT], dstStages = [VK_PIPELINE_STAGE_TRANSFER_BIT], memoryBarriers = [barrier] ) addedBarrier = true renderer.scenedata[scene].vertexBuffers[memoryPerformanceHint].SetData( renderer.queue, mesh[].GetPointer(attribute), mesh[].AttributeSize(attribute), renderer.scenedata[scene].vertexBufferOffsets[(mesh, attribute)] ) mesh[].ClearDirtyAttributes() proc UpdateUniformData*(renderer: var Renderer, scene: var Scene, forceAll = false) = assert scene in renderer.scenedata let dirty = scene.DirtyShaderGlobals if forceAll: debug "Update uniforms because 'forceAll' was given" elif dirty.len > 0: debug &"Update uniforms because of dirty scene globals: {dirty}" # loop over all used shaders/pipelines 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].shaderData[shaderPipeline.vk].uniformBuffers[renderer.swapchain.currentInFlight].vk.Valid if forceAll: for buffer in renderer.scenedata[scene].shaderData[shaderPipeline.vk].uniformBuffers: assert buffer.vk.Valid var offset = 0'u64 # loop over all uniforms of the shader-shaderPipeline for uniform in shaderPipeline.Uniforms: if dirty.contains(uniform.name) or dirtyMaterialAttribs.contains(uniform.name) or forceAll: # only update uniforms if necessary var value = InitDataList(uniform.theType) if scene.shaderGlobals.hasKey(uniform.name): assert scene.shaderGlobals[uniform.name].thetype == uniform.thetype value = scene.shaderGlobals[uniform.name] else: var foundValue = false for material in renderer.scenedata[scene].materials[materialType]: if material.HasMatchingAttribute(uniform): value.AppendValues(material[uniform.name]) foundValue = true assert foundValue, &"Uniform '{uniform.name}' not found in scene shaderGlobals or materials" assert (uniform.arrayCount == 0 and value.len == 1) or value.len.uint <= uniform.arrayCount, &"Uniform '{uniform.name}' found has wrong length (shader declares {uniform.arrayCount} but shaderGlobals and materials provide {value.len})" if value.len.uint <= uniform.arrayCount: debug &"Uniform '{uniform.name}' found has short length (shader declares {uniform.arrayCount} but shaderGlobals and materials provide {value.len})" assert value.Size <= uniform.Size, &"During uniform update: gathered value has size {value.Size} but uniform expects size {uniform.Size}" if value.Size < uniform.Size: debug &"During uniform update: gathered value has size {value.Size} but uniform expects size {uniform.Size}" debug &" update uniform '{uniform.name}' with value: {value}" # 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].shaderData[shaderPipeline.vk].uniformBuffers: buffer.SetData(renderer.queue, value.GetPointer(), value.Size, offset) offset += uniform.Size scene.ClearDirtyShaderGlobals() proc StartNewFrame*(renderer: var Renderer) = # TODO: chance for an infinity-loop? while not renderer.swapchain.AcquireNextFrame(): checkVkResult renderer.device.vk.vkDeviceWaitIdle() let res = renderer.swapchain.Recreate() if res.isSome: var oldSwapchain = renderer.swapchain renderer.swapchain = res.get() checkVkResult renderer.device.vk.vkDeviceWaitIdle() oldSwapchain.Destroy() renderer.nextFrameReady = true proc Render*(renderer: var Renderer, scene: Scene) = assert scene in renderer.scenedata assert renderer.nextFrameReady, "startNewFrame() must be called before calling render()" # preparation renderer.currentFrameCommandBuffer.BeginRenderCommands(renderer.renderPass, renderer.swapchain.CurrentFramebuffer(), oneTimeSubmit = true) # debug output debug "Scene buffers:" for (location, buffer) in renderer.scenedata[scene].vertexBuffers.pairs: debug " ", location, ": ", buffer debug " Index buffer: ", renderer.scenedata[scene].indexBuffer # draw all meshes for (materialType, shaderPipeline) in renderer.renderPass.shaderPipelines: 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].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) # done rendering renderer.currentFrameCommandBuffer.EndRenderCommands() # swap framebuffer if not renderer.swapchain.Swap(renderer.queue, renderer.currentFrameCommandBuffer): let res = renderer.swapchain.Recreate() if res.isSome: var oldSwapchain = renderer.swapchain renderer.swapchain = res.get() checkVkResult renderer.device.vk.vkDeviceWaitIdle() oldSwapchain.Destroy() renderer.swapchain.currentInFlight = (renderer.swapchain.currentInFlight + 1) mod renderer.swapchain.inFlightFrames renderer.nextFrameReady = false func Valid*(renderer: Renderer): bool = renderer.device.vk.Valid 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() var destroyedTextures: seq[VkImage] for (vkPipeline, shaderData) in scenedata.shaderData.mpairs: for buffer in shaderData.uniformBuffers.mitems: assert buffer.vk.Valid buffer.Destroy() 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() shaderData.descriptorPool.Destroy() renderer.scenedata.del(scene) proc Destroy*(renderer: var Renderer) = for scene in renderer.scenedata.keys.toSeq: renderer.Destroy(scene) assert renderer.scenedata.len == 0 renderer.emptyTexture.Destroy() renderer.renderPass.Destroy() renderer.commandBufferPool.Destroy() renderer.swapchain.Destroy()