diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c3f44e..bf32298 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,34 +1,34 @@ -name: Github Actions -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - workflow_dispatch: -jobs: - build: - strategy: - fail-fast: false - matrix: - os: [windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v5 - - uses: treeform/setup-nim-action@v6 - - name: Install dependencies - shell: bash - run: | - cd .. - nimby create - nimby install windy - - name: Install Vulkan SDK - run: winget install -e --id KhronosGroup.VulkanSDK --accept-package-agreements --accept-source-agreements - - run: nim check src/vk14.nim - - run: nim r tests/tests.nim - - run: nim r examples/compile_shaders.nim - - run: nim c examples/basic_screen.nim - - run: nim c examples/basic_triangle.nim - - run: nim c examples/basic_quad.nim - - run: nim c examples/basic_cube.nim - - run: nim c examples/viewer_obj.nim - - run: nim c examples/sprite_sheet.nim +name: Github Actions +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + workflow_dispatch: +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - uses: treeform/setup-nim-action@v6 + - name: Install dependencies + shell: bash + run: | + cd .. + nimby create + nimby install windy + - name: Install Vulkan SDK + run: winget install -e --id KhronosGroup.VulkanSDK --accept-package-agreements --accept-source-agreements + - run: nim check src/vk14.nim + - run: nim r tests/tests.nim + - run: nim r examples/compile_shaders.nim + - run: nim c examples/basic_screen.nim + - run: nim c examples/basic_triangle.nim + - run: nim c examples/basic_quad.nim + - run: nim c examples/basic_cube.nim + - run: nim c examples/viewer_obj.nim + - run: nim c examples/sprite_sheet.nim diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f7af62a..0535105 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,32 +1,32 @@ -name: docs -on: - push: - branches: - - master - workflow_dispatch: -permissions: - contents: write -env: - nim-src: src/vk14.nim - deploy-dir: .gh-pages -jobs: - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: treeform/setup-nim-action@v6 - - name: Restore nimdoc assets - run: | - mkdir -p "$HOME/.nimby/nim/doc" - mkdir -p "$HOME/.nimby/nim/tools/dochack" - curl -fsSL "https://nim-lang.org/docs/nimdoc.out.css?v=2.2.8" -o "$HOME/.nimby/nim/doc/nimdoc.css" - curl -fsSL "https://raw.githubusercontent.com/nim-lang/Nim/v2.2.8/tools/dochack/dochack.nim" -o "$HOME/.nimby/nim/tools/dochack/dochack.nim" - curl -fsSL "https://raw.githubusercontent.com/nim-lang/Nim/v2.2.8/tools/dochack/fuzzysearch.nim" -o "$HOME/.nimby/nim/tools/dochack/fuzzysearch.nim" - - run: nim doc --index:on --project --git.url:https://github.com/${{ github.repository }} --git.commit:master --out:${{ env.deploy-dir }} ${{ env.nim-src }} - - name: Copy to index.html - run: cp ${{ env.deploy-dir }}/vk14.html ${{ env.deploy-dir }}/index.html - - name: Deploy documents - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ${{ env.deploy-dir }} +name: docs +on: + push: + branches: + - master + workflow_dispatch: +permissions: + contents: write +env: + nim-src: src/vk14.nim + deploy-dir: .gh-pages +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: treeform/setup-nim-action@v6 + - name: Restore nimdoc assets + run: | + mkdir -p "$HOME/.nimby/nim/doc" + mkdir -p "$HOME/.nimby/nim/tools/dochack" + curl -fsSL "https://nim-lang.org/docs/nimdoc.out.css?v=2.2.8" -o "$HOME/.nimby/nim/doc/nimdoc.css" + curl -fsSL "https://raw.githubusercontent.com/nim-lang/Nim/v2.2.8/tools/dochack/dochack.nim" -o "$HOME/.nimby/nim/tools/dochack/dochack.nim" + curl -fsSL "https://raw.githubusercontent.com/nim-lang/Nim/v2.2.8/tools/dochack/fuzzysearch.nim" -o "$HOME/.nimby/nim/tools/dochack/fuzzysearch.nim" + - run: nim doc --index:on --project --git.url:https://github.com/${{ github.repository }} --git.commit:master --out:${{ env.deploy-dir }} ${{ env.nim-src }} + - name: Copy to index.html + run: cp ${{ env.deploy-dir }}/vk14.html ${{ env.deploy-dir }}/index.html + - name: Deploy documents + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ${{ env.deploy-dir }} diff --git a/.gitignore b/.gitignore index 88dfd40..a2640cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,21 @@ -# Ignore files with no extention. -* -!*/ -!*.* -!LICENSE - -# Ignore hidden files, but not the license or github files. -.* -!.github/ -!.github/** -!.gitignore - -# Normal ignores. -*.exe -nimcache -*.pdb -*.ilk -*.out - -# Project specific ignores. -*.spv +# Ignore files with no extention. +* +!*/ +!*.* +!LICENSE + +# Ignore hidden files, but not the license or github files. +.* +!.github/ +!.github/** +!.gitignore + +# Normal ignores. +*.exe +nimcache +*.pdb +*.ilk +*.out + +# Project specific ignores. +*.spv diff --git a/LICENSE b/LICENSE index 5620f97..abd14a2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2025 Andre von Houck - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2025 Andre von Houck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/basic_cube.nim b/examples/basic_cube.nim index 6bcbd77..82fefd1 100644 --- a/examples/basic_cube.nim +++ b/examples/basic_cube.nim @@ -1,1193 +1,1193 @@ -import - std/os, - pixie, - vmath, - windy, - vk14 - -const - Width = 1280 - Height = 800 - TexturePath = currentSourcePath().parentDir / "testTexture.png" - PreferredMsaaSamples = 4'u32 - ClearColor = [0.05'f32, 0.05'f32, 0.1'f32, 1.0'f32] - -type - CubeVertex = object - position: array[3, float32] - uv: array[2, float32] - - CubeRenderer = object - descriptorSetLayout: VkDescriptorSetLayout - pipelineLayout: VkPipelineLayout - pipeline: VkPipeline - sampleCount: VkSampleCountFlagBits - imageViews: seq[VkImageView] - colorImages: seq[VkImage] - colorImageMemories: seq[VkDeviceMemory] - colorImageViews: seq[VkImageView] - commandBuffers: seq[VkCommandBuffer] - vertexBuffer: VkBuffer - vertexBufferMemory: VkDeviceMemory - textureImage: VkImage - textureImageMemory: VkDeviceMemory - textureImageView: VkImageView - textureSampler: VkSampler - textureMipLevels: uint32 - descriptorPool: VkDescriptorPool - descriptorSet: VkDescriptorSet - depthImages: seq[VkImage] - depthImageMemories: seq[VkDeviceMemory] - depthImageViews: seq[VkImageView] - transform: array[16, float32] - frameCounter: uint64 - -const - CubeVertices = [ - CubeVertex(position: [-1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), - CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [1.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [0.0'f32, 1.0'f32]), - CubeVertex(position: [1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), - - CubeVertex(position: [1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), - CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [1.0'f32, 0.0'f32]), - CubeVertex(position: [1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [0.0'f32, 1.0'f32]), - CubeVertex(position: [-1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), - - CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), - CubeVertex(position: [-1.0'f32, 1.0'f32, 1.0'f32], uv: [1.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, -1.0'f32, -1.0'f32], uv: [0.0'f32, 1.0'f32]), - CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), - - CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), - CubeVertex(position: [1.0'f32, 1.0'f32, -1.0'f32], uv: [1.0'f32, 0.0'f32]), - CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [1.0'f32, -1.0'f32, 1.0'f32], uv: [0.0'f32, 1.0'f32]), - CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), - - CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), - CubeVertex(position: [1.0'f32, 1.0'f32, -1.0'f32], uv: [1.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 1.0'f32]), - CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), - - CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), - CubeVertex(position: [1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), - CubeVertex(position: [-1.0'f32, -1.0'f32, -1.0'f32], uv: [0.0'f32, 1.0'f32]), - CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]) - ] - -proc checkVk(result: VkResult, action: string) = - if result != VK_SUCCESS: - raise newException(Exception, action & " failed with VkResult " & $result.int32) - -proc requiresSwapChainRecreate(vkResult: VkResult): bool = - ## Returns true if the swapchain is out of date or suboptimal. - let code = vkResult.int32 - code == VK_SUBOPTIMAL_KHR.int32 or - code == VK_ERROR_OUT_OF_DATE_KHR.int32 - -proc identityMatrix(): Mat4 = - gmat4[float32]( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ) - -proc perspectiveDx(fovY, aspect, nearPlane, farPlane: float32): Mat4 = - let - h = 1.0'f32 / tan(fovY * 0.5'f32) - w = h / aspect - depth = farPlane - nearPlane - result[0, 0] = w - result[0, 1] = 0 - result[0, 2] = 0 - result[0, 3] = 0 - result[1, 0] = 0 - result[1, 1] = h - result[1, 2] = 0 - result[1, 3] = 0 - result[2, 0] = 0 - result[2, 1] = 0 - result[2, 2] = farPlane / depth - result[2, 3] = 1 - result[3, 0] = 0 - result[3, 1] = 0 - result[3, 2] = -(nearPlane * farPlane) / depth - result[3, 3] = 0 - -proc mat4ToArray(matrix: Mat4): array[16, float32] = - var index = 0 - for i in 0 ..< 4: - for j in 0 ..< 4: - result[index] = matrix[i, j] - inc index - -proc updateTransform(renderer: var CubeRenderer, aspect: float32) = - let - time = renderer.frameCounter.float32 / 60.0'f32 - model = - translate(vec3(0.0'f32, 0.0'f32, 5.0'f32)) * - rotateY(time * 0.7'f32) * - rotateX(time * 0.35'f32) - view = identityMatrix() - proj = perspectiveDx(60.0'f32.toRadians, aspect, 0.1'f32, 100.0'f32) - mvp = proj * view * model - renderer.transform = mat4ToArray(mvp) - -proc buildMipChain(image: Image): seq[Image] = - result.add(image) - var current = image - while current.width > 1 or current.height > 1: - current = current.minifyBy2() - result.add(current) - -proc findMemoryType(ctx: VulkanContext, typeFilter: uint32, properties: uint32): uint32 = - var memProperties: VkPhysicalDeviceMemoryProperties - vkGetPhysicalDeviceMemoryProperties(ctx.physicalDevice, memProperties.addr) - for i in 0'u32 ..< memProperties.memoryTypeCount: - let flags = memProperties.memoryTypes[i].propertyFlags.uint32 - if ((typeFilter shr i) and 1'u32) == 1'u32 and (flags and properties) == properties: - return i - raise newException(Exception, "Failed to find suitable Vulkan memory type") - -proc chooseMsaaSampleCount(ctx: VulkanContext): VkSampleCountFlagBits = - var props: VkPhysicalDeviceProperties - vkGetPhysicalDeviceProperties(ctx.physicalDevice, props.addr) - let counts = props.limits.framebufferColorSampleCounts.uint32 and - props.limits.framebufferDepthSampleCounts.uint32 - - if PreferredMsaaSamples >= 4 and (counts and VK_SAMPLE_COUNT_4_BIT.uint32) != 0: - return VK_SAMPLE_COUNT_4_BIT - if (counts and VK_SAMPLE_COUNT_2_BIT.uint32) != 0: - return VK_SAMPLE_COUNT_2_BIT - VK_SAMPLE_COUNT_1_BIT - -proc msaaEnabled(renderer: CubeRenderer): bool = - renderer.sampleCount != VK_SAMPLE_COUNT_1_BIT - -proc createShaderModule(device: VkDevice, code: string): VkShaderModule = - var createInfo = VkShaderModuleCreateInfo( - sType: VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, - codeSize: code.len.uint32, - pCode: cast[ptr uint32](code[0].unsafeAddr) - ) - checkVk(vkCreateShaderModule(device, createInfo.addr, nil, result.addr), "Creating shader module") - -proc createBuffer( - ctx: VulkanContext, - size: VkDeviceSize, - usage: uint32, - properties: uint32, - buffer: var VkBuffer, - memory: var VkDeviceMemory -) = - var bufferInfo = VkBufferCreateInfo( - sType: VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, - size: size, - usage: VkBufferUsageFlags(usage), - sharingMode: VK_SHARING_MODE_EXCLUSIVE, - queueFamilyIndexCount: 0, - pQueueFamilyIndices: nil - ) - checkVk(vkCreateBuffer(ctx.device, bufferInfo.addr, nil, buffer.addr), "Creating buffer") - - var memRequirements: VkMemoryRequirements - vkGetBufferMemoryRequirements(ctx.device, buffer, memRequirements.addr) - - var allocInfo = VkMemoryAllocateInfo( - sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - allocationSize: memRequirements.size, - memoryTypeIndex: findMemoryType(ctx, memRequirements.memoryTypeBits, properties) - ) - checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), "Allocating buffer memory") - checkVk(vkBindBufferMemory(ctx.device, buffer, memory, VkDeviceSize(0)), "Binding buffer memory") - -proc beginSingleTimeCommands(ctx: VulkanContext): VkCommandBuffer = - var allocInfo = VkCommandBufferAllocateInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - commandPool: ctx.commandPool, - level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, - commandBufferCount: 1 - ) - checkVk(vkAllocateCommandBuffers(ctx.device, allocInfo.addr, result.addr), "Allocating single-use command buffer") - var beginInfo = VkCommandBufferBeginInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - flags: VkCommandBufferUsageFlags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) - ) - checkVk(vkBeginCommandBuffer(result, beginInfo.addr), "Beginning single-use command buffer") - -proc endSingleTimeCommands(ctx: VulkanContext, commandBuffer: VkCommandBuffer) = - checkVk(vkEndCommandBuffer(commandBuffer), "Ending single-use command buffer") - var - commandBufferInfo = VkCommandBufferSubmitInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, - commandBuffer: commandBuffer, - deviceMask: 0 - ) - submitInfo = VkSubmitInfo2( - sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, - commandBufferInfoCount: 1, - pCommandBufferInfos: commandBufferInfo.addr - ) - checkVk( - vkQueueSubmit2(ctx.graphicsQueue, 1, submitInfo.addr, VkFence(0)), - "Submitting single-use command buffer" - ) - checkVk(vkQueueWaitIdle(ctx.graphicsQueue), "Waiting for graphics queue idle") - vkFreeCommandBuffers(ctx.device, ctx.commandPool, 1, unsafeAddr commandBuffer) - -proc createImage( - ctx: VulkanContext, - width, height: int, - format: VkFormat, - tiling: VkImageTiling, - usage: uint32, - properties: uint32, - mipLevels: uint32, - samples: VkSampleCountFlagBits, - image: var VkImage, - memory: var VkDeviceMemory -) = - var imageInfo = VkImageCreateInfo( - sType: VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - imageType: VK_IMAGE_TYPE_2D, - format: format, - extent: VkExtent3D(width: uint32(width), height: uint32(height), depth: 1), - mipLevels: mipLevels, - arrayLayers: 1, - samples: samples, - tiling: tiling, - usage: VkImageUsageFlags(usage), - sharingMode: VK_SHARING_MODE_EXCLUSIVE, - initialLayout: VK_IMAGE_LAYOUT_UNDEFINED - ) - checkVk(vkCreateImage(ctx.device, imageInfo.addr, nil, image.addr), "Creating image") - - var memRequirements: VkMemoryRequirements - vkGetImageMemoryRequirements(ctx.device, image, memRequirements.addr) - - var allocInfo = VkMemoryAllocateInfo( - sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - allocationSize: memRequirements.size, - memoryTypeIndex: findMemoryType(ctx, memRequirements.memoryTypeBits, properties) - ) - checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), "Allocating image memory") - checkVk(vkBindImageMemory(ctx.device, image, memory, VkDeviceSize(0)), "Binding image memory") - -proc transitionImageLayout( - ctx: VulkanContext, - image: VkImage, - oldLayout, newLayout: VkImageLayout, - aspectMask: VkImageAspectFlags, - mipLevels: uint32 -) = - let commandBuffer = beginSingleTimeCommands(ctx) - var barrier = VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - oldLayout: oldLayout, - newLayout: newLayout, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: image, - subresourceRange: VkImageSubresourceRange( - aspectMask: aspectMask, - baseMipLevel: 0, - levelCount: mipLevels, - baseArrayLayer: 0, - layerCount: 1 - ) - ) - - var - sourceStage: VkPipelineStageFlags2 - destinationStage: VkPipelineStageFlags2 - srcAccess: VkAccessFlags2 - dstAccess: VkAccessFlags2 - - if oldLayout == VK_IMAGE_LAYOUT_UNDEFINED and newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: - srcAccess = VkAccessFlags2(VK_ACCESS_2_NONE) - dstAccess = VkAccessFlags2(VK_ACCESS_2_TRANSFER_WRITE_BIT) - sourceStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE) - destinationStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT) - elif oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL and newLayout == VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL: - srcAccess = VkAccessFlags2(VK_ACCESS_2_TRANSFER_WRITE_BIT) - dstAccess = VkAccessFlags2(VK_ACCESS_2_SHADER_READ_BIT) - sourceStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT) - destinationStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT) - elif oldLayout == VK_IMAGE_LAYOUT_UNDEFINED and newLayout == VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL: - srcAccess = VkAccessFlags2(VK_ACCESS_2_NONE) - dstAccess = - if aspectMask.uint32 == VK_IMAGE_ASPECT_DEPTH_BIT: - VkAccessFlags2(VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT) - else: - VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT) - sourceStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE) - destinationStage = - if aspectMask.uint32 == VK_IMAGE_ASPECT_DEPTH_BIT: - VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT) - else: - VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT) - else: - raise newException(Exception, "Unsupported image layout transition") - - barrier.srcStageMask = sourceStage - barrier.srcAccessMask = srcAccess - barrier.dstStageMask = destinationStage - barrier.dstAccessMask = dstAccess - - var dependencyInfo = VkDependencyInfo( - sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - imageMemoryBarrierCount: 1, - pImageMemoryBarriers: barrier.addr - ) - vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) - endSingleTimeCommands(ctx, commandBuffer) - -proc createTextureResources(ctx: VulkanContext, renderer: var CubeRenderer) = - ## Loads the texture, builds mip chain, and uploads to a device-local image. - let image = readImage(TexturePath) - let - mipImages = buildMipChain(image) - texWidth = mipImages[0].width - texHeight = mipImages[0].height - renderer.textureMipLevels = uint32(mipImages.len) - - var - imageSize = VkDeviceSize(0) - mipOffsets = newSeq[VkDeviceSize](mipImages.len) - for i, mipImage in mipImages: - mipOffsets[i] = imageSize - imageSize = VkDeviceSize(imageSize.uint64 + uint64(mipImage.width * mipImage.height * 4)) - - var stagingBuffer: VkBuffer - var stagingBufferMemory: VkDeviceMemory - createBuffer( - ctx, - imageSize, - VK_BUFFER_USAGE_TRANSFER_SRC_BIT.uint32, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, - stagingBuffer, - stagingBufferMemory - ) - - var mapped: pointer - checkVk( - vkMapMemory(ctx.device, stagingBufferMemory, VkDeviceSize(0), imageSize, VkMemoryMapFlags(0), mapped.addr), - "Mapping staging buffer memory" - ) - for i, mipImage in mipImages: - let dst = cast[pointer](cast[uint](mapped) + uint(mipOffsets[i])) - for y in 0 ..< mipImage.height: - let srcIdx = mipImage.dataIndex(0, y) - let srcPtr = cast[ptr uint8](mipImage.data[srcIdx].addr) - copyMem( - cast[pointer](cast[uint](dst) + uint(y * mipImage.width * 4)), - srcPtr, - mipImage.width * 4 - ) - - vkUnmapMemory(ctx.device, stagingBufferMemory) - - createImage( - ctx, - texWidth, - texHeight, - VK_FORMAT_R8G8B8A8_SRGB, - VK_IMAGE_TILING_OPTIMAL, - VK_IMAGE_USAGE_TRANSFER_DST_BIT.uint32 or VK_IMAGE_USAGE_SAMPLED_BIT.uint32, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.uint32, - renderer.textureMipLevels, - VK_SAMPLE_COUNT_1_BIT, - renderer.textureImage, - renderer.textureImageMemory - ) - transitionImageLayout( - ctx, - renderer.textureImage, - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - renderer.textureMipLevels - ) - let commandBuffer = beginSingleTimeCommands(ctx) - var regions = newSeq[VkBufferImageCopy](mipImages.len) - for i, mipImage in mipImages: - regions[i] = VkBufferImageCopy( - bufferOffset: mipOffsets[i], - bufferRowLength: 0, - bufferImageHeight: 0, - imageSubresource: VkImageSubresourceLayers( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - mipLevel: uint32(i), - baseArrayLayer: 0, - layerCount: 1 - ), - imageOffset: VkOffset3D(x: 0, y: 0, z: 0), - imageExtent: VkExtent3D(width: uint32(mipImage.width), height: uint32(mipImage.height), depth: 1) - ) - vkCmdCopyBufferToImage( - commandBuffer, - stagingBuffer, - renderer.textureImage, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - uint32(regions.len), - regions[0].addr - ) - endSingleTimeCommands(ctx, commandBuffer) - transitionImageLayout( - ctx, - renderer.textureImage, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL, - VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - renderer.textureMipLevels - ) - - vkDestroyBuffer(ctx.device, stagingBuffer, nil) - vkFreeMemory(ctx.device, stagingBufferMemory, nil) - - var imageViewInfo = VkImageViewCreateInfo( - sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - image: renderer.textureImage, - viewType: VK_IMAGE_VIEW_TYPE_2D, - format: VK_FORMAT_R8G8B8A8_SRGB, - components: VkComponentMapping( - r: VK_COMPONENT_SWIZZLE_IDENTITY, - g: VK_COMPONENT_SWIZZLE_IDENTITY, - b: VK_COMPONENT_SWIZZLE_IDENTITY, - a: VK_COMPONENT_SWIZZLE_IDENTITY - ), - subresourceRange: VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - baseMipLevel: 0, - levelCount: renderer.textureMipLevels, - baseArrayLayer: 0, - layerCount: 1 - ) - ) - checkVk(vkCreateImageView(ctx.device, imageViewInfo.addr, nil, renderer.textureImageView.addr), "Creating texture image view") - - var samplerInfo = VkSamplerCreateInfo( - sType: VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, - magFilter: VK_FILTER_LINEAR, - minFilter: VK_FILTER_LINEAR, - mipmapMode: VK_SAMPLER_MIPMAP_MODE_LINEAR, - addressModeU: VK_SAMPLER_ADDRESS_MODE_REPEAT, - addressModeV: VK_SAMPLER_ADDRESS_MODE_REPEAT, - addressModeW: VK_SAMPLER_ADDRESS_MODE_REPEAT, - mipLodBias: 0, - anisotropyEnable: VkBool32(VK_FALSE), - maxAnisotropy: 1, - compareEnable: VkBool32(VK_FALSE), - compareOp: VK_COMPARE_OP_ALWAYS, - minLod: 0, - maxLod: float32(renderer.textureMipLevels - 1), - borderColor: VK_BORDER_COLOR_INT_OPAQUE_BLACK, - unnormalizedCoordinates: VkBool32(VK_FALSE) - ) - checkVk(vkCreateSampler(ctx.device, samplerInfo.addr, nil, renderer.textureSampler.addr), "Creating texture sampler") - -proc createDescriptorSetLayout(ctx: VulkanContext, renderer: var CubeRenderer) = - var layoutBinding = VkDescriptorSetLayoutBinding( - binding: 0, - descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - descriptorCount: 1, - stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_FRAGMENT_BIT), - pImmutableSamplers: nil - ) - var layoutInfo = VkDescriptorSetLayoutCreateInfo( - sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, - bindingCount: 1, - pBindings: layoutBinding.addr - ) - checkVk(vkCreateDescriptorSetLayout(ctx.device, layoutInfo.addr, nil, renderer.descriptorSetLayout.addr), "Creating descriptor set layout") - -proc createDescriptorResources(ctx: VulkanContext, renderer: var CubeRenderer) = - var poolSize = VkDescriptorPoolSize(`type`: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, descriptorCount: 1) - var poolInfo = VkDescriptorPoolCreateInfo( - sType: VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, - maxSets: 1, - poolSizeCount: 1, - pPoolSizes: poolSize.addr - ) - checkVk(vkCreateDescriptorPool(ctx.device, poolInfo.addr, nil, renderer.descriptorPool.addr), "Creating descriptor pool") - - var setLayout = renderer.descriptorSetLayout - var allocInfo = VkDescriptorSetAllocateInfo( - sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, - descriptorPool: renderer.descriptorPool, - descriptorSetCount: 1, - pSetLayouts: setLayout.addr - ) - checkVk(vkAllocateDescriptorSets(ctx.device, allocInfo.addr, renderer.descriptorSet.addr), "Allocating descriptor set") - - var imageInfo = VkDescriptorImageInfo( - sampler: renderer.textureSampler, - imageView: renderer.textureImageView, - imageLayout: VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL - ) - var descriptorWrite = VkWriteDescriptorSet( - sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - dstSet: renderer.descriptorSet, - dstBinding: 0, - dstArrayElement: 0, - descriptorCount: 1, - descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - pImageInfo: imageInfo.addr, - pBufferInfo: nil, - pTexelBufferView: nil - ) - vkUpdateDescriptorSets(ctx.device, 1, descriptorWrite.addr, 0, nil) - -proc createSwapChainImageViews(ctx: VulkanContext, renderer: var CubeRenderer) = - renderer.imageViews.setLen(ctx.swapChainImages.len) - for i, image in ctx.swapChainImages: - var createInfo = VkImageViewCreateInfo( - sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - image: image, - viewType: VK_IMAGE_VIEW_TYPE_2D, - format: ctx.swapChainImageFormat, - components: VkComponentMapping( - r: VK_COMPONENT_SWIZZLE_IDENTITY, - g: VK_COMPONENT_SWIZZLE_IDENTITY, - b: VK_COMPONENT_SWIZZLE_IDENTITY, - a: VK_COMPONENT_SWIZZLE_IDENTITY - ), - subresourceRange: VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - ) - checkVk(vkCreateImageView(ctx.device, createInfo.addr, nil, renderer.imageViews[i].addr), "Creating swapchain image view") - -proc createMsaaResources(ctx: VulkanContext, renderer: var CubeRenderer) = - let attachmentCount = ctx.swapChainImages.len - renderer.depthImages.setLen(attachmentCount) - renderer.depthImageMemories.setLen(attachmentCount) - renderer.depthImageViews.setLen(attachmentCount) - - if renderer.msaaEnabled: - renderer.colorImages.setLen(attachmentCount) - renderer.colorImageMemories.setLen(attachmentCount) - renderer.colorImageViews.setLen(attachmentCount) - else: - renderer.colorImages.setLen(0) - renderer.colorImageMemories.setLen(0) - renderer.colorImageViews.setLen(0) - - for i in 0 ..< attachmentCount: - if renderer.msaaEnabled: - createImage( - ctx, - int(ctx.swapChainExtent.width), - int(ctx.swapChainExtent.height), - ctx.swapChainImageFormat, - VK_IMAGE_TILING_OPTIMAL, - VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT.uint32 or VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT.uint32, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.uint32, - 1, - renderer.sampleCount, - renderer.colorImages[i], - renderer.colorImageMemories[i] - ) - - var colorViewInfo = VkImageViewCreateInfo( - sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - image: renderer.colorImages[i], - viewType: VK_IMAGE_VIEW_TYPE_2D, - format: ctx.swapChainImageFormat, - components: VkComponentMapping( - r: VK_COMPONENT_SWIZZLE_IDENTITY, - g: VK_COMPONENT_SWIZZLE_IDENTITY, - b: VK_COMPONENT_SWIZZLE_IDENTITY, - a: VK_COMPONENT_SWIZZLE_IDENTITY - ), - subresourceRange: VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - ) - checkVk(vkCreateImageView(ctx.device, colorViewInfo.addr, nil, renderer.colorImageViews[i].addr), "Creating MSAA color image view") - - createImage( - ctx, - int(ctx.swapChainExtent.width), - int(ctx.swapChainExtent.height), - VK_FORMAT_D32_SFLOAT, - VK_IMAGE_TILING_OPTIMAL, - VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT.uint32, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.uint32, - 1, - renderer.sampleCount, - renderer.depthImages[i], - renderer.depthImageMemories[i] - ) - - var depthViewInfo = VkImageViewCreateInfo( - sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - image: renderer.depthImages[i], - viewType: VK_IMAGE_VIEW_TYPE_2D, - format: VK_FORMAT_D32_SFLOAT, - components: VkComponentMapping( - r: VK_COMPONENT_SWIZZLE_IDENTITY, - g: VK_COMPONENT_SWIZZLE_IDENTITY, - b: VK_COMPONENT_SWIZZLE_IDENTITY, - a: VK_COMPONENT_SWIZZLE_IDENTITY - ), - subresourceRange: VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - ) - checkVk(vkCreateImageView(ctx.device, depthViewInfo.addr, nil, renderer.depthImageViews[i].addr), "Creating MSAA depth image view") - -proc createGraphicsPipeline(ctx: VulkanContext, renderer: var CubeRenderer) = - const - vertShaderCode = staticRead("shaders/basic_cube.vert.spv") - fragShaderCode = staticRead("shaders/basic_cube.frag.spv") - let - vertShaderModule = createShaderModule(ctx.device, vertShaderCode) - fragShaderModule = createShaderModule(ctx.device, fragShaderCode) - try: - var - colorFormat = ctx.swapChainImageFormat - renderingInfo = VkPipelineRenderingCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, - colorAttachmentCount: 1, - pColorAttachmentFormats: colorFormat.addr, - depthAttachmentFormat: VK_FORMAT_D32_SFLOAT - ) - dynamicStates = [ - VkDynamicState(VK_DYNAMIC_STATE_VIEWPORT), - VkDynamicState(VK_DYNAMIC_STATE_SCISSOR) - ] - dynamicState = VkPipelineDynamicStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, - dynamicStateCount: uint32(dynamicStates.len), - pDynamicStates: dynamicStates[0].addr - ) - vertShaderStageInfo = VkPipelineShaderStageCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - stage: VK_SHADER_STAGE_VERTEX_BIT, - module: vertShaderModule, - pName: "main" - ) - fragShaderStageInfo = VkPipelineShaderStageCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - stage: VK_SHADER_STAGE_FRAGMENT_BIT, - module: fragShaderModule, - pName: "main" - ) - shaderStages = [vertShaderStageInfo, fragShaderStageInfo] - bindingDescription = VkVertexInputBindingDescription( - binding: 0, - stride: uint32(sizeof(CubeVertex)), - inputRate: VK_VERTEX_INPUT_RATE_VERTEX - ) - attributeDescriptions = [ - VkVertexInputAttributeDescription(location: 0, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: 0), - VkVertexInputAttributeDescription(location: 1, binding: 0, format: VK_FORMAT_R32G32_SFLOAT, offset: uint32(sizeof(array[3, float32]))) - ] - vertexInputInfo = VkPipelineVertexInputStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, - vertexBindingDescriptionCount: 1, - pVertexBindingDescriptions: bindingDescription.addr, - vertexAttributeDescriptionCount: uint32(attributeDescriptions.len), - pVertexAttributeDescriptions: attributeDescriptions[0].addr - ) - inputAssembly = VkPipelineInputAssemblyStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, - topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, - primitiveRestartEnable: VkBool32(VK_FALSE) - ) - viewportState = VkPipelineViewportStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, - viewportCount: 1, - scissorCount: 1 - ) - rasterizer = VkPipelineRasterizationStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, - depthClampEnable: VkBool32(VK_FALSE), - rasterizerDiscardEnable: VkBool32(VK_FALSE), - polygonMode: VK_POLYGON_MODE_FILL, - lineWidth: 1.0, - cullMode: VkCullModeFlags(VK_CULL_MODE_BACK_BIT), - frontFace: VK_FRONT_FACE_CLOCKWISE, - depthBiasEnable: VkBool32(VK_FALSE) - ) - multisampling = VkPipelineMultisampleStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, - sampleShadingEnable: VkBool32(VK_FALSE), - rasterizationSamples: renderer.sampleCount - ) - depthStencil = VkPipelineDepthStencilStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, - depthTestEnable: VkBool32(VK_TRUE), - depthWriteEnable: VkBool32(VK_TRUE), - depthCompareOp: VK_COMPARE_OP_LESS, - depthBoundsTestEnable: VkBool32(VK_FALSE), - stencilTestEnable: VkBool32(VK_FALSE), - minDepthBounds: 0, - maxDepthBounds: 1 - ) - colorBlendAttachment = VkPipelineColorBlendAttachmentState( - colorWriteMask: VkColorComponentFlags(0x0000000F), - blendEnable: VkBool32(VK_FALSE), - srcColorBlendFactor: VK_BLEND_FACTOR_ONE, - dstColorBlendFactor: VK_BLEND_FACTOR_ZERO, - colorBlendOp: VK_BLEND_OP_ADD, - srcAlphaBlendFactor: VK_BLEND_FACTOR_ONE, - dstAlphaBlendFactor: VK_BLEND_FACTOR_ZERO, - alphaBlendOp: VK_BLEND_OP_ADD - ) - colorBlending = VkPipelineColorBlendStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, - logicOpEnable: VkBool32(VK_FALSE), - logicOp: VK_LOGIC_OP_COPY, - attachmentCount: 1, - pAttachments: colorBlendAttachment.addr, - blendConstants: [0.0'f32, 0.0'f32, 0.0'f32, 0.0'f32] - ) - pushConstantRange = VkPushConstantRange( - stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_VERTEX_BIT), - offset: 0, - size: uint32(sizeof(array[16, float32])) - ) - pipelineLayoutInfo = VkPipelineLayoutCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, - setLayoutCount: 1, - pSetLayouts: renderer.descriptorSetLayout.addr, - pushConstantRangeCount: 1, - pPushConstantRanges: pushConstantRange.addr - ) - - checkVk(vkCreatePipelineLayout(ctx.device, pipelineLayoutInfo.addr, nil, renderer.pipelineLayout.addr), "Creating pipeline layout") - - var pipelineInfo = VkGraphicsPipelineCreateInfo( - sType: VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, - pNext: renderingInfo.addr, - stageCount: uint32(shaderStages.len), - pStages: shaderStages[0].addr, - pVertexInputState: vertexInputInfo.addr, - pInputAssemblyState: inputAssembly.addr, - pViewportState: viewportState.addr, - pRasterizationState: rasterizer.addr, - pMultisampleState: multisampling.addr, - pDepthStencilState: depthStencil.addr, - pColorBlendState: colorBlending.addr, - pDynamicState: dynamicState.addr, - layout: renderer.pipelineLayout - ) - checkVk(vkCreateGraphicsPipelines(ctx.device, VkPipelineCache(0), 1, pipelineInfo.addr, nil, renderer.pipeline.addr), "Creating graphics pipeline") - finally: - vkDestroyShaderModule(ctx.device, vertShaderModule, nil) - vkDestroyShaderModule(ctx.device, fragShaderModule, nil) - -proc createVertexBuffer(ctx: VulkanContext, renderer: var CubeRenderer) = - let bufferSize = VkDeviceSize(sizeof(CubeVertices)) - createBuffer( - ctx, - bufferSize, - VK_BUFFER_USAGE_VERTEX_BUFFER_BIT.uint32, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, - renderer.vertexBuffer, - renderer.vertexBufferMemory - ) - var mapped: pointer - checkVk(vkMapMemory(ctx.device, renderer.vertexBufferMemory, VkDeviceSize(0), bufferSize, VkMemoryMapFlags(0), mapped.addr), "Mapping vertex buffer memory") - copyMem(mapped, unsafeAddr CubeVertices[0], sizeof(CubeVertices)) - vkUnmapMemory(ctx.device, renderer.vertexBufferMemory) - -proc recordCommandBuffers(ctx: VulkanContext, renderer: var CubeRenderer) - -proc destroySwapChainResources(ctx: VulkanContext, renderer: var CubeRenderer) = - if renderer.commandBuffers.len > 0: - vkFreeCommandBuffers(ctx.device, ctx.commandPool, uint32(renderer.commandBuffers.len), renderer.commandBuffers[0].addr) - renderer.commandBuffers.setLen(0) - for imageView in renderer.colorImageViews: - vkDestroyImageView(ctx.device, imageView, nil) - renderer.colorImageViews.setLen(0) - for image in renderer.colorImages: - vkDestroyImage(ctx.device, image, nil) - renderer.colorImages.setLen(0) - for memory in renderer.colorImageMemories: - vkFreeMemory(ctx.device, memory, nil) - renderer.colorImageMemories.setLen(0) - for imageView in renderer.imageViews: - vkDestroyImageView(ctx.device, imageView, nil) - renderer.imageViews.setLen(0) - for imageView in renderer.depthImageViews: - vkDestroyImageView(ctx.device, imageView, nil) - renderer.depthImageViews.setLen(0) - for image in renderer.depthImages: - vkDestroyImage(ctx.device, image, nil) - renderer.depthImages.setLen(0) - for memory in renderer.depthImageMemories: - vkFreeMemory(ctx.device, memory, nil) - renderer.depthImageMemories.setLen(0) - if renderer.pipeline.int64 != 0: - vkDestroyPipeline(ctx.device, renderer.pipeline, nil) - renderer.pipeline = VkPipeline(0) - if renderer.pipelineLayout.int64 != 0: - vkDestroyPipelineLayout(ctx.device, renderer.pipelineLayout, nil) - renderer.pipelineLayout = VkPipelineLayout(0) - -proc recreateRenderer(ctx: var VulkanContext, renderer: var CubeRenderer, width, height: int) = - discard vkDeviceWaitIdle(ctx.device) - destroySwapChainResources(ctx, renderer) - recreateSwapChain(ctx, width, height) - createSwapChainImageViews(ctx, renderer) - createMsaaResources(ctx, renderer) - createGraphicsPipeline(ctx, renderer) - recordCommandBuffers(ctx, renderer) - -proc recordCommandBuffers(ctx: VulkanContext, renderer: var CubeRenderer) = - renderer.commandBuffers.setLen(ctx.swapChainImages.len) - var allocInfo = VkCommandBufferAllocateInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - commandPool: ctx.commandPool, - level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, - commandBufferCount: uint32(renderer.commandBuffers.len) - ) - checkVk(vkAllocateCommandBuffers(ctx.device, allocInfo.addr, renderer.commandBuffers[0].addr), "Allocating cube command buffers") - -proc initRenderer(ctx: VulkanContext, renderer: var CubeRenderer) = - renderer.sampleCount = chooseMsaaSampleCount(ctx) - createDescriptorSetLayout(ctx, renderer) - createTextureResources(ctx, renderer) - createDescriptorResources(ctx, renderer) - createSwapChainImageViews(ctx, renderer) - createMsaaResources(ctx, renderer) - createGraphicsPipeline(ctx, renderer) - createVertexBuffer(ctx, renderer) - recordCommandBuffers(ctx, renderer) - -proc recordCurrentFrame(ctx: var VulkanContext, renderer: var CubeRenderer, imageIndex: uint32) = - let commandBuffer = renderer.commandBuffers[imageIndex] - discard vkResetCommandBuffer(commandBuffer, VkCommandBufferResetFlags(0)) - var beginInfo = VkCommandBufferBeginInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO) - checkVk(vkBeginCommandBuffer(commandBuffer, beginInfo.addr), "Beginning cube command buffer") - - let - colorRange = VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - depthRange = VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - var barriers: seq[VkImageMemoryBarrier2] - barriers.add VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), - srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), - dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - dstAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), - oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, - newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: ctx.swapChainImages[imageIndex], - subresourceRange: colorRange - ) - if renderer.msaaEnabled: - barriers.add VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), - srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), - dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - dstAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), - oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, - newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: renderer.colorImages[imageIndex], - subresourceRange: colorRange - ) - barriers.add VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), - srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), - dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT), - dstAccessMask: VkAccessFlags2(VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT), - oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, - newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: renderer.depthImages[imageIndex], - subresourceRange: depthRange - ) - var dependencyInfo = VkDependencyInfo( - sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - imageMemoryBarrierCount: uint32(barriers.len), - pImageMemoryBarriers: barriers[0].addr - ) - vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) - - var - colorClear = VkClearValue(color: VkClearColorValue(float32: ClearColor)) - depthClear = VkClearValue( - depthStencil: VkClearDepthStencilValue(depth: 1.0'f32, stencil: 0) - ) - colorAttachment = VkRenderingAttachmentInfo( - sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - imageView: - if renderer.msaaEnabled: renderer.colorImageViews[imageIndex] - else: renderer.imageViews[imageIndex], - imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - resolveMode: - if renderer.msaaEnabled: VK_RESOLVE_MODE_AVERAGE_BIT - else: VK_RESOLVE_MODE_NONE, - resolveImageView: - if renderer.msaaEnabled: renderer.imageViews[imageIndex] - else: VkImageView(0), - resolveImageLayout: - if renderer.msaaEnabled: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL - else: VK_IMAGE_LAYOUT_UNDEFINED, - loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, - storeOp: - if renderer.msaaEnabled: VK_ATTACHMENT_STORE_OP_DONT_CARE - else: VK_ATTACHMENT_STORE_OP_STORE, - clearValue: colorClear - ) - depthAttachment = VkRenderingAttachmentInfo( - sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - imageView: renderer.depthImageViews[imageIndex], - imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, - storeOp: VK_ATTACHMENT_STORE_OP_DONT_CARE, - clearValue: depthClear - ) - renderingInfo = VkRenderingInfo( - sType: VK_STRUCTURE_TYPE_RENDERING_INFO, - renderArea: VkRect2D( - offset: VkOffset2D(x: 0, y: 0), - extent: ctx.swapChainExtent - ), - layerCount: 1, - colorAttachmentCount: 1, - pColorAttachments: colorAttachment.addr, - pDepthAttachment: depthAttachment.addr - ) - viewport = VkViewport( - x: 0, - y: ctx.swapChainExtent.height.float32, - width: ctx.swapChainExtent.width.float32, - height: -ctx.swapChainExtent.height.float32, - minDepth: 0, - maxDepth: 1 - ) - scissor = VkRect2D( - offset: VkOffset2D(x: 0, y: 0), - extent: ctx.swapChainExtent - ) - var vertexBuffers = [renderer.vertexBuffer] - var offsets = [VkDeviceSize(0)] - var descriptorSet = renderer.descriptorSet - - vkCmdBeginRendering(commandBuffer, renderingInfo.addr) - vkCmdSetViewport(commandBuffer, 0, 1, viewport.addr) - vkCmdSetScissor(commandBuffer, 0, 1, scissor.addr) - vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.pipeline) - vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.pipelineLayout, 0, 1, descriptorSet.addr, 0, nil) - vkCmdPushConstants( - commandBuffer, - renderer.pipelineLayout, - VkShaderStageFlags(VK_SHADER_STAGE_VERTEX_BIT), - 0, - uint32(sizeof(renderer.transform)), - unsafeAddr renderer.transform[0] - ) - vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers[0].addr, offsets[0].addr) - vkCmdDraw(commandBuffer, uint32(CubeVertices.len), 1, 0, 0) - vkCmdEndRendering(commandBuffer) - - var toPresent = VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - srcAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), - dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), - dstAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), - oldLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - newLayout: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: ctx.swapChainImages[imageIndex], - subresourceRange: colorRange - ) - dependencyInfo.imageMemoryBarrierCount = 1 - dependencyInfo.pImageMemoryBarriers = toPresent.addr - vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) - checkVk(vkEndCommandBuffer(commandBuffer), "Ending cube command buffer") - -proc drawFrame(ctx: var VulkanContext, renderer: var CubeRenderer): bool = - let frame = ctx.currentFrame - let fence = ctx.inFlightFences[frame] - discard vkWaitForFences(ctx.device, 1, unsafeAddr fence, VkBool32(VK_TRUE), uint64.high) - discard vkResetFences(ctx.device, 1, unsafeAddr fence) - - var imageIndex: uint32 - let acquireResult = vkAcquireNextImageKHR( - ctx.device, - ctx.swapChain, - uint64.high, - ctx.imageAvailableSemaphores[frame], - VkFence(0), - imageIndex.addr - ) - if requiresSwapChainRecreate(acquireResult): - return true - checkVk(acquireResult, "Acquiring next swapchain image") - - recordCurrentFrame(ctx, renderer, imageIndex) - - var - commandBuffer = renderer.commandBuffers[imageIndex] - waitInfo = VkSemaphoreSubmitInfo( - sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, - semaphore: ctx.imageAvailableSemaphores[frame], - stageMask: VkPipelineStageFlags2( - VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT - ), - value: 0, - deviceIndex: 0 - ) - commandBufferInfo = VkCommandBufferSubmitInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, - commandBuffer: commandBuffer, - deviceMask: 0 - ) - signalInfo = VkSemaphoreSubmitInfo( - sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, - semaphore: ctx.renderFinishedSemaphores[frame], - stageMask: VkPipelineStageFlags2( - VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT - ), - value: 0, - deviceIndex: 0 - ) - submitInfo = VkSubmitInfo2( - sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, - waitSemaphoreInfoCount: 1, - pWaitSemaphoreInfos: waitInfo.addr, - commandBufferInfoCount: 1, - pCommandBufferInfos: commandBufferInfo.addr, - signalSemaphoreInfoCount: 1, - pSignalSemaphoreInfos: signalInfo.addr - ) - checkVk( - vkQueueSubmit2(ctx.graphicsQueue, 1, submitInfo.addr, fence), - "Submitting cube command buffer" - ) - - var - swapChains = [ctx.swapChain] - presentInfo = VkPresentInfoKHR( - sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, - waitSemaphoreCount: 1, - pWaitSemaphores: signalInfo.semaphore.addr, - swapchainCount: 1, - pSwapchains: swapChains[0].addr, - pImageIndices: imageIndex.addr - ) - let presentResult = vkQueuePresentKHR(ctx.presentQueue, presentInfo.addr) - let needsRecreate = requiresSwapChainRecreate(presentResult) - if not needsRecreate: - checkVk(presentResult, "Presenting cube frame") - ctx.currentFrame = (ctx.currentFrame + 1) mod FRAME_COUNT - inc renderer.frameCounter - needsRecreate - -proc shutdown(ctx: VulkanContext, renderer: var CubeRenderer) = - destroySwapChainResources(ctx, renderer) - if renderer.descriptorPool.int64 != 0: - vkDestroyDescriptorPool(ctx.device, renderer.descriptorPool, nil) - if renderer.textureSampler.int64 != 0: - vkDestroySampler(ctx.device, renderer.textureSampler, nil) - if renderer.textureImageView.int64 != 0: - vkDestroyImageView(ctx.device, renderer.textureImageView, nil) - if renderer.textureImage.int64 != 0: - vkDestroyImage(ctx.device, renderer.textureImage, nil) - if renderer.textureImageMemory.int64 != 0: - vkFreeMemory(ctx.device, renderer.textureImageMemory, nil) - if renderer.vertexBuffer.int64 != 0: - vkDestroyBuffer(ctx.device, renderer.vertexBuffer, nil) - if renderer.vertexBufferMemory.int64 != 0: - vkFreeMemory(ctx.device, renderer.vertexBufferMemory, nil) - if renderer.descriptorSetLayout.int64 != 0: - vkDestroyDescriptorSetLayout(ctx.device, renderer.descriptorSetLayout, nil) - -when isMainModule: - let window = newWindow("Vulkan 1.4 - Basic Cube", ivec2(Width.int32, Height.int32)) - let hwnd = window.getHWND() - if hwnd == 0: - raise newException(Exception, "Failed to acquire HWND from window") - - var ctx: VulkanContext - ctx.initDevice(hwnd, Width, Height) - - var renderer: CubeRenderer - initRenderer(ctx, renderer) - - var needsResize = false - var frameInProgress = false - window.onResize = proc() = - needsResize = true - window.onFrame = proc() = - if frameInProgress: - return - frameInProgress = true - try: - if needsResize: - let size = window.size - if size.x > 0 and size.y > 0: - recreateRenderer(ctx, renderer, size.x.int, size.y.int) - needsResize = false - else: - return - - let aspect = ctx.swapChainExtent.width.float32 / max(1'u32, ctx.swapChainExtent.height).float32 - updateTransform(renderer, aspect) - if drawFrame(ctx, renderer): - let size = window.size - if size.x > 0 and size.y > 0: - recreateRenderer(ctx, renderer, size.x.int, size.y.int) - else: - needsResize = true - finally: - frameInProgress = false - - try: - while not window.closeRequested: - pollEvents() - finally: - shutdown(ctx, renderer) - ctx.cleanup() +import + std/os, + pixie, + vmath, + windy, + vk14 + +const + Width = 1280 + Height = 800 + TexturePath = currentSourcePath().parentDir / "testTexture.png" + PreferredMsaaSamples = 4'u32 + ClearColor = [0.05'f32, 0.05'f32, 0.1'f32, 1.0'f32] + +type + CubeVertex = object + position: array[3, float32] + uv: array[2, float32] + + CubeRenderer = object + descriptorSetLayout: VkDescriptorSetLayout + pipelineLayout: VkPipelineLayout + pipeline: VkPipeline + sampleCount: VkSampleCountFlagBits + imageViews: seq[VkImageView] + colorImages: seq[VkImage] + colorImageMemories: seq[VkDeviceMemory] + colorImageViews: seq[VkImageView] + commandBuffers: seq[VkCommandBuffer] + vertexBuffer: VkBuffer + vertexBufferMemory: VkDeviceMemory + textureImage: VkImage + textureImageMemory: VkDeviceMemory + textureImageView: VkImageView + textureSampler: VkSampler + textureMipLevels: uint32 + descriptorPool: VkDescriptorPool + descriptorSet: VkDescriptorSet + depthImages: seq[VkImage] + depthImageMemories: seq[VkDeviceMemory] + depthImageViews: seq[VkImageView] + transform: array[16, float32] + frameCounter: uint64 + +const + CubeVertices = [ + CubeVertex(position: [-1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), + CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [1.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [0.0'f32, 1.0'f32]), + CubeVertex(position: [1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), + + CubeVertex(position: [1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), + CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [1.0'f32, 0.0'f32]), + CubeVertex(position: [1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [0.0'f32, 1.0'f32]), + CubeVertex(position: [-1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), + + CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), + CubeVertex(position: [-1.0'f32, 1.0'f32, 1.0'f32], uv: [1.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, -1.0'f32, -1.0'f32], uv: [0.0'f32, 1.0'f32]), + CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), + + CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), + CubeVertex(position: [1.0'f32, 1.0'f32, -1.0'f32], uv: [1.0'f32, 0.0'f32]), + CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [1.0'f32, -1.0'f32, 1.0'f32], uv: [0.0'f32, 1.0'f32]), + CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), + + CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), + CubeVertex(position: [1.0'f32, 1.0'f32, -1.0'f32], uv: [1.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, 1.0'f32, -1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, 1.0'f32, 1.0'f32], uv: [0.0'f32, 1.0'f32]), + CubeVertex(position: [1.0'f32, 1.0'f32, 1.0'f32], uv: [1.0'f32, 1.0'f32]), + + CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]), + CubeVertex(position: [1.0'f32, -1.0'f32, 1.0'f32], uv: [1.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, -1.0'f32, 1.0'f32], uv: [0.0'f32, 0.0'f32]), + CubeVertex(position: [-1.0'f32, -1.0'f32, -1.0'f32], uv: [0.0'f32, 1.0'f32]), + CubeVertex(position: [1.0'f32, -1.0'f32, -1.0'f32], uv: [1.0'f32, 1.0'f32]) + ] + +proc checkVk(result: VkResult, action: string) = + if result != VK_SUCCESS: + raise newException(Exception, action & " failed with VkResult " & $result.int32) + +proc requiresSwapChainRecreate(vkResult: VkResult): bool = + ## Returns true if the swapchain is out of date or suboptimal. + let code = vkResult.int32 + code == VK_SUBOPTIMAL_KHR.int32 or + code == VK_ERROR_OUT_OF_DATE_KHR.int32 + +proc identityMatrix(): Mat4 = + gmat4[float32]( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ) + +proc perspectiveDx(fovY, aspect, nearPlane, farPlane: float32): Mat4 = + let + h = 1.0'f32 / tan(fovY * 0.5'f32) + w = h / aspect + depth = farPlane - nearPlane + result[0, 0] = w + result[0, 1] = 0 + result[0, 2] = 0 + result[0, 3] = 0 + result[1, 0] = 0 + result[1, 1] = h + result[1, 2] = 0 + result[1, 3] = 0 + result[2, 0] = 0 + result[2, 1] = 0 + result[2, 2] = farPlane / depth + result[2, 3] = 1 + result[3, 0] = 0 + result[3, 1] = 0 + result[3, 2] = -(nearPlane * farPlane) / depth + result[3, 3] = 0 + +proc mat4ToArray(matrix: Mat4): array[16, float32] = + var index = 0 + for i in 0 ..< 4: + for j in 0 ..< 4: + result[index] = matrix[i, j] + inc index + +proc updateTransform(renderer: var CubeRenderer, aspect: float32) = + let + time = renderer.frameCounter.float32 / 60.0'f32 + model = + translate(vec3(0.0'f32, 0.0'f32, 5.0'f32)) * + rotateY(time * 0.7'f32) * + rotateX(time * 0.35'f32) + view = identityMatrix() + proj = perspectiveDx(60.0'f32.toRadians, aspect, 0.1'f32, 100.0'f32) + mvp = proj * view * model + renderer.transform = mat4ToArray(mvp) + +proc buildMipChain(image: Image): seq[Image] = + result.add(image) + var current = image + while current.width > 1 or current.height > 1: + current = current.minifyBy2() + result.add(current) + +proc findMemoryType(ctx: VulkanContext, typeFilter: uint32, properties: uint32): uint32 = + var memProperties: VkPhysicalDeviceMemoryProperties + vkGetPhysicalDeviceMemoryProperties(ctx.physicalDevice, memProperties.addr) + for i in 0'u32 ..< memProperties.memoryTypeCount: + let flags = memProperties.memoryTypes[i].propertyFlags.uint32 + if ((typeFilter shr i) and 1'u32) == 1'u32 and (flags and properties) == properties: + return i + raise newException(Exception, "Failed to find suitable Vulkan memory type") + +proc chooseMsaaSampleCount(ctx: VulkanContext): VkSampleCountFlagBits = + var props: VkPhysicalDeviceProperties + vkGetPhysicalDeviceProperties(ctx.physicalDevice, props.addr) + let counts = props.limits.framebufferColorSampleCounts.uint32 and + props.limits.framebufferDepthSampleCounts.uint32 + + if PreferredMsaaSamples >= 4 and (counts and VK_SAMPLE_COUNT_4_BIT.uint32) != 0: + return VK_SAMPLE_COUNT_4_BIT + if (counts and VK_SAMPLE_COUNT_2_BIT.uint32) != 0: + return VK_SAMPLE_COUNT_2_BIT + VK_SAMPLE_COUNT_1_BIT + +proc msaaEnabled(renderer: CubeRenderer): bool = + renderer.sampleCount != VK_SAMPLE_COUNT_1_BIT + +proc createShaderModule(device: VkDevice, code: string): VkShaderModule = + var createInfo = VkShaderModuleCreateInfo( + sType: VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + codeSize: code.len.uint32, + pCode: cast[ptr uint32](code[0].unsafeAddr) + ) + checkVk(vkCreateShaderModule(device, createInfo.addr, nil, result.addr), "Creating shader module") + +proc createBuffer( + ctx: VulkanContext, + size: VkDeviceSize, + usage: uint32, + properties: uint32, + buffer: var VkBuffer, + memory: var VkDeviceMemory +) = + var bufferInfo = VkBufferCreateInfo( + sType: VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + size: size, + usage: VkBufferUsageFlags(usage), + sharingMode: VK_SHARING_MODE_EXCLUSIVE, + queueFamilyIndexCount: 0, + pQueueFamilyIndices: nil + ) + checkVk(vkCreateBuffer(ctx.device, bufferInfo.addr, nil, buffer.addr), "Creating buffer") + + var memRequirements: VkMemoryRequirements + vkGetBufferMemoryRequirements(ctx.device, buffer, memRequirements.addr) + + var allocInfo = VkMemoryAllocateInfo( + sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + allocationSize: memRequirements.size, + memoryTypeIndex: findMemoryType(ctx, memRequirements.memoryTypeBits, properties) + ) + checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), "Allocating buffer memory") + checkVk(vkBindBufferMemory(ctx.device, buffer, memory, VkDeviceSize(0)), "Binding buffer memory") + +proc beginSingleTimeCommands(ctx: VulkanContext): VkCommandBuffer = + var allocInfo = VkCommandBufferAllocateInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + commandPool: ctx.commandPool, + level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, + commandBufferCount: 1 + ) + checkVk(vkAllocateCommandBuffers(ctx.device, allocInfo.addr, result.addr), "Allocating single-use command buffer") + var beginInfo = VkCommandBufferBeginInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + flags: VkCommandBufferUsageFlags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) + ) + checkVk(vkBeginCommandBuffer(result, beginInfo.addr), "Beginning single-use command buffer") + +proc endSingleTimeCommands(ctx: VulkanContext, commandBuffer: VkCommandBuffer) = + checkVk(vkEndCommandBuffer(commandBuffer), "Ending single-use command buffer") + var + commandBufferInfo = VkCommandBufferSubmitInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + commandBuffer: commandBuffer, + deviceMask: 0 + ) + submitInfo = VkSubmitInfo2( + sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + commandBufferInfoCount: 1, + pCommandBufferInfos: commandBufferInfo.addr + ) + checkVk( + vkQueueSubmit2(ctx.graphicsQueue, 1, submitInfo.addr, VkFence(0)), + "Submitting single-use command buffer" + ) + checkVk(vkQueueWaitIdle(ctx.graphicsQueue), "Waiting for graphics queue idle") + vkFreeCommandBuffers(ctx.device, ctx.commandPool, 1, unsafeAddr commandBuffer) + +proc createImage( + ctx: VulkanContext, + width, height: int, + format: VkFormat, + tiling: VkImageTiling, + usage: uint32, + properties: uint32, + mipLevels: uint32, + samples: VkSampleCountFlagBits, + image: var VkImage, + memory: var VkDeviceMemory +) = + var imageInfo = VkImageCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + imageType: VK_IMAGE_TYPE_2D, + format: format, + extent: VkExtent3D(width: uint32(width), height: uint32(height), depth: 1), + mipLevels: mipLevels, + arrayLayers: 1, + samples: samples, + tiling: tiling, + usage: VkImageUsageFlags(usage), + sharingMode: VK_SHARING_MODE_EXCLUSIVE, + initialLayout: VK_IMAGE_LAYOUT_UNDEFINED + ) + checkVk(vkCreateImage(ctx.device, imageInfo.addr, nil, image.addr), "Creating image") + + var memRequirements: VkMemoryRequirements + vkGetImageMemoryRequirements(ctx.device, image, memRequirements.addr) + + var allocInfo = VkMemoryAllocateInfo( + sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + allocationSize: memRequirements.size, + memoryTypeIndex: findMemoryType(ctx, memRequirements.memoryTypeBits, properties) + ) + checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), "Allocating image memory") + checkVk(vkBindImageMemory(ctx.device, image, memory, VkDeviceSize(0)), "Binding image memory") + +proc transitionImageLayout( + ctx: VulkanContext, + image: VkImage, + oldLayout, newLayout: VkImageLayout, + aspectMask: VkImageAspectFlags, + mipLevels: uint32 +) = + let commandBuffer = beginSingleTimeCommands(ctx) + var barrier = VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + oldLayout: oldLayout, + newLayout: newLayout, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: image, + subresourceRange: VkImageSubresourceRange( + aspectMask: aspectMask, + baseMipLevel: 0, + levelCount: mipLevels, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + + var + sourceStage: VkPipelineStageFlags2 + destinationStage: VkPipelineStageFlags2 + srcAccess: VkAccessFlags2 + dstAccess: VkAccessFlags2 + + if oldLayout == VK_IMAGE_LAYOUT_UNDEFINED and newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + srcAccess = VkAccessFlags2(VK_ACCESS_2_NONE) + dstAccess = VkAccessFlags2(VK_ACCESS_2_TRANSFER_WRITE_BIT) + sourceStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE) + destinationStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT) + elif oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL and newLayout == VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL: + srcAccess = VkAccessFlags2(VK_ACCESS_2_TRANSFER_WRITE_BIT) + dstAccess = VkAccessFlags2(VK_ACCESS_2_SHADER_READ_BIT) + sourceStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT) + destinationStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT) + elif oldLayout == VK_IMAGE_LAYOUT_UNDEFINED and newLayout == VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL: + srcAccess = VkAccessFlags2(VK_ACCESS_2_NONE) + dstAccess = + if aspectMask.uint32 == VK_IMAGE_ASPECT_DEPTH_BIT: + VkAccessFlags2(VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT) + else: + VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT) + sourceStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE) + destinationStage = + if aspectMask.uint32 == VK_IMAGE_ASPECT_DEPTH_BIT: + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT) + else: + VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT) + else: + raise newException(Exception, "Unsupported image layout transition") + + barrier.srcStageMask = sourceStage + barrier.srcAccessMask = srcAccess + barrier.dstStageMask = destinationStage + barrier.dstAccessMask = dstAccess + + var dependencyInfo = VkDependencyInfo( + sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + imageMemoryBarrierCount: 1, + pImageMemoryBarriers: barrier.addr + ) + vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) + endSingleTimeCommands(ctx, commandBuffer) + +proc createTextureResources(ctx: VulkanContext, renderer: var CubeRenderer) = + ## Loads the texture, builds mip chain, and uploads to a device-local image. + let image = readImage(TexturePath) + let + mipImages = buildMipChain(image) + texWidth = mipImages[0].width + texHeight = mipImages[0].height + renderer.textureMipLevels = uint32(mipImages.len) + + var + imageSize = VkDeviceSize(0) + mipOffsets = newSeq[VkDeviceSize](mipImages.len) + for i, mipImage in mipImages: + mipOffsets[i] = imageSize + imageSize = VkDeviceSize(imageSize.uint64 + uint64(mipImage.width * mipImage.height * 4)) + + var stagingBuffer: VkBuffer + var stagingBufferMemory: VkDeviceMemory + createBuffer( + ctx, + imageSize, + VK_BUFFER_USAGE_TRANSFER_SRC_BIT.uint32, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, + stagingBuffer, + stagingBufferMemory + ) + + var mapped: pointer + checkVk( + vkMapMemory(ctx.device, stagingBufferMemory, VkDeviceSize(0), imageSize, VkMemoryMapFlags(0), mapped.addr), + "Mapping staging buffer memory" + ) + for i, mipImage in mipImages: + let dst = cast[pointer](cast[uint](mapped) + uint(mipOffsets[i])) + for y in 0 ..< mipImage.height: + let srcIdx = mipImage.dataIndex(0, y) + let srcPtr = cast[ptr uint8](mipImage.data[srcIdx].addr) + copyMem( + cast[pointer](cast[uint](dst) + uint(y * mipImage.width * 4)), + srcPtr, + mipImage.width * 4 + ) + + vkUnmapMemory(ctx.device, stagingBufferMemory) + + createImage( + ctx, + texWidth, + texHeight, + VK_FORMAT_R8G8B8A8_SRGB, + VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_TRANSFER_DST_BIT.uint32 or VK_IMAGE_USAGE_SAMPLED_BIT.uint32, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.uint32, + renderer.textureMipLevels, + VK_SAMPLE_COUNT_1_BIT, + renderer.textureImage, + renderer.textureImageMemory + ) + transitionImageLayout( + ctx, + renderer.textureImage, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + renderer.textureMipLevels + ) + let commandBuffer = beginSingleTimeCommands(ctx) + var regions = newSeq[VkBufferImageCopy](mipImages.len) + for i, mipImage in mipImages: + regions[i] = VkBufferImageCopy( + bufferOffset: mipOffsets[i], + bufferRowLength: 0, + bufferImageHeight: 0, + imageSubresource: VkImageSubresourceLayers( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + mipLevel: uint32(i), + baseArrayLayer: 0, + layerCount: 1 + ), + imageOffset: VkOffset3D(x: 0, y: 0, z: 0), + imageExtent: VkExtent3D(width: uint32(mipImage.width), height: uint32(mipImage.height), depth: 1) + ) + vkCmdCopyBufferToImage( + commandBuffer, + stagingBuffer, + renderer.textureImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + uint32(regions.len), + regions[0].addr + ) + endSingleTimeCommands(ctx, commandBuffer) + transitionImageLayout( + ctx, + renderer.textureImage, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL, + VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + renderer.textureMipLevels + ) + + vkDestroyBuffer(ctx.device, stagingBuffer, nil) + vkFreeMemory(ctx.device, stagingBufferMemory, nil) + + var imageViewInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: renderer.textureImage, + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: VK_FORMAT_R8G8B8A8_SRGB, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: renderer.textureMipLevels, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(ctx.device, imageViewInfo.addr, nil, renderer.textureImageView.addr), "Creating texture image view") + + var samplerInfo = VkSamplerCreateInfo( + sType: VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + magFilter: VK_FILTER_LINEAR, + minFilter: VK_FILTER_LINEAR, + mipmapMode: VK_SAMPLER_MIPMAP_MODE_LINEAR, + addressModeU: VK_SAMPLER_ADDRESS_MODE_REPEAT, + addressModeV: VK_SAMPLER_ADDRESS_MODE_REPEAT, + addressModeW: VK_SAMPLER_ADDRESS_MODE_REPEAT, + mipLodBias: 0, + anisotropyEnable: VkBool32(VK_FALSE), + maxAnisotropy: 1, + compareEnable: VkBool32(VK_FALSE), + compareOp: VK_COMPARE_OP_ALWAYS, + minLod: 0, + maxLod: float32(renderer.textureMipLevels - 1), + borderColor: VK_BORDER_COLOR_INT_OPAQUE_BLACK, + unnormalizedCoordinates: VkBool32(VK_FALSE) + ) + checkVk(vkCreateSampler(ctx.device, samplerInfo.addr, nil, renderer.textureSampler.addr), "Creating texture sampler") + +proc createDescriptorSetLayout(ctx: VulkanContext, renderer: var CubeRenderer) = + var layoutBinding = VkDescriptorSetLayoutBinding( + binding: 0, + descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + descriptorCount: 1, + stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_FRAGMENT_BIT), + pImmutableSamplers: nil + ) + var layoutInfo = VkDescriptorSetLayoutCreateInfo( + sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, + bindingCount: 1, + pBindings: layoutBinding.addr + ) + checkVk(vkCreateDescriptorSetLayout(ctx.device, layoutInfo.addr, nil, renderer.descriptorSetLayout.addr), "Creating descriptor set layout") + +proc createDescriptorResources(ctx: VulkanContext, renderer: var CubeRenderer) = + var poolSize = VkDescriptorPoolSize(`type`: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, descriptorCount: 1) + var poolInfo = VkDescriptorPoolCreateInfo( + sType: VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, + maxSets: 1, + poolSizeCount: 1, + pPoolSizes: poolSize.addr + ) + checkVk(vkCreateDescriptorPool(ctx.device, poolInfo.addr, nil, renderer.descriptorPool.addr), "Creating descriptor pool") + + var setLayout = renderer.descriptorSetLayout + var allocInfo = VkDescriptorSetAllocateInfo( + sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + descriptorPool: renderer.descriptorPool, + descriptorSetCount: 1, + pSetLayouts: setLayout.addr + ) + checkVk(vkAllocateDescriptorSets(ctx.device, allocInfo.addr, renderer.descriptorSet.addr), "Allocating descriptor set") + + var imageInfo = VkDescriptorImageInfo( + sampler: renderer.textureSampler, + imageView: renderer.textureImageView, + imageLayout: VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL + ) + var descriptorWrite = VkWriteDescriptorSet( + sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + dstSet: renderer.descriptorSet, + dstBinding: 0, + dstArrayElement: 0, + descriptorCount: 1, + descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + pImageInfo: imageInfo.addr, + pBufferInfo: nil, + pTexelBufferView: nil + ) + vkUpdateDescriptorSets(ctx.device, 1, descriptorWrite.addr, 0, nil) + +proc createSwapChainImageViews(ctx: VulkanContext, renderer: var CubeRenderer) = + renderer.imageViews.setLen(ctx.swapChainImages.len) + for i, image in ctx.swapChainImages: + var createInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: image, + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: ctx.swapChainImageFormat, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(ctx.device, createInfo.addr, nil, renderer.imageViews[i].addr), "Creating swapchain image view") + +proc createMsaaResources(ctx: VulkanContext, renderer: var CubeRenderer) = + let attachmentCount = ctx.swapChainImages.len + renderer.depthImages.setLen(attachmentCount) + renderer.depthImageMemories.setLen(attachmentCount) + renderer.depthImageViews.setLen(attachmentCount) + + if renderer.msaaEnabled: + renderer.colorImages.setLen(attachmentCount) + renderer.colorImageMemories.setLen(attachmentCount) + renderer.colorImageViews.setLen(attachmentCount) + else: + renderer.colorImages.setLen(0) + renderer.colorImageMemories.setLen(0) + renderer.colorImageViews.setLen(0) + + for i in 0 ..< attachmentCount: + if renderer.msaaEnabled: + createImage( + ctx, + int(ctx.swapChainExtent.width), + int(ctx.swapChainExtent.height), + ctx.swapChainImageFormat, + VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT.uint32 or VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT.uint32, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.uint32, + 1, + renderer.sampleCount, + renderer.colorImages[i], + renderer.colorImageMemories[i] + ) + + var colorViewInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: renderer.colorImages[i], + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: ctx.swapChainImageFormat, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(ctx.device, colorViewInfo.addr, nil, renderer.colorImageViews[i].addr), "Creating MSAA color image view") + + createImage( + ctx, + int(ctx.swapChainExtent.width), + int(ctx.swapChainExtent.height), + VK_FORMAT_D32_SFLOAT, + VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT.uint32, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.uint32, + 1, + renderer.sampleCount, + renderer.depthImages[i], + renderer.depthImageMemories[i] + ) + + var depthViewInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: renderer.depthImages[i], + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: VK_FORMAT_D32_SFLOAT, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(ctx.device, depthViewInfo.addr, nil, renderer.depthImageViews[i].addr), "Creating MSAA depth image view") + +proc createGraphicsPipeline(ctx: VulkanContext, renderer: var CubeRenderer) = + const + vertShaderCode = staticRead("shaders/basic_cube.vert.spv") + fragShaderCode = staticRead("shaders/basic_cube.frag.spv") + let + vertShaderModule = createShaderModule(ctx.device, vertShaderCode) + fragShaderModule = createShaderModule(ctx.device, fragShaderCode) + try: + var + colorFormat = ctx.swapChainImageFormat + renderingInfo = VkPipelineRenderingCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, + colorAttachmentCount: 1, + pColorAttachmentFormats: colorFormat.addr, + depthAttachmentFormat: VK_FORMAT_D32_SFLOAT + ) + dynamicStates = [ + VkDynamicState(VK_DYNAMIC_STATE_VIEWPORT), + VkDynamicState(VK_DYNAMIC_STATE_SCISSOR) + ] + dynamicState = VkPipelineDynamicStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + dynamicStateCount: uint32(dynamicStates.len), + pDynamicStates: dynamicStates[0].addr + ) + vertShaderStageInfo = VkPipelineShaderStageCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + stage: VK_SHADER_STAGE_VERTEX_BIT, + module: vertShaderModule, + pName: "main" + ) + fragShaderStageInfo = VkPipelineShaderStageCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + stage: VK_SHADER_STAGE_FRAGMENT_BIT, + module: fragShaderModule, + pName: "main" + ) + shaderStages = [vertShaderStageInfo, fragShaderStageInfo] + bindingDescription = VkVertexInputBindingDescription( + binding: 0, + stride: uint32(sizeof(CubeVertex)), + inputRate: VK_VERTEX_INPUT_RATE_VERTEX + ) + attributeDescriptions = [ + VkVertexInputAttributeDescription(location: 0, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: 0), + VkVertexInputAttributeDescription(location: 1, binding: 0, format: VK_FORMAT_R32G32_SFLOAT, offset: uint32(sizeof(array[3, float32]))) + ] + vertexInputInfo = VkPipelineVertexInputStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + vertexBindingDescriptionCount: 1, + pVertexBindingDescriptions: bindingDescription.addr, + vertexAttributeDescriptionCount: uint32(attributeDescriptions.len), + pVertexAttributeDescriptions: attributeDescriptions[0].addr + ) + inputAssembly = VkPipelineInputAssemblyStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, + primitiveRestartEnable: VkBool32(VK_FALSE) + ) + viewportState = VkPipelineViewportStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + viewportCount: 1, + scissorCount: 1 + ) + rasterizer = VkPipelineRasterizationStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + depthClampEnable: VkBool32(VK_FALSE), + rasterizerDiscardEnable: VkBool32(VK_FALSE), + polygonMode: VK_POLYGON_MODE_FILL, + lineWidth: 1.0, + cullMode: VkCullModeFlags(VK_CULL_MODE_BACK_BIT), + frontFace: VK_FRONT_FACE_CLOCKWISE, + depthBiasEnable: VkBool32(VK_FALSE) + ) + multisampling = VkPipelineMultisampleStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + sampleShadingEnable: VkBool32(VK_FALSE), + rasterizationSamples: renderer.sampleCount + ) + depthStencil = VkPipelineDepthStencilStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, + depthTestEnable: VkBool32(VK_TRUE), + depthWriteEnable: VkBool32(VK_TRUE), + depthCompareOp: VK_COMPARE_OP_LESS, + depthBoundsTestEnable: VkBool32(VK_FALSE), + stencilTestEnable: VkBool32(VK_FALSE), + minDepthBounds: 0, + maxDepthBounds: 1 + ) + colorBlendAttachment = VkPipelineColorBlendAttachmentState( + colorWriteMask: VkColorComponentFlags(0x0000000F), + blendEnable: VkBool32(VK_FALSE), + srcColorBlendFactor: VK_BLEND_FACTOR_ONE, + dstColorBlendFactor: VK_BLEND_FACTOR_ZERO, + colorBlendOp: VK_BLEND_OP_ADD, + srcAlphaBlendFactor: VK_BLEND_FACTOR_ONE, + dstAlphaBlendFactor: VK_BLEND_FACTOR_ZERO, + alphaBlendOp: VK_BLEND_OP_ADD + ) + colorBlending = VkPipelineColorBlendStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + logicOpEnable: VkBool32(VK_FALSE), + logicOp: VK_LOGIC_OP_COPY, + attachmentCount: 1, + pAttachments: colorBlendAttachment.addr, + blendConstants: [0.0'f32, 0.0'f32, 0.0'f32, 0.0'f32] + ) + pushConstantRange = VkPushConstantRange( + stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_VERTEX_BIT), + offset: 0, + size: uint32(sizeof(array[16, float32])) + ) + pipelineLayoutInfo = VkPipelineLayoutCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + setLayoutCount: 1, + pSetLayouts: renderer.descriptorSetLayout.addr, + pushConstantRangeCount: 1, + pPushConstantRanges: pushConstantRange.addr + ) + + checkVk(vkCreatePipelineLayout(ctx.device, pipelineLayoutInfo.addr, nil, renderer.pipelineLayout.addr), "Creating pipeline layout") + + var pipelineInfo = VkGraphicsPipelineCreateInfo( + sType: VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + pNext: renderingInfo.addr, + stageCount: uint32(shaderStages.len), + pStages: shaderStages[0].addr, + pVertexInputState: vertexInputInfo.addr, + pInputAssemblyState: inputAssembly.addr, + pViewportState: viewportState.addr, + pRasterizationState: rasterizer.addr, + pMultisampleState: multisampling.addr, + pDepthStencilState: depthStencil.addr, + pColorBlendState: colorBlending.addr, + pDynamicState: dynamicState.addr, + layout: renderer.pipelineLayout + ) + checkVk(vkCreateGraphicsPipelines(ctx.device, VkPipelineCache(0), 1, pipelineInfo.addr, nil, renderer.pipeline.addr), "Creating graphics pipeline") + finally: + vkDestroyShaderModule(ctx.device, vertShaderModule, nil) + vkDestroyShaderModule(ctx.device, fragShaderModule, nil) + +proc createVertexBuffer(ctx: VulkanContext, renderer: var CubeRenderer) = + let bufferSize = VkDeviceSize(sizeof(CubeVertices)) + createBuffer( + ctx, + bufferSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT.uint32, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, + renderer.vertexBuffer, + renderer.vertexBufferMemory + ) + var mapped: pointer + checkVk(vkMapMemory(ctx.device, renderer.vertexBufferMemory, VkDeviceSize(0), bufferSize, VkMemoryMapFlags(0), mapped.addr), "Mapping vertex buffer memory") + copyMem(mapped, unsafeAddr CubeVertices[0], sizeof(CubeVertices)) + vkUnmapMemory(ctx.device, renderer.vertexBufferMemory) + +proc recordCommandBuffers(ctx: VulkanContext, renderer: var CubeRenderer) + +proc destroySwapChainResources(ctx: VulkanContext, renderer: var CubeRenderer) = + if renderer.commandBuffers.len > 0: + vkFreeCommandBuffers(ctx.device, ctx.commandPool, uint32(renderer.commandBuffers.len), renderer.commandBuffers[0].addr) + renderer.commandBuffers.setLen(0) + for imageView in renderer.colorImageViews: + vkDestroyImageView(ctx.device, imageView, nil) + renderer.colorImageViews.setLen(0) + for image in renderer.colorImages: + vkDestroyImage(ctx.device, image, nil) + renderer.colorImages.setLen(0) + for memory in renderer.colorImageMemories: + vkFreeMemory(ctx.device, memory, nil) + renderer.colorImageMemories.setLen(0) + for imageView in renderer.imageViews: + vkDestroyImageView(ctx.device, imageView, nil) + renderer.imageViews.setLen(0) + for imageView in renderer.depthImageViews: + vkDestroyImageView(ctx.device, imageView, nil) + renderer.depthImageViews.setLen(0) + for image in renderer.depthImages: + vkDestroyImage(ctx.device, image, nil) + renderer.depthImages.setLen(0) + for memory in renderer.depthImageMemories: + vkFreeMemory(ctx.device, memory, nil) + renderer.depthImageMemories.setLen(0) + if renderer.pipeline.int64 != 0: + vkDestroyPipeline(ctx.device, renderer.pipeline, nil) + renderer.pipeline = VkPipeline(0) + if renderer.pipelineLayout.int64 != 0: + vkDestroyPipelineLayout(ctx.device, renderer.pipelineLayout, nil) + renderer.pipelineLayout = VkPipelineLayout(0) + +proc recreateRenderer(ctx: var VulkanContext, renderer: var CubeRenderer, width, height: int) = + discard vkDeviceWaitIdle(ctx.device) + destroySwapChainResources(ctx, renderer) + recreateSwapChain(ctx, width, height) + createSwapChainImageViews(ctx, renderer) + createMsaaResources(ctx, renderer) + createGraphicsPipeline(ctx, renderer) + recordCommandBuffers(ctx, renderer) + +proc recordCommandBuffers(ctx: VulkanContext, renderer: var CubeRenderer) = + renderer.commandBuffers.setLen(ctx.swapChainImages.len) + var allocInfo = VkCommandBufferAllocateInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + commandPool: ctx.commandPool, + level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, + commandBufferCount: uint32(renderer.commandBuffers.len) + ) + checkVk(vkAllocateCommandBuffers(ctx.device, allocInfo.addr, renderer.commandBuffers[0].addr), "Allocating cube command buffers") + +proc initRenderer(ctx: VulkanContext, renderer: var CubeRenderer) = + renderer.sampleCount = chooseMsaaSampleCount(ctx) + createDescriptorSetLayout(ctx, renderer) + createTextureResources(ctx, renderer) + createDescriptorResources(ctx, renderer) + createSwapChainImageViews(ctx, renderer) + createMsaaResources(ctx, renderer) + createGraphicsPipeline(ctx, renderer) + createVertexBuffer(ctx, renderer) + recordCommandBuffers(ctx, renderer) + +proc recordCurrentFrame(ctx: var VulkanContext, renderer: var CubeRenderer, imageIndex: uint32) = + let commandBuffer = renderer.commandBuffers[imageIndex] + discard vkResetCommandBuffer(commandBuffer, VkCommandBufferResetFlags(0)) + var beginInfo = VkCommandBufferBeginInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO) + checkVk(vkBeginCommandBuffer(commandBuffer, beginInfo.addr), "Beginning cube command buffer") + + let + colorRange = VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + depthRange = VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + var barriers: seq[VkImageMemoryBarrier2] + barriers.add VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), + dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + dstAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), + oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, + newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: ctx.swapChainImages[imageIndex], + subresourceRange: colorRange + ) + if renderer.msaaEnabled: + barriers.add VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), + dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + dstAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), + oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, + newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: renderer.colorImages[imageIndex], + subresourceRange: colorRange + ) + barriers.add VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), + dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT), + dstAccessMask: VkAccessFlags2(VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT), + oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, + newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: renderer.depthImages[imageIndex], + subresourceRange: depthRange + ) + var dependencyInfo = VkDependencyInfo( + sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + imageMemoryBarrierCount: uint32(barriers.len), + pImageMemoryBarriers: barriers[0].addr + ) + vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) + + var + colorClear = VkClearValue(color: VkClearColorValue(float32: ClearColor)) + depthClear = VkClearValue( + depthStencil: VkClearDepthStencilValue(depth: 1.0'f32, stencil: 0) + ) + colorAttachment = VkRenderingAttachmentInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + imageView: + if renderer.msaaEnabled: renderer.colorImageViews[imageIndex] + else: renderer.imageViews[imageIndex], + imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + resolveMode: + if renderer.msaaEnabled: VK_RESOLVE_MODE_AVERAGE_BIT + else: VK_RESOLVE_MODE_NONE, + resolveImageView: + if renderer.msaaEnabled: renderer.imageViews[imageIndex] + else: VkImageView(0), + resolveImageLayout: + if renderer.msaaEnabled: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL + else: VK_IMAGE_LAYOUT_UNDEFINED, + loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, + storeOp: + if renderer.msaaEnabled: VK_ATTACHMENT_STORE_OP_DONT_CARE + else: VK_ATTACHMENT_STORE_OP_STORE, + clearValue: colorClear + ) + depthAttachment = VkRenderingAttachmentInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + imageView: renderer.depthImageViews[imageIndex], + imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, + storeOp: VK_ATTACHMENT_STORE_OP_DONT_CARE, + clearValue: depthClear + ) + renderingInfo = VkRenderingInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_INFO, + renderArea: VkRect2D( + offset: VkOffset2D(x: 0, y: 0), + extent: ctx.swapChainExtent + ), + layerCount: 1, + colorAttachmentCount: 1, + pColorAttachments: colorAttachment.addr, + pDepthAttachment: depthAttachment.addr + ) + viewport = VkViewport( + x: 0, + y: ctx.swapChainExtent.height.float32, + width: ctx.swapChainExtent.width.float32, + height: -ctx.swapChainExtent.height.float32, + minDepth: 0, + maxDepth: 1 + ) + scissor = VkRect2D( + offset: VkOffset2D(x: 0, y: 0), + extent: ctx.swapChainExtent + ) + var vertexBuffers = [renderer.vertexBuffer] + var offsets = [VkDeviceSize(0)] + var descriptorSet = renderer.descriptorSet + + vkCmdBeginRendering(commandBuffer, renderingInfo.addr) + vkCmdSetViewport(commandBuffer, 0, 1, viewport.addr) + vkCmdSetScissor(commandBuffer, 0, 1, scissor.addr) + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.pipeline) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.pipelineLayout, 0, 1, descriptorSet.addr, 0, nil) + vkCmdPushConstants( + commandBuffer, + renderer.pipelineLayout, + VkShaderStageFlags(VK_SHADER_STAGE_VERTEX_BIT), + 0, + uint32(sizeof(renderer.transform)), + unsafeAddr renderer.transform[0] + ) + vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers[0].addr, offsets[0].addr) + vkCmdDraw(commandBuffer, uint32(CubeVertices.len), 1, 0, 0) + vkCmdEndRendering(commandBuffer) + + var toPresent = VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + srcAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), + dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + dstAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), + oldLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + newLayout: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: ctx.swapChainImages[imageIndex], + subresourceRange: colorRange + ) + dependencyInfo.imageMemoryBarrierCount = 1 + dependencyInfo.pImageMemoryBarriers = toPresent.addr + vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) + checkVk(vkEndCommandBuffer(commandBuffer), "Ending cube command buffer") + +proc drawFrame(ctx: var VulkanContext, renderer: var CubeRenderer): bool = + let frame = ctx.currentFrame + let fence = ctx.inFlightFences[frame] + discard vkWaitForFences(ctx.device, 1, unsafeAddr fence, VkBool32(VK_TRUE), uint64.high) + discard vkResetFences(ctx.device, 1, unsafeAddr fence) + + var imageIndex: uint32 + let acquireResult = vkAcquireNextImageKHR( + ctx.device, + ctx.swapChain, + uint64.high, + ctx.imageAvailableSemaphores[frame], + VkFence(0), + imageIndex.addr + ) + if requiresSwapChainRecreate(acquireResult): + return true + checkVk(acquireResult, "Acquiring next swapchain image") + + recordCurrentFrame(ctx, renderer, imageIndex) + + var + commandBuffer = renderer.commandBuffers[imageIndex] + waitInfo = VkSemaphoreSubmitInfo( + sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + semaphore: ctx.imageAvailableSemaphores[frame], + stageMask: VkPipelineStageFlags2( + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT + ), + value: 0, + deviceIndex: 0 + ) + commandBufferInfo = VkCommandBufferSubmitInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + commandBuffer: commandBuffer, + deviceMask: 0 + ) + signalInfo = VkSemaphoreSubmitInfo( + sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + semaphore: ctx.renderFinishedSemaphores[frame], + stageMask: VkPipelineStageFlags2( + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT + ), + value: 0, + deviceIndex: 0 + ) + submitInfo = VkSubmitInfo2( + sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + waitSemaphoreInfoCount: 1, + pWaitSemaphoreInfos: waitInfo.addr, + commandBufferInfoCount: 1, + pCommandBufferInfos: commandBufferInfo.addr, + signalSemaphoreInfoCount: 1, + pSignalSemaphoreInfos: signalInfo.addr + ) + checkVk( + vkQueueSubmit2(ctx.graphicsQueue, 1, submitInfo.addr, fence), + "Submitting cube command buffer" + ) + + var + swapChains = [ctx.swapChain] + presentInfo = VkPresentInfoKHR( + sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + waitSemaphoreCount: 1, + pWaitSemaphores: signalInfo.semaphore.addr, + swapchainCount: 1, + pSwapchains: swapChains[0].addr, + pImageIndices: imageIndex.addr + ) + let presentResult = vkQueuePresentKHR(ctx.presentQueue, presentInfo.addr) + let needsRecreate = requiresSwapChainRecreate(presentResult) + if not needsRecreate: + checkVk(presentResult, "Presenting cube frame") + ctx.currentFrame = (ctx.currentFrame + 1) mod FRAME_COUNT + inc renderer.frameCounter + needsRecreate + +proc shutdown(ctx: VulkanContext, renderer: var CubeRenderer) = + destroySwapChainResources(ctx, renderer) + if renderer.descriptorPool.int64 != 0: + vkDestroyDescriptorPool(ctx.device, renderer.descriptorPool, nil) + if renderer.textureSampler.int64 != 0: + vkDestroySampler(ctx.device, renderer.textureSampler, nil) + if renderer.textureImageView.int64 != 0: + vkDestroyImageView(ctx.device, renderer.textureImageView, nil) + if renderer.textureImage.int64 != 0: + vkDestroyImage(ctx.device, renderer.textureImage, nil) + if renderer.textureImageMemory.int64 != 0: + vkFreeMemory(ctx.device, renderer.textureImageMemory, nil) + if renderer.vertexBuffer.int64 != 0: + vkDestroyBuffer(ctx.device, renderer.vertexBuffer, nil) + if renderer.vertexBufferMemory.int64 != 0: + vkFreeMemory(ctx.device, renderer.vertexBufferMemory, nil) + if renderer.descriptorSetLayout.int64 != 0: + vkDestroyDescriptorSetLayout(ctx.device, renderer.descriptorSetLayout, nil) + +when isMainModule: + let window = newWindow("Vulkan 1.4 - Basic Cube", ivec2(Width.int32, Height.int32)) + let hwnd = window.getHWND() + if hwnd == 0: + raise newException(Exception, "Failed to acquire HWND from window") + + var ctx: VulkanContext + ctx.initDevice(hwnd, Width, Height) + + var renderer: CubeRenderer + initRenderer(ctx, renderer) + + var needsResize = false + var frameInProgress = false + window.onResize = proc() = + needsResize = true + window.onFrame = proc() = + if frameInProgress: + return + frameInProgress = true + try: + if needsResize: + let size = window.size + if size.x > 0 and size.y > 0: + recreateRenderer(ctx, renderer, size.x.int, size.y.int) + needsResize = false + else: + return + + let aspect = ctx.swapChainExtent.width.float32 / max(1'u32, ctx.swapChainExtent.height).float32 + updateTransform(renderer, aspect) + if drawFrame(ctx, renderer): + let size = window.size + if size.x > 0 and size.y > 0: + recreateRenderer(ctx, renderer, size.x.int, size.y.int) + else: + needsResize = true + finally: + frameInProgress = false + + try: + while not window.closeRequested: + pollEvents() + finally: + shutdown(ctx, renderer) + ctx.cleanup() diff --git a/examples/basic_quad.nim b/examples/basic_quad.nim index 6e308f6..9657580 100644 --- a/examples/basic_quad.nim +++ b/examples/basic_quad.nim @@ -1,490 +1,490 @@ -## Vulkan textured quad example using auto-generated vk14 bindings. - -import - std/os, - pixie, - windy, - vk14 - -type - QuadVertex = object - position: array[3, float32] - color: array[3, float32] - uv: array[2, float32] - - QuadRenderer = object - descriptorSetLayout: VkDescriptorSetLayout - pipelineLayout: VkPipelineLayout - pipeline: VkPipeline - imageViews: seq[VkImageView] - commandBuffers: seq[VkCommandBuffer] - vertexBuffer: VkBuffer - vertexBufferMemory: VkDeviceMemory - textureImage: VkImage - textureImageMemory: VkDeviceMemory - textureImageView: VkImageView - textureSampler: VkSampler - descriptorPool: VkDescriptorPool - descriptorSet: VkDescriptorSet - -const - Width = 1280 - Height = 800 - TexturePath = currentSourcePath().parentDir / "testTexture.png" - ClearColor = [0.05'f32, 0.05'f32, 0.1'f32, 1.0'f32] - QuadVertices = [ - QuadVertex(position: [-0.5'f32, 0.5, 0.0], color: [1.0'f32, 0.0, 0.0], uv: [0.0'f32, 0.0]), - QuadVertex(position: [0.5'f32, 0.5, 0.0], color: [0.0'f32, 1.0, 0.0], uv: [1.0'f32, 0.0]), - QuadVertex(position: [0.5'f32, -0.5, 0.0], color: [0.0'f32, 0.0, 1.0], uv: [1.0'f32, 1.0]), - QuadVertex(position: [-0.5'f32, 0.5, 0.0], color: [1.0'f32, 0.0, 0.0], uv: [0.0'f32, 0.0]), - QuadVertex(position: [0.5'f32, -0.5, 0.0], color: [0.0'f32, 0.0, 1.0], uv: [1.0'f32, 1.0]), - QuadVertex(position: [-0.5'f32, -0.5, 0.0], color: [1.0'f32, 1.0, 0.0], uv: [0.0'f32, 1.0]), - ] - -proc requiresSwapChainRecreate(vkResult: VkResult): bool = - ## Returns true if the result indicates the swapchain is out of date. - let code = vkResult.int32 - code == VK_SUBOPTIMAL_KHR.int32 or code == VK_ERROR_OUT_OF_DATE_KHR.int32 - -proc findMemoryType(ctx: VulkanContext, typeFilter, properties: uint32): uint32 = - ## Finds a memory type matching the filter and property flags. - var memProps: VkPhysicalDeviceMemoryProperties - vkGetPhysicalDeviceMemoryProperties(ctx.physicalDevice, memProps.addr) - for i in 0'u32 ..< memProps.memoryTypeCount: - if ((typeFilter shr i) and 1) == 1 and (memProps.memoryTypes[i].propertyFlags.uint32 and properties) == properties: - return i - raise newException(Exception, "No suitable memory type") - -proc createShaderModule(device: VkDevice, code: string): VkShaderModule = - ## Creates a shader module from SPIR-V bytecode. - var ci = VkShaderModuleCreateInfo(sType: VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, codeSize: code.len.uint32, pCode: cast[ptr uint32](code[0].unsafeAddr)) - checkVk(vkCreateShaderModule(device, ci.addr, nil, result.addr), "createShaderModule") - -proc createBuffer(ctx: VulkanContext, size: VkDeviceSize, usage, properties: uint32, buffer: var VkBuffer, memory: var VkDeviceMemory) = - ## Creates a buffer with dedicated memory. - var bi = VkBufferCreateInfo(sType: VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, size: size, usage: VkBufferUsageFlags(usage), sharingMode: VK_SHARING_MODE_EXCLUSIVE) - checkVk(vkCreateBuffer(ctx.device, bi.addr, nil, buffer.addr), "createBuffer") - var mr: VkMemoryRequirements - vkGetBufferMemoryRequirements(ctx.device, buffer, mr.addr) - var ai = VkMemoryAllocateInfo(sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, allocationSize: mr.size, memoryTypeIndex: findMemoryType(ctx, mr.memoryTypeBits, properties)) - checkVk(vkAllocateMemory(ctx.device, ai.addr, nil, memory.addr), "allocBufferMem") - checkVk(vkBindBufferMemory(ctx.device, buffer, memory, VkDeviceSize(0)), "bindBufferMem") - -proc beginSingleTimeCommands(ctx: VulkanContext): VkCommandBuffer = - ## Allocates and begins a one-shot command buffer. - var ai = VkCommandBufferAllocateInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, commandPool: ctx.commandPool, level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, commandBufferCount: 1) - checkVk(vkAllocateCommandBuffers(ctx.device, ai.addr, result.addr), "allocSingleCB") - var bi = VkCommandBufferBeginInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, flags: VkCommandBufferUsageFlags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT)) - checkVk(vkBeginCommandBuffer(result, bi.addr), "beginSingleCB") - -proc endSingleTimeCommands(ctx: VulkanContext, cb: VkCommandBuffer) = - ## Ends, submits, and waits for a one-shot command buffer. - checkVk(vkEndCommandBuffer(cb), "endSingleCB") - var - cbInfo = VkCommandBufferSubmitInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, - commandBuffer: cb, - deviceMask: 0 - ) - si = VkSubmitInfo2( - sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, - commandBufferInfoCount: 1, - pCommandBufferInfos: cbInfo.addr - ) - checkVk( - vkQueueSubmit2(ctx.graphicsQueue, 1, si.addr, VkFence(0)), - "submitSingleCB" - ) - checkVk(vkQueueWaitIdle(ctx.graphicsQueue), "waitIdle") - vkFreeCommandBuffers(ctx.device, ctx.commandPool, 1, unsafeAddr cb) - -proc createImage(ctx: VulkanContext, w, h: int, format: VkFormat, tiling: VkImageTiling, usage, properties: uint32, image: var VkImage, memory: var VkDeviceMemory) = - ## Creates a 2D image with dedicated memory. - var ii = VkImageCreateInfo(sType: VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, imageType: VK_IMAGE_TYPE_2D, format: format, extent: VkExtent3D(width: w.uint32, height: h.uint32, depth: 1), mipLevels: 1, arrayLayers: 1, samples: VK_SAMPLE_COUNT_1_BIT, tiling: tiling, usage: VkImageUsageFlags(usage), sharingMode: VK_SHARING_MODE_EXCLUSIVE, initialLayout: VK_IMAGE_LAYOUT_UNDEFINED) - checkVk(vkCreateImage(ctx.device, ii.addr, nil, image.addr), "createImage") - var mr: VkMemoryRequirements - vkGetImageMemoryRequirements(ctx.device, image, mr.addr) - var ai = VkMemoryAllocateInfo(sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, allocationSize: mr.size, memoryTypeIndex: findMemoryType(ctx, mr.memoryTypeBits, properties)) - checkVk(vkAllocateMemory(ctx.device, ai.addr, nil, memory.addr), "allocImageMem") - checkVk(vkBindImageMemory(ctx.device, image, memory, VkDeviceSize(0)), "bindImageMem") - -proc transitionImageLayout(ctx: VulkanContext, image: VkImage, oldLayout, newLayout: VkImageLayout) = - ## Transitions an image between layouts using a pipeline barrier. - let cb = beginSingleTimeCommands(ctx) - var barrier = VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - oldLayout: oldLayout, - newLayout: newLayout, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: image, - subresourceRange: VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - ) - var - srcStage, dstStage: VkPipelineStageFlags2 - srcAccess, dstAccess: VkAccessFlags2 - if oldLayout == VK_IMAGE_LAYOUT_UNDEFINED and newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: - srcAccess = VkAccessFlags2(VK_ACCESS_2_NONE) - dstAccess = VkAccessFlags2(VK_ACCESS_2_TRANSFER_WRITE_BIT) - srcStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE) - dstStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT) - elif oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL and newLayout == VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL: - srcAccess = VkAccessFlags2(VK_ACCESS_2_TRANSFER_WRITE_BIT) - dstAccess = VkAccessFlags2(VK_ACCESS_2_SHADER_READ_BIT) - srcStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT) - dstStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT) - else: - raise newException(Exception, "Unsupported layout transition") - barrier.srcStageMask = srcStage - barrier.srcAccessMask = srcAccess - barrier.dstStageMask = dstStage - barrier.dstAccessMask = dstAccess - var depInfo = VkDependencyInfo( - sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - imageMemoryBarrierCount: 1, - pImageMemoryBarriers: barrier.addr - ) - vkCmdPipelineBarrier2(cb, depInfo.addr) - endSingleTimeCommands(ctx, cb) - -proc copyBufferToImage(ctx: VulkanContext, buffer: VkBuffer, image: VkImage, w, h: int) = - ## Copies pixel data from a staging buffer into an image. - let cb = beginSingleTimeCommands(ctx) - var region = VkBufferImageCopy(bufferOffset: VkDeviceSize(0), imageSubresource: VkImageSubresourceLayers(aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), mipLevel: 0, baseArrayLayer: 0, layerCount: 1), imageOffset: VkOffset3D(x: 0, y: 0, z: 0), imageExtent: VkExtent3D(width: w.uint32, height: h.uint32, depth: 1)) - vkCmdCopyBufferToImage(cb, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, region.addr) - endSingleTimeCommands(ctx, cb) - -proc createTextureImage(ctx: VulkanContext, r: var QuadRenderer) = - ## Loads a PNG texture into a device-local Vulkan image. - let img = readImage(TexturePath) - let imageSize = VkDeviceSize(img.width * img.height * 4) - var px = newSeq[uint8](int(imageSize)) - for y in 0 ..< img.height: - copyMem(addr px[y * img.width * 4], cast[ptr uint8](img.data[img.dataIndex(0, y)].addr), img.width * 4) - var sb: VkBuffer; var sbm: VkDeviceMemory - createBuffer(ctx, imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, sb, sbm) - var mapped: pointer - checkVk(vkMapMemory(ctx.device, sbm, VkDeviceSize(0), imageSize, VkMemoryMapFlags(0), mapped.addr), "mapStaging") - copyMem(mapped, addr px[0], px.len) - vkUnmapMemory(ctx.device, sbm) - createImage(ctx, img.width, img.height, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT or VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, r.textureImage, r.textureImageMemory) - transitionImageLayout(ctx, r.textureImage, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) - copyBufferToImage(ctx, sb, r.textureImage, img.width, img.height) - transitionImageLayout(ctx, r.textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL) - vkDestroyBuffer(ctx.device, sb, nil) - vkFreeMemory(ctx.device, sbm, nil) - -proc createTextureImageView(ctx: VulkanContext, r: var QuadRenderer) = - ## Creates an image view for the texture. - var ci = VkImageViewCreateInfo(sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, image: r.textureImage, viewType: VK_IMAGE_VIEW_TYPE_2D, format: VK_FORMAT_R8G8B8A8_SRGB, subresourceRange: VkImageSubresourceRange(aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), baseMipLevel: 0, levelCount: 1, baseArrayLayer: 0, layerCount: 1)) - checkVk(vkCreateImageView(ctx.device, ci.addr, nil, r.textureImageView.addr), "createTexView") - -proc createTextureSampler(ctx: VulkanContext, r: var QuadRenderer) = - ## Creates a linear sampler for the texture. - var si = VkSamplerCreateInfo(sType: VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, magFilter: VK_FILTER_LINEAR, minFilter: VK_FILTER_LINEAR, mipmapMode: VK_SAMPLER_MIPMAP_MODE_LINEAR, addressModeU: VK_SAMPLER_ADDRESS_MODE_REPEAT, addressModeV: VK_SAMPLER_ADDRESS_MODE_REPEAT, addressModeW: VK_SAMPLER_ADDRESS_MODE_REPEAT, anisotropyEnable: VkBool32(VK_FALSE), maxAnisotropy: 1, compareEnable: VkBool32(VK_FALSE), compareOp: VK_COMPARE_OP_ALWAYS, borderColor: VK_BORDER_COLOR_INT_OPAQUE_BLACK, unnormalizedCoordinates: VkBool32(VK_FALSE)) - checkVk(vkCreateSampler(ctx.device, si.addr, nil, r.textureSampler.addr), "createSampler") - -proc createDescriptorSetLayout(ctx: VulkanContext, r: var QuadRenderer) = - ## Creates the descriptor set layout for the combined image sampler. - var lb = VkDescriptorSetLayoutBinding(binding: 0, descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, descriptorCount: 1, stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_FRAGMENT_BIT)) - var li = VkDescriptorSetLayoutCreateInfo(sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, bindingCount: 1, pBindings: lb.addr) - checkVk(vkCreateDescriptorSetLayout(ctx.device, li.addr, nil, r.descriptorSetLayout.addr), "createDSLayout") - -proc createImageViews(ctx: VulkanContext, r: var QuadRenderer) = - ## Creates swapchain image views. - r.imageViews.setLen(ctx.swapChainImages.len) - for i, image in ctx.swapChainImages: - var ci = VkImageViewCreateInfo(sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, image: image, viewType: VK_IMAGE_VIEW_TYPE_2D, format: ctx.swapChainImageFormat, subresourceRange: VkImageSubresourceRange(aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), baseMipLevel: 0, levelCount: 1, baseArrayLayer: 0, layerCount: 1)) - checkVk(vkCreateImageView(ctx.device, ci.addr, nil, r.imageViews[i].addr), "createSwapImgView") - -proc createGraphicsPipeline(ctx: VulkanContext, r: var QuadRenderer) = - ## Creates the textured quad graphics pipeline. - const - vertCode = staticRead("shaders/basic_quad.vert.spv") - fragCode = staticRead("shaders/basic_quad.frag.spv") - let vertMod = createShaderModule(ctx.device, vertCode) - let fragMod = createShaderModule(ctx.device, fragCode) - try: - var - colorFormat = ctx.swapChainImageFormat - renderingInfo = VkPipelineRenderingCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, - colorAttachmentCount: 1, - pColorAttachmentFormats: colorFormat.addr - ) - dynamicStates = [ - VkDynamicState(VK_DYNAMIC_STATE_VIEWPORT), - VkDynamicState(VK_DYNAMIC_STATE_SCISSOR) - ] - dynamicState = VkPipelineDynamicStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, - dynamicStateCount: uint32(dynamicStates.len), - pDynamicStates: dynamicStates[0].addr - ) - stages = [ - VkPipelineShaderStageCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, stage: VK_SHADER_STAGE_VERTEX_BIT, module: vertMod, pName: "main"), - VkPipelineShaderStageCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, stage: VK_SHADER_STAGE_FRAGMENT_BIT, module: fragMod, pName: "main"), - ] - bd = VkVertexInputBindingDescription(binding: 0, stride: uint32(sizeof(QuadVertex)), inputRate: VK_VERTEX_INPUT_RATE_VERTEX) - ad = [ - VkVertexInputAttributeDescription(location: 0, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: 0), - VkVertexInputAttributeDescription(location: 1, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: uint32(sizeof(array[3, float32]))), - VkVertexInputAttributeDescription(location: 2, binding: 0, format: VK_FORMAT_R32G32_SFLOAT, offset: uint32(sizeof(array[3, float32]) * 2)), - ] - vi = VkPipelineVertexInputStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, vertexBindingDescriptionCount: 1, pVertexBindingDescriptions: bd.addr, vertexAttributeDescriptionCount: uint32(ad.len), pVertexAttributeDescriptions: ad[0].addr) - ia = VkPipelineInputAssemblyStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, primitiveRestartEnable: VkBool32(VK_FALSE)) - vs = VkPipelineViewportStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, viewportCount: 1, scissorCount: 1) - rs = VkPipelineRasterizationStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, polygonMode: VK_POLYGON_MODE_FILL, lineWidth: 1.0, cullMode: VkCullModeFlags(VK_CULL_MODE_BACK_BIT), frontFace: VK_FRONT_FACE_CLOCKWISE) - ms = VkPipelineMultisampleStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, rasterizationSamples: VK_SAMPLE_COUNT_1_BIT) - cba = VkPipelineColorBlendAttachmentState(colorWriteMask: VkColorComponentFlags(0xF), blendEnable: VkBool32(VK_FALSE), srcColorBlendFactor: VK_BLEND_FACTOR_ONE, dstColorBlendFactor: VK_BLEND_FACTOR_ZERO, colorBlendOp: VK_BLEND_OP_ADD, srcAlphaBlendFactor: VK_BLEND_FACTOR_ONE, dstAlphaBlendFactor: VK_BLEND_FACTOR_ZERO, alphaBlendOp: VK_BLEND_OP_ADD) - cb = VkPipelineColorBlendStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, logicOp: VK_LOGIC_OP_COPY, attachmentCount: 1, pAttachments: cba.addr) - pli = VkPipelineLayoutCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, setLayoutCount: 1, pSetLayouts: r.descriptorSetLayout.addr) - checkVk(vkCreatePipelineLayout(ctx.device, pli.addr, nil, r.pipelineLayout.addr), "createPipelineLayout") - var pi = VkGraphicsPipelineCreateInfo(sType: VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, pNext: renderingInfo.addr, stageCount: 2, pStages: stages[0].addr, pVertexInputState: vi.addr, pInputAssemblyState: ia.addr, pViewportState: vs.addr, pRasterizationState: rs.addr, pMultisampleState: ms.addr, pColorBlendState: cb.addr, pDynamicState: dynamicState.addr, layout: r.pipelineLayout) - checkVk(vkCreateGraphicsPipelines(ctx.device, VkPipelineCache(0), 1, pi.addr, nil, r.pipeline.addr), "createPipeline") - finally: - vkDestroyShaderModule(ctx.device, vertMod, nil) - vkDestroyShaderModule(ctx.device, fragMod, nil) - -proc createVertexBuffer(ctx: VulkanContext, r: var QuadRenderer) = - ## Creates and fills the vertex buffer. - let sz = VkDeviceSize(sizeof(QuadVertices)) - createBuffer(ctx, sz, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, r.vertexBuffer, r.vertexBufferMemory) - var mapped: pointer - checkVk(vkMapMemory(ctx.device, r.vertexBufferMemory, VkDeviceSize(0), sz, VkMemoryMapFlags(0), mapped.addr), "mapVB") - copyMem(mapped, unsafeAddr QuadVertices[0], sizeof(QuadVertices)) - vkUnmapMemory(ctx.device, r.vertexBufferMemory) - -proc createDescriptorResources(ctx: VulkanContext, r: var QuadRenderer) = - ## Creates descriptor pool, allocates descriptor set, and binds the texture. - var ps = VkDescriptorPoolSize(`type`: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, descriptorCount: 1) - var pi = VkDescriptorPoolCreateInfo(sType: VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, maxSets: 1, poolSizeCount: 1, pPoolSizes: ps.addr) - checkVk(vkCreateDescriptorPool(ctx.device, pi.addr, nil, r.descriptorPool.addr), "createDSPool") - var sl = r.descriptorSetLayout - var ai = VkDescriptorSetAllocateInfo(sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, descriptorPool: r.descriptorPool, descriptorSetCount: 1, pSetLayouts: sl.addr) - checkVk(vkAllocateDescriptorSets(ctx.device, ai.addr, r.descriptorSet.addr), "allocDS") - var ii = VkDescriptorImageInfo(sampler: r.textureSampler, imageView: r.textureImageView, imageLayout: VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL) - var dw = VkWriteDescriptorSet(sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, dstSet: r.descriptorSet, dstBinding: 0, descriptorCount: 1, descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pImageInfo: ii.addr) - vkUpdateDescriptorSets(ctx.device, 1, dw.addr, 0, nil) - -proc recordCommandBuffers(ctx: VulkanContext, r: var QuadRenderer) - -proc destroySwapChainResources(ctx: VulkanContext, r: var QuadRenderer) = - ## Destroys swapchain-dependent resources before recreation. - if r.commandBuffers.len > 0: - vkFreeCommandBuffers(ctx.device, ctx.commandPool, uint32(r.commandBuffers.len), r.commandBuffers[0].addr) - r.commandBuffers.setLen(0) - for iv in r.imageViews: vkDestroyImageView(ctx.device, iv, nil) - r.imageViews.setLen(0) - if r.pipeline.int64 != 0: vkDestroyPipeline(ctx.device, r.pipeline, nil); r.pipeline = VkPipeline(0) - if r.pipelineLayout.int64 != 0: vkDestroyPipelineLayout(ctx.device, r.pipelineLayout, nil); r.pipelineLayout = VkPipelineLayout(0) - -proc recreateRenderer(ctx: var VulkanContext, r: var QuadRenderer, w, h: int) = - ## Recreates swapchain-dependent resources after a resize. - discard vkDeviceWaitIdle(ctx.device) - destroySwapChainResources(ctx, r) - recreateSwapChain(ctx, w, h) - createImageViews(ctx, r) - createGraphicsPipeline(ctx, r) - recordCommandBuffers(ctx, r) - -proc recordCommandBuffers(ctx: VulkanContext, r: var QuadRenderer) = - ## Allocates command buffers used for dynamic rendering. - r.commandBuffers.setLen(ctx.swapChainImages.len) - var ai = VkCommandBufferAllocateInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, commandPool: ctx.commandPool, level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, commandBufferCount: uint32(r.commandBuffers.len)) - checkVk(vkAllocateCommandBuffers(ctx.device, ai.addr, r.commandBuffers[0].addr), "allocCBs") - -proc initRenderer(ctx: VulkanContext, r: var QuadRenderer) = - ## Initializes all rendering resources. - createDescriptorSetLayout(ctx, r) - createImageViews(ctx, r) - createGraphicsPipeline(ctx, r) - createVertexBuffer(ctx, r) - createTextureImage(ctx, r) - createTextureImageView(ctx, r) - createTextureSampler(ctx, r) - createDescriptorResources(ctx, r) - recordCommandBuffers(ctx, r) - -proc recordCurrentFrame(ctx: VulkanContext, r: QuadRenderer, imageIndex: uint32) = - ## Records one frame using dynamic rendering and Synchronization2. - let commandBuffer = r.commandBuffers[imageIndex] - discard vkResetCommandBuffer(commandBuffer, VkCommandBufferResetFlags(0)) - var bi = VkCommandBufferBeginInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO) - checkVk(vkBeginCommandBuffer(commandBuffer, bi.addr), "beginCB") - let colorRange = VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - var toAttachment = VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), - srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), - dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - dstAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), - oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, - newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: ctx.swapChainImages[imageIndex], - subresourceRange: colorRange - ) - var depInfo = VkDependencyInfo( - sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - imageMemoryBarrierCount: 1, - pImageMemoryBarriers: toAttachment.addr - ) - vkCmdPipelineBarrier2(commandBuffer, depInfo.addr) - var - cv = VkClearValue(color: VkClearColorValue(float32: ClearColor)) - colorAttachment = VkRenderingAttachmentInfo( - sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - imageView: r.imageViews[imageIndex], - imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, - storeOp: VK_ATTACHMENT_STORE_OP_STORE, - clearValue: cv - ) - renderingInfo = VkRenderingInfo( - sType: VK_STRUCTURE_TYPE_RENDERING_INFO, - renderArea: VkRect2D(offset: VkOffset2D(x: 0, y: 0), extent: ctx.swapChainExtent), - layerCount: 1, - colorAttachmentCount: 1, - pColorAttachments: colorAttachment.addr - ) - viewport = VkViewport(x: 0, y: ctx.swapChainExtent.height.float32, width: ctx.swapChainExtent.width.float32, height: -ctx.swapChainExtent.height.float32, minDepth: 0, maxDepth: 1) - scissor = VkRect2D(offset: VkOffset2D(x: 0, y: 0), extent: ctx.swapChainExtent) - vbs = [r.vertexBuffer] - ofs = [VkDeviceSize(0)] - ds = r.descriptorSet - vkCmdBeginRendering(commandBuffer, renderingInfo.addr) - vkCmdSetViewport(commandBuffer, 0, 1, viewport.addr) - vkCmdSetScissor(commandBuffer, 0, 1, scissor.addr) - vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, r.pipeline) - vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, r.pipelineLayout, 0, 1, ds.addr, 0, nil) - vkCmdBindVertexBuffers(commandBuffer, 0, 1, vbs[0].addr, ofs[0].addr) - vkCmdDraw(commandBuffer, uint32(QuadVertices.len), 1, 0, 0) - vkCmdEndRendering(commandBuffer) - var toPresent = VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - srcAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), - dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), - dstAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), - oldLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - newLayout: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: ctx.swapChainImages[imageIndex], - subresourceRange: colorRange - ) - depInfo.pImageMemoryBarriers = toPresent.addr - vkCmdPipelineBarrier2(commandBuffer, depInfo.addr) - checkVk(vkEndCommandBuffer(commandBuffer), "endCB") - -proc drawFrame(ctx: var VulkanContext, r: QuadRenderer): bool = - ## Draws a frame, returns true if swapchain needs recreation. - let frame = ctx.currentFrame - let fence = ctx.inFlightFences[frame] - discard vkWaitForFences(ctx.device, 1, unsafeAddr fence, VkBool32(VK_TRUE), uint64.high) - discard vkResetFences(ctx.device, 1, unsafeAddr fence) - var imageIndex: uint32 - let acqResult = vkAcquireNextImageKHR(ctx.device, ctx.swapChain, uint64.high, ctx.imageAvailableSemaphores[frame], VkFence(0), imageIndex.addr) - if requiresSwapChainRecreate(acqResult): return true - checkVk(acqResult, "acquireImage") - recordCurrentFrame(ctx, r, imageIndex) - var - cmdBuf = r.commandBuffers[imageIndex] - waitInfo = VkSemaphoreSubmitInfo( - sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, - semaphore: ctx.imageAvailableSemaphores[frame], - stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - value: 0, - deviceIndex: 0 - ) - cmdInfo = VkCommandBufferSubmitInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, - commandBuffer: cmdBuf, - deviceMask: 0 - ) - signalInfo = VkSemaphoreSubmitInfo( - sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, - semaphore: ctx.renderFinishedSemaphores[frame], - stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - value: 0, - deviceIndex: 0 - ) - si = VkSubmitInfo2( - sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, - waitSemaphoreInfoCount: 1, - pWaitSemaphoreInfos: waitInfo.addr, - commandBufferInfoCount: 1, - pCommandBufferInfos: cmdInfo.addr, - signalSemaphoreInfoCount: 1, - pSignalSemaphoreInfos: signalInfo.addr - ) - checkVk(vkQueueSubmit2(ctx.graphicsQueue, 1, si.addr, fence), "submitCB") - var - scs = [ctx.swapChain] - pi = VkPresentInfoKHR(sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, waitSemaphoreCount: 1, pWaitSemaphores: signalInfo.semaphore.addr, swapchainCount: 1, pSwapchains: scs[0].addr, pImageIndices: imageIndex.addr) - let presResult = vkQueuePresentKHR(ctx.presentQueue, pi.addr) - let needsRecreate = requiresSwapChainRecreate(presResult) - if not needsRecreate: checkVk(presResult, "present") - ctx.currentFrame = (ctx.currentFrame + 1) mod FRAME_COUNT - needsRecreate - -proc shutdown(ctx: VulkanContext, r: var QuadRenderer) = - ## Releases all rendering resources. - for iv in r.imageViews: vkDestroyImageView(ctx.device, iv, nil) - if r.descriptorPool.int64 != 0: vkDestroyDescriptorPool(ctx.device, r.descriptorPool, nil) - if r.textureSampler.int64 != 0: vkDestroySampler(ctx.device, r.textureSampler, nil) - if r.textureImageView.int64 != 0: vkDestroyImageView(ctx.device, r.textureImageView, nil) - if r.textureImage.int64 != 0: vkDestroyImage(ctx.device, r.textureImage, nil) - if r.textureImageMemory.int64 != 0: vkFreeMemory(ctx.device, r.textureImageMemory, nil) - if r.vertexBuffer.int64 != 0: vkDestroyBuffer(ctx.device, r.vertexBuffer, nil) - if r.vertexBufferMemory.int64 != 0: vkFreeMemory(ctx.device, r.vertexBufferMemory, nil) - if r.pipeline.int64 != 0: vkDestroyPipeline(ctx.device, r.pipeline, nil) - if r.pipelineLayout.int64 != 0: vkDestroyPipelineLayout(ctx.device, r.pipelineLayout, nil) - if r.descriptorSetLayout.int64 != 0: vkDestroyDescriptorSetLayout(ctx.device, r.descriptorSetLayout, nil) - -when isMainModule: - let window = newWindow("Vulkan 1.4 - Textured Quad", ivec2(Width.int32, Height.int32)) - let hwnd = window.getHWND() - if hwnd == 0: raise newException(Exception, "Failed to acquire HWND") - var ctx: VulkanContext - ctx.initDevice(hwnd, Width, Height) - var renderer: QuadRenderer - initRenderer(ctx, renderer) - var needsResize = false - var frameInProgress = false - window.onResize = proc() = needsResize = true - window.onFrame = proc() = - if frameInProgress: return - frameInProgress = true - try: - if needsResize: - let sz = window.size - if sz.x > 0 and sz.y > 0: - recreateRenderer(ctx, renderer, sz.x.int, sz.y.int) - needsResize = false - else: return - if drawFrame(ctx, renderer): - let sz = window.size - if sz.x > 0 and sz.y > 0: - recreateRenderer(ctx, renderer, sz.x.int, sz.y.int) - else: needsResize = true - finally: frameInProgress = false - try: - while not window.closeRequested: pollEvents() - finally: - shutdown(ctx, renderer) - ctx.cleanup() +## Vulkan textured quad example using auto-generated vk14 bindings. + +import + std/os, + pixie, + windy, + vk14 + +type + QuadVertex = object + position: array[3, float32] + color: array[3, float32] + uv: array[2, float32] + + QuadRenderer = object + descriptorSetLayout: VkDescriptorSetLayout + pipelineLayout: VkPipelineLayout + pipeline: VkPipeline + imageViews: seq[VkImageView] + commandBuffers: seq[VkCommandBuffer] + vertexBuffer: VkBuffer + vertexBufferMemory: VkDeviceMemory + textureImage: VkImage + textureImageMemory: VkDeviceMemory + textureImageView: VkImageView + textureSampler: VkSampler + descriptorPool: VkDescriptorPool + descriptorSet: VkDescriptorSet + +const + Width = 1280 + Height = 800 + TexturePath = currentSourcePath().parentDir / "testTexture.png" + ClearColor = [0.05'f32, 0.05'f32, 0.1'f32, 1.0'f32] + QuadVertices = [ + QuadVertex(position: [-0.5'f32, 0.5, 0.0], color: [1.0'f32, 0.0, 0.0], uv: [0.0'f32, 0.0]), + QuadVertex(position: [0.5'f32, 0.5, 0.0], color: [0.0'f32, 1.0, 0.0], uv: [1.0'f32, 0.0]), + QuadVertex(position: [0.5'f32, -0.5, 0.0], color: [0.0'f32, 0.0, 1.0], uv: [1.0'f32, 1.0]), + QuadVertex(position: [-0.5'f32, 0.5, 0.0], color: [1.0'f32, 0.0, 0.0], uv: [0.0'f32, 0.0]), + QuadVertex(position: [0.5'f32, -0.5, 0.0], color: [0.0'f32, 0.0, 1.0], uv: [1.0'f32, 1.0]), + QuadVertex(position: [-0.5'f32, -0.5, 0.0], color: [1.0'f32, 1.0, 0.0], uv: [0.0'f32, 1.0]), + ] + +proc requiresSwapChainRecreate(vkResult: VkResult): bool = + ## Returns true if the result indicates the swapchain is out of date. + let code = vkResult.int32 + code == VK_SUBOPTIMAL_KHR.int32 or code == VK_ERROR_OUT_OF_DATE_KHR.int32 + +proc findMemoryType(ctx: VulkanContext, typeFilter, properties: uint32): uint32 = + ## Finds a memory type matching the filter and property flags. + var memProps: VkPhysicalDeviceMemoryProperties + vkGetPhysicalDeviceMemoryProperties(ctx.physicalDevice, memProps.addr) + for i in 0'u32 ..< memProps.memoryTypeCount: + if ((typeFilter shr i) and 1) == 1 and (memProps.memoryTypes[i].propertyFlags.uint32 and properties) == properties: + return i + raise newException(Exception, "No suitable memory type") + +proc createShaderModule(device: VkDevice, code: string): VkShaderModule = + ## Creates a shader module from SPIR-V bytecode. + var ci = VkShaderModuleCreateInfo(sType: VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, codeSize: code.len.uint32, pCode: cast[ptr uint32](code[0].unsafeAddr)) + checkVk(vkCreateShaderModule(device, ci.addr, nil, result.addr), "createShaderModule") + +proc createBuffer(ctx: VulkanContext, size: VkDeviceSize, usage, properties: uint32, buffer: var VkBuffer, memory: var VkDeviceMemory) = + ## Creates a buffer with dedicated memory. + var bi = VkBufferCreateInfo(sType: VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, size: size, usage: VkBufferUsageFlags(usage), sharingMode: VK_SHARING_MODE_EXCLUSIVE) + checkVk(vkCreateBuffer(ctx.device, bi.addr, nil, buffer.addr), "createBuffer") + var mr: VkMemoryRequirements + vkGetBufferMemoryRequirements(ctx.device, buffer, mr.addr) + var ai = VkMemoryAllocateInfo(sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, allocationSize: mr.size, memoryTypeIndex: findMemoryType(ctx, mr.memoryTypeBits, properties)) + checkVk(vkAllocateMemory(ctx.device, ai.addr, nil, memory.addr), "allocBufferMem") + checkVk(vkBindBufferMemory(ctx.device, buffer, memory, VkDeviceSize(0)), "bindBufferMem") + +proc beginSingleTimeCommands(ctx: VulkanContext): VkCommandBuffer = + ## Allocates and begins a one-shot command buffer. + var ai = VkCommandBufferAllocateInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, commandPool: ctx.commandPool, level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, commandBufferCount: 1) + checkVk(vkAllocateCommandBuffers(ctx.device, ai.addr, result.addr), "allocSingleCB") + var bi = VkCommandBufferBeginInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, flags: VkCommandBufferUsageFlags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT)) + checkVk(vkBeginCommandBuffer(result, bi.addr), "beginSingleCB") + +proc endSingleTimeCommands(ctx: VulkanContext, cb: VkCommandBuffer) = + ## Ends, submits, and waits for a one-shot command buffer. + checkVk(vkEndCommandBuffer(cb), "endSingleCB") + var + cbInfo = VkCommandBufferSubmitInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + commandBuffer: cb, + deviceMask: 0 + ) + si = VkSubmitInfo2( + sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + commandBufferInfoCount: 1, + pCommandBufferInfos: cbInfo.addr + ) + checkVk( + vkQueueSubmit2(ctx.graphicsQueue, 1, si.addr, VkFence(0)), + "submitSingleCB" + ) + checkVk(vkQueueWaitIdle(ctx.graphicsQueue), "waitIdle") + vkFreeCommandBuffers(ctx.device, ctx.commandPool, 1, unsafeAddr cb) + +proc createImage(ctx: VulkanContext, w, h: int, format: VkFormat, tiling: VkImageTiling, usage, properties: uint32, image: var VkImage, memory: var VkDeviceMemory) = + ## Creates a 2D image with dedicated memory. + var ii = VkImageCreateInfo(sType: VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, imageType: VK_IMAGE_TYPE_2D, format: format, extent: VkExtent3D(width: w.uint32, height: h.uint32, depth: 1), mipLevels: 1, arrayLayers: 1, samples: VK_SAMPLE_COUNT_1_BIT, tiling: tiling, usage: VkImageUsageFlags(usage), sharingMode: VK_SHARING_MODE_EXCLUSIVE, initialLayout: VK_IMAGE_LAYOUT_UNDEFINED) + checkVk(vkCreateImage(ctx.device, ii.addr, nil, image.addr), "createImage") + var mr: VkMemoryRequirements + vkGetImageMemoryRequirements(ctx.device, image, mr.addr) + var ai = VkMemoryAllocateInfo(sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, allocationSize: mr.size, memoryTypeIndex: findMemoryType(ctx, mr.memoryTypeBits, properties)) + checkVk(vkAllocateMemory(ctx.device, ai.addr, nil, memory.addr), "allocImageMem") + checkVk(vkBindImageMemory(ctx.device, image, memory, VkDeviceSize(0)), "bindImageMem") + +proc transitionImageLayout(ctx: VulkanContext, image: VkImage, oldLayout, newLayout: VkImageLayout) = + ## Transitions an image between layouts using a pipeline barrier. + let cb = beginSingleTimeCommands(ctx) + var barrier = VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + oldLayout: oldLayout, + newLayout: newLayout, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: image, + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + var + srcStage, dstStage: VkPipelineStageFlags2 + srcAccess, dstAccess: VkAccessFlags2 + if oldLayout == VK_IMAGE_LAYOUT_UNDEFINED and newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: + srcAccess = VkAccessFlags2(VK_ACCESS_2_NONE) + dstAccess = VkAccessFlags2(VK_ACCESS_2_TRANSFER_WRITE_BIT) + srcStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE) + dstStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT) + elif oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL and newLayout == VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL: + srcAccess = VkAccessFlags2(VK_ACCESS_2_TRANSFER_WRITE_BIT) + dstAccess = VkAccessFlags2(VK_ACCESS_2_SHADER_READ_BIT) + srcStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_TRANSFER_BIT) + dstStage = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT) + else: + raise newException(Exception, "Unsupported layout transition") + barrier.srcStageMask = srcStage + barrier.srcAccessMask = srcAccess + barrier.dstStageMask = dstStage + barrier.dstAccessMask = dstAccess + var depInfo = VkDependencyInfo( + sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + imageMemoryBarrierCount: 1, + pImageMemoryBarriers: barrier.addr + ) + vkCmdPipelineBarrier2(cb, depInfo.addr) + endSingleTimeCommands(ctx, cb) + +proc copyBufferToImage(ctx: VulkanContext, buffer: VkBuffer, image: VkImage, w, h: int) = + ## Copies pixel data from a staging buffer into an image. + let cb = beginSingleTimeCommands(ctx) + var region = VkBufferImageCopy(bufferOffset: VkDeviceSize(0), imageSubresource: VkImageSubresourceLayers(aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), mipLevel: 0, baseArrayLayer: 0, layerCount: 1), imageOffset: VkOffset3D(x: 0, y: 0, z: 0), imageExtent: VkExtent3D(width: w.uint32, height: h.uint32, depth: 1)) + vkCmdCopyBufferToImage(cb, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, region.addr) + endSingleTimeCommands(ctx, cb) + +proc createTextureImage(ctx: VulkanContext, r: var QuadRenderer) = + ## Loads a PNG texture into a device-local Vulkan image. + let img = readImage(TexturePath) + let imageSize = VkDeviceSize(img.width * img.height * 4) + var px = newSeq[uint8](int(imageSize)) + for y in 0 ..< img.height: + copyMem(addr px[y * img.width * 4], cast[ptr uint8](img.data[img.dataIndex(0, y)].addr), img.width * 4) + var sb: VkBuffer; var sbm: VkDeviceMemory + createBuffer(ctx, imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, sb, sbm) + var mapped: pointer + checkVk(vkMapMemory(ctx.device, sbm, VkDeviceSize(0), imageSize, VkMemoryMapFlags(0), mapped.addr), "mapStaging") + copyMem(mapped, addr px[0], px.len) + vkUnmapMemory(ctx.device, sbm) + createImage(ctx, img.width, img.height, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT or VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, r.textureImage, r.textureImageMemory) + transitionImageLayout(ctx, r.textureImage, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) + copyBufferToImage(ctx, sb, r.textureImage, img.width, img.height) + transitionImageLayout(ctx, r.textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL) + vkDestroyBuffer(ctx.device, sb, nil) + vkFreeMemory(ctx.device, sbm, nil) + +proc createTextureImageView(ctx: VulkanContext, r: var QuadRenderer) = + ## Creates an image view for the texture. + var ci = VkImageViewCreateInfo(sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, image: r.textureImage, viewType: VK_IMAGE_VIEW_TYPE_2D, format: VK_FORMAT_R8G8B8A8_SRGB, subresourceRange: VkImageSubresourceRange(aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), baseMipLevel: 0, levelCount: 1, baseArrayLayer: 0, layerCount: 1)) + checkVk(vkCreateImageView(ctx.device, ci.addr, nil, r.textureImageView.addr), "createTexView") + +proc createTextureSampler(ctx: VulkanContext, r: var QuadRenderer) = + ## Creates a linear sampler for the texture. + var si = VkSamplerCreateInfo(sType: VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, magFilter: VK_FILTER_LINEAR, minFilter: VK_FILTER_LINEAR, mipmapMode: VK_SAMPLER_MIPMAP_MODE_LINEAR, addressModeU: VK_SAMPLER_ADDRESS_MODE_REPEAT, addressModeV: VK_SAMPLER_ADDRESS_MODE_REPEAT, addressModeW: VK_SAMPLER_ADDRESS_MODE_REPEAT, anisotropyEnable: VkBool32(VK_FALSE), maxAnisotropy: 1, compareEnable: VkBool32(VK_FALSE), compareOp: VK_COMPARE_OP_ALWAYS, borderColor: VK_BORDER_COLOR_INT_OPAQUE_BLACK, unnormalizedCoordinates: VkBool32(VK_FALSE)) + checkVk(vkCreateSampler(ctx.device, si.addr, nil, r.textureSampler.addr), "createSampler") + +proc createDescriptorSetLayout(ctx: VulkanContext, r: var QuadRenderer) = + ## Creates the descriptor set layout for the combined image sampler. + var lb = VkDescriptorSetLayoutBinding(binding: 0, descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, descriptorCount: 1, stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_FRAGMENT_BIT)) + var li = VkDescriptorSetLayoutCreateInfo(sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, bindingCount: 1, pBindings: lb.addr) + checkVk(vkCreateDescriptorSetLayout(ctx.device, li.addr, nil, r.descriptorSetLayout.addr), "createDSLayout") + +proc createImageViews(ctx: VulkanContext, r: var QuadRenderer) = + ## Creates swapchain image views. + r.imageViews.setLen(ctx.swapChainImages.len) + for i, image in ctx.swapChainImages: + var ci = VkImageViewCreateInfo(sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, image: image, viewType: VK_IMAGE_VIEW_TYPE_2D, format: ctx.swapChainImageFormat, subresourceRange: VkImageSubresourceRange(aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), baseMipLevel: 0, levelCount: 1, baseArrayLayer: 0, layerCount: 1)) + checkVk(vkCreateImageView(ctx.device, ci.addr, nil, r.imageViews[i].addr), "createSwapImgView") + +proc createGraphicsPipeline(ctx: VulkanContext, r: var QuadRenderer) = + ## Creates the textured quad graphics pipeline. + const + vertCode = staticRead("shaders/basic_quad.vert.spv") + fragCode = staticRead("shaders/basic_quad.frag.spv") + let vertMod = createShaderModule(ctx.device, vertCode) + let fragMod = createShaderModule(ctx.device, fragCode) + try: + var + colorFormat = ctx.swapChainImageFormat + renderingInfo = VkPipelineRenderingCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, + colorAttachmentCount: 1, + pColorAttachmentFormats: colorFormat.addr + ) + dynamicStates = [ + VkDynamicState(VK_DYNAMIC_STATE_VIEWPORT), + VkDynamicState(VK_DYNAMIC_STATE_SCISSOR) + ] + dynamicState = VkPipelineDynamicStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + dynamicStateCount: uint32(dynamicStates.len), + pDynamicStates: dynamicStates[0].addr + ) + stages = [ + VkPipelineShaderStageCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, stage: VK_SHADER_STAGE_VERTEX_BIT, module: vertMod, pName: "main"), + VkPipelineShaderStageCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, stage: VK_SHADER_STAGE_FRAGMENT_BIT, module: fragMod, pName: "main"), + ] + bd = VkVertexInputBindingDescription(binding: 0, stride: uint32(sizeof(QuadVertex)), inputRate: VK_VERTEX_INPUT_RATE_VERTEX) + ad = [ + VkVertexInputAttributeDescription(location: 0, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: 0), + VkVertexInputAttributeDescription(location: 1, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: uint32(sizeof(array[3, float32]))), + VkVertexInputAttributeDescription(location: 2, binding: 0, format: VK_FORMAT_R32G32_SFLOAT, offset: uint32(sizeof(array[3, float32]) * 2)), + ] + vi = VkPipelineVertexInputStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, vertexBindingDescriptionCount: 1, pVertexBindingDescriptions: bd.addr, vertexAttributeDescriptionCount: uint32(ad.len), pVertexAttributeDescriptions: ad[0].addr) + ia = VkPipelineInputAssemblyStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, primitiveRestartEnable: VkBool32(VK_FALSE)) + vs = VkPipelineViewportStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, viewportCount: 1, scissorCount: 1) + rs = VkPipelineRasterizationStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, polygonMode: VK_POLYGON_MODE_FILL, lineWidth: 1.0, cullMode: VkCullModeFlags(VK_CULL_MODE_BACK_BIT), frontFace: VK_FRONT_FACE_CLOCKWISE) + ms = VkPipelineMultisampleStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, rasterizationSamples: VK_SAMPLE_COUNT_1_BIT) + cba = VkPipelineColorBlendAttachmentState(colorWriteMask: VkColorComponentFlags(0xF), blendEnable: VkBool32(VK_FALSE), srcColorBlendFactor: VK_BLEND_FACTOR_ONE, dstColorBlendFactor: VK_BLEND_FACTOR_ZERO, colorBlendOp: VK_BLEND_OP_ADD, srcAlphaBlendFactor: VK_BLEND_FACTOR_ONE, dstAlphaBlendFactor: VK_BLEND_FACTOR_ZERO, alphaBlendOp: VK_BLEND_OP_ADD) + cb = VkPipelineColorBlendStateCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, logicOp: VK_LOGIC_OP_COPY, attachmentCount: 1, pAttachments: cba.addr) + pli = VkPipelineLayoutCreateInfo(sType: VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, setLayoutCount: 1, pSetLayouts: r.descriptorSetLayout.addr) + checkVk(vkCreatePipelineLayout(ctx.device, pli.addr, nil, r.pipelineLayout.addr), "createPipelineLayout") + var pi = VkGraphicsPipelineCreateInfo(sType: VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, pNext: renderingInfo.addr, stageCount: 2, pStages: stages[0].addr, pVertexInputState: vi.addr, pInputAssemblyState: ia.addr, pViewportState: vs.addr, pRasterizationState: rs.addr, pMultisampleState: ms.addr, pColorBlendState: cb.addr, pDynamicState: dynamicState.addr, layout: r.pipelineLayout) + checkVk(vkCreateGraphicsPipelines(ctx.device, VkPipelineCache(0), 1, pi.addr, nil, r.pipeline.addr), "createPipeline") + finally: + vkDestroyShaderModule(ctx.device, vertMod, nil) + vkDestroyShaderModule(ctx.device, fragMod, nil) + +proc createVertexBuffer(ctx: VulkanContext, r: var QuadRenderer) = + ## Creates and fills the vertex buffer. + let sz = VkDeviceSize(sizeof(QuadVertices)) + createBuffer(ctx, sz, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, r.vertexBuffer, r.vertexBufferMemory) + var mapped: pointer + checkVk(vkMapMemory(ctx.device, r.vertexBufferMemory, VkDeviceSize(0), sz, VkMemoryMapFlags(0), mapped.addr), "mapVB") + copyMem(mapped, unsafeAddr QuadVertices[0], sizeof(QuadVertices)) + vkUnmapMemory(ctx.device, r.vertexBufferMemory) + +proc createDescriptorResources(ctx: VulkanContext, r: var QuadRenderer) = + ## Creates descriptor pool, allocates descriptor set, and binds the texture. + var ps = VkDescriptorPoolSize(`type`: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, descriptorCount: 1) + var pi = VkDescriptorPoolCreateInfo(sType: VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, maxSets: 1, poolSizeCount: 1, pPoolSizes: ps.addr) + checkVk(vkCreateDescriptorPool(ctx.device, pi.addr, nil, r.descriptorPool.addr), "createDSPool") + var sl = r.descriptorSetLayout + var ai = VkDescriptorSetAllocateInfo(sType: VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, descriptorPool: r.descriptorPool, descriptorSetCount: 1, pSetLayouts: sl.addr) + checkVk(vkAllocateDescriptorSets(ctx.device, ai.addr, r.descriptorSet.addr), "allocDS") + var ii = VkDescriptorImageInfo(sampler: r.textureSampler, imageView: r.textureImageView, imageLayout: VK_IMAGE_LAYOUT_READ_ONLY_OPTIMAL) + var dw = VkWriteDescriptorSet(sType: VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, dstSet: r.descriptorSet, dstBinding: 0, descriptorCount: 1, descriptorType: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, pImageInfo: ii.addr) + vkUpdateDescriptorSets(ctx.device, 1, dw.addr, 0, nil) + +proc recordCommandBuffers(ctx: VulkanContext, r: var QuadRenderer) + +proc destroySwapChainResources(ctx: VulkanContext, r: var QuadRenderer) = + ## Destroys swapchain-dependent resources before recreation. + if r.commandBuffers.len > 0: + vkFreeCommandBuffers(ctx.device, ctx.commandPool, uint32(r.commandBuffers.len), r.commandBuffers[0].addr) + r.commandBuffers.setLen(0) + for iv in r.imageViews: vkDestroyImageView(ctx.device, iv, nil) + r.imageViews.setLen(0) + if r.pipeline.int64 != 0: vkDestroyPipeline(ctx.device, r.pipeline, nil); r.pipeline = VkPipeline(0) + if r.pipelineLayout.int64 != 0: vkDestroyPipelineLayout(ctx.device, r.pipelineLayout, nil); r.pipelineLayout = VkPipelineLayout(0) + +proc recreateRenderer(ctx: var VulkanContext, r: var QuadRenderer, w, h: int) = + ## Recreates swapchain-dependent resources after a resize. + discard vkDeviceWaitIdle(ctx.device) + destroySwapChainResources(ctx, r) + recreateSwapChain(ctx, w, h) + createImageViews(ctx, r) + createGraphicsPipeline(ctx, r) + recordCommandBuffers(ctx, r) + +proc recordCommandBuffers(ctx: VulkanContext, r: var QuadRenderer) = + ## Allocates command buffers used for dynamic rendering. + r.commandBuffers.setLen(ctx.swapChainImages.len) + var ai = VkCommandBufferAllocateInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, commandPool: ctx.commandPool, level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, commandBufferCount: uint32(r.commandBuffers.len)) + checkVk(vkAllocateCommandBuffers(ctx.device, ai.addr, r.commandBuffers[0].addr), "allocCBs") + +proc initRenderer(ctx: VulkanContext, r: var QuadRenderer) = + ## Initializes all rendering resources. + createDescriptorSetLayout(ctx, r) + createImageViews(ctx, r) + createGraphicsPipeline(ctx, r) + createVertexBuffer(ctx, r) + createTextureImage(ctx, r) + createTextureImageView(ctx, r) + createTextureSampler(ctx, r) + createDescriptorResources(ctx, r) + recordCommandBuffers(ctx, r) + +proc recordCurrentFrame(ctx: VulkanContext, r: QuadRenderer, imageIndex: uint32) = + ## Records one frame using dynamic rendering and Synchronization2. + let commandBuffer = r.commandBuffers[imageIndex] + discard vkResetCommandBuffer(commandBuffer, VkCommandBufferResetFlags(0)) + var bi = VkCommandBufferBeginInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO) + checkVk(vkBeginCommandBuffer(commandBuffer, bi.addr), "beginCB") + let colorRange = VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + var toAttachment = VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), + dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + dstAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), + oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, + newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: ctx.swapChainImages[imageIndex], + subresourceRange: colorRange + ) + var depInfo = VkDependencyInfo( + sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + imageMemoryBarrierCount: 1, + pImageMemoryBarriers: toAttachment.addr + ) + vkCmdPipelineBarrier2(commandBuffer, depInfo.addr) + var + cv = VkClearValue(color: VkClearColorValue(float32: ClearColor)) + colorAttachment = VkRenderingAttachmentInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + imageView: r.imageViews[imageIndex], + imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, + storeOp: VK_ATTACHMENT_STORE_OP_STORE, + clearValue: cv + ) + renderingInfo = VkRenderingInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_INFO, + renderArea: VkRect2D(offset: VkOffset2D(x: 0, y: 0), extent: ctx.swapChainExtent), + layerCount: 1, + colorAttachmentCount: 1, + pColorAttachments: colorAttachment.addr + ) + viewport = VkViewport(x: 0, y: ctx.swapChainExtent.height.float32, width: ctx.swapChainExtent.width.float32, height: -ctx.swapChainExtent.height.float32, minDepth: 0, maxDepth: 1) + scissor = VkRect2D(offset: VkOffset2D(x: 0, y: 0), extent: ctx.swapChainExtent) + vbs = [r.vertexBuffer] + ofs = [VkDeviceSize(0)] + ds = r.descriptorSet + vkCmdBeginRendering(commandBuffer, renderingInfo.addr) + vkCmdSetViewport(commandBuffer, 0, 1, viewport.addr) + vkCmdSetScissor(commandBuffer, 0, 1, scissor.addr) + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, r.pipeline) + vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, r.pipelineLayout, 0, 1, ds.addr, 0, nil) + vkCmdBindVertexBuffers(commandBuffer, 0, 1, vbs[0].addr, ofs[0].addr) + vkCmdDraw(commandBuffer, uint32(QuadVertices.len), 1, 0, 0) + vkCmdEndRendering(commandBuffer) + var toPresent = VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + srcAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), + dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + dstAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), + oldLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + newLayout: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: ctx.swapChainImages[imageIndex], + subresourceRange: colorRange + ) + depInfo.pImageMemoryBarriers = toPresent.addr + vkCmdPipelineBarrier2(commandBuffer, depInfo.addr) + checkVk(vkEndCommandBuffer(commandBuffer), "endCB") + +proc drawFrame(ctx: var VulkanContext, r: QuadRenderer): bool = + ## Draws a frame, returns true if swapchain needs recreation. + let frame = ctx.currentFrame + let fence = ctx.inFlightFences[frame] + discard vkWaitForFences(ctx.device, 1, unsafeAddr fence, VkBool32(VK_TRUE), uint64.high) + discard vkResetFences(ctx.device, 1, unsafeAddr fence) + var imageIndex: uint32 + let acqResult = vkAcquireNextImageKHR(ctx.device, ctx.swapChain, uint64.high, ctx.imageAvailableSemaphores[frame], VkFence(0), imageIndex.addr) + if requiresSwapChainRecreate(acqResult): return true + checkVk(acqResult, "acquireImage") + recordCurrentFrame(ctx, r, imageIndex) + var + cmdBuf = r.commandBuffers[imageIndex] + waitInfo = VkSemaphoreSubmitInfo( + sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + semaphore: ctx.imageAvailableSemaphores[frame], + stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + value: 0, + deviceIndex: 0 + ) + cmdInfo = VkCommandBufferSubmitInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + commandBuffer: cmdBuf, + deviceMask: 0 + ) + signalInfo = VkSemaphoreSubmitInfo( + sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + semaphore: ctx.renderFinishedSemaphores[frame], + stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + value: 0, + deviceIndex: 0 + ) + si = VkSubmitInfo2( + sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + waitSemaphoreInfoCount: 1, + pWaitSemaphoreInfos: waitInfo.addr, + commandBufferInfoCount: 1, + pCommandBufferInfos: cmdInfo.addr, + signalSemaphoreInfoCount: 1, + pSignalSemaphoreInfos: signalInfo.addr + ) + checkVk(vkQueueSubmit2(ctx.graphicsQueue, 1, si.addr, fence), "submitCB") + var + scs = [ctx.swapChain] + pi = VkPresentInfoKHR(sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, waitSemaphoreCount: 1, pWaitSemaphores: signalInfo.semaphore.addr, swapchainCount: 1, pSwapchains: scs[0].addr, pImageIndices: imageIndex.addr) + let presResult = vkQueuePresentKHR(ctx.presentQueue, pi.addr) + let needsRecreate = requiresSwapChainRecreate(presResult) + if not needsRecreate: checkVk(presResult, "present") + ctx.currentFrame = (ctx.currentFrame + 1) mod FRAME_COUNT + needsRecreate + +proc shutdown(ctx: VulkanContext, r: var QuadRenderer) = + ## Releases all rendering resources. + for iv in r.imageViews: vkDestroyImageView(ctx.device, iv, nil) + if r.descriptorPool.int64 != 0: vkDestroyDescriptorPool(ctx.device, r.descriptorPool, nil) + if r.textureSampler.int64 != 0: vkDestroySampler(ctx.device, r.textureSampler, nil) + if r.textureImageView.int64 != 0: vkDestroyImageView(ctx.device, r.textureImageView, nil) + if r.textureImage.int64 != 0: vkDestroyImage(ctx.device, r.textureImage, nil) + if r.textureImageMemory.int64 != 0: vkFreeMemory(ctx.device, r.textureImageMemory, nil) + if r.vertexBuffer.int64 != 0: vkDestroyBuffer(ctx.device, r.vertexBuffer, nil) + if r.vertexBufferMemory.int64 != 0: vkFreeMemory(ctx.device, r.vertexBufferMemory, nil) + if r.pipeline.int64 != 0: vkDestroyPipeline(ctx.device, r.pipeline, nil) + if r.pipelineLayout.int64 != 0: vkDestroyPipelineLayout(ctx.device, r.pipelineLayout, nil) + if r.descriptorSetLayout.int64 != 0: vkDestroyDescriptorSetLayout(ctx.device, r.descriptorSetLayout, nil) + +when isMainModule: + let window = newWindow("Vulkan 1.4 - Textured Quad", ivec2(Width.int32, Height.int32)) + let hwnd = window.getHWND() + if hwnd == 0: raise newException(Exception, "Failed to acquire HWND") + var ctx: VulkanContext + ctx.initDevice(hwnd, Width, Height) + var renderer: QuadRenderer + initRenderer(ctx, renderer) + var needsResize = false + var frameInProgress = false + window.onResize = proc() = needsResize = true + window.onFrame = proc() = + if frameInProgress: return + frameInProgress = true + try: + if needsResize: + let sz = window.size + if sz.x > 0 and sz.y > 0: + recreateRenderer(ctx, renderer, sz.x.int, sz.y.int) + needsResize = false + else: return + if drawFrame(ctx, renderer): + let sz = window.size + if sz.x > 0 and sz.y > 0: + recreateRenderer(ctx, renderer, sz.x.int, sz.y.int) + else: needsResize = true + finally: frameInProgress = false + try: + while not window.closeRequested: pollEvents() + finally: + shutdown(ctx, renderer) + ctx.cleanup() diff --git a/examples/nim.cfg b/examples/nim.cfg index 2b07c25..ba54263 100644 --- a/examples/nim.cfg +++ b/examples/nim.cfg @@ -1,2 +1,2 @@ ---path: "$projectDir/../src" ---define:useVulkan +--path: "$projectDir/../src" +--define:useVulkan diff --git a/examples/shaders/basic_cube.frag b/examples/shaders/basic_cube.frag index 604ac47..4d5d54a 100644 --- a/examples/shaders/basic_cube.frag +++ b/examples/shaders/basic_cube.frag @@ -1,10 +1,10 @@ -#version 450 - -layout(set = 0, binding = 0) uniform sampler2D tex0; - -layout(location = 0) in vec2 fragUv; -layout(location = 0) out vec4 outColor; - -void main() { - outColor = texture(tex0, fragUv); -} +#version 450 + +layout(set = 0, binding = 0) uniform sampler2D tex0; + +layout(location = 0) in vec2 fragUv; +layout(location = 0) out vec4 outColor; + +void main() { + outColor = texture(tex0, fragUv); +} diff --git a/examples/shaders/basic_cube.vert b/examples/shaders/basic_cube.vert index 66f8cf8..5eec42a 100644 --- a/examples/shaders/basic_cube.vert +++ b/examples/shaders/basic_cube.vert @@ -1,15 +1,15 @@ -#version 450 - -layout(push_constant) uniform Transform { - mat4 mvp; -} transform; - -layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec2 inUv; - -layout(location = 0) out vec2 fragUv; - -void main() { - gl_Position = transform.mvp * vec4(inPosition, 1.0); - fragUv = inUv; -} +#version 450 + +layout(push_constant) uniform Transform { + mat4 mvp; +} transform; + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec2 inUv; + +layout(location = 0) out vec2 fragUv; + +void main() { + gl_Position = transform.mvp * vec4(inPosition, 1.0); + fragUv = inUv; +} diff --git a/examples/shaders/basic_quad.frag b/examples/shaders/basic_quad.frag index 604ac47..4d5d54a 100644 --- a/examples/shaders/basic_quad.frag +++ b/examples/shaders/basic_quad.frag @@ -1,10 +1,10 @@ -#version 450 - -layout(set = 0, binding = 0) uniform sampler2D tex0; - -layout(location = 0) in vec2 fragUv; -layout(location = 0) out vec4 outColor; - -void main() { - outColor = texture(tex0, fragUv); -} +#version 450 + +layout(set = 0, binding = 0) uniform sampler2D tex0; + +layout(location = 0) in vec2 fragUv; +layout(location = 0) out vec4 outColor; + +void main() { + outColor = texture(tex0, fragUv); +} diff --git a/examples/shaders/basic_quad.vert b/examples/shaders/basic_quad.vert index e0bc7d6..935cc12 100644 --- a/examples/shaders/basic_quad.vert +++ b/examples/shaders/basic_quad.vert @@ -1,12 +1,12 @@ -#version 450 - -layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec3 inColor; -layout(location = 2) in vec2 inUv; - -layout(location = 0) out vec2 fragUv; - -void main() { - gl_Position = vec4(inPosition, 1.0); - fragUv = inUv; -} +#version 450 + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inColor; +layout(location = 2) in vec2 inUv; + +layout(location = 0) out vec2 fragUv; + +void main() { + gl_Position = vec4(inPosition, 1.0); + fragUv = inUv; +} diff --git a/examples/shaders/sprite_sheet.frag b/examples/shaders/sprite_sheet.frag index 604ac47..4d5d54a 100644 --- a/examples/shaders/sprite_sheet.frag +++ b/examples/shaders/sprite_sheet.frag @@ -1,10 +1,10 @@ -#version 450 - -layout(set = 0, binding = 0) uniform sampler2D tex0; - -layout(location = 0) in vec2 fragUv; -layout(location = 0) out vec4 outColor; - -void main() { - outColor = texture(tex0, fragUv); -} +#version 450 + +layout(set = 0, binding = 0) uniform sampler2D tex0; + +layout(location = 0) in vec2 fragUv; +layout(location = 0) out vec4 outColor; + +void main() { + outColor = texture(tex0, fragUv); +} diff --git a/examples/shaders/sprite_sheet.vert b/examples/shaders/sprite_sheet.vert index 8f552fc..899f61d 100644 --- a/examples/shaders/sprite_sheet.vert +++ b/examples/shaders/sprite_sheet.vert @@ -1,11 +1,11 @@ -#version 450 - -layout(location = 0) in vec2 inPosition; -layout(location = 1) in vec2 inUv; - -layout(location = 0) out vec2 fragUv; - -void main() { - gl_Position = vec4(inPosition, 0.0, 1.0); - fragUv = inUv; -} +#version 450 + +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec2 inUv; + +layout(location = 0) out vec2 fragUv; + +void main() { + gl_Position = vec4(inPosition, 0.0, 1.0); + fragUv = inUv; +} diff --git a/examples/shaders/viewer_obj.frag b/examples/shaders/viewer_obj.frag index e0b7a14..3599a30 100644 --- a/examples/shaders/viewer_obj.frag +++ b/examples/shaders/viewer_obj.frag @@ -1,12 +1,12 @@ -#version 450 - -layout(location = 0) in vec3 fragNormal; -layout(location = 0) out vec4 outColor; - -void main() { - vec3 lightDir = normalize(vec3(0.4, 0.8, 0.5)); - float diffuse = abs(dot(normalize(fragNormal), lightDir)); - float light = 0.2 + diffuse * 0.8; - vec3 baseColor = vec3(0.88, 0.84, 0.78); - outColor = vec4(baseColor * light, 1.0); -} +#version 450 + +layout(location = 0) in vec3 fragNormal; +layout(location = 0) out vec4 outColor; + +void main() { + vec3 lightDir = normalize(vec3(0.4, 0.8, 0.5)); + float diffuse = abs(dot(normalize(fragNormal), lightDir)); + float light = 0.2 + diffuse * 0.8; + vec3 baseColor = vec3(0.88, 0.84, 0.78); + outColor = vec4(baseColor * light, 1.0); +} diff --git a/examples/shaders/viewer_obj.vert b/examples/shaders/viewer_obj.vert index 3644a3e..f5ada62 100644 --- a/examples/shaders/viewer_obj.vert +++ b/examples/shaders/viewer_obj.vert @@ -1,16 +1,16 @@ -#version 450 - -layout(push_constant) uniform Transform { - mat4 mvp; - mat4 model; -} transform; - -layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec3 inNormal; - -layout(location = 0) out vec3 fragNormal; - -void main() { - gl_Position = transform.mvp * vec4(inPosition, 1.0); - fragNormal = normalize(mat3(transform.model) * inNormal); -} +#version 450 + +layout(push_constant) uniform Transform { + mat4 mvp; + mat4 model; +} transform; + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; + +layout(location = 0) out vec3 fragNormal; + +void main() { + gl_Position = transform.mvp * vec4(inPosition, 1.0); + fragNormal = normalize(mat3(transform.model) * inNormal); +} diff --git a/examples/viewer_obj.nim b/examples/viewer_obj.nim index 5998211..a463fc6 100644 --- a/examples/viewer_obj.nim +++ b/examples/viewer_obj.nim @@ -1,958 +1,958 @@ -import - std/[math, os, strutils], - vmath, - windy, - vk14 - -const - Width = 1280 - Height = 800 - RotateScale = 0.01'f32 - ZoomScale = 0.2'f32 - MinDistance = 1.0'f32 - MaxDistance = 8.0'f32 - MinPitch = -1.45'f32 - MaxPitch = 1.45'f32 - ClearColor = [0.05'f32, 0.05'f32, 0.08'f32, 1.0'f32] - -type - ViewerObjError = object of CatchableError - - ObjVertex = object - position: array[3, float32] - normal: array[3, float32] - - ObjMesh = object - vertices: seq[ObjVertex] - - CameraState = object - yaw: float32 - pitch: float32 - distance: float32 - - ObjRenderer = object - pipelineLayout: VkPipelineLayout - pipeline: VkPipeline - imageViews: seq[VkImageView] - depthImages: seq[VkImage] - depthImageMemories: seq[VkDeviceMemory] - depthImageViews: seq[VkImageView] - commandBuffers: seq[VkCommandBuffer] - vertexBuffer: VkBuffer - vertexBufferMemory: VkDeviceMemory - transform: array[32, float32] - vertexCount: uint32 - -proc objPath(): string = - currentSourcePath().parentDir / "bunny.obj" - -proc checkVk(result: VkResult, action: string) = - if result != VK_SUCCESS: - raise newException(Exception, action & " failed with VkResult " & $result.int32) - -proc requiresSwapChainRecreate(vkResult: VkResult): bool = - let code = vkResult.int32 - code == VK_SUBOPTIMAL_KHR.int32 or code == VK_ERROR_OUT_OF_DATE_KHR.int32 - -proc identityMatrix(): Mat4 = - gmat4[float32]( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ) - -proc perspectiveDx(fovY, aspect, nearPlane, farPlane: float32): Mat4 = - let - h = 1.0'f32 / tan(fovY * 0.5'f32) - w = h / aspect - depth = farPlane - nearPlane - result[0, 0] = w - result[0, 1] = 0 - result[0, 2] = 0 - result[0, 3] = 0 - result[1, 0] = 0 - result[1, 1] = h - result[1, 2] = 0 - result[1, 3] = 0 - result[2, 0] = 0 - result[2, 1] = 0 - result[2, 2] = farPlane / depth - result[2, 3] = 1 - result[3, 0] = 0 - result[3, 1] = 0 - result[3, 2] = -(nearPlane * farPlane) / depth - result[3, 3] = 0 - -proc mat4ToArray(matrix: Mat4): array[16, float32] = - var index = 0 - for i in 0 ..< 4: - for j in 0 ..< 4: - result[index] = matrix[i, j] - inc index - -proc packTransforms(mvp, model: Mat4): array[32, float32] = - let - mvpValues = mat4ToArray(mvp) - modelValues = mat4ToArray(model) - for i in 0 ..< 16: - result[i] = mvpValues[i] - result[i + 16] = modelValues[i] - -proc toFloatArray(v: Vec3): array[3, float32] = - [v.x, v.y, v.z] - -proc parseFloat32(value: string): float32 = - try: - parseFloat(value).float32 - except ValueError: - raise newException(ViewerObjError, "Invalid float in OBJ: " & value) - -proc parseObjIndex(value: string, vertexCount: int): int = - if value.len == 0: - raise newException(ViewerObjError, "OBJ face is missing a vertex index") - - let rawIndex = - try: - parseInt(value) - except ValueError: - raise newException(ViewerObjError, "Invalid OBJ face index: " & value) - - result = - if rawIndex > 0: - rawIndex - 1 - elif rawIndex < 0: - vertexCount + rawIndex - else: - raise newException(ViewerObjError, "OBJ indices cannot be zero") - - if result < 0 or result >= vertexCount: - raise newException(ViewerObjError, "OBJ face index is out of range: " & value) - -proc parseFaceVertex(token: string, vertexCount: int): int = - let slash = token.find('/') - let indexToken = - if slash >= 0: - token[0 ..< slash] - else: - token - parseObjIndex(indexToken, vertexCount) - -proc normalizeSafe(v, fallback: Vec3): Vec3 = - if v.length() <= 0.000001'f32: - fallback - else: - v.normalize() - -proc loadObjMesh(path: string): ObjMesh = - if not fileExists(path): - raise newException(ViewerObjError, "OBJ file not found: " & path) - - var - positions: seq[Vec3] - triangles: seq[array[3, int]] - - for rawLine in readFile(path).splitLines(): - let line = rawLine.strip() - if line.len == 0 or line[0] == '#': - continue - - let parts = strutils.splitWhitespace(line) - case parts[0] - of "v": - if parts.len < 4: - raise newException(ViewerObjError, "OBJ vertex line is incomplete") - positions.add( - vec3( - parseFloat32(parts[1]), - parseFloat32(parts[2]), - parseFloat32(parts[3]) - ) - ) - of "f": - if parts.len < 4: - raise newException(ViewerObjError, "OBJ face line is incomplete") - var faceIndices: seq[int] - for i in 1 ..< parts.len: - faceIndices.add(parseFaceVertex(parts[i], positions.len)) - for i in 1 ..< faceIndices.len - 1: - triangles.add([faceIndices[0], faceIndices[i], faceIndices[i + 1]]) - else: - discard - - if positions.len == 0: - raise newException(ViewerObjError, "OBJ does not contain any vertices") - if triangles.len == 0: - raise newException(ViewerObjError, "OBJ does not contain any faces") - - var - minPos = positions[0] - maxPos = positions[0] - for i in 1 ..< positions.len: - let p = positions[i] - minPos.x = min(minPos.x, p.x) - minPos.y = min(minPos.y, p.y) - minPos.z = min(minPos.z, p.z) - maxPos.x = max(maxPos.x, p.x) - maxPos.y = max(maxPos.y, p.y) - maxPos.z = max(maxPos.z, p.z) - - let - center = (minPos + maxPos) * 0.5'f32 - size = maxPos - minPos - maxExtent = max(size.x, max(size.y, size.z)) - if maxExtent <= 0.0'f32: - raise newException(ViewerObjError, "OBJ bounds are degenerate") - - let uniformScale = 2.0'f32 / maxExtent - var normalizedPositions = newSeq[Vec3](positions.len) - for i, p in positions: - normalizedPositions[i] = (p - center) * uniformScale - - var smoothedNormals = newSeq[Vec3](normalizedPositions.len) - for tri in triangles: - let - a = normalizedPositions[tri[0]] - b = normalizedPositions[tri[1]] - c = normalizedPositions[tri[2]] - faceNormal = normalizeSafe((b - a).cross(c - a), vec3(0.0'f32, 1.0'f32, 0.0'f32)) - smoothedNormals[tri[0]] += faceNormal - smoothedNormals[tri[1]] += faceNormal - smoothedNormals[tri[2]] += faceNormal - - result.vertices = newSeq[ObjVertex](triangles.len * 3) - var vertexIndex = 0 - for tri in triangles: - for idx in tri: - result.vertices[vertexIndex] = ObjVertex( - position: normalizedPositions[idx].toFloatArray(), - normal: normalizeSafe(smoothedNormals[idx], vec3(0.0'f32, 1.0'f32, 0.0'f32)).toFloatArray() - ) - inc vertexIndex - -proc createShaderModule(device: VkDevice, code: string): VkShaderModule = - var createInfo = VkShaderModuleCreateInfo( - sType: VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, - codeSize: code.len.uint32, - pCode: cast[ptr uint32](code[0].unsafeAddr) - ) - checkVk(vkCreateShaderModule(device, createInfo.addr, nil, result.addr), "Creating shader module") - -proc findMemoryType(ctx: VulkanContext, typeFilter: uint32, properties: uint32): uint32 = - var memProperties: VkPhysicalDeviceMemoryProperties - vkGetPhysicalDeviceMemoryProperties(ctx.physicalDevice, memProperties.addr) - for i in 0'u32 ..< memProperties.memoryTypeCount: - let flags = memProperties.memoryTypes[i].propertyFlags.uint32 - if ((typeFilter shr i) and 1'u32) == 1'u32 and (flags and properties) == properties: - return i - raise newException(Exception, "Failed to find suitable Vulkan memory type") - -proc createBuffer( - ctx: VulkanContext, - size: VkDeviceSize, - usage: uint32, - properties: uint32, - buffer: var VkBuffer, - memory: var VkDeviceMemory -) = - var bufferInfo = VkBufferCreateInfo( - sType: VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, - size: size, - usage: VkBufferUsageFlags(usage), - sharingMode: VK_SHARING_MODE_EXCLUSIVE, - queueFamilyIndexCount: 0, - pQueueFamilyIndices: nil - ) - checkVk(vkCreateBuffer(ctx.device, bufferInfo.addr, nil, buffer.addr), "Creating buffer") - - var memRequirements: VkMemoryRequirements - vkGetBufferMemoryRequirements(ctx.device, buffer, memRequirements.addr) - - var allocInfo = VkMemoryAllocateInfo( - sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - allocationSize: memRequirements.size, - memoryTypeIndex: findMemoryType(ctx, memRequirements.memoryTypeBits, properties) - ) - checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), "Allocating buffer memory") - checkVk(vkBindBufferMemory(ctx.device, buffer, memory, VkDeviceSize(0)), "Binding buffer memory") - -proc createImage( - ctx: VulkanContext, - width, height: int, - format: VkFormat, - usage: uint32, - image: var VkImage, - memory: var VkDeviceMemory -) = - var imageInfo = VkImageCreateInfo( - sType: VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - imageType: VK_IMAGE_TYPE_2D, - format: format, - extent: VkExtent3D(width: uint32(width), height: uint32(height), depth: 1), - mipLevels: 1, - arrayLayers: 1, - samples: VK_SAMPLE_COUNT_1_BIT, - tiling: VK_IMAGE_TILING_OPTIMAL, - usage: VkImageUsageFlags(usage), - sharingMode: VK_SHARING_MODE_EXCLUSIVE, - initialLayout: VK_IMAGE_LAYOUT_UNDEFINED - ) - checkVk(vkCreateImage(ctx.device, imageInfo.addr, nil, image.addr), "Creating image") - - var memRequirements: VkMemoryRequirements - vkGetImageMemoryRequirements(ctx.device, image, memRequirements.addr) - - var allocInfo = VkMemoryAllocateInfo( - sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - allocationSize: memRequirements.size, - memoryTypeIndex: findMemoryType(ctx, memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.uint32) - ) - checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), "Allocating image memory") - checkVk(vkBindImageMemory(ctx.device, image, memory, VkDeviceSize(0)), "Binding image memory") - -proc beginSingleTimeCommands(ctx: VulkanContext): VkCommandBuffer = - var allocInfo = VkCommandBufferAllocateInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - commandPool: ctx.commandPool, - level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, - commandBufferCount: 1 - ) - checkVk(vkAllocateCommandBuffers(ctx.device, allocInfo.addr, result.addr), "Allocating single-use command buffer") - var beginInfo = VkCommandBufferBeginInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - flags: VkCommandBufferUsageFlags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) - ) - checkVk(vkBeginCommandBuffer(result, beginInfo.addr), "Beginning single-use command buffer") - -proc endSingleTimeCommands(ctx: VulkanContext, commandBuffer: VkCommandBuffer) = - checkVk(vkEndCommandBuffer(commandBuffer), "Ending single-use command buffer") - var - commandBufferInfo = VkCommandBufferSubmitInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, - commandBuffer: commandBuffer, - deviceMask: 0 - ) - submitInfo = VkSubmitInfo2( - sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, - commandBufferInfoCount: 1, - pCommandBufferInfos: commandBufferInfo.addr - ) - checkVk( - vkQueueSubmit2(ctx.graphicsQueue, 1, submitInfo.addr, VkFence(0)), - "Submitting single-use command buffer" - ) - checkVk(vkQueueWaitIdle(ctx.graphicsQueue), "Waiting for graphics queue idle") - vkFreeCommandBuffers(ctx.device, ctx.commandPool, 1, unsafeAddr commandBuffer) - -proc transitionDepthLayout(ctx: VulkanContext, image: VkImage) = - let commandBuffer = beginSingleTimeCommands(ctx) - var barrier = VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, - newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: image, - subresourceRange: VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - ) - barrier.srcStageMask = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE) - barrier.srcAccessMask = VkAccessFlags2(VK_ACCESS_2_NONE) - barrier.dstStageMask = VkPipelineStageFlags2( - VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT - ) - barrier.dstAccessMask = VkAccessFlags2( - VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT - ) - var dependencyInfo = VkDependencyInfo( - sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - imageMemoryBarrierCount: 1, - pImageMemoryBarriers: barrier.addr - ) - vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) - endSingleTimeCommands(ctx, commandBuffer) - -proc createSwapChainImageViews(ctx: VulkanContext, renderer: var ObjRenderer) = - renderer.imageViews.setLen(ctx.swapChainImages.len) - for i, image in ctx.swapChainImages: - var createInfo = VkImageViewCreateInfo( - sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - image: image, - viewType: VK_IMAGE_VIEW_TYPE_2D, - format: ctx.swapChainImageFormat, - components: VkComponentMapping( - r: VK_COMPONENT_SWIZZLE_IDENTITY, - g: VK_COMPONENT_SWIZZLE_IDENTITY, - b: VK_COMPONENT_SWIZZLE_IDENTITY, - a: VK_COMPONENT_SWIZZLE_IDENTITY - ), - subresourceRange: VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - ) - checkVk(vkCreateImageView(ctx.device, createInfo.addr, nil, renderer.imageViews[i].addr), "Creating swapchain image view") - -proc createDepthResources(ctx: VulkanContext, renderer: var ObjRenderer) = - let attachmentCount = ctx.swapChainImages.len - renderer.depthImages.setLen(attachmentCount) - renderer.depthImageMemories.setLen(attachmentCount) - renderer.depthImageViews.setLen(attachmentCount) - - for i in 0 ..< attachmentCount: - createImage( - ctx, - int(ctx.swapChainExtent.width), - int(ctx.swapChainExtent.height), - VK_FORMAT_D32_SFLOAT, - VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT.uint32, - renderer.depthImages[i], - renderer.depthImageMemories[i] - ) - - var imageViewInfo = VkImageViewCreateInfo( - sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - image: renderer.depthImages[i], - viewType: VK_IMAGE_VIEW_TYPE_2D, - format: VK_FORMAT_D32_SFLOAT, - components: VkComponentMapping( - r: VK_COMPONENT_SWIZZLE_IDENTITY, - g: VK_COMPONENT_SWIZZLE_IDENTITY, - b: VK_COMPONENT_SWIZZLE_IDENTITY, - a: VK_COMPONENT_SWIZZLE_IDENTITY - ), - subresourceRange: VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - ) - checkVk(vkCreateImageView(ctx.device, imageViewInfo.addr, nil, renderer.depthImageViews[i].addr), "Creating depth image view") - transitionDepthLayout(ctx, renderer.depthImages[i]) - -proc createGraphicsPipeline(ctx: VulkanContext, renderer: var ObjRenderer) = - const - vertShaderCode = staticRead("shaders/viewer_obj.vert.spv") - fragShaderCode = staticRead("shaders/viewer_obj.frag.spv") - let - vertShaderModule = createShaderModule(ctx.device, vertShaderCode) - fragShaderModule = createShaderModule(ctx.device, fragShaderCode) - try: - var - colorFormat = ctx.swapChainImageFormat - renderingInfo = VkPipelineRenderingCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, - colorAttachmentCount: 1, - pColorAttachmentFormats: colorFormat.addr, - depthAttachmentFormat: VK_FORMAT_D32_SFLOAT - ) - dynamicStates = [ - VkDynamicState(VK_DYNAMIC_STATE_VIEWPORT), - VkDynamicState(VK_DYNAMIC_STATE_SCISSOR) - ] - dynamicState = VkPipelineDynamicStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, - dynamicStateCount: uint32(dynamicStates.len), - pDynamicStates: dynamicStates[0].addr - ) - vertShaderStageInfo = VkPipelineShaderStageCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - stage: VK_SHADER_STAGE_VERTEX_BIT, - module: vertShaderModule, - pName: "main" - ) - fragShaderStageInfo = VkPipelineShaderStageCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - stage: VK_SHADER_STAGE_FRAGMENT_BIT, - module: fragShaderModule, - pName: "main" - ) - shaderStages = [vertShaderStageInfo, fragShaderStageInfo] - bindingDescription = VkVertexInputBindingDescription( - binding: 0, - stride: uint32(sizeof(ObjVertex)), - inputRate: VK_VERTEX_INPUT_RATE_VERTEX - ) - attributeDescriptions = [ - VkVertexInputAttributeDescription(location: 0, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: 0), - VkVertexInputAttributeDescription(location: 1, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: uint32(sizeof(array[3, float32]))) - ] - vertexInputInfo = VkPipelineVertexInputStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, - vertexBindingDescriptionCount: 1, - pVertexBindingDescriptions: bindingDescription.addr, - vertexAttributeDescriptionCount: uint32(attributeDescriptions.len), - pVertexAttributeDescriptions: attributeDescriptions[0].addr - ) - inputAssembly = VkPipelineInputAssemblyStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, - topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, - primitiveRestartEnable: VkBool32(VK_FALSE) - ) - viewportState = VkPipelineViewportStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, - viewportCount: 1, - scissorCount: 1 - ) - rasterizer = VkPipelineRasterizationStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, - depthClampEnable: VkBool32(VK_FALSE), - rasterizerDiscardEnable: VkBool32(VK_FALSE), - polygonMode: VK_POLYGON_MODE_FILL, - lineWidth: 1.0, - cullMode: VkCullModeFlags(VK_CULL_MODE_NONE), - frontFace: VK_FRONT_FACE_CLOCKWISE, - depthBiasEnable: VkBool32(VK_FALSE) - ) - multisampling = VkPipelineMultisampleStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, - sampleShadingEnable: VkBool32(VK_FALSE), - rasterizationSamples: VK_SAMPLE_COUNT_1_BIT - ) - depthStencil = VkPipelineDepthStencilStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, - depthTestEnable: VkBool32(VK_TRUE), - depthWriteEnable: VkBool32(VK_TRUE), - depthCompareOp: VK_COMPARE_OP_LESS, - depthBoundsTestEnable: VkBool32(VK_FALSE), - stencilTestEnable: VkBool32(VK_FALSE), - minDepthBounds: 0, - maxDepthBounds: 1 - ) - colorBlendAttachment = VkPipelineColorBlendAttachmentState( - colorWriteMask: VkColorComponentFlags(0x0000000F), - blendEnable: VkBool32(VK_FALSE), - srcColorBlendFactor: VK_BLEND_FACTOR_ONE, - dstColorBlendFactor: VK_BLEND_FACTOR_ZERO, - colorBlendOp: VK_BLEND_OP_ADD, - srcAlphaBlendFactor: VK_BLEND_FACTOR_ONE, - dstAlphaBlendFactor: VK_BLEND_FACTOR_ZERO, - alphaBlendOp: VK_BLEND_OP_ADD - ) - colorBlending = VkPipelineColorBlendStateCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, - logicOpEnable: VkBool32(VK_FALSE), - logicOp: VK_LOGIC_OP_COPY, - attachmentCount: 1, - pAttachments: colorBlendAttachment.addr, - blendConstants: [0.0'f32, 0.0'f32, 0.0'f32, 0.0'f32] - ) - pushConstantRange = VkPushConstantRange( - stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_VERTEX_BIT), - offset: 0, - size: uint32(sizeof(array[32, float32])) - ) - pipelineLayoutInfo = VkPipelineLayoutCreateInfo( - sType: VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, - setLayoutCount: 0, - pSetLayouts: nil, - pushConstantRangeCount: 1, - pPushConstantRanges: pushConstantRange.addr - ) - - checkVk(vkCreatePipelineLayout(ctx.device, pipelineLayoutInfo.addr, nil, renderer.pipelineLayout.addr), "Creating pipeline layout") - - var pipelineInfo = VkGraphicsPipelineCreateInfo( - sType: VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, - pNext: renderingInfo.addr, - stageCount: uint32(shaderStages.len), - pStages: shaderStages[0].addr, - pVertexInputState: vertexInputInfo.addr, - pInputAssemblyState: inputAssembly.addr, - pViewportState: viewportState.addr, - pRasterizationState: rasterizer.addr, - pMultisampleState: multisampling.addr, - pDepthStencilState: depthStencil.addr, - pColorBlendState: colorBlending.addr, - pDynamicState: dynamicState.addr, - layout: renderer.pipelineLayout - ) - checkVk(vkCreateGraphicsPipelines(ctx.device, VkPipelineCache(0), 1, pipelineInfo.addr, nil, renderer.pipeline.addr), "Creating graphics pipeline") - finally: - vkDestroyShaderModule(ctx.device, vertShaderModule, nil) - vkDestroyShaderModule(ctx.device, fragShaderModule, nil) - -proc uploadVertexBuffer(ctx: VulkanContext, renderer: var ObjRenderer, mesh: ObjMesh) = - if mesh.vertices.len == 0: - raise newException(ViewerObjError, "Mesh is empty") - - let bufferSize = VkDeviceSize(mesh.vertices.len * sizeof(ObjVertex)) - createBuffer( - ctx, - bufferSize, - VK_BUFFER_USAGE_VERTEX_BUFFER_BIT.uint32, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, - renderer.vertexBuffer, - renderer.vertexBufferMemory - ) - - var mapped: pointer - checkVk(vkMapMemory(ctx.device, renderer.vertexBufferMemory, VkDeviceSize(0), bufferSize, VkMemoryMapFlags(0), mapped.addr), "Mapping vertex buffer memory") - copyMem(mapped, unsafeAddr mesh.vertices[0], mesh.vertices.len * sizeof(ObjVertex)) - vkUnmapMemory(ctx.device, renderer.vertexBufferMemory) - renderer.vertexCount = uint32(mesh.vertices.len) - -proc recordCommandBuffers(ctx: VulkanContext, renderer: var ObjRenderer) - -proc destroySwapChainResources(ctx: VulkanContext, renderer: var ObjRenderer) = - if renderer.commandBuffers.len > 0: - vkFreeCommandBuffers(ctx.device, ctx.commandPool, uint32(renderer.commandBuffers.len), renderer.commandBuffers[0].addr) - renderer.commandBuffers.setLen(0) - for imageView in renderer.imageViews: - vkDestroyImageView(ctx.device, imageView, nil) - renderer.imageViews.setLen(0) - for imageView in renderer.depthImageViews: - vkDestroyImageView(ctx.device, imageView, nil) - renderer.depthImageViews.setLen(0) - for image in renderer.depthImages: - vkDestroyImage(ctx.device, image, nil) - renderer.depthImages.setLen(0) - for memory in renderer.depthImageMemories: - vkFreeMemory(ctx.device, memory, nil) - renderer.depthImageMemories.setLen(0) - if renderer.pipeline.int64 != 0: - vkDestroyPipeline(ctx.device, renderer.pipeline, nil) - renderer.pipeline = VkPipeline(0) - if renderer.pipelineLayout.int64 != 0: - vkDestroyPipelineLayout(ctx.device, renderer.pipelineLayout, nil) - renderer.pipelineLayout = VkPipelineLayout(0) - -proc recreateRenderer(ctx: var VulkanContext, renderer: var ObjRenderer, width, height: int) = - discard vkDeviceWaitIdle(ctx.device) - destroySwapChainResources(ctx, renderer) - recreateSwapChain(ctx, width, height) - createSwapChainImageViews(ctx, renderer) - createDepthResources(ctx, renderer) - createGraphicsPipeline(ctx, renderer) - recordCommandBuffers(ctx, renderer) - -proc recordCommandBuffers(ctx: VulkanContext, renderer: var ObjRenderer) = - renderer.commandBuffers.setLen(ctx.swapChainImages.len) - var allocInfo = VkCommandBufferAllocateInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - commandPool: ctx.commandPool, - level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, - commandBufferCount: uint32(renderer.commandBuffers.len) - ) - checkVk(vkAllocateCommandBuffers(ctx.device, allocInfo.addr, renderer.commandBuffers[0].addr), "Allocating OBJ command buffers") - -proc initRenderer(ctx: VulkanContext, renderer: var ObjRenderer, mesh: ObjMesh) = - createSwapChainImageViews(ctx, renderer) - createDepthResources(ctx, renderer) - createGraphicsPipeline(ctx, renderer) - uploadVertexBuffer(ctx, renderer, mesh) - recordCommandBuffers(ctx, renderer) - -proc updateCamera(camera: var CameraState, window: windy.Window) = - if window.buttonDown[MouseLeft]: - let delta = window.mouseDelta - camera.yaw += delta.x.float32 * RotateScale - camera.pitch = clamp( - camera.pitch + delta.y.float32 * RotateScale, - MinPitch, - MaxPitch - ) - - let scroll = window.scrollDelta - if scroll.y != 0.0'f32: - camera.distance = clamp( - camera.distance - scroll.y.float32 * ZoomScale, - MinDistance, - MaxDistance - ) - -proc updateTransform(renderer: var ObjRenderer, camera: CameraState, aspect: float32) = - let - cosPitch = cos(camera.pitch) - eye = vec3( - sin(camera.yaw) * cosPitch * camera.distance, - sin(camera.pitch) * camera.distance, - cos(camera.yaw) * cosPitch * camera.distance - ) - target = vec3(0.0'f32, 0.0'f32, 0.0'f32) - model = identityMatrix() - cameraAngles = toAngles(eye, target) - view = inverse(translate(eye) * fromAngles(cameraAngles)) - proj = perspectiveDx(60.0'f32.toRadians, aspect, 0.1'f32, 100.0'f32) - mvp = proj * view * model - renderer.transform = packTransforms(mvp, model) - -proc recordCurrentFrame(ctx: var VulkanContext, renderer: var ObjRenderer, imageIndex: uint32) = - let commandBuffer = renderer.commandBuffers[imageIndex] - discard vkResetCommandBuffer(commandBuffer, VkCommandBufferResetFlags(0)) - var beginInfo = VkCommandBufferBeginInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO) - checkVk(vkBeginCommandBuffer(commandBuffer, beginInfo.addr), "Beginning OBJ command buffer") - - let - colorRange = VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - depthRange = VkImageSubresourceRange( - aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), - baseMipLevel: 0, - levelCount: 1, - baseArrayLayer: 0, - layerCount: 1 - ) - var barriers = [ - VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), - srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), - dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - dstAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), - oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, - newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: ctx.swapChainImages[imageIndex], - subresourceRange: colorRange - ), - VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), - srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), - dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT), - dstAccessMask: VkAccessFlags2(VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT), - oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, - newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: renderer.depthImages[imageIndex], - subresourceRange: depthRange - ) - ] - var dependencyInfo = VkDependencyInfo( - sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - imageMemoryBarrierCount: uint32(barriers.len), - pImageMemoryBarriers: barriers[0].addr - ) - vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) - - var - colorClear = VkClearValue(color: VkClearColorValue(float32: ClearColor)) - depthClear = VkClearValue( - depthStencil: VkClearDepthStencilValue(depth: 1.0'f32, stencil: 0) - ) - colorAttachment = VkRenderingAttachmentInfo( - sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - imageView: renderer.imageViews[imageIndex], - imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, - storeOp: VK_ATTACHMENT_STORE_OP_STORE, - clearValue: colorClear - ) - depthAttachment = VkRenderingAttachmentInfo( - sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - imageView: renderer.depthImageViews[imageIndex], - imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, - storeOp: VK_ATTACHMENT_STORE_OP_DONT_CARE, - clearValue: depthClear - ) - renderingInfo = VkRenderingInfo( - sType: VK_STRUCTURE_TYPE_RENDERING_INFO, - renderArea: VkRect2D(offset: VkOffset2D(x: 0, y: 0), extent: ctx.swapChainExtent), - layerCount: 1, - colorAttachmentCount: 1, - pColorAttachments: colorAttachment.addr, - pDepthAttachment: depthAttachment.addr - ) - viewport = VkViewport( - x: 0, - y: ctx.swapChainExtent.height.float32, - width: ctx.swapChainExtent.width.float32, - height: -ctx.swapChainExtent.height.float32, - minDepth: 0, - maxDepth: 1 - ) - scissor = VkRect2D( - offset: VkOffset2D(x: 0, y: 0), - extent: ctx.swapChainExtent - ) - var vertexBuffers = [renderer.vertexBuffer] - var offsets = [VkDeviceSize(0)] - - vkCmdBeginRendering(commandBuffer, renderingInfo.addr) - vkCmdSetViewport(commandBuffer, 0, 1, viewport.addr) - vkCmdSetScissor(commandBuffer, 0, 1, scissor.addr) - vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.pipeline) - vkCmdPushConstants( - commandBuffer, - renderer.pipelineLayout, - VkShaderStageFlags(VK_SHADER_STAGE_VERTEX_BIT), - 0, - uint32(sizeof(renderer.transform)), - unsafeAddr renderer.transform[0] - ) - vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers[0].addr, offsets[0].addr) - vkCmdDraw(commandBuffer, renderer.vertexCount, 1, 0, 0) - vkCmdEndRendering(commandBuffer) - var toPresent = VkImageMemoryBarrier2( - sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - srcAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), - dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), - dstAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), - oldLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, - newLayout: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, - srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, - image: ctx.swapChainImages[imageIndex], - subresourceRange: colorRange - ) - dependencyInfo.imageMemoryBarrierCount = 1 - dependencyInfo.pImageMemoryBarriers = toPresent.addr - vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) - checkVk(vkEndCommandBuffer(commandBuffer), "Ending OBJ command buffer") - -proc drawFrame(ctx: var VulkanContext, renderer: var ObjRenderer): bool = - let frame = ctx.currentFrame - let fence = ctx.inFlightFences[frame] - discard vkWaitForFences(ctx.device, 1, unsafeAddr fence, VkBool32(VK_TRUE), uint64.high) - discard vkResetFences(ctx.device, 1, unsafeAddr fence) - - var imageIndex: uint32 - let acquireResult = vkAcquireNextImageKHR( - ctx.device, - ctx.swapChain, - uint64.high, - ctx.imageAvailableSemaphores[frame], - VkFence(0), - imageIndex.addr - ) - if requiresSwapChainRecreate(acquireResult): - return true - checkVk(acquireResult, "Acquiring next swapchain image") - - recordCurrentFrame(ctx, renderer, imageIndex) - - var - commandBuffer = renderer.commandBuffers[imageIndex] - waitInfo = VkSemaphoreSubmitInfo( - sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, - semaphore: ctx.imageAvailableSemaphores[frame], - stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - value: 0, - deviceIndex: 0 - ) - commandBufferInfo = VkCommandBufferSubmitInfo( - sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, - commandBuffer: commandBuffer, - deviceMask: 0 - ) - signalInfo = VkSemaphoreSubmitInfo( - sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, - semaphore: ctx.renderFinishedSemaphores[frame], - stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), - value: 0, - deviceIndex: 0 - ) - submitInfo = VkSubmitInfo2( - sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, - waitSemaphoreInfoCount: 1, - pWaitSemaphoreInfos: waitInfo.addr, - commandBufferInfoCount: 1, - pCommandBufferInfos: commandBufferInfo.addr, - signalSemaphoreInfoCount: 1, - pSignalSemaphoreInfos: signalInfo.addr - ) - checkVk( - vkQueueSubmit2(ctx.graphicsQueue, 1, submitInfo.addr, fence), - "Submitting OBJ command buffer" - ) - - var - swapChains = [ctx.swapChain] - presentInfo = VkPresentInfoKHR( - sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, - waitSemaphoreCount: 1, - pWaitSemaphores: signalInfo.semaphore.addr, - swapchainCount: 1, - pSwapchains: swapChains[0].addr, - pImageIndices: imageIndex.addr - ) - let presentResult = vkQueuePresentKHR(ctx.presentQueue, presentInfo.addr) - let needsRecreate = requiresSwapChainRecreate(presentResult) - if not needsRecreate: - checkVk(presentResult, "Presenting OBJ frame") - ctx.currentFrame = (ctx.currentFrame + 1) mod FRAME_COUNT - needsRecreate - -proc shutdown(ctx: VulkanContext, renderer: var ObjRenderer) = - destroySwapChainResources(ctx, renderer) - if renderer.vertexBuffer.int64 != 0: - vkDestroyBuffer(ctx.device, renderer.vertexBuffer, nil) - if renderer.vertexBufferMemory.int64 != 0: - vkFreeMemory(ctx.device, renderer.vertexBufferMemory, nil) - -when isMainModule: - let window = newWindow("Vulkan 1.4 - Bunny Viewer", ivec2(Width.int32, Height.int32)) - - let hwnd = window.getHWND() - if hwnd == 0: - raise newException(Exception, "Failed to acquire HWND from window") - - let mesh = loadObjMesh(objPath()) - - var ctx: VulkanContext - ctx.initDevice(hwnd, Width, Height) - - var renderer: ObjRenderer - initRenderer(ctx, renderer, mesh) - - var camera = CameraState( - yaw: 0.0'f32, - pitch: 0.15'f32, - distance: 2.8'f32 - ) - - var needsResize = false - var frameInProgress = false - window.onResize = proc() = - needsResize = true - window.onFrame = proc() = - if frameInProgress: - return - frameInProgress = true - try: - if needsResize: - let size = window.size - if size.x > 0 and size.y > 0: - recreateRenderer(ctx, renderer, size.x.int, size.y.int) - needsResize = false - else: - return - - updateCamera(camera, window) - let aspect = ctx.swapChainExtent.width.float32 / max(1'u32, ctx.swapChainExtent.height).float32 - updateTransform(renderer, camera, aspect) - if drawFrame(ctx, renderer): - let size = window.size - if size.x > 0 and size.y > 0: - recreateRenderer(ctx, renderer, size.x.int, size.y.int) - else: - needsResize = true - finally: - frameInProgress = false - - try: - while not window.closeRequested: - pollEvents() - finally: - shutdown(ctx, renderer) - ctx.cleanup() +import + std/[math, os, strutils], + vmath, + windy, + vk14 + +const + Width = 1280 + Height = 800 + RotateScale = 0.01'f32 + ZoomScale = 0.2'f32 + MinDistance = 1.0'f32 + MaxDistance = 8.0'f32 + MinPitch = -1.45'f32 + MaxPitch = 1.45'f32 + ClearColor = [0.05'f32, 0.05'f32, 0.08'f32, 1.0'f32] + +type + ViewerObjError = object of CatchableError + + ObjVertex = object + position: array[3, float32] + normal: array[3, float32] + + ObjMesh = object + vertices: seq[ObjVertex] + + CameraState = object + yaw: float32 + pitch: float32 + distance: float32 + + ObjRenderer = object + pipelineLayout: VkPipelineLayout + pipeline: VkPipeline + imageViews: seq[VkImageView] + depthImages: seq[VkImage] + depthImageMemories: seq[VkDeviceMemory] + depthImageViews: seq[VkImageView] + commandBuffers: seq[VkCommandBuffer] + vertexBuffer: VkBuffer + vertexBufferMemory: VkDeviceMemory + transform: array[32, float32] + vertexCount: uint32 + +proc objPath(): string = + currentSourcePath().parentDir / "bunny.obj" + +proc checkVk(result: VkResult, action: string) = + if result != VK_SUCCESS: + raise newException(Exception, action & " failed with VkResult " & $result.int32) + +proc requiresSwapChainRecreate(vkResult: VkResult): bool = + let code = vkResult.int32 + code == VK_SUBOPTIMAL_KHR.int32 or code == VK_ERROR_OUT_OF_DATE_KHR.int32 + +proc identityMatrix(): Mat4 = + gmat4[float32]( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ) + +proc perspectiveDx(fovY, aspect, nearPlane, farPlane: float32): Mat4 = + let + h = 1.0'f32 / tan(fovY * 0.5'f32) + w = h / aspect + depth = farPlane - nearPlane + result[0, 0] = w + result[0, 1] = 0 + result[0, 2] = 0 + result[0, 3] = 0 + result[1, 0] = 0 + result[1, 1] = h + result[1, 2] = 0 + result[1, 3] = 0 + result[2, 0] = 0 + result[2, 1] = 0 + result[2, 2] = farPlane / depth + result[2, 3] = 1 + result[3, 0] = 0 + result[3, 1] = 0 + result[3, 2] = -(nearPlane * farPlane) / depth + result[3, 3] = 0 + +proc mat4ToArray(matrix: Mat4): array[16, float32] = + var index = 0 + for i in 0 ..< 4: + for j in 0 ..< 4: + result[index] = matrix[i, j] + inc index + +proc packTransforms(mvp, model: Mat4): array[32, float32] = + let + mvpValues = mat4ToArray(mvp) + modelValues = mat4ToArray(model) + for i in 0 ..< 16: + result[i] = mvpValues[i] + result[i + 16] = modelValues[i] + +proc toFloatArray(v: Vec3): array[3, float32] = + [v.x, v.y, v.z] + +proc parseFloat32(value: string): float32 = + try: + parseFloat(value).float32 + except ValueError: + raise newException(ViewerObjError, "Invalid float in OBJ: " & value) + +proc parseObjIndex(value: string, vertexCount: int): int = + if value.len == 0: + raise newException(ViewerObjError, "OBJ face is missing a vertex index") + + let rawIndex = + try: + parseInt(value) + except ValueError: + raise newException(ViewerObjError, "Invalid OBJ face index: " & value) + + result = + if rawIndex > 0: + rawIndex - 1 + elif rawIndex < 0: + vertexCount + rawIndex + else: + raise newException(ViewerObjError, "OBJ indices cannot be zero") + + if result < 0 or result >= vertexCount: + raise newException(ViewerObjError, "OBJ face index is out of range: " & value) + +proc parseFaceVertex(token: string, vertexCount: int): int = + let slash = token.find('/') + let indexToken = + if slash >= 0: + token[0 ..< slash] + else: + token + parseObjIndex(indexToken, vertexCount) + +proc normalizeSafe(v, fallback: Vec3): Vec3 = + if v.length() <= 0.000001'f32: + fallback + else: + v.normalize() + +proc loadObjMesh(path: string): ObjMesh = + if not fileExists(path): + raise newException(ViewerObjError, "OBJ file not found: " & path) + + var + positions: seq[Vec3] + triangles: seq[array[3, int]] + + for rawLine in readFile(path).splitLines(): + let line = rawLine.strip() + if line.len == 0 or line[0] == '#': + continue + + let parts = strutils.splitWhitespace(line) + case parts[0] + of "v": + if parts.len < 4: + raise newException(ViewerObjError, "OBJ vertex line is incomplete") + positions.add( + vec3( + parseFloat32(parts[1]), + parseFloat32(parts[2]), + parseFloat32(parts[3]) + ) + ) + of "f": + if parts.len < 4: + raise newException(ViewerObjError, "OBJ face line is incomplete") + var faceIndices: seq[int] + for i in 1 ..< parts.len: + faceIndices.add(parseFaceVertex(parts[i], positions.len)) + for i in 1 ..< faceIndices.len - 1: + triangles.add([faceIndices[0], faceIndices[i], faceIndices[i + 1]]) + else: + discard + + if positions.len == 0: + raise newException(ViewerObjError, "OBJ does not contain any vertices") + if triangles.len == 0: + raise newException(ViewerObjError, "OBJ does not contain any faces") + + var + minPos = positions[0] + maxPos = positions[0] + for i in 1 ..< positions.len: + let p = positions[i] + minPos.x = min(minPos.x, p.x) + minPos.y = min(minPos.y, p.y) + minPos.z = min(minPos.z, p.z) + maxPos.x = max(maxPos.x, p.x) + maxPos.y = max(maxPos.y, p.y) + maxPos.z = max(maxPos.z, p.z) + + let + center = (minPos + maxPos) * 0.5'f32 + size = maxPos - minPos + maxExtent = max(size.x, max(size.y, size.z)) + if maxExtent <= 0.0'f32: + raise newException(ViewerObjError, "OBJ bounds are degenerate") + + let uniformScale = 2.0'f32 / maxExtent + var normalizedPositions = newSeq[Vec3](positions.len) + for i, p in positions: + normalizedPositions[i] = (p - center) * uniformScale + + var smoothedNormals = newSeq[Vec3](normalizedPositions.len) + for tri in triangles: + let + a = normalizedPositions[tri[0]] + b = normalizedPositions[tri[1]] + c = normalizedPositions[tri[2]] + faceNormal = normalizeSafe((b - a).cross(c - a), vec3(0.0'f32, 1.0'f32, 0.0'f32)) + smoothedNormals[tri[0]] += faceNormal + smoothedNormals[tri[1]] += faceNormal + smoothedNormals[tri[2]] += faceNormal + + result.vertices = newSeq[ObjVertex](triangles.len * 3) + var vertexIndex = 0 + for tri in triangles: + for idx in tri: + result.vertices[vertexIndex] = ObjVertex( + position: normalizedPositions[idx].toFloatArray(), + normal: normalizeSafe(smoothedNormals[idx], vec3(0.0'f32, 1.0'f32, 0.0'f32)).toFloatArray() + ) + inc vertexIndex + +proc createShaderModule(device: VkDevice, code: string): VkShaderModule = + var createInfo = VkShaderModuleCreateInfo( + sType: VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, + codeSize: code.len.uint32, + pCode: cast[ptr uint32](code[0].unsafeAddr) + ) + checkVk(vkCreateShaderModule(device, createInfo.addr, nil, result.addr), "Creating shader module") + +proc findMemoryType(ctx: VulkanContext, typeFilter: uint32, properties: uint32): uint32 = + var memProperties: VkPhysicalDeviceMemoryProperties + vkGetPhysicalDeviceMemoryProperties(ctx.physicalDevice, memProperties.addr) + for i in 0'u32 ..< memProperties.memoryTypeCount: + let flags = memProperties.memoryTypes[i].propertyFlags.uint32 + if ((typeFilter shr i) and 1'u32) == 1'u32 and (flags and properties) == properties: + return i + raise newException(Exception, "Failed to find suitable Vulkan memory type") + +proc createBuffer( + ctx: VulkanContext, + size: VkDeviceSize, + usage: uint32, + properties: uint32, + buffer: var VkBuffer, + memory: var VkDeviceMemory +) = + var bufferInfo = VkBufferCreateInfo( + sType: VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + size: size, + usage: VkBufferUsageFlags(usage), + sharingMode: VK_SHARING_MODE_EXCLUSIVE, + queueFamilyIndexCount: 0, + pQueueFamilyIndices: nil + ) + checkVk(vkCreateBuffer(ctx.device, bufferInfo.addr, nil, buffer.addr), "Creating buffer") + + var memRequirements: VkMemoryRequirements + vkGetBufferMemoryRequirements(ctx.device, buffer, memRequirements.addr) + + var allocInfo = VkMemoryAllocateInfo( + sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + allocationSize: memRequirements.size, + memoryTypeIndex: findMemoryType(ctx, memRequirements.memoryTypeBits, properties) + ) + checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), "Allocating buffer memory") + checkVk(vkBindBufferMemory(ctx.device, buffer, memory, VkDeviceSize(0)), "Binding buffer memory") + +proc createImage( + ctx: VulkanContext, + width, height: int, + format: VkFormat, + usage: uint32, + image: var VkImage, + memory: var VkDeviceMemory +) = + var imageInfo = VkImageCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + imageType: VK_IMAGE_TYPE_2D, + format: format, + extent: VkExtent3D(width: uint32(width), height: uint32(height), depth: 1), + mipLevels: 1, + arrayLayers: 1, + samples: VK_SAMPLE_COUNT_1_BIT, + tiling: VK_IMAGE_TILING_OPTIMAL, + usage: VkImageUsageFlags(usage), + sharingMode: VK_SHARING_MODE_EXCLUSIVE, + initialLayout: VK_IMAGE_LAYOUT_UNDEFINED + ) + checkVk(vkCreateImage(ctx.device, imageInfo.addr, nil, image.addr), "Creating image") + + var memRequirements: VkMemoryRequirements + vkGetImageMemoryRequirements(ctx.device, image, memRequirements.addr) + + var allocInfo = VkMemoryAllocateInfo( + sType: VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + allocationSize: memRequirements.size, + memoryTypeIndex: findMemoryType(ctx, memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT.uint32) + ) + checkVk(vkAllocateMemory(ctx.device, allocInfo.addr, nil, memory.addr), "Allocating image memory") + checkVk(vkBindImageMemory(ctx.device, image, memory, VkDeviceSize(0)), "Binding image memory") + +proc beginSingleTimeCommands(ctx: VulkanContext): VkCommandBuffer = + var allocInfo = VkCommandBufferAllocateInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + commandPool: ctx.commandPool, + level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, + commandBufferCount: 1 + ) + checkVk(vkAllocateCommandBuffers(ctx.device, allocInfo.addr, result.addr), "Allocating single-use command buffer") + var beginInfo = VkCommandBufferBeginInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + flags: VkCommandBufferUsageFlags(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT) + ) + checkVk(vkBeginCommandBuffer(result, beginInfo.addr), "Beginning single-use command buffer") + +proc endSingleTimeCommands(ctx: VulkanContext, commandBuffer: VkCommandBuffer) = + checkVk(vkEndCommandBuffer(commandBuffer), "Ending single-use command buffer") + var + commandBufferInfo = VkCommandBufferSubmitInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + commandBuffer: commandBuffer, + deviceMask: 0 + ) + submitInfo = VkSubmitInfo2( + sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + commandBufferInfoCount: 1, + pCommandBufferInfos: commandBufferInfo.addr + ) + checkVk( + vkQueueSubmit2(ctx.graphicsQueue, 1, submitInfo.addr, VkFence(0)), + "Submitting single-use command buffer" + ) + checkVk(vkQueueWaitIdle(ctx.graphicsQueue), "Waiting for graphics queue idle") + vkFreeCommandBuffers(ctx.device, ctx.commandPool, 1, unsafeAddr commandBuffer) + +proc transitionDepthLayout(ctx: VulkanContext, image: VkImage) = + let commandBuffer = beginSingleTimeCommands(ctx) + var barrier = VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, + newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: image, + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + barrier.srcStageMask = VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE) + barrier.srcAccessMask = VkAccessFlags2(VK_ACCESS_2_NONE) + barrier.dstStageMask = VkPipelineStageFlags2( + VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT + ) + barrier.dstAccessMask = VkAccessFlags2( + VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + ) + var dependencyInfo = VkDependencyInfo( + sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + imageMemoryBarrierCount: 1, + pImageMemoryBarriers: barrier.addr + ) + vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) + endSingleTimeCommands(ctx, commandBuffer) + +proc createSwapChainImageViews(ctx: VulkanContext, renderer: var ObjRenderer) = + renderer.imageViews.setLen(ctx.swapChainImages.len) + for i, image in ctx.swapChainImages: + var createInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: image, + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: ctx.swapChainImageFormat, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(ctx.device, createInfo.addr, nil, renderer.imageViews[i].addr), "Creating swapchain image view") + +proc createDepthResources(ctx: VulkanContext, renderer: var ObjRenderer) = + let attachmentCount = ctx.swapChainImages.len + renderer.depthImages.setLen(attachmentCount) + renderer.depthImageMemories.setLen(attachmentCount) + renderer.depthImageViews.setLen(attachmentCount) + + for i in 0 ..< attachmentCount: + createImage( + ctx, + int(ctx.swapChainExtent.width), + int(ctx.swapChainExtent.height), + VK_FORMAT_D32_SFLOAT, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT.uint32, + renderer.depthImages[i], + renderer.depthImageMemories[i] + ) + + var imageViewInfo = VkImageViewCreateInfo( + sType: VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + image: renderer.depthImages[i], + viewType: VK_IMAGE_VIEW_TYPE_2D, + format: VK_FORMAT_D32_SFLOAT, + components: VkComponentMapping( + r: VK_COMPONENT_SWIZZLE_IDENTITY, + g: VK_COMPONENT_SWIZZLE_IDENTITY, + b: VK_COMPONENT_SWIZZLE_IDENTITY, + a: VK_COMPONENT_SWIZZLE_IDENTITY + ), + subresourceRange: VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + ) + checkVk(vkCreateImageView(ctx.device, imageViewInfo.addr, nil, renderer.depthImageViews[i].addr), "Creating depth image view") + transitionDepthLayout(ctx, renderer.depthImages[i]) + +proc createGraphicsPipeline(ctx: VulkanContext, renderer: var ObjRenderer) = + const + vertShaderCode = staticRead("shaders/viewer_obj.vert.spv") + fragShaderCode = staticRead("shaders/viewer_obj.frag.spv") + let + vertShaderModule = createShaderModule(ctx.device, vertShaderCode) + fragShaderModule = createShaderModule(ctx.device, fragShaderCode) + try: + var + colorFormat = ctx.swapChainImageFormat + renderingInfo = VkPipelineRenderingCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, + colorAttachmentCount: 1, + pColorAttachmentFormats: colorFormat.addr, + depthAttachmentFormat: VK_FORMAT_D32_SFLOAT + ) + dynamicStates = [ + VkDynamicState(VK_DYNAMIC_STATE_VIEWPORT), + VkDynamicState(VK_DYNAMIC_STATE_SCISSOR) + ] + dynamicState = VkPipelineDynamicStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + dynamicStateCount: uint32(dynamicStates.len), + pDynamicStates: dynamicStates[0].addr + ) + vertShaderStageInfo = VkPipelineShaderStageCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + stage: VK_SHADER_STAGE_VERTEX_BIT, + module: vertShaderModule, + pName: "main" + ) + fragShaderStageInfo = VkPipelineShaderStageCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + stage: VK_SHADER_STAGE_FRAGMENT_BIT, + module: fragShaderModule, + pName: "main" + ) + shaderStages = [vertShaderStageInfo, fragShaderStageInfo] + bindingDescription = VkVertexInputBindingDescription( + binding: 0, + stride: uint32(sizeof(ObjVertex)), + inputRate: VK_VERTEX_INPUT_RATE_VERTEX + ) + attributeDescriptions = [ + VkVertexInputAttributeDescription(location: 0, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: 0), + VkVertexInputAttributeDescription(location: 1, binding: 0, format: VK_FORMAT_R32G32B32_SFLOAT, offset: uint32(sizeof(array[3, float32]))) + ] + vertexInputInfo = VkPipelineVertexInputStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + vertexBindingDescriptionCount: 1, + pVertexBindingDescriptions: bindingDescription.addr, + vertexAttributeDescriptionCount: uint32(attributeDescriptions.len), + pVertexAttributeDescriptions: attributeDescriptions[0].addr + ) + inputAssembly = VkPipelineInputAssemblyStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + topology: VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, + primitiveRestartEnable: VkBool32(VK_FALSE) + ) + viewportState = VkPipelineViewportStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + viewportCount: 1, + scissorCount: 1 + ) + rasterizer = VkPipelineRasterizationStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + depthClampEnable: VkBool32(VK_FALSE), + rasterizerDiscardEnable: VkBool32(VK_FALSE), + polygonMode: VK_POLYGON_MODE_FILL, + lineWidth: 1.0, + cullMode: VkCullModeFlags(VK_CULL_MODE_NONE), + frontFace: VK_FRONT_FACE_CLOCKWISE, + depthBiasEnable: VkBool32(VK_FALSE) + ) + multisampling = VkPipelineMultisampleStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + sampleShadingEnable: VkBool32(VK_FALSE), + rasterizationSamples: VK_SAMPLE_COUNT_1_BIT + ) + depthStencil = VkPipelineDepthStencilStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, + depthTestEnable: VkBool32(VK_TRUE), + depthWriteEnable: VkBool32(VK_TRUE), + depthCompareOp: VK_COMPARE_OP_LESS, + depthBoundsTestEnable: VkBool32(VK_FALSE), + stencilTestEnable: VkBool32(VK_FALSE), + minDepthBounds: 0, + maxDepthBounds: 1 + ) + colorBlendAttachment = VkPipelineColorBlendAttachmentState( + colorWriteMask: VkColorComponentFlags(0x0000000F), + blendEnable: VkBool32(VK_FALSE), + srcColorBlendFactor: VK_BLEND_FACTOR_ONE, + dstColorBlendFactor: VK_BLEND_FACTOR_ZERO, + colorBlendOp: VK_BLEND_OP_ADD, + srcAlphaBlendFactor: VK_BLEND_FACTOR_ONE, + dstAlphaBlendFactor: VK_BLEND_FACTOR_ZERO, + alphaBlendOp: VK_BLEND_OP_ADD + ) + colorBlending = VkPipelineColorBlendStateCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + logicOpEnable: VkBool32(VK_FALSE), + logicOp: VK_LOGIC_OP_COPY, + attachmentCount: 1, + pAttachments: colorBlendAttachment.addr, + blendConstants: [0.0'f32, 0.0'f32, 0.0'f32, 0.0'f32] + ) + pushConstantRange = VkPushConstantRange( + stageFlags: VkShaderStageFlags(VK_SHADER_STAGE_VERTEX_BIT), + offset: 0, + size: uint32(sizeof(array[32, float32])) + ) + pipelineLayoutInfo = VkPipelineLayoutCreateInfo( + sType: VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + setLayoutCount: 0, + pSetLayouts: nil, + pushConstantRangeCount: 1, + pPushConstantRanges: pushConstantRange.addr + ) + + checkVk(vkCreatePipelineLayout(ctx.device, pipelineLayoutInfo.addr, nil, renderer.pipelineLayout.addr), "Creating pipeline layout") + + var pipelineInfo = VkGraphicsPipelineCreateInfo( + sType: VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + pNext: renderingInfo.addr, + stageCount: uint32(shaderStages.len), + pStages: shaderStages[0].addr, + pVertexInputState: vertexInputInfo.addr, + pInputAssemblyState: inputAssembly.addr, + pViewportState: viewportState.addr, + pRasterizationState: rasterizer.addr, + pMultisampleState: multisampling.addr, + pDepthStencilState: depthStencil.addr, + pColorBlendState: colorBlending.addr, + pDynamicState: dynamicState.addr, + layout: renderer.pipelineLayout + ) + checkVk(vkCreateGraphicsPipelines(ctx.device, VkPipelineCache(0), 1, pipelineInfo.addr, nil, renderer.pipeline.addr), "Creating graphics pipeline") + finally: + vkDestroyShaderModule(ctx.device, vertShaderModule, nil) + vkDestroyShaderModule(ctx.device, fragShaderModule, nil) + +proc uploadVertexBuffer(ctx: VulkanContext, renderer: var ObjRenderer, mesh: ObjMesh) = + if mesh.vertices.len == 0: + raise newException(ViewerObjError, "Mesh is empty") + + let bufferSize = VkDeviceSize(mesh.vertices.len * sizeof(ObjVertex)) + createBuffer( + ctx, + bufferSize, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT.uint32, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT.uint32 or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT.uint32, + renderer.vertexBuffer, + renderer.vertexBufferMemory + ) + + var mapped: pointer + checkVk(vkMapMemory(ctx.device, renderer.vertexBufferMemory, VkDeviceSize(0), bufferSize, VkMemoryMapFlags(0), mapped.addr), "Mapping vertex buffer memory") + copyMem(mapped, unsafeAddr mesh.vertices[0], mesh.vertices.len * sizeof(ObjVertex)) + vkUnmapMemory(ctx.device, renderer.vertexBufferMemory) + renderer.vertexCount = uint32(mesh.vertices.len) + +proc recordCommandBuffers(ctx: VulkanContext, renderer: var ObjRenderer) + +proc destroySwapChainResources(ctx: VulkanContext, renderer: var ObjRenderer) = + if renderer.commandBuffers.len > 0: + vkFreeCommandBuffers(ctx.device, ctx.commandPool, uint32(renderer.commandBuffers.len), renderer.commandBuffers[0].addr) + renderer.commandBuffers.setLen(0) + for imageView in renderer.imageViews: + vkDestroyImageView(ctx.device, imageView, nil) + renderer.imageViews.setLen(0) + for imageView in renderer.depthImageViews: + vkDestroyImageView(ctx.device, imageView, nil) + renderer.depthImageViews.setLen(0) + for image in renderer.depthImages: + vkDestroyImage(ctx.device, image, nil) + renderer.depthImages.setLen(0) + for memory in renderer.depthImageMemories: + vkFreeMemory(ctx.device, memory, nil) + renderer.depthImageMemories.setLen(0) + if renderer.pipeline.int64 != 0: + vkDestroyPipeline(ctx.device, renderer.pipeline, nil) + renderer.pipeline = VkPipeline(0) + if renderer.pipelineLayout.int64 != 0: + vkDestroyPipelineLayout(ctx.device, renderer.pipelineLayout, nil) + renderer.pipelineLayout = VkPipelineLayout(0) + +proc recreateRenderer(ctx: var VulkanContext, renderer: var ObjRenderer, width, height: int) = + discard vkDeviceWaitIdle(ctx.device) + destroySwapChainResources(ctx, renderer) + recreateSwapChain(ctx, width, height) + createSwapChainImageViews(ctx, renderer) + createDepthResources(ctx, renderer) + createGraphicsPipeline(ctx, renderer) + recordCommandBuffers(ctx, renderer) + +proc recordCommandBuffers(ctx: VulkanContext, renderer: var ObjRenderer) = + renderer.commandBuffers.setLen(ctx.swapChainImages.len) + var allocInfo = VkCommandBufferAllocateInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + commandPool: ctx.commandPool, + level: VK_COMMAND_BUFFER_LEVEL_PRIMARY, + commandBufferCount: uint32(renderer.commandBuffers.len) + ) + checkVk(vkAllocateCommandBuffers(ctx.device, allocInfo.addr, renderer.commandBuffers[0].addr), "Allocating OBJ command buffers") + +proc initRenderer(ctx: VulkanContext, renderer: var ObjRenderer, mesh: ObjMesh) = + createSwapChainImageViews(ctx, renderer) + createDepthResources(ctx, renderer) + createGraphicsPipeline(ctx, renderer) + uploadVertexBuffer(ctx, renderer, mesh) + recordCommandBuffers(ctx, renderer) + +proc updateCamera(camera: var CameraState, window: windy.Window) = + if window.buttonDown[MouseLeft]: + let delta = window.mouseDelta + camera.yaw += delta.x.float32 * RotateScale + camera.pitch = clamp( + camera.pitch + delta.y.float32 * RotateScale, + MinPitch, + MaxPitch + ) + + let scroll = window.scrollDelta + if scroll.y != 0.0'f32: + camera.distance = clamp( + camera.distance - scroll.y.float32 * ZoomScale, + MinDistance, + MaxDistance + ) + +proc updateTransform(renderer: var ObjRenderer, camera: CameraState, aspect: float32) = + let + cosPitch = cos(camera.pitch) + eye = vec3( + sin(camera.yaw) * cosPitch * camera.distance, + sin(camera.pitch) * camera.distance, + cos(camera.yaw) * cosPitch * camera.distance + ) + target = vec3(0.0'f32, 0.0'f32, 0.0'f32) + model = identityMatrix() + cameraAngles = toAngles(eye, target) + view = inverse(translate(eye) * fromAngles(cameraAngles)) + proj = perspectiveDx(60.0'f32.toRadians, aspect, 0.1'f32, 100.0'f32) + mvp = proj * view * model + renderer.transform = packTransforms(mvp, model) + +proc recordCurrentFrame(ctx: var VulkanContext, renderer: var ObjRenderer, imageIndex: uint32) = + let commandBuffer = renderer.commandBuffers[imageIndex] + discard vkResetCommandBuffer(commandBuffer, VkCommandBufferResetFlags(0)) + var beginInfo = VkCommandBufferBeginInfo(sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO) + checkVk(vkBeginCommandBuffer(commandBuffer, beginInfo.addr), "Beginning OBJ command buffer") + + let + colorRange = VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_COLOR_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + depthRange = VkImageSubresourceRange( + aspectMask: VkImageAspectFlags(VK_IMAGE_ASPECT_DEPTH_BIT), + baseMipLevel: 0, + levelCount: 1, + baseArrayLayer: 0, + layerCount: 1 + ) + var barriers = [ + VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), + dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + dstAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), + oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, + newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: ctx.swapChainImages[imageIndex], + subresourceRange: colorRange + ), + VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + srcAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), + dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT), + dstAccessMask: VkAccessFlags2(VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT), + oldLayout: VK_IMAGE_LAYOUT_UNDEFINED, + newLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: renderer.depthImages[imageIndex], + subresourceRange: depthRange + ) + ] + var dependencyInfo = VkDependencyInfo( + sType: VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + imageMemoryBarrierCount: uint32(barriers.len), + pImageMemoryBarriers: barriers[0].addr + ) + vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) + + var + colorClear = VkClearValue(color: VkClearColorValue(float32: ClearColor)) + depthClear = VkClearValue( + depthStencil: VkClearDepthStencilValue(depth: 1.0'f32, stencil: 0) + ) + colorAttachment = VkRenderingAttachmentInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + imageView: renderer.imageViews[imageIndex], + imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, + storeOp: VK_ATTACHMENT_STORE_OP_STORE, + clearValue: colorClear + ) + depthAttachment = VkRenderingAttachmentInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + imageView: renderer.depthImageViews[imageIndex], + imageLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, + storeOp: VK_ATTACHMENT_STORE_OP_DONT_CARE, + clearValue: depthClear + ) + renderingInfo = VkRenderingInfo( + sType: VK_STRUCTURE_TYPE_RENDERING_INFO, + renderArea: VkRect2D(offset: VkOffset2D(x: 0, y: 0), extent: ctx.swapChainExtent), + layerCount: 1, + colorAttachmentCount: 1, + pColorAttachments: colorAttachment.addr, + pDepthAttachment: depthAttachment.addr + ) + viewport = VkViewport( + x: 0, + y: ctx.swapChainExtent.height.float32, + width: ctx.swapChainExtent.width.float32, + height: -ctx.swapChainExtent.height.float32, + minDepth: 0, + maxDepth: 1 + ) + scissor = VkRect2D( + offset: VkOffset2D(x: 0, y: 0), + extent: ctx.swapChainExtent + ) + var vertexBuffers = [renderer.vertexBuffer] + var offsets = [VkDeviceSize(0)] + + vkCmdBeginRendering(commandBuffer, renderingInfo.addr) + vkCmdSetViewport(commandBuffer, 0, 1, viewport.addr) + vkCmdSetScissor(commandBuffer, 0, 1, scissor.addr) + vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, renderer.pipeline) + vkCmdPushConstants( + commandBuffer, + renderer.pipelineLayout, + VkShaderStageFlags(VK_SHADER_STAGE_VERTEX_BIT), + 0, + uint32(sizeof(renderer.transform)), + unsafeAddr renderer.transform[0] + ) + vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers[0].addr, offsets[0].addr) + vkCmdDraw(commandBuffer, renderer.vertexCount, 1, 0, 0) + vkCmdEndRendering(commandBuffer) + var toPresent = VkImageMemoryBarrier2( + sType: VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + srcStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + srcAccessMask: VkAccessFlags2(VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT), + dstStageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_NONE), + dstAccessMask: VkAccessFlags2(VK_ACCESS_2_NONE), + oldLayout: VK_IMAGE_LAYOUT_ATTACHMENT_OPTIMAL, + newLayout: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED, + image: ctx.swapChainImages[imageIndex], + subresourceRange: colorRange + ) + dependencyInfo.imageMemoryBarrierCount = 1 + dependencyInfo.pImageMemoryBarriers = toPresent.addr + vkCmdPipelineBarrier2(commandBuffer, dependencyInfo.addr) + checkVk(vkEndCommandBuffer(commandBuffer), "Ending OBJ command buffer") + +proc drawFrame(ctx: var VulkanContext, renderer: var ObjRenderer): bool = + let frame = ctx.currentFrame + let fence = ctx.inFlightFences[frame] + discard vkWaitForFences(ctx.device, 1, unsafeAddr fence, VkBool32(VK_TRUE), uint64.high) + discard vkResetFences(ctx.device, 1, unsafeAddr fence) + + var imageIndex: uint32 + let acquireResult = vkAcquireNextImageKHR( + ctx.device, + ctx.swapChain, + uint64.high, + ctx.imageAvailableSemaphores[frame], + VkFence(0), + imageIndex.addr + ) + if requiresSwapChainRecreate(acquireResult): + return true + checkVk(acquireResult, "Acquiring next swapchain image") + + recordCurrentFrame(ctx, renderer, imageIndex) + + var + commandBuffer = renderer.commandBuffers[imageIndex] + waitInfo = VkSemaphoreSubmitInfo( + sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + semaphore: ctx.imageAvailableSemaphores[frame], + stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + value: 0, + deviceIndex: 0 + ) + commandBufferInfo = VkCommandBufferSubmitInfo( + sType: VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, + commandBuffer: commandBuffer, + deviceMask: 0 + ) + signalInfo = VkSemaphoreSubmitInfo( + sType: VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + semaphore: ctx.renderFinishedSemaphores[frame], + stageMask: VkPipelineStageFlags2(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT), + value: 0, + deviceIndex: 0 + ) + submitInfo = VkSubmitInfo2( + sType: VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + waitSemaphoreInfoCount: 1, + pWaitSemaphoreInfos: waitInfo.addr, + commandBufferInfoCount: 1, + pCommandBufferInfos: commandBufferInfo.addr, + signalSemaphoreInfoCount: 1, + pSignalSemaphoreInfos: signalInfo.addr + ) + checkVk( + vkQueueSubmit2(ctx.graphicsQueue, 1, submitInfo.addr, fence), + "Submitting OBJ command buffer" + ) + + var + swapChains = [ctx.swapChain] + presentInfo = VkPresentInfoKHR( + sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, + waitSemaphoreCount: 1, + pWaitSemaphores: signalInfo.semaphore.addr, + swapchainCount: 1, + pSwapchains: swapChains[0].addr, + pImageIndices: imageIndex.addr + ) + let presentResult = vkQueuePresentKHR(ctx.presentQueue, presentInfo.addr) + let needsRecreate = requiresSwapChainRecreate(presentResult) + if not needsRecreate: + checkVk(presentResult, "Presenting OBJ frame") + ctx.currentFrame = (ctx.currentFrame + 1) mod FRAME_COUNT + needsRecreate + +proc shutdown(ctx: VulkanContext, renderer: var ObjRenderer) = + destroySwapChainResources(ctx, renderer) + if renderer.vertexBuffer.int64 != 0: + vkDestroyBuffer(ctx.device, renderer.vertexBuffer, nil) + if renderer.vertexBufferMemory.int64 != 0: + vkFreeMemory(ctx.device, renderer.vertexBufferMemory, nil) + +when isMainModule: + let window = newWindow("Vulkan 1.4 - Bunny Viewer", ivec2(Width.int32, Height.int32)) + + let hwnd = window.getHWND() + if hwnd == 0: + raise newException(Exception, "Failed to acquire HWND from window") + + let mesh = loadObjMesh(objPath()) + + var ctx: VulkanContext + ctx.initDevice(hwnd, Width, Height) + + var renderer: ObjRenderer + initRenderer(ctx, renderer, mesh) + + var camera = CameraState( + yaw: 0.0'f32, + pitch: 0.15'f32, + distance: 2.8'f32 + ) + + var needsResize = false + var frameInProgress = false + window.onResize = proc() = + needsResize = true + window.onFrame = proc() = + if frameInProgress: + return + frameInProgress = true + try: + if needsResize: + let size = window.size + if size.x > 0 and size.y > 0: + recreateRenderer(ctx, renderer, size.x.int, size.y.int) + needsResize = false + else: + return + + updateCamera(camera, window) + let aspect = ctx.swapChainExtent.width.float32 / max(1'u32, ctx.swapChainExtent.height).float32 + updateTransform(renderer, camera, aspect) + if drawFrame(ctx, renderer): + let size = window.size + if size.x > 0 and size.y > 0: + recreateRenderer(ctx, renderer, size.x.int, size.y.int) + else: + needsResize = true + finally: + frameInProgress = false + + try: + while not window.closeRequested: + pollEvents() + finally: + shutdown(ctx, renderer) + ctx.cleanup() diff --git a/tests/nim.cfg b/tests/nim.cfg index 31d14b8..bca9644 100644 --- a/tests/nim.cfg +++ b/tests/nim.cfg @@ -1 +1 @@ ---path: "$projectDir/../src" +--path: "$projectDir/../src" diff --git a/tests/run_all.nim b/tests/run_all.nim index b6ed997..94c3f52 100644 --- a/tests/run_all.nim +++ b/tests/run_all.nim @@ -1,54 +1,54 @@ -## Compiles all examples first, then runs them sequentially. - -import std/[osproc, os, strformat] - -const Examples = [ - "basic_screen", - "basic_triangle", - "basic_quad", - "basic_cube", - "viewer_obj", - "sprite_sheet", -] - -proc main() = - ## Compile all examples, then run all examples in sequence. - let - startDir = getCurrentDir() - rootDir = currentSourcePath().parentDir.parentDir - defer: - setCurrentDir(startDir) - - echo "=== vk14 Examples Runner ===" - echo "Compiling all examples first." - echo "Running all examples after successful compilation." - echo "Close each window to proceed to the next example.\n" - - for i, name in Examples: - let nimFile = "examples" / (name & ".nim") - echo fmt"[{i + 1}/{Examples.len}] Compiling: {name}" - - setCurrentDir(rootDir) - let exitCode = execCmd(fmt"nim c {nimFile}") - if exitCode != 0: - echo fmt" ERROR: {name} failed to compile with exit code {exitCode}" - quit(exitCode) - echo "" - - echo "=== Compilation complete ===\n" - - for i, name in Examples: - let binaryPath = "examples" / name - echo fmt"[{i + 1}/{Examples.len}] Running: {name}" - - setCurrentDir(rootDir) - let exitCode = execCmd(binaryPath) - if exitCode != 0: - echo fmt" ERROR: {name} failed with exit code {exitCode}" - quit(exitCode) - echo "" - - echo "=== All examples completed ===" - -when isMainModule: - main() +## Compiles all examples first, then runs them sequentially. + +import std/[osproc, os, strformat] + +const Examples = [ + "basic_screen", + "basic_triangle", + "basic_quad", + "basic_cube", + "viewer_obj", + "sprite_sheet", +] + +proc main() = + ## Compile all examples, then run all examples in sequence. + let + startDir = getCurrentDir() + rootDir = currentSourcePath().parentDir.parentDir + defer: + setCurrentDir(startDir) + + echo "=== vk14 Examples Runner ===" + echo "Compiling all examples first." + echo "Running all examples after successful compilation." + echo "Close each window to proceed to the next example.\n" + + for i, name in Examples: + let nimFile = "examples" / (name & ".nim") + echo fmt"[{i + 1}/{Examples.len}] Compiling: {name}" + + setCurrentDir(rootDir) + let exitCode = execCmd(fmt"nim c {nimFile}") + if exitCode != 0: + echo fmt" ERROR: {name} failed to compile with exit code {exitCode}" + quit(exitCode) + echo "" + + echo "=== Compilation complete ===\n" + + for i, name in Examples: + let binaryPath = "examples" / name + echo fmt"[{i + 1}/{Examples.len}] Running: {name}" + + setCurrentDir(rootDir) + let exitCode = execCmd(binaryPath) + if exitCode != 0: + echo fmt" ERROR: {name} failed with exit code {exitCode}" + quit(exitCode) + echo "" + + echo "=== All examples completed ===" + +when isMainModule: + main() diff --git a/tools/capture.nim b/tools/capture.nim index 15ef3b0..9c283fb 100644 --- a/tools/capture.nim +++ b/tools/capture.nim @@ -1,231 +1,231 @@ -## Captures a screenshot of a running example. -## Usage: nim r tools/capture.nim [delay_seconds] [output_png] -## -## Launches the executable, waits for its window to appear, -## waits the specified delay (default 3s), takes a screenshot, -## saves it as PNG via Pixie, then kills the process. - -import std/[os, osproc, strutils, strformat, times] -import pixie - -type - HWND = pointer - HDC = pointer - HBITMAP = pointer - HGDIOBJ = pointer - WINBOOL = int32 - - RECT = object - left, top, right, bottom: int32 - - BITMAPINFOHEADER = object - biSize: uint32 - biWidth: int32 - biHeight: int32 - biPlanes: uint16 - biBitCount: uint16 - biCompression: uint32 - biSizeImage: uint32 - biXPelsPerMeter: int32 - biYPelsPerMeter: int32 - biClrUsed: uint32 - biClrImportant: uint32 - - BITMAPINFO = object - bmiHeader: BITMAPINFOHEADER - -proc GetWindowRect(hWnd: HWND, lpRect: ptr RECT): WINBOOL - {.dynlib: "user32", stdcall, importc.} - -proc SetForegroundWindow(hWnd: HWND): WINBOOL - {.dynlib: "user32", stdcall, importc.} - -proc ShowWindow(hWnd: HWND, nCmdShow: int32): WINBOOL - {.dynlib: "user32", stdcall, importc.} - -proc EnumWindows(lpEnumFunc: pointer, lParam: pointer): WINBOOL - {.dynlib: "user32", stdcall, importc.} - -proc GetWindowThreadProcessId(hWnd: HWND, lpdwProcessId: ptr uint32): uint32 - {.dynlib: "user32", stdcall, importc.} - -proc IsWindowVisible(hWnd: HWND): WINBOOL - {.dynlib: "user32", stdcall, importc.} - -proc GetDC(hWnd: HWND): HDC - {.dynlib: "user32", stdcall, importc.} - -proc ReleaseDC(hWnd: HWND, hDC: HDC): int32 - {.dynlib: "user32", stdcall, importc.} - -proc CreateCompatibleDC(hdc: HDC): HDC - {.dynlib: "gdi32", stdcall, importc.} - -proc CreateCompatibleBitmap(hdc: HDC, cx, cy: int32): HBITMAP - {.dynlib: "gdi32", stdcall, importc.} - -proc SelectObject(hdc: HDC, h: HGDIOBJ): HGDIOBJ - {.dynlib: "gdi32", stdcall, importc.} - -proc BitBlt(hdc: HDC, x, y, cx, cy: int32, hdcSrc: HDC, x1, y1: int32, rop: uint32): WINBOOL - {.dynlib: "gdi32", stdcall, importc.} - -proc GetDIBits(hdc: HDC, hbm: HBITMAP, start, cLines: uint32, lpvBits: pointer, lpbmi: ptr BITMAPINFO, usage: uint32): int32 - {.dynlib: "gdi32", stdcall, importc.} - -proc DeleteObject(ho: HGDIOBJ): WINBOOL - {.dynlib: "gdi32", stdcall, importc.} - -proc DeleteDC(hdc: HDC): WINBOOL - {.dynlib: "gdi32", stdcall, importc.} - -const - SW_SHOW = 5'i32 - SRCCOPY = 0x00CC0020'u32 - DIB_RGB_COLORS = 0'u32 - BI_RGB = 0'u32 - -type - FindWindowData = object - pid: uint32 - result: HWND - -proc GetWindowTextLengthW(hWnd: HWND): int32 - {.dynlib: "user32", stdcall, importc.} - -proc enumCallback(hWnd: HWND, lParam: pointer): WINBOOL {.stdcall.} = - var data = cast[ptr FindWindowData](lParam) - var windowPid: uint32 - discard GetWindowThreadProcessId(hWnd, addr windowPid) - if windowPid == data.pid and IsWindowVisible(hWnd) != 0: - # Only accept windows with a title (skip message-only/internal windows) - if GetWindowTextLengthW(hWnd) > 0: - var rect: RECT - if GetWindowRect(hWnd, addr rect) != 0: - let w = rect.right - rect.left - let h = rect.bottom - rect.top - if w > 50 and h > 50: - data.result = hWnd - return 0 - return 1 - -proc findWindowByPid(pid: uint32): HWND = - var data = FindWindowData(pid: pid, result: nil) - discard EnumWindows(cast[pointer](enumCallback), addr data) - return data.result - -proc captureWindow(hWnd: HWND, width, height, left, top: int32): Image = - let screenDC = GetDC(nil) - let memDC = CreateCompatibleDC(screenDC) - let hBitmap = CreateCompatibleBitmap(screenDC, width, height) - let oldBitmap = SelectObject(memDC, cast[HGDIOBJ](hBitmap)) - - discard BitBlt(memDC, 0, 0, width, height, screenDC, left, top, SRCCOPY) - - var bi = BITMAPINFO( - bmiHeader: BITMAPINFOHEADER( - biSize: uint32(sizeof(BITMAPINFOHEADER)), - biWidth: width, - biHeight: -height, # top-down - biPlanes: 1, - biBitCount: 32, - biCompression: BI_RGB, - ) - ) - - var pixels = newSeq[uint8](width * height * 4) - discard GetDIBits(memDC, hBitmap, 0, uint32(height), addr pixels[0], addr bi, DIB_RGB_COLORS) - - discard SelectObject(memDC, oldBitmap) - discard DeleteObject(cast[HGDIOBJ](hBitmap)) - discard DeleteDC(memDC) - discard ReleaseDC(nil, screenDC) - - # Convert BGRA -> RGBA and create Pixie image - result = newImage(width, height) - for i in 0 ..< width * height: - let offset = i * 4 - let b = pixels[offset] - let g = pixels[offset + 1] - let r = pixels[offset + 2] - let a = 255'u8 - result.data[i] = rgbx(r, g, b, a) - -proc main() = - let args = commandLineParams() - if args.len < 1: - echo "Usage: capture [delay_seconds] [output_png]" - echo " exe_path Path to the executable to run" - echo " delay_seconds Seconds to wait before screenshot (default: 3)" - echo " output_png Output PNG path (default: .png in docs/)" - quit(1) - - let exePath = args[0] - let delaySec = if args.len >= 2: parseInt(args[1]) else: 3 - let scriptDir = parentDir(currentSourcePath()) - let docsDir = normalizedPath(scriptDir / ".." / "docs") - let defaultPng = docsDir / (exePath.splitFile().name & ".png") - let outputPng = if args.len >= 3: args[2] else: defaultPng - - if not fileExists(exePath): - echo &"ERROR: Executable not found: {exePath}" - quit(1) - - createDir(parentDir(outputPng)) - - echo &"Launching: {exePath}" - let process = startProcess(exePath, workingDir = getCurrentDir()) - let pid = processID(process) - echo &" PID: {pid}" - - # Wait for window to appear - echo "Waiting for window..." - var hWnd: HWND = nil - let deadline = getTime() + initDuration(seconds = 15) - while getTime() < deadline: - hWnd = findWindowByPid(uint32(pid)) - if hWnd != nil: - break - sleep(250) - - if hWnd == nil: - echo "ERROR: No window found within 15 seconds" - process.kill() - process.close() - quit(1) - - # Bring to front and wait the delay - discard ShowWindow(hWnd, SW_SHOW) - discard SetForegroundWindow(hWnd) - echo &"Window found. Waiting {delaySec}s for content to render..." - sleep(delaySec * 1000) - - # Get window rect and capture - var rect: RECT - if GetWindowRect(hWnd, addr rect) == 0: - echo "ERROR: GetWindowRect failed" - process.kill() - process.close() - quit(1) - - let w = rect.right - rect.left - let h = rect.bottom - rect.top - echo &" Window size: {w}x{h} at ({rect.left}, {rect.top})" - - if w <= 0 or h <= 0: - echo "ERROR: Invalid window dimensions" - process.kill() - process.close() - quit(1) - - let img = captureWindow(hWnd, w, h, rect.left, rect.top) - img.writeFile(outputPng) - echo &" Saved: {outputPng}" - - # Kill the process - process.kill() - process.close() - echo "Done." - -when isMainModule: - main() +## Captures a screenshot of a running example. +## Usage: nim r tools/capture.nim [delay_seconds] [output_png] +## +## Launches the executable, waits for its window to appear, +## waits the specified delay (default 3s), takes a screenshot, +## saves it as PNG via Pixie, then kills the process. + +import std/[os, osproc, strutils, strformat, times] +import pixie + +type + HWND = pointer + HDC = pointer + HBITMAP = pointer + HGDIOBJ = pointer + WINBOOL = int32 + + RECT = object + left, top, right, bottom: int32 + + BITMAPINFOHEADER = object + biSize: uint32 + biWidth: int32 + biHeight: int32 + biPlanes: uint16 + biBitCount: uint16 + biCompression: uint32 + biSizeImage: uint32 + biXPelsPerMeter: int32 + biYPelsPerMeter: int32 + biClrUsed: uint32 + biClrImportant: uint32 + + BITMAPINFO = object + bmiHeader: BITMAPINFOHEADER + +proc GetWindowRect(hWnd: HWND, lpRect: ptr RECT): WINBOOL + {.dynlib: "user32", stdcall, importc.} + +proc SetForegroundWindow(hWnd: HWND): WINBOOL + {.dynlib: "user32", stdcall, importc.} + +proc ShowWindow(hWnd: HWND, nCmdShow: int32): WINBOOL + {.dynlib: "user32", stdcall, importc.} + +proc EnumWindows(lpEnumFunc: pointer, lParam: pointer): WINBOOL + {.dynlib: "user32", stdcall, importc.} + +proc GetWindowThreadProcessId(hWnd: HWND, lpdwProcessId: ptr uint32): uint32 + {.dynlib: "user32", stdcall, importc.} + +proc IsWindowVisible(hWnd: HWND): WINBOOL + {.dynlib: "user32", stdcall, importc.} + +proc GetDC(hWnd: HWND): HDC + {.dynlib: "user32", stdcall, importc.} + +proc ReleaseDC(hWnd: HWND, hDC: HDC): int32 + {.dynlib: "user32", stdcall, importc.} + +proc CreateCompatibleDC(hdc: HDC): HDC + {.dynlib: "gdi32", stdcall, importc.} + +proc CreateCompatibleBitmap(hdc: HDC, cx, cy: int32): HBITMAP + {.dynlib: "gdi32", stdcall, importc.} + +proc SelectObject(hdc: HDC, h: HGDIOBJ): HGDIOBJ + {.dynlib: "gdi32", stdcall, importc.} + +proc BitBlt(hdc: HDC, x, y, cx, cy: int32, hdcSrc: HDC, x1, y1: int32, rop: uint32): WINBOOL + {.dynlib: "gdi32", stdcall, importc.} + +proc GetDIBits(hdc: HDC, hbm: HBITMAP, start, cLines: uint32, lpvBits: pointer, lpbmi: ptr BITMAPINFO, usage: uint32): int32 + {.dynlib: "gdi32", stdcall, importc.} + +proc DeleteObject(ho: HGDIOBJ): WINBOOL + {.dynlib: "gdi32", stdcall, importc.} + +proc DeleteDC(hdc: HDC): WINBOOL + {.dynlib: "gdi32", stdcall, importc.} + +const + SW_SHOW = 5'i32 + SRCCOPY = 0x00CC0020'u32 + DIB_RGB_COLORS = 0'u32 + BI_RGB = 0'u32 + +type + FindWindowData = object + pid: uint32 + result: HWND + +proc GetWindowTextLengthW(hWnd: HWND): int32 + {.dynlib: "user32", stdcall, importc.} + +proc enumCallback(hWnd: HWND, lParam: pointer): WINBOOL {.stdcall.} = + var data = cast[ptr FindWindowData](lParam) + var windowPid: uint32 + discard GetWindowThreadProcessId(hWnd, addr windowPid) + if windowPid == data.pid and IsWindowVisible(hWnd) != 0: + # Only accept windows with a title (skip message-only/internal windows) + if GetWindowTextLengthW(hWnd) > 0: + var rect: RECT + if GetWindowRect(hWnd, addr rect) != 0: + let w = rect.right - rect.left + let h = rect.bottom - rect.top + if w > 50 and h > 50: + data.result = hWnd + return 0 + return 1 + +proc findWindowByPid(pid: uint32): HWND = + var data = FindWindowData(pid: pid, result: nil) + discard EnumWindows(cast[pointer](enumCallback), addr data) + return data.result + +proc captureWindow(hWnd: HWND, width, height, left, top: int32): Image = + let screenDC = GetDC(nil) + let memDC = CreateCompatibleDC(screenDC) + let hBitmap = CreateCompatibleBitmap(screenDC, width, height) + let oldBitmap = SelectObject(memDC, cast[HGDIOBJ](hBitmap)) + + discard BitBlt(memDC, 0, 0, width, height, screenDC, left, top, SRCCOPY) + + var bi = BITMAPINFO( + bmiHeader: BITMAPINFOHEADER( + biSize: uint32(sizeof(BITMAPINFOHEADER)), + biWidth: width, + biHeight: -height, # top-down + biPlanes: 1, + biBitCount: 32, + biCompression: BI_RGB, + ) + ) + + var pixels = newSeq[uint8](width * height * 4) + discard GetDIBits(memDC, hBitmap, 0, uint32(height), addr pixels[0], addr bi, DIB_RGB_COLORS) + + discard SelectObject(memDC, oldBitmap) + discard DeleteObject(cast[HGDIOBJ](hBitmap)) + discard DeleteDC(memDC) + discard ReleaseDC(nil, screenDC) + + # Convert BGRA -> RGBA and create Pixie image + result = newImage(width, height) + for i in 0 ..< width * height: + let offset = i * 4 + let b = pixels[offset] + let g = pixels[offset + 1] + let r = pixels[offset + 2] + let a = 255'u8 + result.data[i] = rgbx(r, g, b, a) + +proc main() = + let args = commandLineParams() + if args.len < 1: + echo "Usage: capture [delay_seconds] [output_png]" + echo " exe_path Path to the executable to run" + echo " delay_seconds Seconds to wait before screenshot (default: 3)" + echo " output_png Output PNG path (default: .png in docs/)" + quit(1) + + let exePath = args[0] + let delaySec = if args.len >= 2: parseInt(args[1]) else: 3 + let scriptDir = parentDir(currentSourcePath()) + let docsDir = normalizedPath(scriptDir / ".." / "docs") + let defaultPng = docsDir / (exePath.splitFile().name & ".png") + let outputPng = if args.len >= 3: args[2] else: defaultPng + + if not fileExists(exePath): + echo &"ERROR: Executable not found: {exePath}" + quit(1) + + createDir(parentDir(outputPng)) + + echo &"Launching: {exePath}" + let process = startProcess(exePath, workingDir = getCurrentDir()) + let pid = processID(process) + echo &" PID: {pid}" + + # Wait for window to appear + echo "Waiting for window..." + var hWnd: HWND = nil + let deadline = getTime() + initDuration(seconds = 15) + while getTime() < deadline: + hWnd = findWindowByPid(uint32(pid)) + if hWnd != nil: + break + sleep(250) + + if hWnd == nil: + echo "ERROR: No window found within 15 seconds" + process.kill() + process.close() + quit(1) + + # Bring to front and wait the delay + discard ShowWindow(hWnd, SW_SHOW) + discard SetForegroundWindow(hWnd) + echo &"Window found. Waiting {delaySec}s for content to render..." + sleep(delaySec * 1000) + + # Get window rect and capture + var rect: RECT + if GetWindowRect(hWnd, addr rect) == 0: + echo "ERROR: GetWindowRect failed" + process.kill() + process.close() + quit(1) + + let w = rect.right - rect.left + let h = rect.bottom - rect.top + echo &" Window size: {w}x{h} at ({rect.left}, {rect.top})" + + if w <= 0 or h <= 0: + echo "ERROR: Invalid window dimensions" + process.kill() + process.close() + quit(1) + + let img = captureWindow(hWnd, w, h, rect.left, rect.top) + img.writeFile(outputPng) + echo &" Saved: {outputPng}" + + # Kill the process + process.kill() + process.close() + echo "Done." + +when isMainModule: + main() diff --git a/vk14.nimble b/vk14.nimble index 0cbc556..277f0ab 100644 --- a/vk14.nimble +++ b/vk14.nimble @@ -1,8 +1,8 @@ -version = "0.1.0" -author = "treeform" -description = "Vulkan 1.4 wrapper for Nim." -license = "MIT" - -srcDir = "src" - -requires "nim >= 2.2.4" +version = "0.1.0" +author = "treeform" +description = "Vulkan 1.4 wrapper for Nim." +license = "MIT" + +srcDir = "src" + +requires "nim >= 2.2.4"