From 0036ca7c3d0e6f15ceeb029170884988f5b6ecff Mon Sep 17 00:00:00 2001 From: IC Rainbow Date: Tue, 30 Jun 2026 22:25:26 +0300 Subject: [PATCH 1/3] vulkan-utils-spirv: New package Compile-time generation of Haskell types and stuff using SPIR-v bytecode reflection. Co-Authored-By: Claude Opus 4.8 --- cabal.project | 1 + examples/compute-reflect/Main.hs | 229 ++++++ examples/compute-reflect/build-shaders.sh | 8 + examples/compute-reflect/shader.comp | 63 ++ examples/compute/Main.hs | 13 +- examples/depth-headless/Main.hs | 15 +- examples/hlsl/Main.hs | 10 +- examples/lib/HeadlessBoot.hs | 76 +- examples/lib/Triangle.hs | 2 +- examples/lib/WindowedBoot.hs | 22 +- examples/mesh-reflect/Main.hs | 434 +++++++++++ examples/mesh-reflect/build-shaders.sh | 9 + examples/mesh-reflect/shader.frag | 24 + examples/mesh-reflect/shader.vert | 33 + examples/package.yaml | 81 ++ examples/pathtrace-reflect.png | Bin 0 -> 296902 bytes examples/pathtrace-reflect/Main.hs | 591 +++++++++++++++ examples/pathtrace-reflect/build-shaders.sh | 9 + examples/pathtrace-reflect/shader.comp | 261 +++++++ examples/rays/Main.hs | 10 +- examples/resize/Main.hs | 10 +- examples/texture-reflect/Main.hs | 442 +++++++++++ examples/texture-reflect/build-shaders.sh | 9 + examples/texture-reflect/cube.frag | 23 + examples/texture-reflect/cube.vert | 44 ++ examples/texture-reflect/tri.frag | 17 + examples/texture-reflect/tri.vert | 19 + examples/triangle-dynamic/Main.hs | 10 +- examples/triangle-dynamic/TriangleDynamic.hs | 2 +- examples/triangle-glfw/Main.hs | 10 +- examples/triangle-headless-dynamic/Main.hs | 17 +- examples/triangle-headless/Main.hs | 20 +- examples/triangle-sdl2/Main.hs | 10 +- examples/vulkan-examples.cabal | 257 +++++++ stack.yaml | 8 + utils-spirv/README.md | 73 ++ utils-spirv/package.yaml | 85 +++ utils-spirv/src/Vulkan/Utils/SpirV/Array.hs | 198 +++++ utils-spirv/src/Vulkan/Utils/SpirV/Block.hs | 288 ++++++++ utils-spirv/src/Vulkan/Utils/SpirV/Buffer.hs | 138 ++++ .../src/Vulkan/Utils/SpirV/Descriptors.hs | 143 ++++ .../src/Vulkan/Utils/SpirV/DeviceAddress.hs | 48 ++ utils-spirv/src/Vulkan/Utils/SpirV/Layout.hs | 423 +++++++++++ .../src/Vulkan/Utils/SpirV/Pipeline.hs | 114 +++ utils-spirv/src/Vulkan/Utils/SpirV/Reflect.hs | 32 + .../Vulkan/Utils/SpirV/Reflect/OffsetMaps.hs | 57 ++ .../src/Vulkan/Utils/SpirV/Signature.hs | 299 ++++++++ .../src/Vulkan/Utils/SpirV/Specialization.hs | 145 ++++ utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs | 372 ++++++++++ utils-spirv/src/Vulkan/Utils/SpirV/TH.hs | 140 ++++ utils-spirv/src/Vulkan/Utils/SpirV/Types.hs | 292 ++++++++ .../src/Vulkan/Utils/SpirV/VertexInput.hs | 91 +++ utils-spirv/test/LayoutSpec.hs | 229 ++++++ utils-spirv/test/Spec.hs | 696 ++++++++++++++++++ utils-spirv/test/fixtures/array-field.comp | 25 + utils-spirv/test/fixtures/array2d.comp | 25 + utils-spirv/test/fixtures/bda.comp | 39 + utils-spirv/test/fixtures/build-shaders.sh | 19 + utils-spirv/test/fixtures/mesh.frag | 32 + utils-spirv/test/fixtures/mesh.vert | 29 + utils-spirv/test/fixtures/nested.comp | 31 + utils-spirv/test/fixtures/push.comp | 28 + utils-spirv/test/fixtures/spec.comp | 21 + utils-spirv/test/fixtures/ssbo-struct.comp | 24 + utils-spirv/test/fixtures/wide.comp | 27 + utils-spirv/vulkan-utils-spirv.cabal | 143 ++++ 66 files changed, 6996 insertions(+), 99 deletions(-) create mode 100644 examples/compute-reflect/Main.hs create mode 100644 examples/compute-reflect/build-shaders.sh create mode 100644 examples/compute-reflect/shader.comp create mode 100644 examples/mesh-reflect/Main.hs create mode 100755 examples/mesh-reflect/build-shaders.sh create mode 100644 examples/mesh-reflect/shader.frag create mode 100644 examples/mesh-reflect/shader.vert create mode 100644 examples/pathtrace-reflect.png create mode 100644 examples/pathtrace-reflect/Main.hs create mode 100644 examples/pathtrace-reflect/build-shaders.sh create mode 100644 examples/pathtrace-reflect/shader.comp create mode 100644 examples/texture-reflect/Main.hs create mode 100755 examples/texture-reflect/build-shaders.sh create mode 100644 examples/texture-reflect/cube.frag create mode 100644 examples/texture-reflect/cube.vert create mode 100644 examples/texture-reflect/tri.frag create mode 100644 examples/texture-reflect/tri.vert create mode 100644 utils-spirv/README.md create mode 100644 utils-spirv/package.yaml create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Array.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Block.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Buffer.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Descriptors.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/DeviceAddress.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Layout.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Pipeline.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Reflect.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Reflect/OffsetMaps.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Signature.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Specialization.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/TH.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/Types.hs create mode 100644 utils-spirv/src/Vulkan/Utils/SpirV/VertexInput.hs create mode 100644 utils-spirv/test/LayoutSpec.hs create mode 100644 utils-spirv/test/Spec.hs create mode 100644 utils-spirv/test/fixtures/array-field.comp create mode 100644 utils-spirv/test/fixtures/array2d.comp create mode 100644 utils-spirv/test/fixtures/bda.comp create mode 100644 utils-spirv/test/fixtures/build-shaders.sh create mode 100644 utils-spirv/test/fixtures/mesh.frag create mode 100644 utils-spirv/test/fixtures/mesh.vert create mode 100644 utils-spirv/test/fixtures/nested.comp create mode 100644 utils-spirv/test/fixtures/push.comp create mode 100644 utils-spirv/test/fixtures/spec.comp create mode 100644 utils-spirv/test/fixtures/ssbo-struct.comp create mode 100644 utils-spirv/test/fixtures/wide.comp create mode 100644 utils-spirv/vulkan-utils-spirv.cabal diff --git a/cabal.project b/cabal.project index dee63d1c1..52f23f0cb 100644 --- a/cabal.project +++ b/cabal.project @@ -3,6 +3,7 @@ packages: ./openxr ./VulkanMemoryAllocator ./utils + ./utils-spirv ./utils-init/vulkan-init-sdl2 ./utils-init/vulkan-init-glfw ./examples diff --git a/examples/compute-reflect/Main.hs b/examples/compute-reflect/Main.hs new file mode 100644 index 000000000..d382517a5 --- /dev/null +++ b/examples/compute-reflect/Main.hs @@ -0,0 +1,229 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +{-| A compute example whose shader interface is derived from the compiled +SPIR-V at build time by @vulkan-utils-spirv@: + + * 'reflectShaderTypes' generates the 'Params' push-constant record (with a + gl-block std430 'Storable') from @shader.comp.spv@; + * 'singleDescriptorSetLayoutInfo' builds the descriptor set layout for the + output SSBO from the same module at runtime; + * 'pushConstantRanges' builds the pipeline layout's push-constant range; and + * 'allocateSpecializationInfo' builds the pipeline's specialization info + (@maxIterations@, @escapeRadius@) from the reflected constant ids. + +Compare with the @compute@ example, which hand-writes all of this. +-} +module Main + ( main + ) +where + +import qualified Codec.Picture as JP +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT) +import Data.ByteString (ByteString) +import Data.FileEmbed (embedFile) +import qualified Data.Vector as V +import Data.Word (Word32) +import Foreign.Marshal.Array (peekArray) +import Foreign.Marshal.Utils (with) +import Foreign.Ptr (Ptr, castPtr, plusPtr) +import Foreign.Storable (sizeOf) +import qualified Geomancy +import Geomancy.UVec2 (uvec2) +import Geomancy.Vec2 (vec2) +import Graphics.Gl.Block (Std430 (..)) +import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), submitAndWait, withHeadlessVk) +import ImageReadback (captureImageRGBA8, savePng) +import Vulkan.CStruct.Extends (SomeStruct (..)) +import Vulkan.CStruct.Utils (FixedArray, lowerArrayPtr) +import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) +import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) +import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import Vulkan.Utils.Descriptors (bufferWrite) +import Vulkan.Utils.QueueAssignment (QueueFamilyIndex (..)) +import Vulkan.Utils.Queues (Queues (..)) +import Vulkan.Utils.Shader (shaderModuleStage) +import Vulkan.Utils.SpirV.Descriptors (pushConstantRanges, singleDescriptorSetLayoutInfo) +import Vulkan.Utils.SpirV.Reflect (reflectBytes) +import Vulkan.Utils.SpirV.Specialization (allocateSpecializationInfo) +import Vulkan.Utils.SpirV.TH (reflectShaderTypes) +import Vulkan.Zero (zero) +import qualified VulkanMemoryAllocator as VMA + +-- | The compiled shader, embedded for runtime module creation. +compCode :: ByteString +compCode = $(embedFile "compute-reflect/shader.comp.spv") + +-- Generate the @Params@ push-constant record (and its std430 'Storable') from +-- the same SPIR-V the runtime loads. +reflectShaderTypes "compute-reflect/shader.comp.spv" + +main :: IO () +main = runResourceT $ do + HeadlessVk{..} <- + withHeadlessVk + HeadlessConfig + { appName = "Haskell Vulkan compute-reflect example" + , instanceReqs = [] + , deviceReqs = [] + , vmaFlags = zero + } + let QueueFamilyIndex computeQueueFamilyIndex = fst (qCompute queues) + + image <- render allocator device computeQueueFamilyIndex + Vk.deviceWaitIdle device + savePng "julia-reflect.png" image + +render + :: VMA.Allocator + -> Vk.Device + -> Word32 + -> ResourceT IO (JP.Image JP.PixelRGBA8) +render allocator dev computeQueueFamilyIndex = do + let + width, height, workgroup :: Int + width = 512 + height = width + workgroup = 16 + + -- Push-constant block (reflected as 'Params'). + params :: Params + params = + Params + { center = vec2 (-0.8) 0.156 + , resolution = uvec2 (fromIntegral width) (fromIntegral height) + } + + -- Specialization constants, in ascending constant_id order: + -- id 0 = maxIterations (uint), id 1 = escapeRadius (float). + maxIterations :: Word32 + maxIterations = 1000 + escapeRadius :: Float + escapeRadius = 2.0 + + -- Reflect the embedded module once; reuse it for the descriptor set layout + -- and the push-constant range. + reflected <- reflectBytes compCode + + -- Output storage buffer: one RGBA32F texel per pixel, mapped GPU_TO_CPU. + (_, (outBuffer, outAllocation, outInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral $ width * height * 4 * sizeOf (0 :: Float) + , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_GPU_TO_CPU + } + allocate + + -- Descriptor set layout generated from the reflected module (the output SSBO). + setLayoutInfo <- either fail pure (singleDescriptorSetLayoutInfo reflected) + (_, descriptorSetLayout) <- Vk.withDescriptorSetLayout dev setLayoutInfo Nothing allocate + + (_, descriptorPool) <- + Vk.withDescriptorPool + dev + zero + { Vk.maxSets = 1 + , Vk.poolSizes = + [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER 1 + ] + } + Nothing + allocate + [descriptorSet] <- + Vk.allocateDescriptorSets + dev + zero + { Vk.descriptorPool = descriptorPool + , Vk.setLayouts = [descriptorSetLayout] + } + + Vk.updateDescriptorSets + dev + [ bufferWrite descriptorSet 0 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER outBuffer + ] + [] + + -- Pipeline. The specialization info (maxIterations, escapeRadius) is built + -- from the reflected constant ids and lives for this resource scope. + mSpec <- allocateSpecializationInfo reflected (maxIterations, escapeRadius) + (_, shader) <- shaderModuleStage dev Vk.SHADER_STAGE_COMPUTE_BIT mSpec compCode + -- Pipeline layout: descriptor set + push-constant range, both from reflection. + (_, pipelineLayout) <- + Vk.withPipelineLayout + dev + zero + { PipelineLayoutCreateInfo.setLayouts = [descriptorSetLayout] + , PipelineLayoutCreateInfo.pushConstantRanges = + V.fromList (pushConstantRanges reflected) + } + Nothing + allocate + let + pipelineCreateInfo :: Vk.ComputePipelineCreateInfo '[] + pipelineCreateInfo = + zero + { Vk.layout = pipelineLayout + , Vk.stage = shader + , Vk.basePipelineHandle = zero + } + (_, (_, [computePipeline])) <- + Vk.withComputePipelines dev zero [SomeStruct pipelineCreateInfo] Nothing allocate + + (_, commandPool) <- + Vk.withCommandPool dev zero{CommandPoolCreateInfo.queueFamilyIndex = computeQueueFamilyIndex} Nothing allocate + (_, [cb]) <- + Vk.withCommandBuffers + dev + zero + { Vk.commandPool = commandPool + , Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY + , Vk.commandBufferCount = 1 + } + allocate + + Vk.useCommandBuffer cb zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_COMPUTE computePipeline + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_COMPUTE pipelineLayout 0 [descriptorSet] [] + -- Push the reflected 'Params' (std430) for this dispatch. + liftIO $ with params $ \pParams -> + Vk.cmdPushConstants + cb + pipelineLayout + Vk.SHADER_STAGE_COMPUTE_BIT + 0 + (fromIntegral (sizeOf params)) + (castPtr pParams) + Vk.cmdDispatch + cb + (ceiling (realToFrac width / realToFrac @_ @Float workgroup)) + (ceiling (realToFrac height / realToFrac @_ @Float workgroup)) + 1 + + computeQueue <- Vk.getDeviceQueue dev computeQueueFamilyIndex 0 + submitAndWait dev computeQueue cb "Timed out waiting for compute" + + let + pixelAddr :: Int -> Int -> Ptr (FixedArray 4 Float) + pixelAddr x y = + plusPtr + (VMA.mappedData outInfo) + (((y * width) + x) * 4 * sizeOf (0 :: Float)) + captureImageRGBA8 allocator outAllocation width height $ \x y -> do + let ptr = pixelAddr x y + [r, g, b, a] <- fmap (\f -> round (f * 255)) <$> peekArray 4 (lowerArrayPtr ptr) + pure $ JP.PixelRGBA8 r g b a diff --git a/examples/compute-reflect/build-shaders.sh b/examples/compute-reflect/build-shaders.sh new file mode 100644 index 000000000..9b8277604 --- /dev/null +++ b/examples/compute-reflect/build-shaders.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Compile the GLSL shaders to SPIR-V with a local glslang. The resulting .spv is +# committed and consumed both at runtime (Data.FileEmbed.embedFile) and at +# compile time (Vulkan.Utils.SpirV reflection). +set -eu +cd "$(dirname "$0")" +glslangValidator -V shader.comp -o shader.comp.spv +echo "wrote shader.comp.spv" diff --git a/examples/compute-reflect/shader.comp b/examples/compute-reflect/shader.comp new file mode 100644 index 000000000..8f07b91a0 --- /dev/null +++ b/examples/compute-reflect/shader.comp @@ -0,0 +1,63 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable + +// A Julia-set compute shader whose entire interface is reflected at build time +// by vulkan-utils-spirv: +// * the `Params` push-constant block -> a Haskell record (gl-block std430 +// Storable) + the pipeline layout's push-constant range; +// * the output SSBO -> the descriptor set layout; and +// * the specialization constants below -> the pipeline's SpecializationInfo. + +layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +// Specialized at pipeline-creation time (see Vulkan.Utils.SpirV.Specialization). +layout(constant_id = 0) const uint maxIterations = 1000u; +layout(constant_id = 1) const float escapeRadius = 2.0; + +// Fields are ordered by non-increasing alignment (vec2/uvec2 = 8) so gl-block's +// Generic std430 layout matches the shader; see the guardrail in +// Vulkan.Utils.SpirV.Block. +layout(push_constant, std430) uniform Params { + vec2 center; // c in the Julia iteration + uvec2 resolution; // output size in pixels +} params; + +layout(set = 0, binding = 0, std430) buffer OutputBuffer { + vec4 pixels[]; +}; + +// From https://iquilezles.org/articles/palettes/ +vec3 palette(float t) { + const vec3 a = vec3(0.5); + const vec3 b = vec3(0.5); + const vec3 c = vec3(1.0); + const vec3 d = vec3(0.0, 0.10, 0.20); + return a + b * cos(6.28318530718 * (c * t + d)); +} + +void main() { + uvec2 gid = gl_GlobalInvocationID.xy; + if (gid.x >= params.resolution.x || gid.y >= params.resolution.y) { + return; + } + + vec2 uv = + ( vec2(gid) / vec2(params.resolution) * 2.0 - 1.0 ) * escapeRadius; + + vec2 z = uv; + int i = 0; + for (; i < int(maxIterations); ++i) { + vec2 z2 = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y); + z = z2 + params.center; + if (dot(z, z) > escapeRadius * escapeRadius) { + break; + } + } + + uint idx = gid.y * params.resolution.x + gid.x; + if (i == int(maxIterations)) { + pixels[idx] = vec4(0.0, 0.0, 0.0, 1.0); + } else { + pixels[idx] = vec4(palette(float(i) / float(maxIterations)), 1.0); + } +} diff --git a/examples/compute/Main.hs b/examples/compute/Main.hs index df85ae6b7..c02c4384e 100644 --- a/examples/compute/Main.hs +++ b/examples/compute/Main.hs @@ -34,14 +34,15 @@ main = runResourceT $ do HeadlessVk{..} <- withHeadlessVk HeadlessConfig - { hcAppName = "Haskell Vulkan compute example" - , hcInstanceReqs = [] - , hcDeviceReqs = [] + { appName = "Haskell Vulkan compute example" + , instanceReqs = [] + , deviceReqs = [] + , vmaFlags = zero } - let QueueFamilyIndex computeQueueFamilyIndex = fst (qCompute hvQueues) + let QueueFamilyIndex computeQueueFamilyIndex = fst (qCompute queues) - image <- render hvAllocator hvDevice computeQueueFamilyIndex - Vk.deviceWaitIdle hvDevice + image <- render allocator device computeQueueFamilyIndex + Vk.deviceWaitIdle device savePng "julia.png" image -- | Render the Julia set diff --git a/examples/depth-headless/Main.hs b/examples/depth-headless/Main.hs index 57eef6162..65bc363da 100644 --- a/examples/depth-headless/Main.hs +++ b/examples/depth-headless/Main.hs @@ -107,17 +107,18 @@ main = runResourceT $ do HeadlessVk{..} <- withHeadlessVk HeadlessConfig - { hcAppName = "Haskell Vulkan depth + vertex-buffer (headless) test" - , hcInstanceReqs = [] - , hcDeviceReqs = + { appName = "Haskell Vulkan depth + vertex-buffer (headless) test" + , instanceReqs = [] + , deviceReqs = [U.reqs| VK_KHR_dynamic_rendering PhysicalDeviceDynamicRenderingFeatures.dynamicRendering |] + , vmaFlags = zero } - let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics hvQueues) - results <- render hvAllocator hvDevice graphicsQueueFamilyIndex - Vk.deviceWaitIdle hvDevice + let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics queues) + results <- render allocator device graphicsQueueFamilyIndex + Vk.deviceWaitIdle device centres <- forM results $ \(name, image) -> do savePng ("depth-headless-" <> name <> ".png") image @@ -192,7 +193,7 @@ render allocator dev graphicsQueueFamilyIndex = do , Dynamic.depthFormat = Just depthFormat , Dynamic.vertexInput = vertexInput } - () -- no specialization constants + () [(Vk.SHADER_STAGE_VERTEX_BIT, vertCode), (Vk.SHADER_STAGE_FRAGMENT_BIT, fragCode)] let poolCreateInfo = zero{CommandPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex} diff --git a/examples/hlsl/Main.hs b/examples/hlsl/Main.hs index 7e637a18b..01f8a78c3 100644 --- a/examples/hlsl/Main.hs +++ b/examples/hlsl/Main.hs @@ -32,11 +32,11 @@ main = runResourceT $ do (vc, _vma, initialSC) <- withWindowedVk WindowedConfig - { wcAppName = "Vulkan 🚀 Haskell" - , wcInstanceReqs = [] - , wcDeviceReqs = [] - , wcVmaFlags = zero - , wcSwapchainConfig = defaultSwapchainConfig + { appName = "Vulkan 🚀 Haskell" + , instanceReqs = [] + , deviceReqs = [] + , vmaFlags = zero + , swapchainConfig = defaultSwapchainConfig } (sdl2Adapter win) let dev = vcDevice vc diff --git a/examples/lib/HeadlessBoot.hs b/examples/lib/HeadlessBoot.hs index 41f45ad7e..e8a9002e3 100644 --- a/examples/lib/HeadlessBoot.hs +++ b/examples/lib/HeadlessBoot.hs @@ -1,27 +1,30 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoFieldSelectors #-} -{-| Shared boot prelude for the headless examples (compute, triangle-headless). +{-| Shared boot prelude for the headless examples. Mirrors 'WindowedBoot.withWindowedVk' but with no surface, no swapchain — just an instance, a logical device with the standard 'Queues' triple, and a VMA allocator. -Headless callers project the queue they actually need from 'hvQueues' -(@qCompute@ for compute, @qGraphics@ for triangle-headless) — there's no -per-example queue-family configuration. +Headless callers project the queue they actually need from 'queues' +(@qCompute@ for compute examples, @qGraphics@ for graphics examples) — there's +no per-example queue-family configuration. -} module HeadlessBoot ( HeadlessConfig (..) , HeadlessVk (..) , withHeadlessVk , submitAndWait + , submitAndWaitFor ) where import Control.Exception.Safe (MonadThrow, throwString) import Control.Monad.IO.Class (liftIO) -import Control.Monad.Trans.Resource (MonadResource, allocate) +import Control.Monad.Trans.Resource (MonadResource, allocate, release) import Data.Text (Text) import qualified Data.Text.Encoding as Text +import Data.Word (Word64) import Say (sayErr) import Vulkan.CStruct.Extends (SomeStruct (..)) import qualified Vulkan.Core10 as Vk @@ -41,18 +44,20 @@ import WindowedBoot (debugMessengerCreateInfo) the helper always provisions the standard G/C/T 'Queues' triple. -} data HeadlessConfig = HeadlessConfig - { hcAppName :: Text - , hcInstanceReqs :: [InstanceRequirement] - , hcDeviceReqs :: [DeviceRequirement] + { appName :: Text + , instanceReqs :: [InstanceRequirement] + , deviceReqs :: [DeviceRequirement] + , vmaFlags :: VMA.AllocatorCreateFlags + -- ^ VMA flags (e.g. @ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT@ for BDA). } -- | Long-lived handles produced by 'withHeadlessVk'. data HeadlessVk = HeadlessVk - { hvInstance :: Vk.Instance - , hvPhysicalDevice :: Vk.PhysicalDevice - , hvDevice :: Vk.Device - , hvAllocator :: VMA.Allocator - , hvQueues :: Queues (QueueFamilyIndex, Vk.Queue) + { inst :: Vk.Instance + , physicalDevice :: Vk.PhysicalDevice + , device :: Vk.Device + , allocator :: VMA.Allocator + , queues :: Queues (QueueFamilyIndex, Vk.Queue) } {- | Open a Vulkan instance + headless device + VMA allocator. Logs the @@ -67,30 +72,31 @@ withHeadlessVk HeadlessConfig{..} = do Init.allocateInstance ( Just zero - { Vk.applicationName = Just (Text.encodeUtf8 hcAppName) + { Vk.applicationName = Just (Text.encodeUtf8 appName) , Vk.apiVersion = API_VERSION_1_3 } ) ( RequireInstanceExtension Nothing EXT_DEBUG_UTILS_EXTENSION_NAME minBound - : hcInstanceReqs + : instanceReqs ) [RequireInstanceLayer "VK_LAYER_KHRONOS_validation" minBound] _ <- withDebugUtilsMessengerEXT inst debugMessengerCreateInfo Nothing allocate - (phys, dev, qs) <- allocateDevice inst Nothing hcDeviceReqs + (phys, dev, qs) <- allocateDevice inst Nothing deviceReqs (_, vma) <- - VMA.withAllocator (allocatorCreateInfo zero API_VERSION_1_3 inst phys dev) allocate + VMA.withAllocator (allocatorCreateInfo vmaFlags API_VERSION_1_3 inst phys dev) allocate sayErr . ("Using device: " <>) =<< physicalDeviceName phys pure HeadlessVk - { hvInstance = inst - , hvPhysicalDevice = phys - , hvDevice = dev - , hvAllocator = vma - , hvQueues = qs + { inst + , physicalDevice = phys + , device = dev + , allocator = vma + , queues = qs } {- | Submit a single command buffer to the queue, wait up to a second on a -fresh fence for it to complete, throw on timeout. +fresh fence for it to complete, throw on timeout. For longer-running work (e.g. +a heavy compute dispatch) use 'submitAndWaitFor' with an explicit budget. -} submitAndWait :: (MonadResource m, MonadThrow m) @@ -99,12 +105,26 @@ submitAndWait -> Vk.CommandBuffer -> String -> m () -submitAndWait dev queue commandBuffer timeoutMessage = do - (_, fence) <- Vk.withFence dev zero Nothing allocate +submitAndWait = submitAndWaitFor (1 * 1000 * 1000 * 1000) -- 1 second + +-- | As 'submitAndWait', but with a caller-supplied timeout in nanoseconds. +submitAndWaitFor + :: (MonadResource m, MonadThrow m) + => Word64 + -- ^ Timeout in nanoseconds. + -> Vk.Device + -> Vk.Queue + -> Vk.CommandBuffer + -> String + -- ^ Message to throw if the wait times out. + -> m () +submitAndWaitFor fenceTimeout dev queue commandBuffer timeoutMessage = do + (fenceKey, fence) <- Vk.withFence dev zero Nothing allocate let submitInfo = - zero{Vk.commandBuffers = [Vk.commandBufferHandle commandBuffer]} + zero + { Vk.commandBuffers = [Vk.commandBufferHandle commandBuffer] + } Vk.queueSubmit queue [SomeStruct submitInfo] fence - let fenceTimeout = 1e9 -- 1 second liftIO (Vk.waitForFences dev [fence] True fenceTimeout) >>= \case Vk.TIMEOUT -> throwString timeoutMessage - _ -> pure () + _ -> release fenceKey diff --git a/examples/lib/Triangle.hs b/examples/lib/Triangle.hs index 71a28a71d..684ad4646 100644 --- a/examples/lib/Triangle.hs +++ b/examples/lib/Triangle.hs @@ -117,7 +117,7 @@ createGraphicsPipeline dev renderPass colorFormat = renderPass zero { RenderPass.colorFormats = [colorFormat] - , RenderPass.dynamicStates = Just minimalDynamicStates -- just viewport+scissor; set below with cmdSetViewport/Scissor + , RenderPass.dynamicStates = Just minimalDynamicStates -- just viewport+scissor; set with cmdSetViewport/Scissor } () -- no specialization constants [ (Vk.SHADER_STAGE_VERTEX_BIT, vertCode) diff --git a/examples/lib/WindowedBoot.hs b/examples/lib/WindowedBoot.hs index 49954ed43..9c28196b2 100644 --- a/examples/lib/WindowedBoot.hs +++ b/examples/lib/WindowedBoot.hs @@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NoFieldSelectors #-} {-| Shared boot prelude for the windowed examples. Drives the standard @allocateInstance \/ allocateSurface \/ allocateDevice \/ withAllocator \/ allocateSwapchain@ @@ -39,15 +40,15 @@ import VulkanMemoryAllocator.Utils (allocatorCreateInfo) -- | Per-example knobs the helper can't infer. data WindowedConfig = WindowedConfig - { wcAppName :: Text + { appName :: Text -- ^ Shows up in the application info; also used as the window title. - , wcInstanceReqs :: [InstanceRequirement] + , instanceReqs :: [InstanceRequirement] -- ^ Extra instance requirements (frame's defaults are added automatically). - , wcDeviceReqs :: [DeviceRequirement] + , deviceReqs :: [DeviceRequirement] -- ^ Extra device requirements (frame's defaults are added automatically). - , wcVmaFlags :: VMA.AllocatorCreateFlags + , vmaFlags :: VMA.AllocatorCreateFlags -- ^ VMA flags (e.g. @ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT@ for rays). - , wcSwapchainConfig :: SwapchainConfig + , swapchainConfig :: SwapchainConfig {- ^ Knobs for the initial swapchain. Pass 'defaultSwapchainConfig' for the common case; compute-shader callers (e.g. @resize@) tweak the storage bits. -} @@ -66,30 +67,29 @@ withWindowedVk WindowedConfig{..} WindowAdapter{..} = do waAllocateInstance ( Just zero - { Vk.applicationName = Just (Text.encodeUtf8 wcAppName) + { Vk.applicationName = Just (Text.encodeUtf8 appName) , Vk.apiVersion = API_VERSION_1_3 } ) ( RequireInstanceExtension Nothing EXT_DEBUG_UTILS_EXTENSION_NAME minBound : frameInstanceRequirements - ++ wcInstanceReqs + ++ instanceReqs ) [RequireInstanceLayer "VK_LAYER_KHRONOS_validation" minBound] _ <- withDebugUtilsMessengerEXT inst debugMessengerCreateInfo Nothing allocate surf <- waAllocateSurface inst (phys, dev, qs) <- - allocateDevice inst (Just surf) (frameDeviceRequirements ++ wcDeviceReqs) + allocateDevice inst (Just surf) (frameDeviceRequirements ++ deviceReqs) (_, vma) <- VMA.withAllocator - (allocatorCreateInfo wcVmaFlags API_VERSION_1_3 inst phys dev) + (allocatorCreateInfo vmaFlags API_VERSION_1_3 inst phys dev) allocate props <- Vk.getPhysicalDeviceProperties phys sayErr $ "Using device: " <> decodeUtf8 (Vk.deviceName props) vc <- liftIO $ mkVulkanContext inst phys dev qs initialSize <- waDrawableSize - initialSC <- - allocateSwapchain phys dev wcSwapchainConfig Vk.NULL_HANDLE initialSize surf + initialSC <- allocateSwapchain phys dev swapchainConfig Vk.NULL_HANDLE initialSize surf pure (vc, vma, initialSC) {- | Standard validation/perf debug messenger create info, shared with diff --git a/examples/mesh-reflect/Main.hs b/examples/mesh-reflect/Main.hs new file mode 100644 index 000000000..8df1102cd --- /dev/null +++ b/examples/mesh-reflect/Main.hs @@ -0,0 +1,434 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} + +{-| Headless demonstration of type-verified pipeline assembly from reflected +SPIR-V. A single triangle's vertices live in an SSBO; ONE vertex shader pulls +them (indexed by @gl_VertexIndex@) and drives two pipelines built from the same +reflected resources: + + * a __depth-only__ pipeline (vertex stage alone, no colour attachment) — a + z-prepass whose depth buffer is read back and checked (near depth written at + the centre, the cleared far value at the corner). + + * a __depth+colour__ pipeline (vertex + fragment) — the fragment stage shades + the surface with a Lambert (N·L) light from the shared @Scene@ UBO, producing + a top-bright/bottom-dark gradient that is read back and checked. + +The fragment stage's compatibility with the vertex stage — matching @out@/@in@ +interface and shared use of the @Scene@ descriptor — is verified __at compile +time__ by 'MatchInterface' / 'CompatibleResources' (see 'pipelineComposes'); the +merged pipeline layout (Scene visible to both stages, the Mesh SSBO to the vertex +stage) and each pipeline come from 'allocateReflectedLayout' / 'allocateGraphicsPipeline'. +Exits non-zero on mismatch. +-} +module Main where + +import qualified Codec.Picture as JP +import Control.Monad (unless, zipWithM_) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT) +import Data.Bits ((.|.)) +import Data.ByteString (ByteString) +import Data.FileEmbed (embedFile) +import Data.Proxy (Proxy (..)) +import qualified Data.Vector as V +import Data.Word (Word32) +import Foreign.Ptr (castPtr) +import Foreign.Storable (peekElemOff, poke, sizeOf) +import qualified Geomancy +import qualified Geomancy.Mat4 as Mat4 +import Geomancy.Vec3 (vec3) +import Geomancy.Vec4 (vec4) +import Graphics.Gl.Block (Std140 (..), Std430 (..)) +import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), submitAndWait, withHeadlessVk) +import ImageReadback (copyImageToHost, makeReadbackImage, savePng) +import RenderTarget (createColorTarget) +import System.Exit (exitFailure) +import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) +import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import qualified Vulkan.Core13 as Vk +import Vulkan.Utils.Barrier (imageBarrier, transitionColorAttachment, transitionDepthAttachment) +import Vulkan.Utils.Descriptors (bufferWrite) +import qualified Vulkan.Utils.DynamicRendering as Dynamic +import Vulkan.Utils.DynamicState (DynamicState (..), allDynamicStates, applyDynamicStates, depthOnlyDynamicStates, dynamicStateFor, fullScissor) +import Vulkan.Utils.QueueAssignment (QueueFamilyIndex (..)) +import Vulkan.Utils.Queues (Queues (..)) +import Vulkan.Zero (zero) +import qualified VulkanMemoryAllocator as AllocationCreateInfo (AllocationCreateInfo (..)) +import qualified VulkanMemoryAllocator as VMA + +import Data.SpirV.Reflect.FFI (loadBytes) +import qualified Vulkan.Utils.SpirV.Array as Array +import Vulkan.Utils.SpirV.Buffer (Buffer, unsafeAsBufferPtr, writeBufferElem) +import Vulkan.Utils.SpirV.Pipeline (ReflectedLayout (..), allocateGraphicsPipeline, allocateReflectedLayout) +import Vulkan.Utils.SpirV.Signature (ArrayOf, Sig) +import Vulkan.Utils.SpirV.Stage (CompatibleResources, MatchInterface, reflectStageSig) +import Vulkan.Utils.SpirV.TH (reflectShaderTypes) + +-- Reflection at compile time: the @Scene@ UBO record and the @Vertex@ SSBO +-- element record, plus a stage signature for each shader. +reflectShaderTypes "mesh-reflect/shader.vert.spv" +reflectStageSig "VertSig" "mesh-reflect/shader.vert.spv" +reflectStageSig "FragSig" "mesh-reflect/shader.frag.spv" + +-- Compile-time proof that the fragment stage composes with the vertex stage: +-- matching interface and compatible shared resources. Evaluating it forces GHC to +-- discharge those constraints. +pipelineComposes :: (MatchInterface VertSig FragSig, CompatibleResources VertSig FragSig) => Bool +pipelineComposes = True + +-- Committed SPIR-V, embedded for the runtime shader modules and reflection. +vertSpv :: ByteString +vertSpv = $(embedFile "mesh-reflect/shader.vert.spv") + +fragSpv :: ByteString +fragSpv = $(embedFile "mesh-reflect/shader.frag.spv") + +width, height :: Word32 +width = 256 +height = 256 + +colorFormat :: Vk.Format +colorFormat = Vk.FORMAT_R8G8B8A8_UNORM + +depthFormat :: Vk.Format +depthFormat = Vk.FORMAT_D32_SFLOAT + +floatSize :: Int +floatSize = sizeOf (0 :: Float) + +{- | The triangle, stored in the SSBO. Apex at the top (Vulkan clip space is +Y +down), facing the light; the base faces away — so Lambert shading gives a +top-bright/bottom-dark gradient. Vertices are white; the colour comes from the +light. +-} +meshVertices :: [Vertex] +meshVertices = + [ Vertex{position = vec3 0.0 (-0.7) 0.5, normal = vec3 0.0 (-1.0) 0.6, color = vec3 1 1 1} + , Vertex{position = vec3 (-0.7) 0.6 0.5, normal = vec3 0.0 1.0 0.6, color = vec3 1 1 1} + , Vertex{position = vec3 0.7 0.6 0.5, normal = vec3 0.0 1.0 0.6, color = vec3 1 1 1} + ] + +vertexCount :: Word32 +vertexCount = 3 + +-- | Identity transform, a light from the top-front, and a warm light colour. +sceneValue :: Scene +sceneValue = + Scene + { transform = Mat4.identity + , lightDir = vec4 0.0 (-1.0) 0.6 0.0 + , lightColor = vec4 1.0 0.85 0.6 1.0 + } + +main :: IO () +main = runResourceT $ do + HeadlessVk{..} <- + withHeadlessVk + HeadlessConfig + { appName = "Haskell Vulkan type-verified pipeline assembly (headless)" + , instanceReqs = [] + , deviceReqs = Dynamic.dynamicRenderingRequirements + , vmaFlags = zero + } + liftIO $ putStrLn $ "vertex+fragment pipeline composes (compile-time): " <> show pipelineComposes + let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics queues) + + (depthCentre, depthCorner, colorImage) <- + render allocator device graphicsQueueFamilyIndex + Vk.deviceWaitIdle device + + savePng "mesh-reflect-color.png" colorImage + let + pixel x y = JP.pixelAt colorImage x y + lum (JP.PixelRGBA8 r g b _) = fromIntegral r + fromIntegral g + fromIntegral b :: Int + cx = fromIntegral width `div` 2 + upper = lum (pixel cx (fromIntegral height `div` 4)) -- near the lit apex + lower = lum (pixel cx (3 * fromIntegral height `div` 4)) -- near the dark base + centre = lum (pixel cx (fromIntegral height `div` 2)) + corner = lum (pixel 4 4) -- background + liftIO $ do + putStrLn $ "depth-only: centre=" <> show depthCentre <> " corner=" <> show depthCorner + putStrLn $ + "depth+color: luminance upper=" + <> show upper + <> " lower=" + <> show lower + <> " centre=" + <> show centre + <> " corner=" + <> show corner + + let + checks :: [(String, Bool)] + checks = + [ ("depth-only wrote near depth at the centre", depthCentre < 0.9) + , ("depth-only left the corner at the far plane", depthCorner > 0.99) + , ("depth+color lit the geometry above the background", centre > corner + 60) + , ("Lambert shading is brighter near the light (top) than away (bottom)", upper > lower + 60) + ] + liftIO $ do + mapM_ (\(label, ok) -> putStrLn $ "[" <> (if ok then "PASS" else "FAIL") <> "] " <> label) checks + unless (all snd checks) exitFailure + putStrLn "All type-verified pipeline-assembly checks passed." + +render + :: VMA.Allocator + -> Vk.Device + -> Word32 + -> ResourceT IO (Float, Float, JP.Image JP.PixelRGBA8) +render allocator dev graphicsQueueFamilyIndex = do + let extent = Vk.Extent2D width height + + -- Reflected modules drive the merged pipeline layout. + vertModule <- loadBytes vertSpv + fragModule <- loadBytes fragSpv + + (_, (image, imageView)) <- createColorTarget allocator dev colorFormat extent + -- Depth target with TRANSFER_SRC so the prepass result can be read back. + (depthImage, depthView) <- createReadableDepthTarget allocator dev depthFormat extent + (cpuImage, readback) <- makeReadbackImage allocator dev colorFormat extent + + -- Geometry SSBO (host-visible, mapped). The mapped pointer is tagged as a typed + -- runtime array of the reflected @Vertex@ element — @'ArrayOf' ('Sig' Vertex)@ — + -- and each vertex is written with 'writeBufferElem', which is gated by + -- 'Vulkan.Utils.SpirV.Signature.FitsTail': a wrong element layout (or indexing a + -- non-array buffer) is a compile error, and the element stride comes from the + -- signature, not a hand-computed offset. + (_, (meshBuffer, _, meshInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (length meshVertices * Array.std430Stride (Proxy @Vertex)) + , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + } + mappedAlloc + allocate + meshBuf <- unsafeAsBufferPtr (VMA.mappedData meshInfo) :: ResourceT IO (Buffer (ArrayOf (Sig Vertex))) + zipWithM_ (writeBufferElem meshBuf) [0 ..] meshVertices + + -- Scene UBO (host-visible, mapped). + (_, (sceneBuffer, _, sceneInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (sizeOf sceneValue) + , Vk.usage = Vk.BUFFER_USAGE_UNIFORM_BUFFER_BIT + } + mappedAlloc + allocate + liftIO $ poke (castPtr (VMA.mappedData sceneInfo)) sceneValue + + -- Descriptor set layouts + pipeline layout from reflection, merged across both + -- stages: Scene visible to vertex AND fragment (stage-flag union), the Mesh SSBO + -- to the vertex stage. One layout, shared by both pipelines below. + (_, layout) <- allocateReflectedLayout dev [vertModule, fragModule] + + -- One descriptor set: binding 0 -> Scene UBO, binding 1 -> Mesh SSBO. + (_, descriptorPool) <- + Vk.withDescriptorPool + dev + zero + { Vk.maxSets = 1 + , Vk.poolSizes = + [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER 1 + , Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER 1 + ] + } + Nothing + allocate + descriptorSets <- + Vk.allocateDescriptorSets + dev + zero + { Vk.descriptorPool = descriptorPool + , Vk.setLayouts = V.fromList (map snd layout.setLayouts) + } + let descriptorSet = V.head descriptorSets + Vk.updateDescriptorSets + dev + [ bufferWrite descriptorSet 0 Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER sceneBuffer + , bufferWrite descriptorSet 1 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER meshBuffer + ] + [] + + -- Two pipelines from the same vertex shader and the same layout: depth-only (no + -- colour attachment) and depth+colour. Vertex input is empty — geometry comes + -- from the SSBO via gl_VertexIndex. + (_, depthOnlyPipeline) <- + allocateGraphicsPipeline + dev + layout + zero{Dynamic.depthFormat = Just depthFormat} + () + [(vertModule, vertSpv)] + (_, colorPipeline) <- + allocateGraphicsPipeline + dev + layout + zero + { Dynamic.colorFormats = [colorFormat] + , Dynamic.depthFormat = Just depthFormat + } + () + [(vertModule, vertSpv), (fragModule, fragSpv)] + + (_, commandPool) <- + Vk.withCommandPool dev zero{CommandPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex} Nothing allocate + graphicsQueue <- Vk.getDeviceQueue dev graphicsQueueFamilyIndex 0 + + -- Depth buffer readback target (host-visible buffer of width*height floats). + (_, (depthBuffer, depthAllocation, depthBufInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (fromIntegral width * fromIntegral height * floatSize) + , Vk.usage = Vk.BUFFER_USAGE_TRANSFER_DST_BIT + } + readbackAlloc + allocate + + let + oneShot = zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} + -- Apply only the dynamic states the bound pipeline declares (the depth-only + -- pipeline has no colour-blend state). + bindCommon dynStates cb pipeline = do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_GRAPHICS pipeline + applyDynamicStates + dynStates + cb + (dynamicStateFor extent){depthTest = True, depthWrite = True, depthCompareOp = Vk.COMPARE_OP_LESS} + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_GRAPHICS layout.pipelineLayout 0 [descriptorSet] [] + Vk.cmdDraw cb vertexCount 1 0 0 + + -- Pass 1: depth-only z-prepass, then copy the depth image to the host buffer. + cb1 <- oneCommandBuffer dev commandPool + Vk.useCommandBuffer cb1 oneShot do + transitionDepthAttachment cb1 depthImage + Vk.cmdUseRendering cb1 (Dynamic.renderingInfo (fullScissor extent) [] (Just (depthView, 1))) $ + bindCommon depthOnlyDynamicStates cb1 depthOnlyPipeline + depthToTransferSrc cb1 depthImage + Vk.cmdCopyImageToBuffer cb1 depthImage Vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL depthBuffer [depthCopyRegion extent] + submitAndWait dev graphicsQueue cb1 "Timed out in the depth-only prepass" + VMA.invalidateAllocation allocator depthAllocation 0 Vk.WHOLE_SIZE + let depthPtr = castPtr (VMA.mappedData depthBufInfo) + depthCentre <- liftIO $ peekElemOff depthPtr (fromIntegral (height `div` 2) * fromIntegral width + fromIntegral (width `div` 2)) + depthCorner <- liftIO $ peekElemOff depthPtr 0 + + -- Pass 2: depth+colour, then copy the colour image to the host image. + cb2 <- oneCommandBuffer dev commandPool + Vk.useCommandBuffer cb2 oneShot do + transitionColorAttachment cb2 image + transitionDepthAttachment cb2 depthImage + Vk.cmdUseRendering + cb2 + (Dynamic.renderingInfo (fullScissor extent) [(imageView, Vk.Float32 0.05 0.05 0.05 1)] (Just (depthView, 1))) + $ bindCommon allDynamicStates cb2 colorPipeline + copyImageToHost cb2 extent image cpuImage + submitAndWait dev graphicsQueue cb2 "Timed out in the colour pass" + colorImage <- readback + + pure (depthCentre, depthCorner, colorImage) + +mappedAlloc :: VMA.AllocationCreateInfo +mappedAlloc = + zero + { AllocationCreateInfo.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , AllocationCreateInfo.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + , AllocationCreateInfo.requiredFlags = Vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT + } + +readbackAlloc :: VMA.AllocationCreateInfo +readbackAlloc = + zero + { AllocationCreateInfo.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , AllocationCreateInfo.usage = VMA.MEMORY_USAGE_GPU_TO_CPU + } + +oneCommandBuffer :: Vk.Device -> Vk.CommandPool -> ResourceT IO Vk.CommandBuffer +oneCommandBuffer dev pool = do + (_, cbs) <- + Vk.withCommandBuffers + dev + zero + { Vk.commandPool = pool + , Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY + , Vk.commandBufferCount = 1 + } + allocate + pure (V.head cbs) + +depthCopyRegion :: Vk.Extent2D -> Vk.BufferImageCopy +depthCopyRegion (Vk.Extent2D w h) = + Vk.BufferImageCopy + { Vk.bufferOffset = 0 + , Vk.bufferRowLength = 0 + , Vk.bufferImageHeight = 0 + , Vk.imageSubresource = Vk.ImageSubresourceLayers Vk.IMAGE_ASPECT_DEPTH_BIT 0 0 1 + , Vk.imageOffset = Vk.Offset3D 0 0 0 + , Vk.imageExtent = Vk.Extent3D w h 1 + } + +{- | A GPU-only depth attachment that can also be a transfer source (so the +prepass depth can be copied back). Like 'RenderTarget.createDepthTarget' but +with the extra usage flag. +-} +createReadableDepthTarget + :: VMA.Allocator -> Vk.Device -> Vk.Format -> Vk.Extent2D -> ResourceT IO (Vk.Image, Vk.ImageView) +createReadableDepthTarget allocator dev format (Vk.Extent2D w h) = do + (_, (image, _, _)) <- VMA.withImage allocator imageCreateInfo gpuAlloc allocate + (_, view) <- Vk.withImageView dev (viewCreateInfo image) Nothing allocate + pure (image, view) + where + gpuAlloc = zero{AllocationCreateInfo.usage = VMA.MEMORY_USAGE_GPU_ONLY} + imageCreateInfo = + zero + { Vk.imageType = Vk.IMAGE_TYPE_2D + , Vk.format = format + , Vk.extent = Vk.Extent3D w h 1 + , Vk.mipLevels = 1 + , Vk.arrayLayers = 1 + , Vk.samples = Vk.SAMPLE_COUNT_1_BIT + , Vk.tiling = Vk.IMAGE_TILING_OPTIMAL + , Vk.usage = Vk.IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT .|. Vk.IMAGE_USAGE_TRANSFER_SRC_BIT + , Vk.initialLayout = Vk.IMAGE_LAYOUT_UNDEFINED + } + viewCreateInfo image = + zero + { Vk.image = image + , Vk.viewType = Vk.IMAGE_VIEW_TYPE_2D + , Vk.format = format + , Vk.subresourceRange = Vk.ImageSubresourceRange Vk.IMAGE_ASPECT_DEPTH_BIT 0 1 0 1 + } + +{- | Barrier the depth image from a depth attachment (after the prepass) to a +transfer source, so it can be copied out. +-} +depthToTransferSrc :: Vk.CommandBuffer -> Vk.Image -> ResourceT IO () +depthToTransferSrc cb img = + Vk.cmdPipelineBarrier + cb + Vk.PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT + Vk.PIPELINE_STAGE_TRANSFER_BIT + zero + [] + [] + [ imageBarrier + Vk.IMAGE_ASPECT_DEPTH_BIT + Vk.ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + Vk.ACCESS_TRANSFER_READ_BIT + Vk.IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL + Vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + img + ] diff --git a/examples/mesh-reflect/build-shaders.sh b/examples/mesh-reflect/build-shaders.sh new file mode 100755 index 000000000..953f2ebfc --- /dev/null +++ b/examples/mesh-reflect/build-shaders.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Compile this example's shaders to committed SPIR-V (input to reflection + runtime). +set -eu +cd "$(dirname "$0")" +for src in *.vert *.frag; do + [ -e "$src" ] || continue + glslangValidator -V "$src" -o "$src.spv" + echo "wrote $src.spv" +done diff --git a/examples/mesh-reflect/shader.frag b/examples/mesh-reflect/shader.frag new file mode 100644 index 000000000..6f8366113 --- /dev/null +++ b/examples/mesh-reflect/shader.frag @@ -0,0 +1,24 @@ +#version 450 + +// Fragment stage of the depth+color pipeline. Its inputs (normal @loc 0, color +// @loc 1) must match the vertex stage's outputs, and it shares the Scene UBO +// (using the light fields the vertex stage ignores) — both checked at compile +// time by MatchInterface / CompatibleResources. Shades the surface with a simple +// Lambert (N·L) term plus ambient. + +layout(location = 0) in vec3 inNormal; +layout(location = 1) in vec3 inColor; + +layout(set = 0, binding = 0, std140) uniform Scene { + mat4 transform; + vec4 lightDir; + vec4 lightColor; +} scene; + +layout(location = 0) out vec4 outColor; + +void main() { + float lambert = max(dot(normalize(inNormal), normalize(scene.lightDir.xyz)), 0.0); + float shade = 0.15 + 0.85 * lambert; + outColor = vec4(inColor * scene.lightColor.rgb * shade, 1.0); +} diff --git a/examples/mesh-reflect/shader.vert b/examples/mesh-reflect/shader.vert new file mode 100644 index 000000000..b72fe290f --- /dev/null +++ b/examples/mesh-reflect/shader.vert @@ -0,0 +1,33 @@ +#version 450 + +// Vertex stage shared by both pipelines (depth-only z-prepass and depth+color). +// The geometry is pulled from the `Mesh` SSBO (set 0, binding 1) indexed by +// gl_VertexIndex — no vertex buffer. The Camera/Scene UBO (set 0, binding 0) is +// shared with the fragment stage: the vertex stage uses `transform`, the fragment +// stage uses the light fields. + +struct Vertex { + vec3 position; + vec3 normal; + vec3 color; +}; + +layout(set = 0, binding = 1, std430) readonly buffer Mesh { + Vertex verts[]; +}; + +layout(set = 0, binding = 0, std140) uniform Scene { + mat4 transform; + vec4 lightDir; + vec4 lightColor; +} scene; + +layout(location = 0) out vec3 outNormal; +layout(location = 1) out vec3 outColor; + +void main() { + Vertex v = verts[gl_VertexIndex]; + gl_Position = scene.transform * vec4(v.position, 1.0); + outNormal = v.normal; + outColor = v.color; +} diff --git a/examples/package.yaml b/examples/package.yaml index 00a1d820b..a7b4899ec 100644 --- a/examples/package.yaml +++ b/examples/package.yaml @@ -131,6 +131,87 @@ executables: - vulkan - vulkan-utils + compute-reflect: + main: Main.hs + source-dirs: compute-reflect + dependencies: + - JuicyPixels + - VulkanMemoryAllocator + - vulkan-examples + - base <5 + - bytestring + - file-embed + - geomancy + - gl-block + - resourcet + - safe-exceptions + - say + - text + - transformers + - vector + - vulkan + - vulkan-utils + - vulkan-utils-spirv + + pathtrace-reflect: + main: Main.hs + source-dirs: pathtrace-reflect + dependencies: + - JuicyPixels + - VulkanMemoryAllocator + - vulkan-examples + - base <5 + - bytestring + - file-embed + - geomancy + - gl-block + - optparse-applicative + - random + - resourcet + - transformers + - vector + - vulkan + - vulkan-utils + - vulkan-utils-spirv + + mesh-reflect: + main: Main.hs + source-dirs: mesh-reflect + dependencies: + - JuicyPixels + - VulkanMemoryAllocator + - vulkan-examples + - base <5 + - bytestring + - file-embed + - geomancy + - gl-block + - resourcet + - say + - spirv-reflect-ffi + - vector + - vulkan + - vulkan-utils + - vulkan-utils-spirv + + texture-reflect: + main: Main.hs + source-dirs: texture-reflect + dependencies: + - JuicyPixels + - VulkanMemoryAllocator + - vulkan-examples + - base <5 + - bytestring + - file-embed + - gl-block + - resourcet + - spirv-reflect-ffi + - vector + - vulkan + - vulkan-utils + - vulkan-utils-spirv + resize: main: Main.hs source-dirs: resize diff --git a/examples/pathtrace-reflect.png b/examples/pathtrace-reflect.png new file mode 100644 index 0000000000000000000000000000000000000000..cec6141e89305dc559d2d778adc2614cd556090a GIT binary patch literal 296902 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%0A*DBesIKu(Z)C(-W^KT#{y~LQT^|L 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≤v{~loYrwTue&*SvJDu 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 literal 0 HcmV?d00001 diff --git a/examples/pathtrace-reflect/Main.hs b/examples/pathtrace-reflect/Main.hs new file mode 100644 index 000000000..6cdf65a47 --- /dev/null +++ b/examples/pathtrace-reflect/Main.hs @@ -0,0 +1,591 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +{-| A single-frame, procedural compute path tracer whose /entire/ shader +interface is derived from the compiled SPIR-V at build time by +@vulkan-utils-spirv@ — exercising every reflected resource kind at once: + + * the @Camera@ uniform block -> a generated record (std140); + * the @Scene@ input storage buffer -> a descriptor binding holding the leaf + sphere geometry; its element record 'Sphere' is generated from the SSBO's + array element type; + * the @Image@ output storage buffer -> a descriptor binding; + * the @BvhNode@ @buffer_reference@ type -> a generated record whose children are + @DeviceAddress BvhNode@; the host builds a BVH, links the nodes by device + address, and the shader hops those addresses to intersect the scene; + * the @Frame@ push-constant block -> a generated record + range, carrying the + root node's device address; and + * the @SAMPLES@ / @MAX_BOUNCES@ specialization constants -> the pipeline's + 'Vk.SpecializationInfo'. + +The scene (a "Ray Tracing in One Weekend" arrangement) is generated on the host +from a command-line seed: the spheres go into the @Scene@ buffer and a BVH over +them into a separate device-address-linked node buffer. +-} +module Main + ( main + ) +where + +import qualified Codec.Picture as JP +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT) +import Control.Monad.Trans.State (State, evalState, runState, state) +import Data.Bits ((.|.)) +import Data.ByteString (ByteString) +import Data.FileEmbed (embedFile) +import Data.Foldable (for_) +import Data.List (sortOn) +import Data.Maybe (catMaybes) +import Data.Proxy (Proxy (..)) +import qualified Data.Vector as V +import qualified Data.Vector.Storable as VS +import Data.Word (Word32, Word64) +import Foreign.Marshal.Array (peekArray) +import Foreign.Marshal.Utils (with) +import Foreign.Ptr (Ptr, castPtr, plusPtr) +import Foreign.Storable (poke, sizeOf) +import qualified Geomancy +import Geomancy.UVec2 (uvec2) +import Geomancy.Vec3 (Vec3, emap2, vec3, pattern WithVec3) +import qualified Geomancy.Vec3 as V3 +import Geomancy.Vec4 (Vec4, fromVec3, vec4, pattern WithVec4) +import Graphics.Gl.Block (Std140 (..), Std430 (..)) +import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), submitAndWaitFor, withHeadlessVk) +import ImageReadback (captureImageRGBA8, savePng) +import Options.Applicative +import System.Random (StdGen, mkStdGen, uniformR) +import Vulkan.CStruct.Extends (SomeStruct (..)) +import Vulkan.CStruct.Utils (FixedArray, lowerArrayPtr) +import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) +import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) +import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import Vulkan.Core12.Promoted_From_VK_KHR_buffer_device_address (BufferDeviceAddressInfo (..), PhysicalDeviceBufferDeviceAddressFeatures (..), getBufferDeviceAddress) +import Vulkan.Requirement (DeviceRequirement) +import Vulkan.Utils.Descriptors (bufferWrite) +import Vulkan.Utils.QueueAssignment (QueueFamilyIndex (..)) +import Vulkan.Utils.Queues (Queues (..)) +import Vulkan.Utils.Requirements.TH (reqs) +import Vulkan.Utils.Shader (shaderModuleStage) +import qualified Vulkan.Utils.SpirV.Array as Array +import Vulkan.Utils.SpirV.Descriptors (pushConstantRanges, singleDescriptorSetLayoutInfo) +import Vulkan.Utils.SpirV.DeviceAddress (DeviceAddress (..)) +import Vulkan.Utils.SpirV.Reflect (reflectBytes) +import Vulkan.Utils.SpirV.Specialization (allocateSpecializationInfo) +import Vulkan.Utils.SpirV.TH (reflectShaderTypes) +import Vulkan.Zero (zero) +import qualified VulkanMemoryAllocator as VMA + +-- | The compiled shader, embedded for runtime module creation. +compCode :: ByteString +compCode = $(embedFile "pathtrace-reflect/shader.comp.spv") + +-- Generate the records from the same SPIR-V the runtime loads: +-- * @Camera@ (std140 UBO); +-- * @Frame@ (std430 push constant); and +-- * @Sphere@ (std430), the element type of the @Scene@ SSBO's @Sphere[]@ — +-- generated even though the runtime-array @Scene@/@Image@ blocks themselves +-- aren't (they're @[Sphere]@ / raw @vec4@ texels on the host). +reflectShaderTypes "pathtrace-reflect/shader.comp.spv" + +main :: IO () +main = do + opts <- execParser optionsInfo + runResourceT $ do + HeadlessVk{..} <- + withHeadlessVk + HeadlessConfig + { appName = "Haskell Vulkan pathtrace-reflect example" + , instanceReqs = [] + , deviceReqs = bdaDeviceReqs + , -- the BVH node buffer is reached by device address + vmaFlags = VMA.ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT + } + let QueueFamilyIndex computeQueueFamilyIndex = fst (qCompute queues) + + image <- render allocator device computeQueueFamilyIndex opts + Vk.deviceWaitIdle device + liftIO $ savePng opts.output image + liftIO $ putStrLn ("Wrote " <> opts.output) + +{- | Enable buffer device addresses so the BVH nodes can be linked on the host +and traversed by 'DeviceAddress' on the GPU. +-} +bdaDeviceReqs :: [DeviceRequirement] +bdaDeviceReqs = + [reqs| PhysicalDeviceBufferDeviceAddressFeatures.bufferDeviceAddress |] + +render + :: VMA.Allocator + -> Vk.Device + -> Word32 + -> Options + -> ResourceT IO (JP.Image JP.PixelRGBA8) +render allocator dev computeQueueFamilyIndex opts = do + let + width = opts.width + height = opts.height + workgroup = 16 :: Int + + -- The runtime-sized Scene SSBO is a std430 array of the reflected 'Sphere'; + -- the BVH bounds each sphere from its reflected centerRadius (a 'Vec4'). + spheres :: VS.Vector Sphere + spheres = VS.fromList (buildScene opts) + sphereCount = VS.length spheres + + -- One BVH leaf per sphere, flattened to an array with the root at index 0. + bvhFlats = flattenBvh (buildBvh (zip [0 ..] (map sphereAabb (VS.toList spheres)))) + + aspect = fromIntegral width / fromIntegral height + camera = buildCamera aspect opts.fov (vec3 13 2 3) (vec3 0 0 0) (vec3 0 1 0) + + -- Reflect the embedded module once; reuse it for the descriptor set layout, + -- push-constant range and specialization info. + reflected <- reflectBytes compCode + + -- Output storage buffer: one RGBA32F texel per pixel, mapped GPU_TO_CPU. + (_, (outBuffer, outAllocation, outInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral $ width * height * 4 * sizeOf (0 :: Float) + , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_GPU_TO_CPU + } + allocate + + -- Input storage buffer: the scene's spheres, written from the host. + (_, (sceneBuffer, _sceneAllocation, sceneInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (sphereCount * Array.std430Stride (Proxy @Sphere)) + , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + } + allocate + liftIO $ Array.pokeStd430 (VMA.mappedData sceneInfo) spheres + + -- Uniform buffer holding the reflected 'Camera', written from the host. + (_, (camBuffer, _camAllocation, camInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (sizeOf camera) + , Vk.usage = Vk.BUFFER_USAGE_UNIFORM_BUFFER_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + } + allocate + liftIO $ poke (castPtr (VMA.mappedData camInfo)) camera + + -- BVH node buffer, reached purely by device address (not a descriptor). + -- Allocate it, learn its base address, then write each node with its + -- children linked by address — the generated 'BvhNode' record's + -- @DeviceAddress BvhNode@ fields carry the pointers the shader hops. + let + nodeStride = Array.std430Stride (Proxy @BvhNode) + nodeCount = length bvhFlats + (_, (nodeBuffer, _nodeAllocation, nodeInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (nodeCount * nodeStride) + , Vk.usage = + Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + .|. Vk.BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + } + allocate + bvhBase <- getBufferDeviceAddress dev zero{buffer = nodeBuffer} + liftIO $ + Array.pokeStd430 + (VMA.mappedData nodeInfo) + (VS.fromList (map (toBvhNode bvhBase nodeStride) bvhFlats)) + + -- The reflected 'Frame' push constant carries the root node's address + -- (the flattened BVH puts the root at index 0, i.e. the buffer base). + let frame = + Frame + { root = DeviceAddress bvhBase + , resolution = uvec2 (fromIntegral width) (fromIntegral height) + , seed = opts.seed + , rowOffset = 0 -- set per band below + } + + -- Descriptor set layout generated from the reflected module (UBO + 2 SSBOs). + setLayoutInfo <- either fail pure (singleDescriptorSetLayoutInfo reflected) + (_, descriptorSetLayout) <- Vk.withDescriptorSetLayout dev setLayoutInfo Nothing allocate + + (_, descriptorPool) <- + Vk.withDescriptorPool + dev + zero + { Vk.maxSets = 1 + , Vk.poolSizes = + [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER 1 + , Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER 2 + ] + } + Nothing + allocate + [descriptorSet] <- + Vk.allocateDescriptorSets + dev + zero + { Vk.descriptorPool = descriptorPool + , Vk.setLayouts = [descriptorSetLayout] + } + + Vk.updateDescriptorSets + dev + [ bufferWrite descriptorSet 0 Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER camBuffer + , bufferWrite descriptorSet 1 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER sceneBuffer + , bufferWrite descriptorSet 2 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER outBuffer + ] + [] + + -- Pipeline. The specialization info (SAMPLES, MAX_BOUNCES) is built from the + -- reflected constant ids and lives for this resource scope. + mSpec <- allocateSpecializationInfo reflected (opts.samples, opts.bounces) + (_, shader) <- shaderModuleStage dev Vk.SHADER_STAGE_COMPUTE_BIT mSpec compCode + (_, pipelineLayout) <- + Vk.withPipelineLayout + dev + zero + { PipelineLayoutCreateInfo.setLayouts = [descriptorSetLayout] + , PipelineLayoutCreateInfo.pushConstantRanges = + V.fromList (pushConstantRanges reflected) + } + Nothing + allocate + let + pipelineCreateInfo :: Vk.ComputePipelineCreateInfo '[] + pipelineCreateInfo = + zero + { Vk.layout = pipelineLayout + , Vk.stage = shader + , Vk.basePipelineHandle = zero + } + (_, (_, [computePipeline])) <- + Vk.withComputePipelines dev zero [SomeStruct pipelineCreateInfo] Nothing allocate + + -- Command buffer, reset and re-recorded per band (hence the pool's + -- RESET_COMMAND_BUFFER flag). + (_, commandPool) <- + Vk.withCommandPool + dev + zero + { CommandPoolCreateInfo.queueFamilyIndex = computeQueueFamilyIndex + , CommandPoolCreateInfo.flags = Vk.COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT + } + Nothing + allocate + (_, [cb]) <- + Vk.withCommandBuffers + dev + zero + { Vk.commandPool = commandPool + , Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY + , Vk.commandBufferCount = 1 + } + allocate + computeQueue <- Vk.getDeviceQueue dev computeQueueFamilyIndex 0 + let timeoutNanos = round (opts.timeout * 1e9) + + -- Split the dispatch into horizontal bands so no single compute submission runs + -- long enough to trip the GPU's hang-recovery watchdog — which resets the queue + -- and drops the unfinished (bottom) workgroups, corrupting that region. Each + -- band is an independent short submission; a low-cost render stays one dispatch. + let + samples = max 1 (fromIntegral opts.samples) :: Int + bounces = max 1 (fromIntegral opts.bounces) :: Int + -- A conservative per-submission budget in ray-bounces (well under what tripped + -- the watchdog here, with margin for heavier scenes). + budget = 6 * 1000 * 1000 * 1000 :: Int + bandRows = max 1 (min height (budget `div` (width * samples * bounces))) + for_ ([0, bandRows .. height - 1] :: [Int]) $ \row0 -> do + let + rowsThis = min bandRows (height - row0) + bandFrame = frame{rowOffset = fromIntegral row0} + Vk.resetCommandBuffer cb zero + Vk.useCommandBuffer cb zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_COMPUTE computePipeline + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_COMPUTE pipelineLayout 0 [descriptorSet] [] + -- Push the reflected 'Frame' (std430) with this band's row offset. + liftIO $ with bandFrame $ \pFrame -> + Vk.cmdPushConstants + cb + pipelineLayout + Vk.SHADER_STAGE_COMPUTE_BIT + 0 + (fromIntegral (sizeOf bandFrame)) + (castPtr pFrame) + Vk.cmdDispatch + cb + (ceiling (realToFrac width / realToFrac @_ @Float workgroup)) + (ceiling (realToFrac rowsThis / realToFrac @_ @Float workgroup)) + 1 + submitAndWaitFor timeoutNanos dev computeQueue cb $ + "Timed out waiting for compute band at row " + <> show row0 + <> " after " + <> show opts.timeout + <> "s (raise --timeout)" + + let + pixelAddr :: Int -> Int -> Ptr (FixedArray 4 Float) + pixelAddr x y = + plusPtr + (VMA.mappedData outInfo) + (((y * width) + x) * 4 * sizeOf (0 :: Float)) + captureImageRGBA8 allocator outAllocation width height $ \x y -> do + let ptr = pixelAddr x y + [r, g, b, a] <- fmap (\f -> round (min 1 f * 255)) <$> peekArray 4 (lowerArrayPtr ptr) + pure $ JP.PixelRGBA8 r g b a + +-------------------------------------------------------------------------------- +-- Scene +-------------------------------------------------------------------------------- + +{- | Build the reflected 'Camera' for a simple look-at camera with unit focal +length (no defocus blur). +-} +buildCamera :: Float -> Float -> Vec3 -> Vec3 -> Vec3 -> Camera +buildCamera aspect vfovDeg lookFrom lookAt vup = + Camera + { origin = lookFrom + , lowerLeft = lowerLeft + , horizontal = horizontal + , vertical = vertical + , skyTop = vec3 0.5 0.7 1.0 + , skyBottom = vec3 1.0 1.0 1.0 + } + where + theta = vfovDeg * pi / 180 + viewportH = 2 * tan (theta / 2) + viewportW = aspect * viewportH + w = V3.normalize (lookFrom - lookAt) + u = V3.normalize (V3.cross vup w) + v = V3.cross w u + horizontal = u V3.^* viewportW + vertical = v V3.^* viewportH + lowerLeft = lookFrom - (horizontal V3.^* 0.5) - (vertical V3.^* 0.5) - w + +{- | The full scene: ground, three feature spheres, and @--spheres@ random small +ones laid out on a grid (seeded by @--seed@). +-} +buildScene :: Options -> [Sphere] +buildScene opts = ground : features ++ small + where + ground = lambertian (vec3 0 (-1000) 0) 1000 (vec3 0.5 0.5 0.5) + features = + [ dielectric (vec3 0 1 0) 1 1.5 + , lambertian (vec3 (-4) 1 0) 1 (vec3 0.4 0.2 0.1) + , metal (vec3 4 1 0) 1 (vec3 0.7 0.6 0.5) 0 + ] + small = evalState (smallSpheres opts.spheres) (mkStdGen (fromIntegral opts.seed)) + +{- | Up to @n@ small spheres scattered over a grid, skipping any too close to the +feature spheres. Materials are randomly diffuse\/metal\/glass. +-} +smallSpheres :: Int -> State StdGen [Sphere] +smallSpheres n = catMaybes <$> mapM gen (take n grid) + where + -- A square field of unit cells centred on the origin, sized to hold @n@ + -- spheres so they expand toward the horizon as the count grows. (@n = 484@ + -- reproduces the classic @[-11 .. 10]^2@ "Ray Tracing in One Weekend" grid.) + side = ceiling (sqrt (fromIntegral (max 1 n) :: Double)) :: Int + lo = negate (side `div` 2) + grid = [(a, b) | a <- [lo .. lo + side - 1], b <- [lo .. lo + side - 1]] + gen (a, b) = do + rx <- rF (0, 0.9) + rz <- rF (0, 0.9) + let + center = vec3 (fromIntegral a + rx) 0.2 (fromIntegral b + rz) + d = center - vec3 4 0.2 0 + -- Skip spheres too close to the metal feature sphere (compare squared). + if V3.dot d d <= 0.9 * 0.9 + then pure Nothing + else do + choose <- rF (0, 1) + fmap Just $ + if choose < (0.8 :: Float) + then do + c1 <- rF (0, 1) + c2 <- rF (0, 1) + c3 <- rF (0, 1) + pure $ lambertian center 0.2 (vec3 (c1 * c1) (c2 * c2) (c3 * c3)) + else + if choose < 0.95 + then do + c1 <- rF (0.5, 1) + c2 <- rF (0.5, 1) + c3 <- rF (0.5, 1) + fz <- rF (0, 0.5) + pure $ metal center 0.2 (vec3 c1 c2 c3) fz + else pure $ dielectric center 0.2 1.5 + rF range = state (uniformR range) + +-- Sphere constructors (material encoding matches the shader). The center is a +-- 'Vec3' and the albedo a 'Vec3'; @centerRadius@ packs the radius into w. + +mkSphere :: Vec3 -> Float -> Vec4 -> Vec4 -> Sphere +mkSphere center r = Sphere (fromVec3 center r) + +lambertian :: Vec3 -> Float -> Vec3 -> Sphere +lambertian c r albedo = mkSphere c r (fromVec3 albedo 1) (vec4 0 0 0 0) + +metal :: Vec3 -> Float -> Vec3 -> Float -> Sphere +metal c r albedo fuzz = mkSphere c r (fromVec3 albedo 1) (vec4 1 fuzz 0 0) + +dielectric :: Vec3 -> Float -> Float -> Sphere +dielectric c r ior = mkSphere c r (vec4 1 1 1 1) (vec4 2 0 ior 0) + +-------------------------------------------------------------------------------- +-- BVH (built on the host; linked and traversed on the GPU by device address) +-------------------------------------------------------------------------------- + +-- | An axis-aligned bounding box (min, max corners). +data Aabb = Aabb !Vec3 !Vec3 + +-- | Bound a sphere from its reflected @centerRadius@ (xyz centre, w radius). +sphereAabb :: Sphere -> Aabb +sphereAabb sphere = case centerRadius sphere of + WithVec4 cx cy cz r -> + Aabb (vec3 (cx - r) (cy - r) (cz - r)) (vec3 (cx + r) (cy + r) (cz + r)) + +aabbUnion :: Aabb -> Aabb -> Aabb +aabbUnion (Aabb amin amax) (Aabb bmin bmax) = + Aabb (emap2 min amin bmin) (emap2 max amax bmax) + +-- | A binary BVH over sphere indices; each leaf bounds one sphere. +data Bvh = BvhLeaf Int Aabb | BvhSplit Aabb Bvh Bvh + +-- | Build a BVH by recursively median-splitting the longest axis. +buildBvh :: [(Int, Aabb)] -> Bvh +buildBvh [] = error "buildBvh: empty scene" +buildBvh [(i, bb)] = BvhLeaf i bb +buildBvh xs = BvhSplit bb (buildBvh l) (buildBvh r) + where + bb = foldr1 aabbUnion (map snd xs) + Aabb lo hi = bb + axis = longestAxis (hi - lo) + (l, r) = splitAt (length xs `div` 2) (sortOn (axisKey axis . snd) xs) + +longestAxis :: Vec3 -> Int +longestAxis (WithVec3 x y z) + | x >= y && x >= z = 0 + | y >= z = 1 + | otherwise = 2 + +-- | Twice the box centre on an axis (the constant factor is irrelevant to sorting). +axisKey :: Int -> Aabb -> Float +axisKey axis (Aabb (WithVec3 mnx mny mnz) (WithVec3 mxx mxy mxz)) = case axis of + 0 -> mnx + mxx + 1 -> mny + mxy + _ -> mnz + mxz + +{- | A flattened node: bounds, child array indices (-1 if none), and the leaf's +sphere index (-1 for an internal node). +-} +data BvhFlat = BvhFlat Vec3 Vec3 Int Int Int + +{- | Flatten the tree to an array in pre-order so the root lands at index 0; +children are referenced by array index, later resolved to device addresses. +-} +flattenBvh :: Bvh -> [BvhFlat] +flattenBvh tree = map snd (sortOn fst nodes) + where + (_, (_, nodes)) = runState (go tree) (0 :: Int, []) + fresh = state (\(n, acc) -> (n, (n + 1, acc))) + emit i f = state (\(n, acc) -> ((), (n, (i, f) : acc))) + go (BvhLeaf i (Aabb mn mx)) = do + idx <- fresh + emit idx (BvhFlat mn mx (-1) (-1) i) + pure idx + go (BvhSplit (Aabb mn mx) l r) = do + idx <- fresh + li <- go l + ri <- go r + emit idx (BvhFlat mn mx li ri (-1)) + pure idx + +{- | Realise a flattened node as the reflected 'BvhNode' record, resolving child +indices to device addresses within the node buffer (base + index * stride). +-} +toBvhNode :: Word64 -> Int -> BvhFlat -> BvhNode +toBvhNode base stride (BvhFlat mn mx li ri si) = + BvhNode + { boundsMin = fromVec3 mn 0 + , boundsMax = fromVec3 mx 0 + , left = childAddr li + , right = childAddr ri + , sphereIndex = fromIntegral si + } + where + childAddr i + | i < 0 = DeviceAddress 0 + | otherwise = DeviceAddress (base + fromIntegral (i * stride)) + +-------------------------------------------------------------------------------- +-- Command line +-------------------------------------------------------------------------------- + +data Options = Options + { width :: Int + , height :: Int + , samples :: Word32 + , bounces :: Word32 + , spheres :: Int + , seed :: Word32 + , fov :: Float + , timeout :: Double + -- ^ GPU wait budget, seconds + , output :: FilePath + } + +optionsInfo :: ParserInfo Options +optionsInfo = + info + (optionsParser <**> helper) + ( fullDesc + <> header "pathtrace-reflect - reflection-driven compute path tracer" + <> progDesc + "Render one frame of a procedural sphere scene with a compute path \ + \tracer whose entire interface is derived from SPIR-V reflection." + ) + +optionsParser :: Parser Options +optionsParser = + Options + <$> option auto (long "width" <> metavar "N" <> value 600 <> showDefault <> help "Image width") + <*> option auto (long "height" <> metavar "N" <> value 400 <> showDefault <> help "Image height") + <*> option auto (long "samples" <> metavar "N" <> value 32 <> showDefault <> help "Samples per pixel (spec constant)") + <*> option auto (long "bounces" <> metavar "N" <> value 12 <> showDefault <> help "Max ray bounces (spec constant)") + <*> option auto (long "spheres" <> metavar "N" <> value 64 <> showDefault <> help "Random spheres in the scene") + <*> option auto (long "seed" <> metavar "N" <> value 1 <> showDefault <> help "Scene and sampling seed") + <*> option auto (long "fov" <> metavar "DEG" <> value 20 <> showDefault <> help "Vertical field of view") + <*> option auto (long "timeout" <> metavar "SEC" <> value 60 <> showDefault <> help "GPU wait budget in seconds") + <*> strOption (long "output" <> metavar "FILE" <> value "pathtrace-reflect.png" <> showDefault <> help "Output PNG path") diff --git a/examples/pathtrace-reflect/build-shaders.sh b/examples/pathtrace-reflect/build-shaders.sh new file mode 100644 index 000000000..cbbfe5788 --- /dev/null +++ b/examples/pathtrace-reflect/build-shaders.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Compile the GLSL shader to SPIR-V with a local glslang. The resulting .spv is +# committed and consumed both at runtime (Data.FileEmbed.embedFile) and at +# compile time (Vulkan.Utils.SpirV reflection). +set -eu +cd "$(dirname "$0")" +# buffer_reference (BDA) needs SPIR-V 1.3+, i.e. a Vulkan 1.2 target. +glslangValidator -V --target-env vulkan1.2 shader.comp -o shader.comp.spv +echo "wrote shader.comp.spv" diff --git a/examples/pathtrace-reflect/shader.comp b/examples/pathtrace-reflect/shader.comp new file mode 100644 index 000000000..a60adb90a --- /dev/null +++ b/examples/pathtrace-reflect/shader.comp @@ -0,0 +1,261 @@ +#version 460 +#extension GL_EXT_buffer_reference : require + +// A single-frame procedural compute path tracer whose *entire* shader interface +// is reflected at build time by vulkan-utils-spirv. Scene intersection walks a +// BVH whose nodes are linked by 64-bit buffer_reference device addresses (BDA): +// +// * Camera UBO (std140) -> generated Haskell record (Geomancy.Vec3); +// * Scene input SSBO (binding 1, readonly) -> leaf sphere geometry, indexed +// by a BVH leaf's `sphereIndex`. Its element `Sphere` is hand-written; +// * Image output SSBO (binding 2, writeonly) -> descriptor set layout; +// * BvhNode (buffer_reference) -> generated record with DeviceAddress children; +// * Frame push constants (std430) carry the root BvhNode device address; +// * SAMPLES / MAX_BOUNCES spec constants -> the pipeline's SpecializationInfo. +// +// NOTE: buffer_reference needs SPIR-V 1.3+, so build-shaders.sh compiles this +// with --target-env vulkan1.2. + +layout(local_size_x = 16, local_size_y = 16) in; + +// Compile-time tunables, specialized at pipeline creation. +layout(constant_id = 0) const uint SAMPLES = 16u; +layout(constant_id = 1) const uint MAX_BOUNCES = 8u; + +// All members are vec3, so they share std140 base alignment 16 (the gl-block +// layout guardrail wants non-increasing alignments). +layout(set = 0, binding = 0, std140) uniform Camera { + vec3 origin; + vec3 lowerLeft; + vec3 horizontal; + vec3 vertical; + vec3 skyTop; + vec3 skyBottom; +} cam; + +struct Sphere { + vec4 centerRadius; // xyz: center, w: radius + vec4 albedo; // rgb: albedo + vec4 material; // x: type (0 lambertian, 1 metal, 2 dielectric), y: fuzz, z: ior +}; + +layout(set = 0, binding = 1, std430) readonly buffer Scene { + Sphere spheres[]; +}; + +layout(set = 0, binding = 2, std430) writeonly buffer Image { + vec4 pixels[]; +}; + +// A BVH node reached by device address. Internal nodes have child addresses and +// sphereIndex < 0; leaves have sphereIndex >= 0 (into the Scene buffer) and null +// children. Fields are ordered by non-increasing alignment (16,16,8,8,4). +layout(buffer_reference) buffer BvhNode; // fwd decl enables self-reference +layout(buffer_reference, std430) buffer BvhNode { + vec4 boundsMin; // xyz AABB min + vec4 boundsMax; // xyz AABB max + BvhNode left; // child device address (null for a leaf) + BvhNode right; // child device address (null for a leaf) + int sphereIndex; // >= 0: leaf sphere index; < 0: internal node +}; + +layout(push_constant, std430) uniform Frame { + BvhNode root; // entry address into the BVH + uvec2 resolution; // image size in pixels + uint seed; // varies sampling between runs + uint rowOffset; // first image row this dispatch (band) covers +} frame; + +struct Ray { + vec3 o; + vec3 d; +}; + +struct Hit { + float t; + vec3 p; + vec3 n; + bool front; + vec4 albedo; + vec4 material; +}; + +// PCG hash RNG, advanced in place. +uint pcg(inout uint s) { + s = s * 747796405u + 2891336453u; + uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u; + return (w >> 22u) ^ w; +} + +float rnd(inout uint s) { + return float(pcg(s)) * (1.0 / 4294967296.0); +} + +// Uniformly distributed point on the unit sphere. +vec3 rndUnit(inout uint s) { + float z = rnd(s) * 2.0 - 1.0; + float a = rnd(s) * 6.28318530718; + float r = sqrt(max(0.0, 1.0 - z * z)); + return vec3(r * cos(a), r * sin(a), z); +} + +bool hitSphere(Sphere sp, Ray ray, float tmin, float tmax, inout Hit h) { + vec3 center = sp.centerRadius.xyz; + float radius = sp.centerRadius.w; + vec3 oc = ray.o - center; + float a = dot(ray.d, ray.d); + float halfB = dot(oc, ray.d); + float c = dot(oc, oc) - radius * radius; + float disc = halfB * halfB - a * c; + if (disc < 0.0) { + return false; + } + float sq = sqrt(disc); + float t = (-halfB - sq) / a; + if (t < tmin || t > tmax) { + t = (-halfB + sq) / a; + if (t < tmin || t > tmax) { + return false; + } + } + h.t = t; + h.p = ray.o + t * ray.d; + vec3 outwardN = (h.p - center) / radius; + h.front = dot(ray.d, outwardN) < 0.0; + h.n = h.front ? outwardN : -outwardN; + h.albedo = sp.albedo; + h.material = sp.material; + return true; +} + +// Ray vs AABB slab test, gated to [tmin, tmax]. +bool aabbHit(vec3 bmin, vec3 bmax, Ray ray, float tmin, float tmax) { + vec3 inv = 1.0 / ray.d; + vec3 t0 = (bmin - ray.o) * inv; + vec3 t1 = (bmax - ray.o) * inv; + vec3 tsmall = min(t0, t1); + vec3 tbig = max(t0, t1); + float lo = max(max(tsmall.x, tsmall.y), max(tsmall.z, tmin)); + float hi = min(min(tbig.x, tbig.y), min(tbig.z, tmax)); + return hi >= lo; +} + +// Max BVH traversal depth. A balanced median-split BVH of N leaves is ~log2(N) +// deep, so this covers far more spheres than the host will ever generate; the +// bound guards against a pathologically deep tree (a subtree is skipped rather +// than overflowing the stack). +const int BVH_STACK = 64; + +// Walk the BVH from the root device address, hopping pointers to find the +// closest sphere hit. An explicit stack stands in for recursion. +bool worldHit(Ray ray, float tmin, float tmax, out Hit h) { + bool hitAny = false; + float closest = tmax; + + BvhNode stack[BVH_STACK]; + int sp = 0; + stack[sp++] = frame.root; + + while (sp > 0) { + BvhNode node = stack[--sp]; + if (!aabbHit(node.boundsMin.xyz, node.boundsMax.xyz, ray, tmin, closest)) { + continue; + } + if (node.sphereIndex >= 0) { + Hit tmp; + if (hitSphere(spheres[node.sphereIndex], ray, tmin, closest, tmp)) { + hitAny = true; + closest = tmp.t; + h = tmp; + } + } else if (sp + 2 <= BVH_STACK) { + stack[sp++] = node.left; + stack[sp++] = node.right; + } + } + return hitAny; +} + +vec3 skyColor(vec3 dir) { + float t = 0.5 * (normalize(dir).y + 1.0); + return mix(cam.skyBottom, cam.skyTop, t); +} + +float schlick(float cosine, float ref) { + float r0 = (1.0 - ref) / (1.0 + ref); + r0 = r0 * r0; + return r0 + (1.0 - r0) * pow(1.0 - cosine, 5.0); +} + +vec3 rayColor(Ray ray, inout uint s) { + vec3 attenuation = vec3(1.0); + for (uint bounce = 0u; bounce < MAX_BOUNCES; ++bounce) { + Hit h; + if (!worldHit(ray, 0.001, 1e30, h)) { + return attenuation * skyColor(ray.d); + } + + int mat = int(h.material.x + 0.5); + if (mat == 0) { + // Lambertian: scatter about the normal. + vec3 dir = h.n + rndUnit(s); + if (dot(dir, dir) < 1e-8) { + dir = h.n; + } + ray = Ray(h.p, dir); + attenuation *= h.albedo.rgb; + } else if (mat == 1) { + // Metal: reflect, perturbed by fuzz. + vec3 refl = reflect(normalize(ray.d), h.n); + vec3 dir = refl + h.material.y * rndUnit(s); + if (dot(dir, h.n) <= 0.0) { + return vec3(0.0); + } + ray = Ray(h.p, dir); + attenuation *= h.albedo.rgb; + } else { + // Dielectric: refract or reflect (Schlick). + float ior = h.material.z; + float ratio = h.front ? (1.0 / ior) : ior; + vec3 ud = normalize(ray.d); + float cosT = min(dot(-ud, h.n), 1.0); + float sinT = sqrt(max(0.0, 1.0 - cosT * cosT)); + vec3 dir; + if (ratio * sinT > 1.0 || schlick(cosT, ratio) > rnd(s)) { + dir = reflect(ud, h.n); + } else { + dir = refract(ud, h.n, ratio); + } + ray = Ray(h.p, dir); + } + } + return vec3(0.0); // exceeded the bounce budget +} + +void main() { + // gl_GlobalInvocationID.y is relative to this band; rowOffset places it into + // the full image. The host splits the render into horizontal bands so no single + // compute submission runs long enough to trip the GPU's hang-recovery watchdog. + uvec2 gid = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y + frame.rowOffset); + if (gid.x >= frame.resolution.x || gid.y >= frame.resolution.y) { + return; + } + + uint s = (gid.x * 1973u + gid.y * 9277u + frame.seed * 26699u) | 1u; + + vec3 color = vec3(0.0); + for (uint i = 0u; i < SAMPLES; ++i) { + float u = (float(gid.x) + rnd(s)) / float(frame.resolution.x); + float v = (float(gid.y) + rnd(s)) / float(frame.resolution.y); + // Image rows grow downward; flip v so the top row maps to the top of the + // viewport. + vec3 dir = + cam.lowerLeft + u * cam.horizontal + (1.0 - v) * cam.vertical - cam.origin; + color += rayColor(Ray(cam.origin, dir), s); + } + color /= float(SAMPLES); + color = sqrt(color); // gamma 2.0 + + uint idx = gid.y * frame.resolution.x + gid.x; + pixels[idx] = vec4(color, 1.0); +} diff --git a/examples/rays/Main.hs b/examples/rays/Main.hs index 69491212f..29d06a80c 100644 --- a/examples/rays/Main.hs +++ b/examples/rays/Main.hs @@ -31,11 +31,11 @@ main = runResourceT $ do (vc, vma, initialSC) <- withWindowedVk WindowedConfig - { wcAppName = "Vulkan ⚡ Haskell" - , wcInstanceReqs = instanceRequirements - , wcDeviceReqs = deviceRequirements - , wcVmaFlags = VMA.ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT - , wcSwapchainConfig = defaultSwapchainConfig + { appName = "Vulkan ⚡ Haskell" + , instanceReqs = instanceRequirements + , deviceReqs = deviceRequirements + , vmaFlags = VMA.ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT + , swapchainConfig = defaultSwapchainConfig } (sdl2Adapter win) let diff --git a/examples/resize/Main.hs b/examples/resize/Main.hs index 68dc70b59..ca1db22af 100644 --- a/examples/resize/Main.hs +++ b/examples/resize/Main.hs @@ -86,11 +86,11 @@ main = prettyError . runResourceT $ do windowConfig :: WindowedConfig windowConfig = WindowedConfig - { wcAppName = "Haskell ❤️ Vulkan" - , wcInstanceReqs = [] - , wcDeviceReqs = [] - , wcVmaFlags = zero - , wcSwapchainConfig = + { appName = "Haskell ❤️ Vulkan" + , instanceReqs = [] + , deviceReqs = [] + , vmaFlags = zero + , swapchainConfig = defaultSwapchainConfig { scRequiredUsageFlags = -- TRANSFER_DST for the blit; COLOR_ATTACHMENT so the diff --git a/examples/texture-reflect/Main.hs b/examples/texture-reflect/Main.hs new file mode 100644 index 000000000..54e3cb82c --- /dev/null +++ b/examples/texture-reflect/Main.hs @@ -0,0 +1,442 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} + +{-| Headless demonstration of __using a colour attachment as a texture__ +(render-to-texture), driven by reflected SPIR-V. Two pipelines run back to back +in one command buffer: + + * __offscreen pass__: a full-screen triangle with red/green/blue corners is + drawn into an offscreen colour image (@COLOR_ATTACHMENT | SAMPLED@). A barrier + then moves that image from @COLOR_ATTACHMENT_OPTIMAL@ to + @SHADER_READ_ONLY_OPTIMAL@. + + * __cube pass__: a spinning cube — its @position@ + @uv@ vertex attributes + described from reflection ("Vulkan.Utils.SpirV.VertexInput") — samples the + offscreen image through a @sampler2D@ at set 1, binding 0. + +Both pipelines share a @Globals@ UBO at __set 0, binding 0__ (a @time@). The set 0 +descriptor-set layout is a single object reused by both pipeline layouts, so the +layouts are /compatible for set 0/: the UBO is bound __once__ before the offscreen +pass and is never rebound — the cube pass only binds its sampler at set 1. (The +cross-pipeline layout match is not type-enforced here; each pipeline's own +vertex↔fragment composition still is, via 'MatchInterface' / 'CompatibleResources'.) +-} +module Main where + +import qualified Codec.Picture as JP +import Control.Monad (unless) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT) +import Data.Bits ((.|.)) +import Data.ByteString (ByteString) +import Data.FileEmbed (embedFile) +import Data.List (foldl') +import qualified Data.Vector as V +import Data.Word (Word32) +import Foreign.Marshal.Array (pokeArray) +import Foreign.Ptr (castPtr) +import Foreign.Storable (poke, sizeOf) +import Graphics.Gl.Block (Std140 (..)) +import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), submitAndWait, withHeadlessVk) +import ImageReadback (copyImageToHost, makeReadbackImage, savePng) +import RenderTarget (createColorTarget, createDepthTarget) +import System.Exit (exitFailure) +import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) +import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) +import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) +import qualified Vulkan.Core10 as SamplerCreateInfo (SamplerCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import qualified Vulkan.Core13 as Vk +import Vulkan.Utils.Barrier (imageBarrier, transitionColorAttachment, transitionDepthAttachment) +import Vulkan.Utils.Descriptors (bufferWrite, combinedImageSamplerWrite) +import qualified Vulkan.Utils.DynamicRendering as Dynamic +import Vulkan.Utils.DynamicState (DynamicState (..), allDynamicStates, applyDynamicStates, dynamicStateFor, fullScissor) +import Vulkan.Utils.QueueAssignment (QueueFamilyIndex (..)) +import Vulkan.Utils.Queues (Queues (..)) +import Vulkan.Zero (zero) +import qualified VulkanMemoryAllocator as AllocationCreateInfo (AllocationCreateInfo (..)) +import qualified VulkanMemoryAllocator as VMA + +import Data.SpirV.Reflect.FFI (loadBytes) +import Vulkan.Utils.SpirV.Descriptors (mergedDescriptorSetLayoutInfos) +import Vulkan.Utils.SpirV.Stage (CompatibleResources, MatchInterface, reflectStageSig) +import Vulkan.Utils.SpirV.TH (reflectShaderTypes) +import Vulkan.Utils.SpirV.VertexInput (vertexInputState) + +-- Compile-time reflection: the shared @Globals@ UBO record (from any shader that +-- declares it) and a stage signature per shader. +reflectShaderTypes "texture-reflect/tri.frag.spv" +reflectStageSig "TriVert" "texture-reflect/tri.vert.spv" +reflectStageSig "TriFrag" "texture-reflect/tri.frag.spv" +reflectStageSig "CubeVert" "texture-reflect/cube.vert.spv" +reflectStageSig "CubeFrag" "texture-reflect/cube.frag.spv" + +-- Each pipeline's own vertex↔fragment composition, proved at compile time. +triComposes :: (MatchInterface TriVert TriFrag, CompatibleResources TriVert TriFrag) => Bool +triComposes = True + +cubeComposes :: (MatchInterface CubeVert CubeFrag, CompatibleResources CubeVert CubeFrag) => Bool +cubeComposes = True + +triVertSpv, triFragSpv, cubeVertSpv, cubeFragSpv :: ByteString +triVertSpv = $(embedFile "texture-reflect/tri.vert.spv") +triFragSpv = $(embedFile "texture-reflect/tri.frag.spv") +cubeVertSpv = $(embedFile "texture-reflect/cube.vert.spv") +cubeFragSpv = $(embedFile "texture-reflect/cube.frag.spv") + +width, height :: Word32 +width = 256 +height = 256 + +colorFormat :: Vk.Format +colorFormat = Vk.FORMAT_R8G8B8A8_UNORM + +depthFormat :: Vk.Format +depthFormat = Vk.FORMAT_D32_SFLOAT + +floatSize :: Int +floatSize = sizeOf (0 :: Float) + +-- | The shared uniform: a single @time@ both pipelines read. +globalsValue :: Globals +globalsValue = Globals{time = 0.7} + +{- | A unit cube centred at the origin: 6 faces × 2 triangles, each vertex five +floats @px py pz u v@ (tightly packed, matching the reflected vertex input). +-} +cubeVertices :: [Float] +cubeVertices = concatMap faceVerts faces + where + h = 0.5 + add :: (Float, Float, Float) -> (Float, Float, Float) -> (Float, Float, Float) + add (a, b, c) (d, e, f) = (a + d, b + e, c + f) + -- Each face: an origin corner and two edge vectors (length 1). + faces :: [((Float, Float, Float), (Float, Float, Float), (Float, Float, Float))] + faces = + [ ((-h, -h, h), (1, 0, 0), (0, 1, 0)) -- +Z + , ((h, -h, -h), (-1, 0, 0), (0, 1, 0)) -- -Z + , ((h, -h, h), (0, 0, -1), (0, 1, 0)) -- +X + , ((-h, -h, -h), (0, 0, 1), (0, 1, 0)) -- -X + , ((-h, h, h), (1, 0, 0), (0, 0, -1)) -- +Y + , ((-h, -h, -h), (1, 0, 0), (0, 0, 1)) -- -Y + ] + faceVerts :: ((Float, Float, Float), (Float, Float, Float), (Float, Float, Float)) -> [Float] + faceVerts (o, du, dv) = + let + c00 = o + c10 = add o du + c11 = add (add o du) dv + c01 = add o dv + -- V is flipped (top edge -> v = 0): Vulkan's texture origin is top-left, + -- so v = 0 is the top row of the sampled offscreen image. + quad :: [((Float, Float, Float), (Float, Float))] + quad = [(c00, (0, 1)), (c10, (1, 1)), (c11, (1, 0)), (c00, (0, 1)), (c11, (1, 0)), (c01, (0, 0))] + in + concatMap (\((x, y, z), (u, v)) -> [x, y, z, u, v]) quad + +{- | Five floats per vertex (@px py pz u v@), so the draw count is derived from +the geometry rather than asserted separately. +-} +cubeVertexCount :: Word32 +cubeVertexCount = fromIntegral (length cubeVertices `div` 5) + +main :: IO () +main = runResourceT $ do + HeadlessVk{..} <- + withHeadlessVk + HeadlessConfig + { appName = "Haskell Vulkan colour-attachment-as-texture (headless)" + , instanceReqs = [] + , deviceReqs = Dynamic.dynamicRenderingRequirements + , vmaFlags = zero + } + liftIO $ do + putStrLn $ "offscreen pipeline composes (compile-time): " <> show triComposes + putStrLn $ "cube pipeline composes (compile-time): " <> show cubeComposes + let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics queues) + + colorImage <- render allocator device graphicsQueueFamilyIndex + Vk.deviceWaitIdle device + + savePng "texture-reflect.png" colorImage + let + pixel x y = JP.pixelAt colorImage x y + allPixels = [pixel x y | y <- [0 .. fromIntegral height - 1], x <- [0 .. fromIntegral width - 1]] + -- Brightest sampled R/G/B across the image, in a single pass. + (maxR, maxG, maxB) = + foldl' + (\(!r, !g, !b) (JP.PixelRGBA8 pr pg pb _) -> (max r (fromIntegral pr), max g (fromIntegral pg), max b (fromIntegral pb))) + ((0, 0, 0) :: (Int, Int, Int)) + allPixels + JP.PixelRGBA8 cr cg cb _ = pixel 2 2 + cornerChannels = [fromIntegral cr, fromIntegral cg, fromIntegral cb] :: [Int] + cornerLum = sum cornerChannels + cornerSaturation = maximum cornerChannels - minimum cornerChannels + liftIO $ + putStrLn $ + "sampled max channels: R=" + <> show maxR + <> " G=" + <> show maxG + <> " B=" + <> show maxB + <> "; background corner luminance=" + <> show cornerLum + <> " saturation=" + <> show cornerSaturation + + let + checks :: [(String, Bool)] + checks = + [ ("offscreen red was sampled onto the cube", maxR > 150) + , ("offscreen green was sampled onto the cube", maxG > 150) + , ("offscreen blue was sampled onto the cube", maxB > 150) + , ("background is the neutral clear colour, not the texture", cornerLum > 80 && cornerSaturation < 40) + ] + liftIO $ do + mapM_ (\(label, ok) -> putStrLn $ "[" <> (if ok then "PASS" else "FAIL") <> "] " <> label) checks + unless (all snd checks) exitFailure + putStrLn "All render-to-texture checks passed." + +render + :: VMA.Allocator + -> Vk.Device + -> Word32 + -> ResourceT IO (JP.Image JP.PixelRGBA8) +render allocator dev graphicsQueueFamilyIndex = do + let extent = Vk.Extent2D width height + + -- Reflected modules drive the merged set layouts and the cube's vertex input. + triVertModule <- loadBytes triVertSpv + triFragModule <- loadBytes triFragSpv + cubeVertModule <- loadBytes cubeVertSpv + cubeFragModule <- loadBytes cubeFragSpv + + -- Offscreen target (sampled in the cube pass), final colour target (read back), + -- depth for the cube, and the host readback image. + (offscreenImage, offscreenView) <- createSampledColorTarget allocator dev colorFormat extent + (_, (sceneImage, sceneView)) <- createColorTarget allocator dev colorFormat extent + (_, (depthImage, depthView)) <- createDepthTarget allocator dev depthFormat extent + (cpuImage, readback) <- makeReadbackImage allocator dev colorFormat extent + + -- Shared Globals UBO (host-visible, mapped). + (_, (uboBuffer, _, uboInfo)) <- + VMA.withBuffer + allocator + zero{Vk.size = fromIntegral (sizeOf globalsValue), Vk.usage = Vk.BUFFER_USAGE_UNIFORM_BUFFER_BIT} + mappedAlloc + allocate + liftIO $ poke (castPtr (VMA.mappedData uboInfo)) globalsValue + + -- Cube vertex buffer (host-visible, mapped) — plain floats, interpreted by the + -- reflected attribute descriptions. + (_, (cubeBuffer, _, cubeBufInfo)) <- + VMA.withBuffer + allocator + zero{Vk.size = fromIntegral (length cubeVertices * floatSize), Vk.usage = Vk.BUFFER_USAGE_VERTEX_BUFFER_BIT} + mappedAlloc + allocate + liftIO $ pokeArray (castPtr (VMA.mappedData cubeBufInfo)) cubeVertices + + (_, sampler) <- Vk.withSampler dev samplerInfo Nothing allocate + + -- ONE set 0 layout (the Globals UBO, visible to all stages that read it) and one + -- set 1 layout (the sampler), merged across all four shaders. Reusing the set 0 + -- layout object in both pipeline layouts makes them compatible for set 0. + setInfos <- orDie (mergedDescriptorSetLayoutInfos [triVertModule, triFragModule, cubeVertModule, cubeFragModule]) + setLayouts <- + mapM + (\(setNo, info) -> do (_, l) <- Vk.withDescriptorSetLayout dev info Nothing allocate; pure (setNo, l)) + setInfos + let + layoutFor n = maybe (error ("missing descriptor set " <> show n)) id (lookup n setLayouts) + set0Layout = layoutFor 0 + set1Layout = layoutFor 1 + (_, triPipelineLayout) <- + Vk.withPipelineLayout dev zero{PipelineLayoutCreateInfo.setLayouts = [set0Layout]} Nothing allocate + (_, cubePipelineLayout) <- + Vk.withPipelineLayout dev zero{PipelineLayoutCreateInfo.setLayouts = [set0Layout, set1Layout]} Nothing allocate + + -- Descriptor sets: set 0 = Globals UBO, set 1 = the offscreen image + sampler. + (_, descriptorPool) <- + Vk.withDescriptorPool + dev + zero + { Vk.maxSets = 2 + , Vk.poolSizes = + [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER 1 + , Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER 1 + ] + } + Nothing + allocate + descriptorSets <- + Vk.allocateDescriptorSets + dev + zero{Vk.descriptorPool = descriptorPool, Vk.setLayouts = [set0Layout, set1Layout]} + let + globalsSet = descriptorSets V.! 0 + samplerSet = descriptorSets V.! 1 + Vk.updateDescriptorSets + dev + [ bufferWrite globalsSet 0 Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER uboBuffer + , combinedImageSamplerWrite samplerSet 0 sampler offscreenView Vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + ] + [] + + -- Offscreen triangle pipeline (colour only, no vertex input) and cube pipeline + -- (colour + depth, vertex attributes from reflection). + let cubeVertexInput = vertexInputState cubeVertModule + (_, triPipeline) <- + Dynamic.allocatePipelineFromShaders + dev + zero + { Dynamic.colorFormats = [colorFormat] + , Dynamic.layout = Just triPipelineLayout + } + () + [(Vk.SHADER_STAGE_VERTEX_BIT, triVertSpv), (Vk.SHADER_STAGE_FRAGMENT_BIT, triFragSpv)] + (_, cubePipeline) <- + Dynamic.allocatePipelineFromShaders + dev + zero + { Dynamic.colorFormats = [colorFormat] + , Dynamic.depthFormat = Just depthFormat + , Dynamic.vertexInput = cubeVertexInput + , Dynamic.layout = Just cubePipelineLayout + } + () + [(Vk.SHADER_STAGE_VERTEX_BIT, cubeVertSpv), (Vk.SHADER_STAGE_FRAGMENT_BIT, cubeFragSpv)] + + (_, commandPool) <- + Vk.withCommandPool dev zero{CommandPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex} Nothing allocate + graphicsQueue <- Vk.getDeviceQueue dev graphicsQueueFamilyIndex 0 + cb <- oneCommandBuffer dev commandPool + + let oneShot = zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} + Vk.useCommandBuffer cb oneShot $ do + transitionColorAttachment cb offscreenImage + transitionColorAttachment cb sceneImage + transitionDepthAttachment cb depthImage + + -- Pass 1: draw the RGB triangle into the offscreen image. The shared Globals + -- descriptor is bound here, ONCE, under the (compatible) triangle layout. + Vk.cmdUseRendering cb (Dynamic.renderingInfo (fullScissor extent) [(offscreenView, Vk.Float32 0 0 0 1)] Nothing) $ do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_GRAPHICS triPipeline + applyDynamicStates allDynamicStates cb (dynamicStateFor extent) + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_GRAPHICS triPipelineLayout 0 [globalsSet] [] + Vk.cmdDraw cb 3 1 0 0 + + -- Make the offscreen colour image readable by the cube's fragment shader. + colorToSampled cb offscreenImage + + -- Pass 2: draw the cube sampling the offscreen image. Set 0 (Globals) is still + -- bound — only the sampler at set 1 is bound now. + Vk.cmdUseRendering + cb + (Dynamic.renderingInfo (fullScissor extent) [(sceneView, Vk.Float32 0.30 0.32 0.38 1)] (Just (depthView, 1))) + $ do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_GRAPHICS cubePipeline + applyDynamicStates + allDynamicStates + cb + (dynamicStateFor extent){depthTest = True, depthWrite = True, depthCompareOp = Vk.COMPARE_OP_LESS} + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_GRAPHICS cubePipelineLayout 1 [samplerSet] [] + Vk.cmdBindVertexBuffers cb 0 [cubeBuffer] [0] + Vk.cmdDraw cb cubeVertexCount 1 0 0 + + copyImageToHost cb extent sceneImage cpuImage + submitAndWait dev graphicsQueue cb "Timed out in the render-to-texture passes" + readback + where + samplerInfo = + zero + { SamplerCreateInfo.magFilter = Vk.FILTER_LINEAR + , SamplerCreateInfo.minFilter = Vk.FILTER_LINEAR + , SamplerCreateInfo.addressModeU = Vk.SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE + , SamplerCreateInfo.addressModeV = Vk.SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE + } + +-- | Print a merged-layout conflict and exit non-zero. +orDie :: Either String a -> ResourceT IO a +orDie = either (\e -> liftIO (putStrLn ("merged layout error: " <> e) >> exitFailure)) pure + +mappedAlloc :: VMA.AllocationCreateInfo +mappedAlloc = + zero + { AllocationCreateInfo.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , AllocationCreateInfo.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + , AllocationCreateInfo.requiredFlags = Vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT + } + +oneCommandBuffer :: Vk.Device -> Vk.CommandPool -> ResourceT IO Vk.CommandBuffer +oneCommandBuffer dev pool = do + (_, cbs) <- + Vk.withCommandBuffers + dev + zero{Vk.commandPool = pool, Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY, Vk.commandBufferCount = 1} + allocate + pure (V.head cbs) + +{- | A GPU-only colour target that can be a colour attachment AND be sampled (the +offscreen render-to-texture target). Like 'RenderTarget.createColorTarget' but +with @SAMPLED@ instead of @TRANSFER_SRC@. +-} +createSampledColorTarget + :: VMA.Allocator -> Vk.Device -> Vk.Format -> Vk.Extent2D -> ResourceT IO (Vk.Image, Vk.ImageView) +createSampledColorTarget allocator dev format (Vk.Extent2D w h) = do + (_, (image, _, _)) <- VMA.withImage allocator imageCreateInfo gpuAlloc allocate + (_, view) <- Vk.withImageView dev (viewCreateInfo image) Nothing allocate + pure (image, view) + where + gpuAlloc = zero{AllocationCreateInfo.usage = VMA.MEMORY_USAGE_GPU_ONLY} + imageCreateInfo = + zero + { Vk.imageType = Vk.IMAGE_TYPE_2D + , Vk.format = format + , Vk.extent = Vk.Extent3D w h 1 + , Vk.mipLevels = 1 + , Vk.arrayLayers = 1 + , Vk.samples = Vk.SAMPLE_COUNT_1_BIT + , Vk.tiling = Vk.IMAGE_TILING_OPTIMAL + , Vk.usage = Vk.IMAGE_USAGE_COLOR_ATTACHMENT_BIT .|. Vk.IMAGE_USAGE_SAMPLED_BIT + , Vk.initialLayout = Vk.IMAGE_LAYOUT_UNDEFINED + } + viewCreateInfo image = + zero + { Vk.image = image + , Vk.viewType = Vk.IMAGE_VIEW_TYPE_2D + , Vk.format = format + , Vk.subresourceRange = Vk.ImageSubresourceRange Vk.IMAGE_ASPECT_COLOR_BIT 0 1 0 1 + } + +{- | Barrier a colour image from a colour attachment (after the offscreen pass) to +a shader-readable texture for the next pass's fragment sampling. +-} +colorToSampled :: Vk.CommandBuffer -> Vk.Image -> ResourceT IO () +colorToSampled cb img = + Vk.cmdPipelineBarrier + cb + Vk.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT + Vk.PIPELINE_STAGE_FRAGMENT_SHADER_BIT + zero + [] + [] + [ imageBarrier + Vk.IMAGE_ASPECT_COLOR_BIT + Vk.ACCESS_COLOR_ATTACHMENT_WRITE_BIT + Vk.ACCESS_SHADER_READ_BIT + Vk.IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL + Vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + img + ] diff --git a/examples/texture-reflect/build-shaders.sh b/examples/texture-reflect/build-shaders.sh new file mode 100755 index 000000000..953f2ebfc --- /dev/null +++ b/examples/texture-reflect/build-shaders.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Compile this example's shaders to committed SPIR-V (input to reflection + runtime). +set -eu +cd "$(dirname "$0")" +for src in *.vert *.frag; do + [ -e "$src" ] || continue + glslangValidator -V "$src" -o "$src.spv" + echo "wrote $src.spv" +done diff --git a/examples/texture-reflect/cube.frag b/examples/texture-reflect/cube.frag new file mode 100644 index 000000000..a63480a46 --- /dev/null +++ b/examples/texture-reflect/cube.frag @@ -0,0 +1,23 @@ +#version 450 + +// Cube pass, fragment stage. Samples the offscreen RGB triangle — produced by +// the first pipeline and barriered into a shader-read layout — through a combined +// image sampler at set 1, binding 0. The shared Globals UBO (set 0, binding 0) +// is read again here for a time-based tint; it is the SAME descriptor the +// offscreen pass bound, never rebound between the two draws. + +layout(set = 0, binding = 0, std140) uniform Globals { + float time; +} g; + +layout(set = 1, binding = 0) uniform sampler2D offscreen; + +layout(location = 0) in vec2 vUv; + +layout(location = 0) out vec4 outColor; + +void main() { + vec3 c = texture(offscreen, vUv).rgb; + c *= 0.85 + 0.15 * sin(g.time); + outColor = vec4(c, 1.0); +} diff --git a/examples/texture-reflect/cube.vert b/examples/texture-reflect/cube.vert new file mode 100644 index 000000000..c8c1fc29d --- /dev/null +++ b/examples/texture-reflect/cube.vert @@ -0,0 +1,44 @@ +#version 450 + +// Cube pass, vertex stage. Geometry comes from a vertex buffer with per-vertex +// position + uv attributes — the binding/attribute descriptions are built from +// reflection (Vulkan.Utils.SpirV.VertexInput), not hand-written. The cube spins +// by Globals.time (the same shared UBO at set 0, binding 0 the offscreen pass +// used). A perspective projection is built inline. + +layout(set = 0, binding = 0, std140) uniform Globals { + float time; +} g; + +layout(location = 0) in vec3 position; +layout(location = 1) in vec2 uv; + +layout(location = 0) out vec2 vUv; + +mat4 rotY(float a) { + float c = cos(a), s = sin(a); + return mat4(c, 0.0, -s, 0.0, 0.0, 1.0, 0.0, 0.0, s, 0.0, c, 0.0, 0.0, 0.0, 0.0, 1.0); +} + +mat4 rotX(float a) { + float c = cos(a), s = sin(a); + return mat4(1.0, 0.0, 0.0, 0.0, 0.0, c, s, 0.0, 0.0, -s, c, 0.0, 0.0, 0.0, 0.0, 1.0); +} + +// Vulkan-clip perspective (z in [0,1], y flipped), column-major. +mat4 perspective(float fovy, float aspect, float near, float far) { + float f = 1.0 / tan(fovy * 0.5); + return mat4( + f / aspect, 0.0, 0.0, 0.0, + 0.0, -f, 0.0, 0.0, + 0.0, 0.0, far / (near - far), -1.0, + 0.0, 0.0, (near * far) / (near - far), 0.0); +} + +void main() { + // Scale the unit cube down so the framed image has padding around it. + vec4 world = rotY(g.time) * rotX(0.5) * vec4(position * 0.62, 1.0); + world.z -= 3.0; + gl_Position = perspective(radians(50.0), 1.0, 0.1, 10.0) * world; + vUv = uv; +} diff --git a/examples/texture-reflect/tri.frag b/examples/texture-reflect/tri.frag new file mode 100644 index 000000000..b6c39f0aa --- /dev/null +++ b/examples/texture-reflect/tri.frag @@ -0,0 +1,17 @@ +#version 450 + +// Offscreen pass, fragment stage. Writes the interpolated RGB gradient, pulsed +// by Globals.time — the first read of the shared UBO (set 0, binding 0). + +layout(set = 0, binding = 0, std140) uniform Globals { + float time; +} g; + +layout(location = 0) in vec3 vColor; + +layout(location = 0) out vec4 outColor; + +void main() { + float pulse = 0.7 + 0.3 * abs(sin(g.time)); + outColor = vec4(vColor * pulse, 1.0); +} diff --git a/examples/texture-reflect/tri.vert b/examples/texture-reflect/tri.vert new file mode 100644 index 000000000..27fc00079 --- /dev/null +++ b/examples/texture-reflect/tri.vert @@ -0,0 +1,19 @@ +#version 450 + +// Offscreen pass, vertex stage. A screen-filling triangle with saturated +// red / green / blue corners, so the offscreen colour image holds the full RGB +// range (each primary saturated near a vertex). That image is then sampled as a +// texture by the cube pass. The geometry is generated from gl_VertexIndex (no +// vertex buffer). The shared Globals UBO (set 0, binding 0) is read by the +// fragment stage; this pipeline and the cube pipeline keep set 0 compatible so +// the descriptor need not be rebound between them. + +layout(location = 0) out vec3 vColor; + +void main() { + // Vulkan clip space is +Y down: y = +1 is the bottom, y = -1 the top. + vec2 base[3] = vec2[](vec2(-1.0, 1.0), vec2(1.0, 1.0), vec2(0.0, -1.0)); + vec3 cols[3] = vec3[](vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0)); + gl_Position = vec4(base[gl_VertexIndex], 0.0, 1.0); + vColor = cols[gl_VertexIndex]; +} diff --git a/examples/triangle-dynamic/Main.hs b/examples/triangle-dynamic/Main.hs index 13d73f9cd..c67ceb779 100644 --- a/examples/triangle-dynamic/Main.hs +++ b/examples/triangle-dynamic/Main.hs @@ -20,15 +20,15 @@ main = runResourceT $ do (vc, _vma, initialSC) <- withWindowedVk WindowedConfig - { wcAppName = appName - , wcInstanceReqs = [] - , wcDeviceReqs = + { appName = appName + , instanceReqs = [] + , deviceReqs = [U.reqs| VK_KHR_dynamic_rendering PhysicalDeviceDynamicRenderingFeatures.dynamicRendering |] - , wcVmaFlags = zero - , wcSwapchainConfig = defaultSwapchainConfig + , vmaFlags = zero + , swapchainConfig = defaultSwapchainConfig } (Window.glfwAdapter window) TriangleDynamic.runTriangle diff --git a/examples/triangle-dynamic/TriangleDynamic.hs b/examples/triangle-dynamic/TriangleDynamic.hs index aaebcafd9..30509109f 100644 --- a/examples/triangle-dynamic/TriangleDynamic.hs +++ b/examples/triangle-dynamic/TriangleDynamic.hs @@ -56,7 +56,7 @@ runTriangle vc initialSC getDrawableSize shouldQuit = do Dynamic.allocatePipelineFromShaders (vcDevice vc) zero{Dynamic.colorFormats = [KHR.format (sFormat initialSC)]} - () -- no specialization constants + () [ (Vk.SHADER_STAGE_VERTEX_BIT, Triangle.vertCode) , (Vk.SHADER_STAGE_FRAGMENT_BIT, Triangle.fragCode) ] diff --git a/examples/triangle-glfw/Main.hs b/examples/triangle-glfw/Main.hs index ea4580bee..fb261f77a 100644 --- a/examples/triangle-glfw/Main.hs +++ b/examples/triangle-glfw/Main.hs @@ -16,11 +16,11 @@ main = runResourceT $ do (vc, _vma, initialSC) <- withWindowedVk WindowedConfig - { wcAppName = appName - , wcInstanceReqs = [] - , wcDeviceReqs = [] - , wcVmaFlags = zero - , wcSwapchainConfig = defaultSwapchainConfig + { appName = appName + , instanceReqs = [] + , deviceReqs = [] + , vmaFlags = zero + , swapchainConfig = defaultSwapchainConfig } (Window.glfwAdapter window) Triangle.runTriangle diff --git a/examples/triangle-headless-dynamic/Main.hs b/examples/triangle-headless-dynamic/Main.hs index 63e7c180f..f6f15c31b 100644 --- a/examples/triangle-headless-dynamic/Main.hs +++ b/examples/triangle-headless-dynamic/Main.hs @@ -49,17 +49,18 @@ main = runResourceT $ do HeadlessVk{..} <- withHeadlessVk HeadlessConfig - { hcAppName = "Haskell Vulkan dynamic-state (headless) test" - , hcInstanceReqs = [] - , hcDeviceReqs = + { appName = "Haskell Vulkan dynamic-state (headless) test" + , instanceReqs = [] + , deviceReqs = [U.reqs| VK_KHR_dynamic_rendering PhysicalDeviceDynamicRenderingFeatures.dynamicRendering |] + , vmaFlags = zero } - let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics hvQueues) - results <- render hvAllocator hvDevice graphicsQueueFamilyIndex - Vk.deviceWaitIdle hvDevice + let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics queues) + results <- render allocator device graphicsQueueFamilyIndex + Vk.deviceWaitIdle device centres <- forM results $ \(name, image) -> do savePng ("headless-dynamic-" <> name <> ".png") image @@ -105,12 +106,12 @@ render allocator dev graphicsQueueFamilyIndex = do (cpuImage, readback) <- makeReadbackImage allocator dev imageFormat extent - -- One pipeline, full always-on dynamic state (Nothing). + -- One pipeline; dynamic states default to full always-on. (_, pipeline) <- Dynamic.allocatePipelineFromShaders dev zero{Dynamic.colorFormats = [imageFormat]} - () -- no specialization constants + () [(Vk.SHADER_STAGE_VERTEX_BIT, vertCode), (Vk.SHADER_STAGE_FRAGMENT_BIT, fragCode)] let poolCreateInfo = zero{CommandPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex} diff --git a/examples/triangle-headless/Main.hs b/examples/triangle-headless/Main.hs index 5f417b0bf..aa2d5c44c 100644 --- a/examples/triangle-headless/Main.hs +++ b/examples/triangle-headless/Main.hs @@ -37,13 +37,14 @@ main = runResourceT $ do HeadlessVk{..} <- withHeadlessVk HeadlessConfig - { hcAppName = "Haskell Vulkan triangle (headless) example" - , hcInstanceReqs = [] - , hcDeviceReqs = [] + { appName = "Haskell Vulkan triangle (headless) example" + , instanceReqs = [] + , deviceReqs = [] + , vmaFlags = zero } - let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics hvQueues) - image <- render hvAllocator hvDevice graphicsQueueFamilyIndex - Vk.deviceWaitIdle hvDevice + let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics queues) + image <- render allocator device graphicsQueueFamilyIndex + Vk.deviceWaitIdle device savePng "triangle.png" image {- | This function renders a triangle and reads the image on the CPU @@ -177,7 +178,12 @@ render allocator dev graphicsQueueFamilyIndex = do (vertKey, vertStage) <- shaderStage dev Vk.SHADER_STAGE_VERTEX_BIT () vertCode (fragKey, fragStage) <- shaderStage dev Vk.SHADER_STAGE_FRAGMENT_BIT () fragCode - (_, graphicsPipeline) <- RenderPass.allocatePipeline dev renderPass zero{RenderPass.colorFormats = [imageFormat], RenderPass.dynamicStates = Just minimalDynamicStates} [vertStage, fragStage] + (_, graphicsPipeline) <- + RenderPass.allocatePipeline + dev + renderPass + zero{RenderPass.colorFormats = [imageFormat], RenderPass.dynamicStates = Just minimalDynamicStates} + [vertStage, fragStage] release vertKey release fragKey diff --git a/examples/triangle-sdl2/Main.hs b/examples/triangle-sdl2/Main.hs index 2e6d3c357..97ebb2b34 100644 --- a/examples/triangle-sdl2/Main.hs +++ b/examples/triangle-sdl2/Main.hs @@ -16,11 +16,11 @@ main = runResourceT $ do (vc, _vma, initialSC) <- withWindowedVk WindowedConfig - { wcAppName = appName - , wcInstanceReqs = [] - , wcDeviceReqs = [] - , wcVmaFlags = zero - , wcSwapchainConfig = defaultSwapchainConfig + { appName = appName + , instanceReqs = [] + , deviceReqs = [] + , vmaFlags = zero + , swapchainConfig = defaultSwapchainConfig } (Window.sdl2Adapter window) Triangle.runTriangle diff --git a/examples/vulkan-examples.cabal b/examples/vulkan-examples.cabal index 8a7bde774..94f45234e 100644 --- a/examples/vulkan-examples.cabal +++ b/examples/vulkan-examples.cabal @@ -177,6 +177,72 @@ executable compute if os(windows) ghc-options: -optl-mconsole +executable compute-reflect + main-is: Main.hs + other-modules: + Paths_vulkan_examples + autogen-modules: + Paths_vulkan_examples + hs-source-dirs: + compute-reflect + default-extensions: + BlockArguments + DataKinds + DefaultSignatures + DeriveFoldable + DeriveFunctor + DeriveTraversable + DerivingStrategies + DuplicateRecordFields + FlexibleContexts + FlexibleInstances + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + LambdaCase + MagicHash + NamedFieldPuns + NoMonomorphismRestriction + NumDecimals + OverloadedStrings + PatternSynonyms + PolyKinds + QuantifiedConstraints + RankNTypes + RecordWildCards + RoleAnnotations + ScopedTypeVariables + StandaloneDeriving + Strict + TupleSections + TypeApplications + TypeFamilyDependencies + TypeOperators + TypeSynonymInstances + ViewPatterns + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N + build-depends: + JuicyPixels + , VulkanMemoryAllocator + , base <5 + , bytestring + , file-embed + , geomancy + , gl-block + , resourcet + , safe-exceptions + , say + , text + , transformers + , vector + , vulkan + , vulkan-examples + , vulkan-utils + , vulkan-utils-spirv + default-language: Haskell2010 + if os(windows) + ghc-options: -optl-mconsole + executable hlsl main-is: Main.hs other-modules: @@ -297,6 +363,135 @@ executable info if os(windows) ghc-options: -optl-mconsole +executable mesh-reflect + main-is: Main.hs + other-modules: + Paths_vulkan_examples + autogen-modules: + Paths_vulkan_examples + hs-source-dirs: + mesh-reflect + default-extensions: + BlockArguments + DataKinds + DefaultSignatures + DeriveFoldable + DeriveFunctor + DeriveTraversable + DerivingStrategies + DuplicateRecordFields + FlexibleContexts + FlexibleInstances + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + LambdaCase + MagicHash + NamedFieldPuns + NoMonomorphismRestriction + NumDecimals + OverloadedStrings + PatternSynonyms + PolyKinds + QuantifiedConstraints + RankNTypes + RecordWildCards + RoleAnnotations + ScopedTypeVariables + StandaloneDeriving + Strict + TupleSections + TypeApplications + TypeFamilyDependencies + TypeOperators + TypeSynonymInstances + ViewPatterns + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N + build-depends: + JuicyPixels + , VulkanMemoryAllocator + , base <5 + , bytestring + , file-embed + , geomancy + , gl-block + , resourcet + , say + , spirv-reflect-ffi + , vector + , vulkan + , vulkan-examples + , vulkan-utils + , vulkan-utils-spirv + default-language: Haskell2010 + if os(windows) + ghc-options: -optl-mconsole + +executable pathtrace-reflect + main-is: Main.hs + other-modules: + Paths_vulkan_examples + autogen-modules: + Paths_vulkan_examples + hs-source-dirs: + pathtrace-reflect + default-extensions: + BlockArguments + DataKinds + DefaultSignatures + DeriveFoldable + DeriveFunctor + DeriveTraversable + DerivingStrategies + DuplicateRecordFields + FlexibleContexts + FlexibleInstances + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + LambdaCase + MagicHash + NamedFieldPuns + NoMonomorphismRestriction + NumDecimals + OverloadedStrings + PatternSynonyms + PolyKinds + QuantifiedConstraints + RankNTypes + RecordWildCards + RoleAnnotations + ScopedTypeVariables + StandaloneDeriving + Strict + TupleSections + TypeApplications + TypeFamilyDependencies + TypeOperators + TypeSynonymInstances + ViewPatterns + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N + build-depends: + JuicyPixels + , VulkanMemoryAllocator + , base <5 + , bytestring + , file-embed + , geomancy + , gl-block + , optparse-applicative + , random + , resourcet + , transformers + , vector + , vulkan + , vulkan-examples + , vulkan-utils + , vulkan-utils-spirv + default-language: Haskell2010 + if os(windows) + ghc-options: -optl-mconsole + executable rays main-is: Main.hs other-modules: @@ -441,6 +636,68 @@ executable resize if os(windows) ghc-options: -optl-mconsole +executable texture-reflect + main-is: Main.hs + other-modules: + Paths_vulkan_examples + autogen-modules: + Paths_vulkan_examples + hs-source-dirs: + texture-reflect + default-extensions: + BlockArguments + DataKinds + DefaultSignatures + DeriveFoldable + DeriveFunctor + DeriveTraversable + DerivingStrategies + DuplicateRecordFields + FlexibleContexts + FlexibleInstances + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + LambdaCase + MagicHash + NamedFieldPuns + NoMonomorphismRestriction + NumDecimals + OverloadedStrings + PatternSynonyms + PolyKinds + QuantifiedConstraints + RankNTypes + RecordWildCards + RoleAnnotations + ScopedTypeVariables + StandaloneDeriving + Strict + TupleSections + TypeApplications + TypeFamilyDependencies + TypeOperators + TypeSynonymInstances + ViewPatterns + ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N + build-depends: + JuicyPixels + , VulkanMemoryAllocator + , base <5 + , bytestring + , file-embed + , gl-block + , resourcet + , spirv-reflect-ffi + , vector + , vulkan + , vulkan-examples + , vulkan-utils + , vulkan-utils-spirv + default-language: Haskell2010 + if os(windows) + ghc-options: -optl-mconsole + executable triangle-dynamic main-is: Main.hs other-modules: diff --git a/stack.yaml b/stack.yaml index e7388215a..6734c6f57 100644 --- a/stack.yaml +++ b/stack.yaml @@ -2,6 +2,7 @@ resolver: lts-22.44 packages: - . - utils +- utils-spirv - utils-init/vulkan-init-sdl2 - utils-init/vulkan-init-glfw - examples @@ -13,6 +14,13 @@ extra-deps: - unification-fd-0.12.0.2 # generate-new - validation-selective-0.2.0.0 +- geomancy-0.3.0.1 +- gl-block-1.1 +- ptrdiff-0 +- spirv-enum-1.104.350.0 +- spirv-reflect-ffi-0.4 +- spirv-reflect-types-0.4 +- webcolor-labels-0.1.0.0 # use ghcup, nix, setup actions system-ghc: true diff --git a/utils-spirv/README.md b/utils-spirv/README.md new file mode 100644 index 000000000..2e6860510 --- /dev/null +++ b/utils-spirv/README.md @@ -0,0 +1,73 @@ +# vulkan-utils-spirv + +Generate Haskell data types and Vulkan descriptor-set / pipeline-layout +`*CreateInfo` values from compiled SPIR-V, at compile time, via +[`spirv-reflect`](https://hackage.haskell.org/package/spirv-reflect-ffi) +reflection and [`gl-block`](https://hackage.haskell.org/package/gl-block) +std140/std430 layout. + +## Types from a shader + +A Template Haskell splice generates a record — with a std140/std430 `Storable` +derived via gl-block — for every uniform / storage / push-constant block the +shader declares: + +```haskell +import Vulkan.Utils.SpirV.TH (reflectShaderTypes) + +-- e.g. `Scene { view :: Mat4, lightDir :: Vec3, time :: Float }` (geomancy types), +-- ready to poke straight into a mapped buffer. +reflectShaderTypes "shaders/scene.vert.spv" +``` + +## Pipeline layout from reflection + +`allocateReflectedLayout` merges the descriptor-set layouts and push-constant +ranges across a family of shaders — stage flags OR-ed, shared blocks +cross-checked — into one `PipelineLayout`. `allocateGraphicsPipeline` then builds +each pipeline against it, folding in the vertex stage's reflected vertex input: + +```haskell +import Data.SpirV.Reflect.FFI (loadBytes) +import Vulkan.Utils.DynamicRendering qualified as Dynamic +import Vulkan.Utils.SpirV.Pipeline (allocateGraphicsPipeline, allocateReflectedLayout) +import Vulkan.Zero (zero) + +vertModule <- loadBytes vertSpv +fragModule <- loadBytes fragSpv + +-- one layout for the whole family +(_, layout) <- allocateReflectedLayout dev [vertModule, fragModule] + +(_, pipeline) <- + allocateGraphicsPipeline dev layout + zero{Dynamic.colorFormats = [colorFormat], Dynamic.depthFormat = Just depthFormat} + () -- specialization; () for none + [(vertModule, vertSpv), (fragModule, fragSpv)] +``` + +## Compile-time stage composition + +`reflectStageSig` emits a per-shader signature; `MatchInterface` / +`CompatibleResources` then check — at compile time — that the fragment inputs +match the vertex outputs and that any shared descriptor blocks agree. A mismatch +is a type error, not a validation-layer message at runtime: + +```haskell +import Vulkan.Utils.SpirV.Stage (CompatibleResources, MatchInterface, reflectStageSig) + +reflectStageSig "VertSig" "shaders/scene.vert.spv" +reflectStageSig "FragSig" "shaders/scene.frag.spv" + +-- only type-checks if the two stages compose +pipelineComposes :: (MatchInterface VertSig FragSig, CompatibleResources VertSig FragSig) => Bool +pipelineComposes = True +``` + +## Examples + +Four end-to-end, validation-clean programs under +[`examples/`](../examples): `compute-reflect`, `pathtrace-reflect` (buffer +device address / BVH), `mesh-reflect` (a vertex shader driving a z-prepass and a +shaded pass off one merged layout), and `texture-reflect` (colour-attachment-as- +texture with reflected vertex attributes). diff --git a/utils-spirv/package.yaml b/utils-spirv/package.yaml new file mode 100644 index 000000000..4c3af9d61 --- /dev/null +++ b/utils-spirv/package.yaml @@ -0,0 +1,85 @@ +name: vulkan-utils-spirv +version: "0.1.0.0" +synopsis: Generate Haskell types and Vulkan descriptor/pipeline layouts from SPIR-V reflection +category: Graphics +maintainer: Ellie Hermaszewska +github: expipiplus1/vulkan +extra-source-files: +- README.md +- package.yaml +- test/fixtures/array-field.comp.spv +- test/fixtures/array2d.comp.spv +- test/fixtures/julia.comp.spv +- test/fixtures/mesh.frag.spv +- test/fixtures/mesh.vert.spv +- test/fixtures/nested.comp.spv +- test/fixtures/push.comp.spv +- test/fixtures/spec.comp.spv +- test/fixtures/ssbo-struct.comp.spv +- test/fixtures/tri.vert.spv +- test/fixtures/wide.comp.spv + +library: + source-dirs: src + dependencies: + - base >= 4.16 && <5 + - bytestring + - containers + - gl-block + - ptrdiff + - resourcet + - spirv-enum + - spirv-reflect-ffi + - spirv-reflect-types + - template-haskell + - text + - unliftio-core + - vector + - vulkan >= 3.27 && < 3.28 + - vulkan-utils + +tests: + spec: + main: Spec.hs + source-dirs: test + dependencies: + - base <5 + - containers + - geomancy + - gl-block + - spirv-reflect-ffi + - spirv-reflect-types + - tasty + - tasty-hunit + - template-haskell + - text + - vector + - vulkan + - vulkan-utils-spirv + +ghc-options: +- -Wall + +default-extensions: +- BlockArguments +- DataKinds +- DeriveAnyClass +- DeriveGeneric +- DerivingStrategies +- DerivingVia +- DuplicateRecordFields +- FlexibleContexts +- FlexibleInstances +- ImportQualifiedPost +- LambdaCase +- NamedFieldPuns +- OverloadedRecordDot +- OverloadedStrings +- PatternSynonyms +- RecordWildCards +- ScopedTypeVariables +- TemplateHaskell +- TupleSections +- TypeApplications +- TypeOperators +- ViewPatterns diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Array.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Array.hs new file mode 100644 index 000000000..815190e05 --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Array.hs @@ -0,0 +1,198 @@ +-- These extensions are not in the package-wide default-extensions. +{-# LANGUAGE ExplicitNamespaces #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} + +{-| A fixed-length array suitable as a std140\/std430 block /field/. + +@'Array' n a@ is @n@ elements of @a@ laid out as an array member: each element +occupies a layout-dependent stride (std140 rounds the element stride up to a +multiple of 16; std430 packs to the element's own alignment). The element type's +'Block' instance does the per-element encoding, so anything that is a 'Block' (a +scalar, a vector, or a generated struct record) can be the element. + +The backing 'VS.Vector' should hold exactly @n@ elements; 'write140' \/ 'write430' +write the first @n@ and ignore any extra (so a buffer is never overrun). +-} +module Vulkan.Utils.SpirV.Array + ( Array (..) + , unsafeFromList + , toList + + -- * Runtime-sized arrays + -- $runtime + , std140Stride + , std430Stride + , pokeStd140 + , pokeStd430 + , peekStd140 + , peekStd430 + ) where + +import Control.Monad.IO.Class (MonadIO (..)) +import Data.Proxy (Proxy (..)) +import Data.Vector.Storable qualified as VS +import Foreign.Ptr (Ptr, castPtr) +import Foreign.Ptr.Diff (Diff (..)) +import Foreign.Storable (Storable (..)) +import GHC.TypeNats (KnownNat, Nat, natVal) +import GHC.TypeNats qualified as TN +import Graphics.Gl.Block (Block (..), roundUp) + +-- | @n@ elements of @a@, laid out as an array block member. +newtype Array (n :: Nat) a = Array (VS.Vector a) + +deriving instance (Storable a, Eq a) => Eq (Array n a) +deriving instance (Storable a, Show a) => Show (Array n a) + +-- | Build an 'Array' from a list (should be @n@ elements long). +{-# INLINE unsafeFromList #-} +unsafeFromList :: forall n a. (KnownNat n, Storable a) => [a] -> Array n a +unsafeFromList = Array . VS.fromListN (count (Proxy @n)) + +-- | The elements of an 'Array' as a list. +toList :: (Storable a) => Array n a -> [a] +toList (Array v) = VS.toList v + +{- | A tight, host-side 'Storable' (n contiguous elements). This is unrelated to +the gl-block layout below — it only lets an 'Array' nest as the element of +another 'Array' (multi-dimensional arrays: @'Array' h ('Array' w a)@). +-} +instance (KnownNat n, Storable a) => Storable (Array n a) where + sizeOf _ = count (Proxy @n) * sizeOf (undefined :: a) + alignment _ = alignment (undefined :: a) + peek ptr = Array <$> VS.generateM (count (Proxy @n)) (peekElemOff (castPtr ptr)) + poke ptr (Array v) = VS.imapM_ (pokeElemOff (castPtr ptr)) (VS.take (count (Proxy @n)) v) + +instance (KnownNat n, Block a, Storable a) => Block (Array n a) where + type PackedSize (Array n a) = n TN.* PackedSize a + + isStruct _ = True + + alignment140 _ = lcm 16 (alignment140 (Proxy @a)) + alignment430 _ = alignment430 (Proxy @a) + + sizeOf140 _ = count (Proxy @n) * std140Stride (Proxy @a) + sizeOf430 _ = count (Proxy @n) * std430Stride (Proxy @a) + sizeOfPacked _ = count (Proxy @n) * sizeOfPacked (Proxy @a) + + read140 = readArrayWith (count (Proxy @n)) (std140Stride (Proxy @a)) read140 + read430 = readArrayWith (count (Proxy @n)) (std430Stride (Proxy @a)) read430 + readPacked = readArrayWith (count (Proxy @n)) (sizeOfPacked (Proxy @a)) readPacked + + write140 = writeArrayWith (count (Proxy @n)) (std140Stride (Proxy @a)) write140 + write430 = writeArrayWith (count (Proxy @n)) (std430Stride (Proxy @a)) write430 + writePacked = writeArrayWith (count (Proxy @n)) (sizeOfPacked (Proxy @a)) writePacked + {-# INLINE alignment140 #-} + {-# INLINE alignment430 #-} + {-# INLINE isStruct #-} + {-# INLINE read140 #-} + {-# INLINE read430 #-} + {-# INLINE readPacked #-} + {-# INLINE sizeOf140 #-} + {-# INLINE sizeOf430 #-} + {-# INLINE write140 #-} + {-# INLINE write430 #-} + {-# INLINE writePacked #-} + +{-# INLINE count #-} +count :: (KnownNat n) => Proxy n -> Int +count = fromIntegral . natVal + +{- | Per-element stride for an array member: std140 rounds the element size up to +a multiple of 16. +-} +std140Stride :: (Block a) => Proxy a -> Int +std140Stride p = roundUp (sizeOf140 p) (lcm 16 (alignment140 p)) + +{- | Per-element stride for an array member: std430 packs to the element's own +base alignment. +-} +std430Stride :: (Block a) => Proxy a -> Int +std430Stride p = roundUp (sizeOf430 p) (alignment430 p) + +{- $runtime +A runtime-sized array (the trailing @T[]@ of a storage buffer) has no +compile-time length, so it is /not/ a fixed-size 'Block' and is represented +directly as a @'VS.Vector' a@. These helpers (de)serialize such a vector at the +layout's element stride, using the element's own 'Block' instance — so an element +whose array stride differs from its packed size (e.g. a @vec3@, or a struct +padded up to its alignment) is still placed correctly. The 'Ptr' should point at +the start of the array (for a buffer whose only contents are the array, that is +the mapped base pointer). +-} + +-- | Write a runtime-length array of std140 elements starting at @ptr@. +{-# INLINE pokeStd140 #-} +pokeStd140 :: forall a x m. (Block a, Storable a, MonadIO m) => Ptr x -> VS.Vector a -> m () +pokeStd140 = pokeArrayAt (std140Stride (Proxy @a)) write140 + +-- | Write a runtime-length array of std430 elements starting at @ptr@. +{-# INLINE pokeStd430 #-} +pokeStd430 :: forall a x m. (Block a, Storable a, MonadIO m) => Ptr x -> VS.Vector a -> m () +pokeStd430 = pokeArrayAt (std430Stride (Proxy @a)) write430 + +-- | Read @n@ std140 elements starting at @ptr@. +{-# INLINE peekStd140 #-} +peekStd140 :: forall a x m. (Block a, Storable a, MonadIO m) => Int -> Ptr x -> m (VS.Vector a) +peekStd140 = peekArrayAt (std140Stride (Proxy @a)) read140 + +-- | Read @n@ std430 elements starting at @ptr@. +{-# INLINE peekStd430 #-} +peekStd430 :: forall a x m. (Block a, Storable a, MonadIO m) => Int -> Ptr x -> m (VS.Vector a) +peekStd430 = peekArrayAt (std430Stride (Proxy @a)) read430 + +{-# INLINE pokeArrayAt #-} +pokeArrayAt + :: (Storable a, MonadIO m) + => Int + -> (Ptr x -> Diff x a -> a -> IO ()) + -> Ptr x + -> VS.Vector a + -> m () +pokeArrayAt stride wr ptr v = + liftIO $ VS.imapM_ (\i x -> wr ptr (Diff (i * stride)) x) v + +{-# INLINE peekArrayAt #-} +peekArrayAt + :: (Storable a, MonadIO m) + => Int + -> (Ptr x -> Diff x a -> IO a) + -> Int + -> Ptr x + -> m (VS.Vector a) +peekArrayAt stride rd n ptr = + liftIO $ VS.generateM n (\i -> rd ptr (Diff (i * stride))) + +{-# INLINE readArrayWith #-} +readArrayWith + :: (Storable a, MonadIO m) + => Int + -- ^ element count + -> Int + -- ^ element stride + -> (Ptr x -> Diff x a -> IO a) + -- ^ element reader + -> Ptr x + -> Diff x (Array n a) + -> m (Array n a) +readArrayWith n stride rd ptr (Diff o) = + liftIO $ Array <$> VS.generateM n (\i -> rd ptr (Diff (o + i * stride))) + +{-# INLINE writeArrayWith #-} +writeArrayWith + :: (Storable a, MonadIO m) + => Int + -- ^ element count + -> Int + -- ^ element stride + -> (Ptr x -> Diff x a -> a -> IO ()) + -- ^ element writer + -> Ptr x + -> Diff x (Array n a) + -> Array n a + -> m () +writeArrayWith n stride wr ptr (Diff o) (Array v) = + liftIO $ VS.imapM_ (\i x -> wr ptr (Diff (o + i * stride)) x) (VS.take n v) diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Block.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Block.hs new file mode 100644 index 000000000..5c8e7268e --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Block.hs @@ -0,0 +1,288 @@ +{-# LANGUAGE NoFieldSelectors #-} + +{-| Generate a Haskell record type for a reflected uniform/storage/push-constant +block, deriving its layout from gl-block: @deriving anyclass 'Block'@ (via +"GHC.Generics") plus @deriving 'Storable' via ('Std140' \/ 'Std430')@. + +Splice sites must therefore have @Graphics.Gl.Block (Std140(..), Std430(..))@ in +scope (the @via@ coercion needs the newtype constructor visible) and enable +@DataKinds@ and @TypeFamilies@ (each record also gets a +'Vulkan.Utils.SpirV.Signature.KnownLayout' instance carrying its type-level +layout signature). + +== Guardrail +gl-block's @Generic@ 'Block' instance lays a struct out correctly only when its +field alignments are non-increasing. A lower-aligned field that precedes a +higher-aligned one (e.g. @float@ before @uvec2@) is over-padded and no longer +matches the shader's std140/std430 offsets. Until that is fixed in gl-block, +'structRecordDec' refuses (with a compile error) to generate such a record rather +than emit a silently-wrong layout. +-} +module Vulkan.Utils.SpirV.Block + ( structTypeName + , structRecordDec + , collectStructs + , allMembers16 + ) where + +import Control.Monad (filterM) +import Data.Char (toLower) +import Data.List (intercalate, mapAccumL) +import Data.Maybe (fromMaybe) +import Data.Text qualified as Text +import Data.Vector qualified as V +import Foreign.Storable (Storable) +import GHC.Generics (Generic) +import Graphics.Gl.Block (Block, Std140, Std430) +import Language.Haskell.TH + +import Data.SpirV.Reflect.TypeDescription (TypeDescription) +import Data.SpirV.Reflect.TypeDescription qualified + +import Vulkan.Utils.SpirV.Array (Array) +import Vulkan.Utils.SpirV.DeviceAddress (DeviceAddress) +import Vulkan.Utils.SpirV.Layout (offsetMapOf) +import Vulkan.Utils.SpirV.Signature (knownLayoutInstance) +import Vulkan.Utils.SpirV.Types (LayoutMode (..), NumericType (..), TypeMap, arrayBaseAlign, arrayDims, classifyType, isBdaPointer, leafAlignment, structBaseAlign) + +{- | A block's struct together with every nested and array-element struct +reachable from it, each tagged with the layout to generate it under. This lets +an SSBO array of structs (e.g. @Sphere spheres[]@) yield a record for the +element type even though the wrapping block — a runtime array — isn't itself +representable as a flat record (the caller skips it). +-} +collectStructs :: LayoutMode -> TypeDescription -> [(LayoutMode, TypeDescription)] +collectStructs layout root = snd (go [] root) + where + -- Thread a visited set of SPIR-V ids so a recursive @buffer_reference@ type + -- (a shared DAG / cycle) is collected exactly once rather than unrolled + -- forever — a self-referential @Node@ would otherwise loop here. + go seen td + | maybe False (`elem` seen) td.id = (seen, []) + | otherwise = + let + seen' = maybe seen (: seen) td.id + (seenN, kids) = mapAccumL go seen' (concatMap targets (V.toList td.members)) + in + (seenN, (layout, td) : concat kids) + + -- The struct(s) reachable from a member: an array- or pointer-element struct + -- (via @struct_type_description@), or a directly-nested struct. Leaf members + -- (scalars/vectors/matrices, and device-address pointers) contribute none. + targets m = + case m.struct_type_description of + Just s -> [s] + Nothing + | not (V.null m.members) -> [m] + | otherwise -> [] + +{- | The Haskell type name to generate for an @OpTypeStruct@ 'TypeDescription': +its @type_name@, used verbatim (SPIR-V struct names are already capitalised +Haskell-friendly identifiers). +-} +structTypeName :: TypeDescription -> Maybe String +structTypeName td = case td.type_name of + Just t | not (Text.null t) -> Just (Text.unpack t) + _ -> Nothing + +{- | A classified record field: its name, Haskell type, base alignment (bytes) +under the layout, and whether it is a leaf (scalar/vector/matrix) as opposed to +a nested struct. +-} +data Field = Field + { name :: Name + , type' :: Type + , align :: Int + , leaf :: Bool + } + +{- | Generate the record @data@ declaration for an @OpTypeStruct@, deriving its +'Block' / 'Storable' from gl-block. Returns 'Nothing' if it has no usable name +or a member cannot be represented as a field (e.g. an array — see the +arrays-in-records gap), in which case the caller should skip it. Fails the +splice (guardrail) if the field order would defeat gl-block's std140/std430 +layout. Nested struct members become fields of the corresponding generated +record type. +-} +structRecordDec :: TypeMap -> LayoutMode -> TypeDescription -> Q (Maybe [Dec]) +structRecordDec tymap layout td = + case (structTypeName td, classifiedFields tymap layout (V.toList td.members)) of + (Just nameStr, Just classified) -> + case layoutViolation classified of + Just msg -> + fail $ + "vulkan-utils-spirv: refusing to generate record '" + <> nameStr + <> "': " + <> msg + Nothing -> do + let + tyName = mkName nameStr + recFields = + [ (f.name, Bang NoSourceUnpackedness NoSourceStrictness, f.type') + | f <- classified + ] + wrapper = case layout of + Std140Layout -> ''Std140 + Std430Layout -> ''Std430 + -- Derive @Show@/@Eq@ only when every field type supports them. Some + -- mapped types (e.g. geomancy's @Mat4@) lack @Eq@, and a nested struct + -- field's instances aren't yet visible mid-splice, so a record with a + -- struct field derives neither. + stockExtra <- + if all (.leaf) classified + then filterM (allFieldsAreInstances [f.type' | f <- classified]) [''Show, ''Eq] + else pure [] + let + dataDec = + DataD + [] + tyName + [] + Nothing + [RecC tyName recFields] + [ DerivClause (Just StockStrategy) (ConT ''Generic : map ConT stockExtra) + , DerivClause (Just AnyclassStrategy) [ConT ''Block] + , DerivClause + (Just (ViaStrategy (ConT wrapper `AppT` ConT tyName))) + [ConT ''Storable] + ] + -- Tie a type-level layout signature to the record, computed from + -- the same reflection (the value-level offset map is the oracle). Skipped + -- if the offset map can't be computed (shouldn't happen for a record we + -- already classified). + sigDecs = either (const []) (knownLayoutInstance tyName) (offsetMapOf layout td) + pure (Just (dataDec : sigDecs)) + _ -> pure Nothing + +-- | True when every field type is an instance of the given class. +allFieldsAreInstances :: [Type] -> Name -> Q Bool +allFieldsAreInstances tys cls = and <$> traverse (\t -> isInstance cls [t]) tys + +{- | Classify every member, failing (with 'Nothing') if any cannot be represented +as a record field (e.g. an array member). +-} +classifiedFields :: TypeMap -> LayoutMode -> [TypeDescription] -> Maybe [Field] +classifiedFields tymap layout = traverse $ \mem -> do + fname <- memberFieldName mem + (ty, align, isLeaf) <- fieldOf tymap layout mem + pure Field{name = mkName fname, type' = ty, align, leaf = isLeaf} + +{- | The Haskell type, base alignment and leaf-ness of a single member, or +'Nothing' if it can't be a field (a runtime\/multi-dimensional array, or an +unrecognised type). A fixed-size array becomes an @'Array' n@ field. +-} +fieldOf :: TypeMap -> LayoutMode -> TypeDescription -> Maybe (Type, Int, Bool) +fieldOf tymap layout mem = + case arrayDims mem of + [] -> elementOf tymap layout mem + dims + | all (> 0) dims -> do + -- Fixed-size array (any dimensionality). Multi-dimensional arrays nest: + -- @a[h][w]@ -> @Array h (Array w a)@ (outermost dimension first). + (elemTy, elemAlign, _) <- elementOf tymap layout mem + pure (foldr wrapArray elemTy dims, arrayBaseAlign layout elemAlign, False) + | otherwise -> Nothing -- contains a runtime (0) dimension + where + wrapArray d ty = ConT ''Array `AppT` LitT (NumTyLit (fromIntegral d)) `AppT` ty + +{- | The Haskell type, base alignment and leaf-ness of a member's element type +(i.e. ignoring any array dimensions). +-} +elementOf :: TypeMap -> LayoutMode -> TypeDescription -> Maybe (Type, Int, Bool) +elementOf tymap layout mem + -- A buffer_reference pointer: an 8-byte device address, typed by its pointee + -- record when that is a named struct (else @DeviceAddress ()@). The pointee + -- struct is generated separately by 'collectStructs', not inlined here. The + -- REF check precedes 'classifyType' (a pointer also carries the pointee's leaf + -- flags, e.g. @REF INT@). + | isBdaPointer mem = + -- Reported as non-leaf so the record skips auto-deriving Show/Eq: the + -- pointee (often the record being defined — a self-referential @Node@) is + -- not in scope mid-splice, so probing @Show (DeviceAddress Node)@ would + -- fail, exactly as for a nested-struct field. + let + pointee = mem.struct_type_description >>= structTypeName + arg = maybe (TupleT 0) (ConT . mkName) pointee + in + Just (ConT ''DeviceAddress `AppT` arg, 8, False) + | otherwise = + case classifyType mem of + Just numeric -> do + let numeric' = numeric{array = []} + ty <- tymap numeric' + pure (ty, leafAlignment layout numeric'.scalar numeric'.shape, True) + Nothing -> do + -- A nested struct member: field of the corresponding generated record. + let s = fromMaybe mem mem.struct_type_description + nm <- structTypeName s + align <- structAlignment layout s + pure (ConT (mkName nm), align, False) + +{- | Guardrail: gl-block's 'Generic' layout matches std140/std430 only when field +alignments are non-increasing. Report the first inversion, if any. +-} +layoutViolation :: [Field] -> Maybe String +layoutViolation classified = go [(nameBase f.name, f.align) | f <- classified] + where + go ((n1, a1) : rest@((n2, a2) : _)) + | a1 < a2 = + Just $ + intercalate + " " + [ "field '" <> n1 <> "' (alignment " <> show a1 <> ")" + , "precedes higher-aligned field '" <> n2 <> "' (alignment " <> show a2 <> ");" + , "gl-block's Generic layout would not match std140/std430." + , "Reorder the block's fields by non-increasing alignment" + , "(temporary gl-block limitation)." + ] + | otherwise = go rest + go _ = Nothing + +{- | The base alignment (bytes) of a struct under the given layout: the largest +member alignment, rounded up to 16 for std140. 'Nothing' if any member's +alignment can't be determined. +-} +structAlignment :: LayoutMode -> TypeDescription -> Maybe Int +structAlignment layout td = do + as <- traverse (memberBaseAlignment layout) (V.toList td.members) + pure (structBaseAlign layout as) + +-- | The base alignment of any member (leaf, nested struct, or fixed array). +memberBaseAlignment :: LayoutMode -> TypeDescription -> Maybe Int +memberBaseAlignment layout mem = + case arrayDims mem of + [] -> elementBaseAlignment layout mem + dims + | all (> 0) dims -> arrayBaseAlign layout <$> elementBaseAlignment layout mem + | otherwise -> Nothing + +-- | The base alignment of a member's element type (ignoring array dimensions). +elementBaseAlignment :: LayoutMode -> TypeDescription -> Maybe Int +elementBaseAlignment layout mem + | isBdaPointer mem = Just 8 -- device address: 8-byte scalar + | otherwise = + case classifyType mem of + Just numeric -> Just (leafAlignment layout numeric.scalar numeric.shape) + Nothing -> structAlignment layout (fromMaybe mem mem.struct_type_description) + +{- | True when every member of the struct has 16-byte base alignment. Such a +struct lays out identically under std140 and std430 (same offsets, same size), +so a single generated record can be shared across both layouts ("promotion"). +Anything else used across layouts is rejected by the sharing guardrail. +-} +allMembers16 :: TypeDescription -> Bool +allMembers16 td = not (null ms) && all is16 ms + where + ms = V.toList td.members + is16 m = memberBaseAlignment Std430Layout m == Just 16 + +uncapitalize :: String -> String +uncapitalize [] = [] +uncapitalize (c : cs) = toLower c : cs + +{- | The Haskell record-field name for a reflected member: its declared name, +uncapitalised (SPIR-V capitalises member names, Haskell fields are lower-case). +-} +memberFieldName :: TypeDescription -> Maybe String +memberFieldName = fmap (uncapitalize . Text.unpack) . (.struct_member_name) diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Buffer.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Buffer.hs new file mode 100644 index 000000000..2c0026d68 --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Buffer.hs @@ -0,0 +1,138 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} + +{-| A block of memory tagged with a type-level layout signature. + +A @'Buffer' sig@ is host memory whose byte layout is described by @sig@ (a +'SigOffsetMap' from "Vulkan.Utils.SpirV.Signature"). 'readBuffer' \/ 'writeBuffer' +move whole records in and out, but only for a record whose own layout 'Fits' the +buffer's signature — so a layout mismatch is a /compile/ error, and because +layout-equivalent records share a signature (and 'Fits' accepts a compatible one), +a value written as one record can be read back through any layout-compatible record +(a structural view). + +@ +buf <- 'newBuffer' (MyUbo …) -- Buffer (Sig MyUbo) +view <- 'readBuffer' buf -- any r with Fits (Sig r) (Sig MyUbo) +@ + +The signature is the soundness boundary: 'newBuffer' derives it from the record +it writes (sound by construction); 'unsafeAsBuffer' tags a foreign pointer you +already hold (e.g. VMA @mappedData@) with a signature /you/ assert it satisfies — +that assertion is exactly the reflected buffer's layout. +-} +module Vulkan.Utils.SpirV.Buffer + ( Buffer + , newBuffer + , allocBuffer + , allocArrayBuffer + , readBuffer + , writeBuffer + , readBufferElem + , writeBufferElem + , bufferSize + , withBufferPtr + , unsafeAsBuffer + , unsafeAsBufferPtr + ) where + +import Control.Monad.IO.Class (MonadIO (..)) +import Data.Proxy (Proxy (..)) +import Data.Word (Word8) +import Foreign.ForeignPtr (ForeignPtr, castForeignPtr, mallocForeignPtrBytes, newForeignPtr_, withForeignPtr) +import Foreign.Ptr (Ptr, castPtr, plusPtr) +import Foreign.Storable (Storable (..)) + +import Vulkan.Utils.SpirV.Layout qualified -- brings OffsetMap's fields into scope for HasField/ORD +import Vulkan.Utils.SpirV.Signature (Fits, FitsTail, KnownArrayTail, KnownLayout, KnownSigOffsetMap, Sig, SigOffsetMap, arrayTailBaseStride, sigOffsetMapVal) + +-- | Host memory laid out as @sig@. +newtype Buffer (sig :: SigOffsetMap) = Buffer (ForeignPtr Word8) + +-- | Allocate and initialize a buffer from a record, taking its layout signature. +newBuffer :: forall r io. (KnownLayout r, Storable r, MonadIO io) => r -> io (Buffer (Sig r)) +newBuffer r = liftIO $ do + fp <- mallocForeignPtrBytes (sizeOf r) + withForeignPtr fp $ \p -> poke (castPtr p) r + pure (Buffer fp) + +{- | Allocate an uninitialized buffer sized for the (statically-sized) signature +@sig@; use with a type application, e.g. @allocBuffer \@(Sig MyUbo)@. +-} +allocBuffer :: forall sig io. (KnownSigOffsetMap sig, MonadIO io) => io (Buffer sig) +allocBuffer = liftIO $ case sigSize @sig of + Just n -> Buffer <$> mallocForeignPtrBytes n + Nothing -> + ioError (userError "allocBuffer: runtime-sized layout has no static size") + +-- | Read a record out of a buffer, if its layout fits the buffer's signature. +readBuffer :: forall r sig io. (Storable r, Fits (Sig r) sig, MonadIO io) => Buffer sig -> io r +readBuffer (Buffer fp) = liftIO (withForeignPtr fp (peek . castPtr)) + +-- | Write a record into a buffer, if its layout fits the buffer's signature. +writeBuffer :: forall r sig io. (Storable r, Fits (Sig r) sig, MonadIO io) => Buffer sig -> r -> io () +writeBuffer (Buffer fp) r = liftIO (withForeignPtr fp (\p -> poke (castPtr p) r)) + +{- | Allocate a runtime-array buffer (an 'ArrayOf' signature) sized for @n@ +elements: @base + n * stride@ bytes, both taken from the signature's tail. +-} +allocArrayBuffer :: forall sig io. (KnownArrayTail sig, MonadIO io) => Int -> io (Buffer sig) +allocArrayBuffer n = liftIO (Buffer <$> mallocForeignPtrBytes (base + n * stride)) + where + (base, stride) = arrayTailBaseStride @sig + +{- | Read the @i@-th element of a runtime-array buffer (an SSBO @T[]@), if the +record's layout fits the buffer's array tail. The element is at byte offset +@base + i * stride@ taken from the buffer's signature. +-} +readBufferElem :: forall r sig io. (Storable r, FitsTail (Sig r) sig, KnownArrayTail sig, MonadIO io) => Buffer sig -> Int -> io r +readBufferElem (Buffer fp) i = + liftIO (withForeignPtr fp (\p -> peek (castPtr (p `plusPtr` elemOffset @sig i)))) + +{- | Write the @i@-th element of a runtime-array buffer, if the record's layout +fits the buffer's array tail. +-} +writeBufferElem :: forall r sig io. (Storable r, FitsTail (Sig r) sig, KnownArrayTail sig, MonadIO io) => Buffer sig -> Int -> r -> io () +writeBufferElem (Buffer fp) i r = + liftIO (withForeignPtr fp (\p -> poke (castPtr (p `plusPtr` elemOffset @sig i)) r)) + +{- | The byte offset of array element @i@ from the buffer signature's tail. + +'KnownArrayTail' provides the @(base, stride)@ from the type level. +-} +elemOffset :: forall sig. (KnownArrayTail sig) => Int -> Int +elemOffset i = base + i * stride + where + (base, stride) = arrayTailBaseStride @sig + +-- | The buffer's size in bytes, from its signature ('Nothing' if open-ended). +bufferSize :: forall sig. (KnownSigOffsetMap sig) => Buffer sig -> Maybe Int +bufferSize _ = sigSize @sig + +-- | The static byte size of a signature, if it is not runtime-sized. +sigSize :: forall sig. (KnownSigOffsetMap sig) => Maybe Int +sigSize = (sigOffsetMapVal (Proxy @sig)).size + +-- | Run an action on the raw buffer pointer (e.g. to upload to a GPU buffer). +withBufferPtr :: (MonadIO io) => Buffer sig -> (Ptr Word8 -> IO a) -> io a +withBufferPtr (Buffer fp) k = liftIO (withForeignPtr fp k) + +{- | Tag a foreign pointer with a layout signature you assert it satisfies. The +safety obligation is the caller's: @sig@ must match the memory's real layout +(for a reflected GPU buffer, its reflected block layout). +-} +unsafeAsBuffer :: ForeignPtr a -> Buffer sig +unsafeAsBuffer = Buffer . castForeignPtr + +{- | Tag a raw pointer (e.g. VMA @mappedData@) with an asserted layout signature. +The pointer's memory is owned elsewhere — no finalizer is attached, so the +caller must keep it alive for the buffer's lifetime. Same obligation as +'unsafeAsBuffer'. +-} +unsafeAsBufferPtr :: (MonadIO io) => Ptr a -> io (Buffer sig) +unsafeAsBufferPtr p = liftIO (Buffer . castForeignPtr <$> newForeignPtr_ p) diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Descriptors.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Descriptors.hs new file mode 100644 index 000000000..ed990378e --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Descriptors.hs @@ -0,0 +1,143 @@ +{-| Build Vulkan descriptor-set-layout and push-constant values from a reflected +'Module' at runtime. The reflected SPIR-V enums share Vulkan's numeric values, so +'Vk.DescriptorType' / 'Vk.ShaderStageFlags' come straight from the reflected +integers. +-} +module Vulkan.Utils.SpirV.Descriptors + ( descriptorSetLayoutInfos + , singleDescriptorSetLayoutInfo + , pushConstantRanges + , mergedDescriptorSetLayoutInfos + , mergedPushConstantRanges + , moduleStageFlags + ) where + +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.Maybe (fromMaybe) +import Data.Vector qualified as V +import Data.Word (Word32) +import Vulkan.Core10 qualified as Vk +import Vulkan.Zero (zero) + +import Data.SpirV.Reflect.BlockVariable qualified +import Data.SpirV.Reflect.DescriptorBinding qualified as DescriptorBinding +import Data.SpirV.Reflect.DescriptorSet qualified +import Data.SpirV.Reflect.Enums.DescriptorType qualified as R +import Data.SpirV.Reflect.Module (Module) +import Data.SpirV.Reflect.Module qualified + +import Vulkan.Utils.PipelineLayout (DescriptorBindingConflict (..), mergeDescriptorSetLayoutBindings, mergePushConstantRanges) +import Vulkan.Utils.SpirV.Layout (OffsetMap, mergeKeyed, renderMismatch) +import Vulkan.Utils.SpirV.Reflect.OffsetMaps (pushOffsetMaps, resourceOffsetMaps) + +{- | One @(set number, layout create-info)@ per reflected descriptor set, with +all bindings tagged with this module's shader stage. +-} +descriptorSetLayoutInfos :: Module -> [(Word32, Vk.DescriptorSetLayoutCreateInfo '[])] +descriptorSetLayoutInfos m = + [ ( ds.set + , zero + { Vk.bindings = + V.fromList [bindingToVk stage b | b <- V.toList ds.bindings] + } + ) + | ds <- V.toList m.descriptor_sets + ] + where + stage = moduleStageFlags m + +bindingToVk :: Vk.ShaderStageFlags -> DescriptorBinding.DescriptorBinding -> Vk.DescriptorSetLayoutBinding +bindingToVk stage b = + zero + { Vk.binding = b.binding + , Vk.descriptorType = Vk.DescriptorType (fromIntegral (descriptorTypeInt b)) + , Vk.descriptorCount = fromMaybe 1 b.count + , Vk.stageFlags = stage + } + +{- | The create-info of a module's sole descriptor set — the common single-set +shader. 'Left' if the module declares no sets or more than one. +-} +singleDescriptorSetLayoutInfo :: Module -> Either String (Vk.DescriptorSetLayoutCreateInfo '[]) +singleDescriptorSetLayoutInfo m = + case descriptorSetLayoutInfos m of + [(_, info)] -> Right info + [] -> Left "shader declares no descriptor sets" + sets -> Left ("shader declares " <> show (length sets) <> " descriptor sets, expected exactly one") + +{- | One range per reflected push-constant block, tagged with this module's +shader stage. +-} +pushConstantRanges :: Module -> [Vk.PushConstantRange] +pushConstantRanges m = + [ zero + { Vk.stageFlags = stage + , Vk.offset = pc.absolute_offset + , Vk.size = pc.size + } + | pc <- V.toList m.push_constants + ] + where + stage = moduleStageFlags m + +{- | Descriptor set layouts for a /pipeline/ built from several stages: bindings +are collected across all modules and each binding's @stageFlags@ is the OR of the +stages that declare it — correct for a set shared between, say, vertex and +fragment (unlike the single-stage 'descriptorSetLayoutInfos'). + +On top of the generic Vulkan-binding merge ('mergeDescriptorSetLayoutBindings'), +stages sharing a @(set, binding)@ must also agree on its /block layout/ (their +reflected offset maps are unified, see "Vulkan.Utils.SpirV.Layout"). Either a +descriptor-type or a layout disagreement is a 'Left' naming the offending binding. +-} +mergedDescriptorSetLayoutInfos :: [Module] -> Either String [(Word32, Vk.DescriptorSetLayoutCreateInfo '[])] +mergedDescriptorSetLayoutInfos modules = do + verifyKeyed descKey (concatMap resourceOffsetMaps modules) + traverse mergeSet (Map.toAscList bindingsBySet) + where + descKey (s, b) = "descriptor set " <> show s <> " binding " <> show b + + -- set -> the bindings every stage contributes to it (each tagged with its stage). + bindingsBySet :: Map Word32 [Vk.DescriptorSetLayoutBinding] + bindingsBySet = + Map.fromListWith + (flip (++)) + [ (b.set, [bindingToVk (moduleStageFlags m) b]) + | m <- modules + , b <- V.toList m.descriptor_bindings + ] + + mergeSet (setNo, bs) = case mergeDescriptorSetLayoutBindings bs of + Right merged -> Right (setNo, zero{Vk.bindings = V.fromList merged}) + Left c -> + Left (descKey (setNo, c.binding) <> ": descriptor type disagreement between stages") + +{- | Push-constant ranges for a multi-stage pipeline: ranges with the same offset +and size are merged with their stage flags OR-ed. Stages that share a range must +agree on its block layout (the offset maps are unified); a disagreement is a 'Left'. +-} +mergedPushConstantRanges :: [Module] -> Either String [Vk.PushConstantRange] +mergedPushConstantRanges modules = do + verifyKeyed pushKey (concatMap pushOffsetMaps modules) + pure (mergePushConstantRanges (concatMap pushConstantRanges modules)) + where + pushKey (o, sz) = "push constant at offset " <> show o <> " size " <> show sz + +-- | A module's shader stage as a Vulkan flag (SPIR-V shares Vulkan's values). +moduleStageFlags :: Module -> Vk.ShaderStageFlags +moduleStageFlags m = Vk.ShaderStageFlagBits (fromIntegral m.shader_stage) + +-- Layout agreement across stages. ----------------------------------------------- + +{- | Entries sharing a key must unify; the first disagreement is reported with the +key rendered by @shw@. +-} +verifyKeyed :: (Ord k) => (k -> String) -> [(k, OffsetMap)] -> Either String () +verifyKeyed shw entries = case mergeKeyed entries of + Right _ -> Right () + Left (k, mm) -> Left (shw k <> ": " <> renderMismatch mm) + +-- | The reflected descriptor type as its raw integer (Vulkan-compatible). +descriptorTypeInt :: DescriptorBinding.DescriptorBinding -> Int +descriptorTypeInt b = let R.DescriptorType n = b.descriptor_type in n diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/DeviceAddress.hs b/utils-spirv/src/Vulkan/Utils/SpirV/DeviceAddress.hs new file mode 100644 index 000000000..29fb9c8c1 --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/DeviceAddress.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TypeFamilies #-} + +{-| A 64-bit buffer device address — a GPU pointer to a @buffer_reference@ / +@PhysicalStorageBuffer@ block. + +A @buffer_reference@ member of a shader block is stored as an 8-byte address, not +the pointee inline, so reflection-driven codegen maps it to a 'DeviceAddress' +field. The phantom @a@ records the pointee's generated record type (e.g. +@DeviceAddress Node@), purely for documentation and type-safety at the call site; +the runtime representation is just the 'Word64' address. + +The 'Graphics.Gl.Block.Block' instance lays it out as a single 8-byte scalar +(alignment 8) under both std140 and std430 — matching @VkDeviceAddress@ — so a +generated record carrying one gets the right offsets via @deriving Storable via +(Std430 …)@. +-} +module Vulkan.Utils.SpirV.DeviceAddress + ( DeviceAddress (..) + ) where + +import Data.Word (Word64) +import Foreign.Ptr.Diff (peekDiffOff, pokeDiffOff) +import Foreign.Storable (Storable) +import Graphics.Gl.Block (Block (..)) + +-- | A device address pointing at a @buffer_reference@ block of type @a@. +newtype DeviceAddress a = DeviceAddress Word64 + deriving stock (Eq, Ord, Show) + deriving newtype (Storable) + +{- | One 8-byte scalar (alignment 8) under both layouts; mirrors the @Double@ +scalar instance gl-block ships, since @a@ is phantom. +-} +instance Block (DeviceAddress a) where + type PackedSize (DeviceAddress a) = 8 + alignment140 _ = 8 + sizeOf140 = sizeOfPacked + alignment430 = alignment140 + sizeOf430 = sizeOf140 + isStruct _ = False + read140 = peekDiffOff + write140 = pokeDiffOff + read430 = read140 + write430 = write140 + readPacked = read140 + writePacked = write140 diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Layout.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Layout.hs new file mode 100644 index 000000000..246451a7d --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Layout.hs @@ -0,0 +1,423 @@ +{-# LANGUAGE NoFieldSelectors #-} + +{-| A self-contained, reversible description of a block's memory layout, plus a +value-level unifier over those layouts. + +This is the IR the reflection-driven type machinery is built on. Two levels: + + * 'Layout' \/ 'Member' \/ 'FieldType' — the /structured/ form. It keeps every + member's name, byte offset, and full semantic shape (scalar kind, vector \/ + matrix dimensions, array sizes, nested structs), so it carries enough + information to drive code generation /and/ to be run backwards (e.g. towards + authoring SPIR-V), not merely the flattened offsets. + + * 'OffsetMap' — the /normalized/ form: the flattened offset table of occupied + scalar 'Slot's (each a byte offset + scalar type), plus an optional + runtime-array tail. This is exactly the offset map the GPU sees, so it is the + canonical form for /layout equivalence/: @mat4@, @vec4[4]@ and @float[16]@ + reduce to the same std430 offset map (and correctly diverge under std140, + where the array strides round to 16). This is structural/layout equivalence — + identical normalized bytes — not memory aliasing in the SPIR-V @Aliased@ \/ C + pointer sense. + +'unify' is the compatibility relation: not equality and not a prefix match, but +unification. Two layouts unify when their offset maps can be reconciled (a runtime +tail absorbs a concrete repeat, pinning its length); the result is the +most-defined offset map, which can be folded against further layouts ('foldUnify') +to accumulate a combined picture across pipelines. A failure is reported as a +'Mismatch' with a legible 'renderMismatch'. +-} +module Vulkan.Utils.SpirV.Layout + ( -- * Layout mode + layoutForDescriptor + + -- * Structured layout + , Layout (..) + , Member (..) + , FieldType (..) + , ArraySize (..) + , layoutOf + , fromFields + , leafFieldType + + -- * Normalized offset map + , Slot (..) + , OffsetMap (..) + , RuntimeTail (..) + , normalize + , offsetMapOf + + -- * Unification + , Mismatch (..) + , renderMismatch + , unify + , foldUnify + , mergeKeyed + ) where + +import Data.List (foldl', sortOn) +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.Maybe (fromMaybe, listToMaybe, mapMaybe) +import Data.Text (Text) +import Data.Text qualified as Text +import Data.Vector qualified as V +import Graphics.Gl.Block (roundUp) + +import Data.SpirV.Reflect.Enums.DescriptorType qualified as R +import Data.SpirV.Reflect.TypeDescription (TypeDescription) +import Data.SpirV.Reflect.TypeDescription qualified + +import Vulkan.Utils.SpirV.Types (LayoutMode (..), MemberShape (..), NumericType (..), ScalarType (..), arrayBaseAlign, arrayDims, classifyType, isBdaPointer, leafAlignment, scalarWidth, structBaseAlign) + +{- | The gl-block layout a descriptor's block follows: uniform buffers use +std140, storage buffers std430. Other descriptor types carry no block layout +('Nothing'). +-} +layoutForDescriptor :: R.DescriptorType -> Maybe LayoutMode +layoutForDescriptor = \case + R.DESCRIPTOR_TYPE_UNIFORM_BUFFER -> Just Std140Layout + R.DESCRIPTOR_TYPE_STORAGE_BUFFER -> Just Std430Layout + _ -> Nothing + +{- | A struct's layout under a given 'LayoutMode': its members at their resolved +byte offsets, total size and base alignment. 'size' is 'Nothing' when the struct +ends in a runtime-sized array (its size is not known statically). +-} +data Layout = Layout + { name :: Maybe Text + , mode :: LayoutMode + , members :: [Member] + , size :: Maybe Int + , align :: Int + } + deriving (Eq, Show) + +-- | A struct member: its name, byte offset within the struct, and structured type. +data Member = Member + { name :: Text + , offset :: Int + , type' :: FieldType + } + deriving (Eq, Show) + +{- | The structured type of a member. Multi-dimensional arrays nest +(@'ArrayOf' a ('ArrayOf' b t)@, outermost dimension first). +-} +data FieldType + = Scalar ScalarType + | -- | component count (2..4) + Vector ScalarType Int + | -- | columns, rows + Matrix ScalarType Int Int + | ArrayOf ArraySize FieldType + | Struct Layout + deriving (Eq, Show) + +-- | An array dimension: a fixed length, or a runtime (unsized) trailing array. +data ArraySize = Sized Int | Runtime + deriving (Eq, Show) + +-- Layout arithmetic (std140/std430). -------------------------------------------- + +{- | @(base alignment, size)@ of a field type; size is 'Nothing' for a runtime +array (open). +-} +fieldExtent :: LayoutMode -> FieldType -> (Int, Maybe Int) +fieldExtent mode = \case + Scalar s -> (leafAlignment mode s ShScalar, Just (scalarWidth s)) + Vector s n -> (leafAlignment mode s (ShVector n), Just (n * scalarWidth s)) + Matrix s c r -> + let colStride = leafAlignment mode s (ShMatrix c r) + in (colStride, Just (c * colStride)) + ArrayOf Runtime e -> + let (ea, _) = fieldExtent mode e + in (arrayBaseAlign mode ea, Nothing) + ArrayOf (Sized n) e -> + let + (ea, es) = fieldExtent mode e + align = arrayBaseAlign mode ea + stride = roundUp (fromMaybe 0 es) align + in + (align, Just (n * stride)) + Struct l -> (l.align, l.size) + +{- | The stride between elements of an array of @e@ under the layout. Defined only +when @e@ has a static size (i.e. is not itself a runtime array). +-} +arrayStride :: LayoutMode -> FieldType -> Int +arrayStride mode e = + let (ea, es) = fieldExtent mode e + in roundUp (fromMaybe 0 es) (arrayBaseAlign mode ea) + +-- Building from reflection. ----------------------------------------------------- + +-- | Compute the layout of a reflected @OpTypeStruct@ under the given mode. +layoutOf :: LayoutMode -> TypeDescription -> Either String Layout +layoutOf mode td = do + members <- traverse (memberOf mode) (V.toList td.members) + pure (fromFields mode (structName td) members) + +{- | Build a layout from named fields directly, computing each member's offset +and the struct's size and alignment (no reflection). Besides constructing +expected layouts, this is the direction an authoring front end would use. +-} +fromFields :: LayoutMode -> Maybe Text -> [(Text, FieldType)] -> Layout +fromFields mode name members = + Layout + { name + , mode + , members = [m | (m, _, _) <- placed] + , size = if openEnded then Nothing else Just (roundUp end baseAlign) + , align = baseAlign + } + where + placed = place mode members + baseAlign = structBaseAlign mode [a | (_, a, _) <- placed] + openEnded = any (\(_, _, sz) -> sz == Nothing) placed + end = case reverse placed of + [] -> 0 + ((Member _ off _, _, msz) : _) -> maybe off (off +) msz + +{- | Place members in declaration order, returning each at its offset together +with its alignment and (maybe-open) size. +-} +place :: LayoutMode -> [(Text, FieldType)] -> [(Member, Int, Maybe Int)] +place mode = go 0 + where + go _ [] = [] + go cursor ((nm, ft) : rest) = + let + (align, msz) = fieldExtent mode ft + off = roundUp cursor align + cursor' = maybe off (off +) msz + in + (Member nm off ft, align, msz) : go cursor' rest + +-- | The structured type of a single reflected member. +memberOf :: LayoutMode -> TypeDescription -> Either String (Text, FieldType) +memberOf mode mem = do + nm <- maybe (Left "member without a name") Right mem.struct_member_name + base <- baseFieldType mode mem + let + dims = arrayDims mem + wrap d ft = ArrayOf (if d == 0 then Runtime else Sized (fromIntegral d)) ft + pure (nm, foldr wrap base dims) + +{- | The element type of a member, ignoring array dimensions: a leaf +(scalar\/vector\/matrix) or a nested struct. +-} +baseFieldType :: LayoutMode -> TypeDescription -> Either String FieldType +baseFieldType mode mem + -- A buffer_reference pointer is an 8-byte device address, not the pointee + -- inline; the REF check precedes 'classifyType' because the pointer also + -- carries the pointee's leaf flags (e.g. @REF INT@). + | isBdaPointer mem = Right (Scalar STAddress) + | otherwise = + case classifyType mem of + Just numeric -> Right (leafFieldType numeric) + Nothing -> Struct <$> layoutOf mode (fromMaybe mem mem.struct_type_description) + +leafFieldType :: NumericType -> FieldType +leafFieldType NumericType{scalar, shape} = case shape of + ShScalar -> Scalar scalar + ShVector n -> Vector scalar n + ShMatrix c r -> Matrix scalar c r + +-- Normalization to the scalar offset map. --------------------------------------------- + +-- | One occupied scalar component at a byte offset. +data Slot = Slot + { offset :: Int + , scalar :: ScalarType + } + deriving (Eq, Ord, Show) + +{- | A runtime-sized array tail: a repeating element offset map starting at 'base' +with the given stride. The element 'Slot's are relative to the element start. +-} +data RuntimeTail = RuntimeTail + { base :: Int + , stride :: Int + , element :: [Slot] + } + deriving (Eq, Show) + +{- | A normalized layout: the concrete occupied scalar slots (sorted by offset), +the total size (if statically known), and an optional runtime-array tail. +-} +data OffsetMap = OffsetMap + { slots :: [Slot] + , size :: Maybe Int + , tail :: Maybe RuntimeTail + } + deriving (Eq, Show) + +-- | Flatten a structured 'Layout' to its offset map. +normalize :: Layout -> OffsetMap +normalize l = + OffsetMap + { slots = sortOn (.offset) (concatMap fst pieces) + , size = l.size + , tail = listToMaybe (mapMaybe snd pieces) + } + where + mode = l.mode + pieces = map (flattenMember mode) l.members + +-- | Build the offset map straight from reflection. +offsetMapOf :: LayoutMode -> TypeDescription -> Either String OffsetMap +offsetMapOf mode = fmap normalize . layoutOf mode + +flattenMember :: LayoutMode -> Member -> ([Slot], Maybe RuntimeTail) +flattenMember mode (Member _ off ft) = case ft of + ArrayOf Runtime e -> + ([], Just (RuntimeTail off (arrayStride mode e) (flattenAt mode 0 e))) + _ -> (flattenAt mode off ft, Nothing) + +-- | The scalar slots of a (statically-sized) field type at a base offset. +flattenAt :: LayoutMode -> Int -> FieldType -> [Slot] +flattenAt mode off = \case + Scalar s -> [Slot off s] + Vector s n -> let w = scalarWidth s in [Slot (off + i * w) s | i <- [0 .. n - 1]] + Matrix s c r -> + let + w = scalarWidth s + colStride = leafAlignment mode s (ShMatrix c r) + in + [Slot (off + col * colStride + row * w) s | col <- [0 .. c - 1], row <- [0 .. r - 1]] + ArrayOf (Sized n) e -> + let stride = arrayStride mode e + in concat [flattenAt mode (off + i * stride) e | i <- [0 .. n - 1]] + ArrayOf Runtime _ -> [] -- a runtime array can only be a struct's last member + Struct l -> concatMap (\(Member _ mo mft) -> flattenAt mode (off + mo) mft) l.members + +-- Unification. ------------------------------------------------------------------ + +-- | Why two offset maps fail to unify. +data Mismatch + = -- | differing scalar at a byte offset + SlotMismatch Int ScalarType ScalarType + | -- | differing number of occupied slots + CountMismatch Int Int + | -- | differing total size + SizeMismatch Int Int + | -- | incompatible runtime-array tails + TailMismatch String + deriving (Eq, Show) + +renderMismatch :: Mismatch -> String +renderMismatch = \case + SlotMismatch off l r -> + "scalar mismatch at byte offset " + <> show off + <> ": " + <> showScalar l + <> " vs " + <> showScalar r + CountMismatch l r -> + "different number of components: " <> show l <> " vs " <> show r + SizeMismatch l r -> + "different total size: " <> show l <> " vs " <> show r <> " bytes" + TailMismatch msg -> + "incompatible runtime arrays: " <> msg + where + showScalar STFloat = "float" + showScalar STDouble = "double" + showScalar STInt = "int" + showScalar STUInt = "uint" + showScalar STInt64 = "int64" + showScalar STUInt64 = "uint64" + showScalar STBool = "bool" + showScalar STAddress = "address" + +{- | Unify two offset maps into their most-defined common offset map, or report the first +'Mismatch'. A runtime tail on one side absorbs a matching concrete repeat on +the other (pinning the array length); two closed offset maps must coincide exactly. +-} +unify :: OffsetMap -> OffsetMap -> Either Mismatch OffsetMap +unify a b = case (a.tail, b.tail) of + (Nothing, Nothing) -> do + matchSlots a.slots b.slots + matchSize a.size b.size + pure a + (Just _, Nothing) -> absorb a b + (Nothing, Just _) -> absorb b a + (Just ta, Just tb) -> do + matchSlots a.slots b.slots + if ta == tb + then pure a + else Left (TailMismatch "tails differ in stride, element shape or base offset") + +{- | Reconcile an open offset map (with a runtime tail) against a closed one, requiring +the closed offset map to be the open offset map's prefix followed by whole repeats of its +tail element. Returns the closed (pinned) offset map. +-} +absorb :: OffsetMap -> OffsetMap -> Either Mismatch OffsetMap +absorb open closed = do + let + pre = open.slots + n = length pre + matchSlots pre (take n closed.slots) + let + tl = fromMaybe (error "absorb: open offset map has no tail") open.tail + rest = drop n closed.slots + matchRepeats tl rest + pure closed + +-- | The remaining slots must be zero or more whole repeats of the tail element. +matchRepeats :: RuntimeTail -> [Slot] -> Either Mismatch () +matchRepeats tl = go 0 + where + el = tl.element + m = length el + go _ [] = Right () + go j rest + | length chunk < m = + Left (TailMismatch "trailing bytes are not a whole array element") + | otherwise = do + matchSlots (shift (tl.base + j * tl.stride) el) chunk + go (j + 1) more + where + (chunk, more) = splitAt m rest + shift base = map (\(Slot o s) -> Slot (o + base) s) + +matchSlots :: [Slot] -> [Slot] -> Either Mismatch () +matchSlots xs ys + | length xs /= length ys = Left (CountMismatch (length xs) (length ys)) + | otherwise = + case [ (x.offset, x.scalar, y.scalar) + | (x, y) <- zip xs ys + , x.scalar /= y.scalar || x.offset /= y.offset + ] of + ((off, l, r) : _) -> Left (SlotMismatch off l r) + [] -> Right () + +matchSize :: Maybe Int -> Maybe Int -> Either Mismatch () +matchSize (Just l) (Just r) | l /= r = Left (SizeMismatch l r) +matchSize _ _ = Right () + +{- | Fold a layout against further layouts, accumulating the combined offset map (the +running most-general unifier). Use to add pipelines into a shared layout. +-} +foldUnify :: OffsetMap -> [OffsetMap] -> Either Mismatch OffsetMap +foldUnify = foldl' (\acc g -> acc >>= (`unify` g)) . Right + +{- | Merge layouts tagged by a key (e.g. a @(set, binding)@ or vertex @location@): +entries sharing a key must unify, disjoint keys union in. Reports the offending +key with its 'Mismatch'. +-} +mergeKeyed :: (Ord k) => [(k, OffsetMap)] -> Either (k, Mismatch) (Map k OffsetMap) +mergeKeyed = foldl' step (Right Map.empty) + where + step acc (k, g) = do + m <- acc + case Map.lookup k m of + Nothing -> Right (Map.insert k g m) + Just g' -> case unify g' g of + Right u -> Right (Map.insert k u m) + Left e -> Left (k, e) + +-- | The struct's @type_name@, treating an empty name as absent. +structName :: TypeDescription -> Maybe Text +structName td = td.type_name >>= \t -> if Text.null t then Nothing else Just t diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Pipeline.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Pipeline.hs new file mode 100644 index 000000000..18fcba861 --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Pipeline.hs @@ -0,0 +1,114 @@ +{-# LANGUAGE NoFieldSelectors #-} + +{-| Assemble a dynamic-rendering graphics pipeline from reflected SPIR-V: one +verified pipeline layout, merged across a /family/ of shaders, shared by every +pipeline built against it — each folding in its own reflected vertex input. + +Build the layout once from all the family's distinct modules +('allocateReflectedLayout'), so pipelines that differ only by specialization, +attachment formats or dynamic state share one 'Vk.PipelineLayout' and bind the +same descriptor sets. 'allocateGraphicsPipeline' then folds that layout and the +vertex stage's reflected vertex input into "Vulkan.Utils.DynamicRendering", +removing the by-hand wiring of merged set layouts + vertex input. +-} +module Vulkan.Utils.SpirV.Pipeline + ( ReflectedLayout (..) + , allocateReflectedLayout + , allocateGraphicsPipeline + ) where + +import Control.Monad.IO.Unlift (MonadUnliftIO) +import Control.Monad.Trans.Resource (MonadResource, ReleaseKey, allocate) +import Data.ByteString (ByteString) +import Data.List (find) +import Data.Vector qualified as V +import Data.Word (Word32) +import Vulkan.Core10 qualified as Vk +import Vulkan.Zero (zero) + +import Data.SpirV.Reflect.Module (Module) + +import Vulkan.Utils.DynamicRendering qualified as Dynamic +import Vulkan.Utils.Pipeline.Specialization (Specialization) +import Vulkan.Utils.SpirV.Descriptors (mergedDescriptorSetLayoutInfos, mergedPushConstantRanges, moduleStageFlags) +import Vulkan.Utils.SpirV.VertexInput (vertexInputState) + +{- | A materialized, verified pipeline layout reflected from a family of shaders: +the 'Vk.PipelineLayout' to bind against, and the per-set 'Vk.DescriptorSetLayout's +(keyed by set number) for allocating the descriptor sets. +-} +data ReflectedLayout = ReflectedLayout + { pipelineLayout :: Vk.PipelineLayout + , setLayouts :: [(Word32, Vk.DescriptorSetLayout)] + } + +{- | Build the descriptor-set layouts and pipeline layout for a family of pipelines +from their reflected modules. Bindings and push-constant ranges are merged across +every module — stage flags OR-ed, shared block layouts cross-checked (see +'mergedDescriptorSetLayoutInfos' / 'mergedPushConstantRanges') — and a conflict +'fail's in @m@. + +Pass every distinct shader the family uses, so the one layout stays compatible +with each pipeline built against it ('allocateGraphicsPipeline'). The layout (and the +set layouts) are owned by @m@'s 'Control.Monad.Trans.Resource.ResourceT' and must +outlive the pipelines. +-} +allocateReflectedLayout + :: (MonadResource m, MonadFail m) + => Vk.Device + -> [Module] + -> m (ReleaseKey, ReflectedLayout) +allocateReflectedLayout dev modules = do + setInfos <- orFail (mergedDescriptorSetLayoutInfos modules) + pushRanges <- orFail (mergedPushConstantRanges modules) + setLayouts <- + traverse + ( \(setNo, info) -> do + (_, layout) <- Vk.withDescriptorSetLayout dev info Nothing allocate + pure (setNo, layout) + ) + setInfos + (key, pipelineLayout) <- + Vk.withPipelineLayout + dev + zero + { Vk.setLayouts = V.fromList (map snd setLayouts) + , Vk.pushConstantRanges = V.fromList pushRanges + } + Nothing + allocate + pure (key, ReflectedLayout{pipelineLayout, setLayouts}) + where + orFail = either fail pure + +{- | Build one pipeline of a family against a shared 'ReflectedLayout', folding in +both that layout and the vertex stage's reflected vertex input. + +Each stage is its reflected 'Module' paired with the SPIR-V to compile; the stage +flag is taken from the module. This fills in the config's 'Dynamic.layout' (from the +'ReflectedLayout') and 'Dynamic.vertexInput' (from the vertex stage's reflection), +overwriting any values set on them; set the per-variant 'Dynamic.colorFormats' \/ +'Dynamic.depthFormat' \/ 'Dynamic.dynamicStates' and vary @spec@ for specialization. +For custom vertex input, drive "Vulkan.Utils.DynamicRendering" directly. +-} +allocateGraphicsPipeline + :: (MonadResource m, MonadUnliftIO m, MonadFail m, Specialization spec) + => Vk.Device + -> ReflectedLayout + -> Dynamic.PipelineConfig + -> spec + -- ^ Specialization shared by every stage; @()@ for none. + -> [(Module, ByteString)] + -- ^ Each stage's reflected module and the SPIR-V to compile. + -> m (ReleaseKey, Vk.Pipeline) +allocateGraphicsPipeline dev reflected config spec stages = + Dynamic.allocatePipelineFromShaders + dev + config + { Dynamic.layout = Just reflected.pipelineLayout + , Dynamic.vertexInput = maybe zero vertexInputState vertexModule + } + spec + [(moduleStageFlags m, spv) | (m, spv) <- stages] + where + vertexModule = fst <$> find (\(m, _) -> moduleStageFlags m == Vk.SHADER_STAGE_VERTEX_BIT) stages diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Reflect.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Reflect.hs new file mode 100644 index 000000000..6e0777f28 --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Reflect.hs @@ -0,0 +1,32 @@ +{-| Reflect compiled SPIR-V into a 'Module', both at runtime and inside +Template Haskell splices. +-} +module Vulkan.Utils.SpirV.Reflect + ( Module + , reflectFile + , reflectBytes + , reflectFileQ + ) where + +import Control.Monad.IO.Class (MonadIO (..)) +import Data.ByteString (ByteString) +import Language.Haskell.TH.Syntax (Q, addDependentFile, runIO) + +import Data.SpirV.Reflect.FFI (load, loadBytes) +import Data.SpirV.Reflect.Module (Module) + +-- | Reflect a @.spv@ file into a 'Module'. +reflectFile :: (MonadIO m) => FilePath -> m Module +reflectFile = load + +-- | Reflect SPIR-V bytecode into a 'Module'. +reflectBytes :: (MonadIO m) => ByteString -> m Module +reflectBytes = loadBytes + +{- | Reflect a @.spv@ file at compile time, registering it as a dependency so +the splice is rerun when the file changes. +-} +reflectFileQ :: FilePath -> Q Module +reflectFileQ path = do + addDependentFile path + runIO $ reflectFile path diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Reflect/OffsetMaps.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Reflect/OffsetMaps.hs new file mode 100644 index 000000000..8e60b1cb9 --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Reflect/OffsetMaps.hs @@ -0,0 +1,57 @@ +{-| Extract normalized 'OffsetMap's from a reflected 'Module''s resources. + +Both the runtime descriptor/push-constant builders +("Vulkan.Utils.SpirV.Descriptors") and the compile-time stage-signature +machinery ("Vulkan.Utils.SpirV.Stage") need the same thing: the std140\/std430 +'OffsetMap' of each buffer-typed descriptor binding and of each push-constant block. +That extraction — pick the layout mode from the descriptor type, pull the block +'TypeDescription', run 'offsetMapOf' — lives here once so the two callers can't drift. +-} +module Vulkan.Utils.SpirV.Reflect.OffsetMaps + ( resourceOffsetMap + , resourceOffsetMaps + , pushOffsetMap + , pushOffsetMaps + ) where + +import Data.Maybe (mapMaybe) +import Data.Vector qualified as V +import Data.Word (Word32) + +import Data.SpirV.Reflect.BlockVariable (BlockVariable) +import Data.SpirV.Reflect.BlockVariable qualified +import Data.SpirV.Reflect.DescriptorBinding (DescriptorBinding) +import Data.SpirV.Reflect.DescriptorBinding qualified +import Data.SpirV.Reflect.Module (Module) +import Data.SpirV.Reflect.Module qualified + +import Vulkan.Utils.SpirV.Layout (OffsetMap, layoutForDescriptor, offsetMapOf) +import Vulkan.Utils.SpirV.Types (LayoutMode (..)) + +{- | The @((set, binding), offset map)@ of a single buffer-typed descriptor +binding. 'Nothing' for non-buffer descriptors (samplers, images, …) and for blocks +whose offset map can't be computed. +-} +resourceOffsetMap :: DescriptorBinding -> Maybe ((Word32, Word32), OffsetMap) +resourceOffsetMap b = do + mode <- layoutForDescriptor b.descriptor_type + td <- b.type_description + g <- eitherToMaybe (offsetMapOf mode td) + pure ((b.set, b.binding), g) + +-- | The reflected block offset map of each buffer-typed binding, keyed by @(set, binding)@. +resourceOffsetMaps :: Module -> [((Word32, Word32), OffsetMap)] +resourceOffsetMaps = mapMaybe resourceOffsetMap . V.toList . (.descriptor_bindings) + +-- | The std430 offset map of a push-constant block, if it can be computed. +pushOffsetMap :: BlockVariable -> Maybe OffsetMap +pushOffsetMap pc = pc.type_description >>= eitherToMaybe . offsetMapOf Std430Layout + +-- | The reflected offset map of each push-constant block, keyed by @(offset, size)@. +pushOffsetMaps :: Module -> [((Word32, Word32), OffsetMap)] +pushOffsetMaps = mapMaybe keyed . V.toList . (.push_constants) + where + keyed pc = (,) (pc.absolute_offset, pc.size) <$> pushOffsetMap pc + +eitherToMaybe :: Either e a -> Maybe a +eitherToMaybe = either (const Nothing) Just diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Signature.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Signature.hs new file mode 100644 index 000000000..fe017d43f --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Signature.hs @@ -0,0 +1,299 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE UndecidableSuperClasses #-} + +{-| Type-level layout signatures, tied to the records reflection generates. + +Each generated block record carries a ground 'SigOffsetMap' — the promotion of its +normalized 'OffsetMap' ("Vulkan.Utils.SpirV.Layout"), computed value-level at splice +time (the value-level layout is the oracle; see 'knownLayoutInstance'). Because +the offset map is /normalized/ before promotion, layout-equivalent layouts — +@mat4@, @vec4[4]@, @float[16]@ under std430 — promote to the /same/ 'SigOffsetMap', +so the type-level matcher 'Fits' accepts them with no special cases. + +'Fits' is intentionally thin: a use-site /matcher/ against an already-ground +signature, not a unifier (the heavy unification stays value-level in @Q@). The +ground offset map can be reflected back to a value with 'layoutSig' \/ +'sigOffsetMapVal', which is how the type level is validated against the value-level +unifier. +-} +module Vulkan.Utils.SpirV.Signature + ( -- * Type-level offset map + SigOffsetMap (..) + , SigSlot (..) + , SigTail (..) + + -- * Records carrying a signature + , KnownLayout (..) + , layoutSig + + -- * Reflecting a signature back to a value + , KnownSigOffsetMap (..) + , KnownArrayTail (..) + , KnownMaybeSig (..) + , KnownKeyedSigs (..) + + -- * Matching + , Fits + , FitsTail + , ArrayOf + + -- * Generation (Template Haskell) + , knownLayoutInstance + , promoteOffsetMap + , promoteList + , promoteMaybe + , natT + ) where + +import Data.Kind (Constraint) +import Data.Proxy (Proxy (..)) +import Data.Word (Word32) +import GHC.TypeLits (ErrorMessage (..), KnownNat, Nat, TypeError, natVal) +import Language.Haskell.TH + +import Vulkan.Utils.SpirV.Layout (OffsetMap (..), RuntimeTail (..), Slot (..)) +import Vulkan.Utils.SpirV.Types (ScalarType (..)) + +-- | One occupied scalar component at a byte offset (type-level mirror of 'Slot'). +data SigSlot = SigSlot Nat ScalarType + +{- | A runtime-array tail (mirror of 'RuntimeTail'): base offset, stride, and the +element's slots (relative to the element start). +-} +data SigTail = SigTail Nat Nat [SigSlot] + +{- | A normalized layout at the type level (mirror of 'OffsetMap'): concrete slots, the +total size if statically known, and an optional runtime-array tail. +-} +data SigOffsetMap = SigOffsetMap [SigSlot] (Maybe Nat) (Maybe SigTail) + +{- | A Haskell type with a ground, reflection-derived layout signature. Generated +records get an instance via 'knownLayoutInstance'. +-} +class (KnownSigOffsetMap (Sig a)) => KnownLayout a where + type Sig a :: SigOffsetMap + +-- | The value-level normal form of a record's signature (the oracle view). +layoutSig :: forall a. (KnownLayout a) => OffsetMap +layoutSig = sigOffsetMapVal (Proxy @(Sig a)) + +-- Reflection: type-level offset map -> value 'OffsetMap'. ---------------------------------- + +class KnownScalar (s :: ScalarType) where + scalarVal :: Proxy s -> ScalarType + +instance KnownScalar 'STFloat where scalarVal _ = STFloat +instance KnownScalar 'STDouble where scalarVal _ = STDouble +instance KnownScalar 'STInt where scalarVal _ = STInt +instance KnownScalar 'STUInt where scalarVal _ = STUInt +instance KnownScalar 'STInt64 where scalarVal _ = STInt64 +instance KnownScalar 'STUInt64 where scalarVal _ = STUInt64 +instance KnownScalar 'STBool where scalarVal _ = STBool +instance KnownScalar 'STAddress where scalarVal _ = STAddress + +class KnownSlots (xs :: [SigSlot]) where + slotsVal :: Proxy xs -> [Slot] + +instance KnownSlots '[] where + slotsVal _ = [] + +instance (KnownNat o, KnownScalar s, KnownSlots rest) => KnownSlots ('SigSlot o s ': rest) where + slotsVal _ = + Slot (fromIntegral (natVal (Proxy @o))) (scalarVal (Proxy @s)) : slotsVal (Proxy @rest) + +class KnownMaybeNat (m :: Maybe Nat) where + maybeNatVal :: Proxy m -> Maybe Int + +instance KnownMaybeNat 'Nothing where + maybeNatVal _ = Nothing + +instance (KnownNat n) => KnownMaybeNat ('Just n) where + maybeNatVal _ = Just (fromIntegral (natVal (Proxy @n))) + +class KnownTail (t :: SigTail) where + tailVal :: Proxy t -> RuntimeTail + +instance (KnownNat b, KnownNat s, KnownSlots es) => KnownTail ('SigTail b s es) where + tailVal _ = + RuntimeTail + (fromIntegral (natVal (Proxy @b))) + (fromIntegral (natVal (Proxy @s))) + (slotsVal (Proxy @es)) + +class KnownMaybeTail (m :: Maybe SigTail) where + maybeTailVal :: Proxy m -> Maybe RuntimeTail + +instance KnownMaybeTail 'Nothing where + maybeTailVal _ = Nothing + +instance (KnownTail t) => KnownMaybeTail ('Just t) where + maybeTailVal _ = Just (tailVal (Proxy @t)) + +-- | Reflect a ground type-level offset map back to its value form. +class KnownSigOffsetMap (g :: SigOffsetMap) where + sigOffsetMapVal :: Proxy g -> OffsetMap + +instance (KnownSlots ss, KnownMaybeNat sz, KnownMaybeTail tl) => KnownSigOffsetMap ('SigOffsetMap ss sz tl) where + sigOffsetMapVal _ = + OffsetMap (slotsVal (Proxy @ss)) (maybeNatVal (Proxy @sz)) (maybeTailVal (Proxy @tl)) + +{- | The @(base offset, element stride)@ of a buffer signature's runtime-array +tail, recovered straight from the type level. Only a layout that /has/ a tail has +an instance, so reading it is total — and unlike reflecting the whole 'OffsetMap' and +inspecting its 'Maybe' tail ('sigOffsetMapVal'), it forces only the two 'Nat's it +needs, not the element's slot list. So an element loop pays @O(1)@ per element, +not @O(offset map)@. +-} +class KnownArrayTail (t :: SigOffsetMap) where + arrayTailBaseStride :: (Int, Int) + +instance (KnownNat base, KnownNat stride) => KnownArrayTail ('SigOffsetMap cs csz ('Just ('SigTail base stride es))) where + arrayTailBaseStride = + (fromIntegral (natVal (Proxy @base)), fromIntegral (natVal (Proxy @stride))) + +-- | Reflect an optional ground offset map (e.g. a push-constant layout) back to a value. +class KnownMaybeSig (m :: Maybe SigOffsetMap) where + maybeSigVal :: Proxy m -> Maybe OffsetMap + +instance KnownMaybeSig 'Nothing where + maybeSigVal _ = Nothing + +instance (KnownSigOffsetMap g) => KnownMaybeSig ('Just g) where + maybeSigVal _ = Just (sigOffsetMapVal (Proxy @g)) + +-- | Reflect a list of @((set, binding), offset map)@ entries back to values. +class KnownKeyedSigs (xs :: [((Nat, Nat), SigOffsetMap)]) where + keyedSigsVal :: Proxy xs -> [((Word32, Word32), OffsetMap)] + +instance KnownKeyedSigs '[] where + keyedSigsVal _ = [] + +instance + (KnownNat s, KnownNat b, KnownSigOffsetMap g, KnownKeyedSigs rest) + => KnownKeyedSigs ('( '(s, b), g) ': rest) + where + keyedSigsVal _ = + ((nat32 (Proxy @s), nat32 (Proxy @b)), sigOffsetMapVal (Proxy @g)) + : keyedSigsVal (Proxy @rest) + +nat32 :: (KnownNat n) => Proxy n -> Word32 +nat32 = fromIntegral . natVal + +-- Matching. --------------------------------------------------------------------- + +{- | The use-site matcher: @'Fits' r t@ holds when a value of layout @r@ may be +viewed at a slot of layout @t@. Since both are normalized before promotion, +layout-equivalent layouts share a signature and match by equality; a difference is +a legible compile error. (Runtime-tail absorption is handled value-level when a +buffer's signature is built; this matcher covers the ground case.) +-} +type family Fits (r :: SigOffsetMap) (t :: SigOffsetMap) :: Constraint where + Fits g g = () + Fits r t = + TypeError + ( 'Text "Layout mismatch." + ':$$: 'Text " have: " + ':<>: 'ShowType r + ':$$: 'Text " want: " + ':<>: 'ShowType t + ) + +{- | The signature of a tightly-packed runtime array @r[]@ — an SSBO whose content +is a runtime array of the element layout @r@. The element becomes the array +tail: its (statically known) size is the stride, the base offset is 0, and the +closed slots clear. Use to tag a mapped SSBO pointer, e.g. +@'Vulkan.Utils.SpirV.Buffer.unsafeAsBufferPtr' p :: io (Buffer ('ArrayOf' ('Sig' Vertex)))@. +(The element's std430 standalone size equals its array stride, so this is exact +for a tightly-packed @T[]@.) +-} +type family ArrayOf (r :: SigOffsetMap) :: SigOffsetMap where + ArrayOf ('SigOffsetMap slots ('Just size) 'Nothing) = + 'SigOffsetMap '[] 'Nothing ('Just ('SigTail 0 size slots)) + ArrayOf r = + TypeError + ( 'Text "ArrayOf: the element layout must be statically sized and not itself open:" + ':$$: 'Text " " + ':<>: 'ShowType r + ) + +{- | @'FitsTail' r t@ holds when a record of layout @r@ is one element of the +runtime-array tail of buffer layout @t@: @r@ is closed with a known size equal +to the tail stride, and its slots are exactly the tail element's. Layout-equivalent +element records share a signature, so they match too; a mismatch (or a @t@ +without a tail) is a legible compile error. +-} +type family FitsTail (r :: SigOffsetMap) (t :: SigOffsetMap) :: Constraint where + FitsTail ('SigOffsetMap es ('Just sz) 'Nothing) ('SigOffsetMap _ _ ('Just ('SigTail _ sz es))) = () + FitsTail r t = + TypeError + ( 'Text "Element layout does not fit the buffer's array tail." + ':$$: 'Text " element: " + ':<>: 'ShowType r + ':$$: 'Text " buffer: " + ':<>: 'ShowType t + ) + +-- Generation. ------------------------------------------------------------------- + +{- | Emit @instance 'KnownLayout' where type 'Sig' = @, promoting the value-level normal form to the type level. +-} +knownLayoutInstance :: Name -> OffsetMap -> [Dec] +knownLayoutInstance name om = + [ InstanceD + Nothing + [] + (ConT ''KnownLayout `AppT` ConT name) + [TySynInstD (TySynEqn Nothing (ConT ''Sig `AppT` ConT name) (promoteOffsetMap om))] + ] + +promoteOffsetMap :: OffsetMap -> Type +promoteOffsetMap (OffsetMap slots msize mtail) = + PromotedT 'SigOffsetMap + `AppT` promoteSlots slots + `AppT` promoteMaybe natT msize + `AppT` promoteMaybe promoteTail mtail + +promoteTail :: RuntimeTail -> Type +promoteTail (RuntimeTail base stride es) = + PromotedT 'SigTail `AppT` natT base `AppT` natT stride `AppT` promoteSlots es + +promoteSlots :: [Slot] -> Type +promoteSlots = promoteList promoteSlot + +promoteSlot :: Slot -> Type +promoteSlot (Slot off sc) = + PromotedT 'SigSlot `AppT` natT off `AppT` PromotedT (scalarName sc) + +-- | Promote a list, element-wise, to a promoted @'[..]@. +promoteList :: (a -> Type) -> [a] -> Type +promoteList f = foldr (\x acc -> PromotedConsT `AppT` f x `AppT` acc) PromotedNilT + +-- | Promote a 'Maybe' to a promoted @'Nothing@ \/ @'Just@. +promoteMaybe :: (a -> Type) -> Maybe a -> Type +promoteMaybe _ Nothing = PromotedT 'Nothing +promoteMaybe f (Just x) = PromotedT 'Just `AppT` f x + +-- | An integral value as a type-level 'Nat' literal. +natT :: (Integral a) => a -> Type +natT = LitT . NumTyLit . fromIntegral + +scalarName :: ScalarType -> Name +scalarName = \case + STFloat -> 'STFloat + STDouble -> 'STDouble + STInt -> 'STInt + STUInt -> 'STUInt + STInt64 -> 'STInt64 + STUInt64 -> 'STUInt64 + STBool -> 'STBool + STAddress -> 'STAddress diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Specialization.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Specialization.hs new file mode 100644 index 000000000..591bc6a7f --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Specialization.hs @@ -0,0 +1,145 @@ +{-| Build Vulkan specialization-constant values from a reflected 'Module'. + +Reflection yields each constant's @constant_id@ and name but not its type or +width, and the value side (the 'Specialization' class) is a stack of 'Word32's — +so only 32-bit scalar constants (@int@\/@uint@\/@float@\/@bool@, the +@GL_KHR_vulkan_glsl@ baseline) are supported, one 4-byte slot each. +'specializationMapEntries' lays them out in ascending @constant_id@ order (the +ids need not be contiguous). Wider constants are out of scope (see +@spirv-cleanup.md@); a 64-bit value supplied as two 'Word32's trips 'packed'\'s +arity check rather than being silently truncated. + +Supply the values via the 'Specialization' class from +"Vulkan.Utils.Pipeline.Specialization", in ascending @constant_id@ order. (That +module shares the 32-bit limit and additionally assumes @constantID = offset \/ 4@.) +-} +module Vulkan.Utils.SpirV.Specialization + ( specializationConstants + , specializationMapEntries + , withSpecializationInfo + , allocateSpecializationInfo + ) where + +import Control.Monad.IO.Class (liftIO) +import Control.Monad.IO.Unlift (MonadUnliftIO, withRunInIO) +import Control.Monad.Trans.Resource (MonadResource, allocate) +import Data.List (sortOn) +import Data.Text (Text) +import Data.Vector (Vector) +import Data.Vector qualified as V +import Data.Vector.Storable qualified as VS +import Data.Word (Word32) +import Foreign.Marshal.Alloc (free) +import Foreign.Marshal.Array (mallocArray, pokeArray) +import Foreign.Ptr (Ptr, castPtr) +import Vulkan.Core10 qualified as Vk +import Vulkan.Utils.Pipeline.Specialization (Specialization (..)) + +import Data.SpirV.Reflect.Module (Module) +import Data.SpirV.Reflect.Module qualified as Module +import Data.SpirV.Reflect.SpecializationConstant qualified as SC + +-- | Reflected specialization constants as @(constant_id, name)@, ascending by id. +specializationConstants :: Module -> [(Word32, Maybe Text)] +specializationConstants m = + sortOn fst [(scId c, scName c) | c <- V.toList (moduleSpecConstants m)] + +{- | One tightly-packed 32-bit map entry per reflected spec constant, ascending +by @constant_id@ (offset @= index * 4@, size 4). +-} +specializationMapEntries :: Module -> Vector Vk.SpecializationMapEntry +specializationMapEntries m = + V.fromList + [ Vk.SpecializationMapEntry + { Vk.constantID = cid + , Vk.offset = fromIntegral (ix * 4) + , Vk.size = 4 + } + | (ix, (cid, _)) <- zip [0 :: Int ..] (specializationConstants m) + ] + +{- | Pack a 'Specialization' against the reflected map entries, yielding a +'Vk.SpecializationInfo' valid for the callback's duration (pipeline creation +copies the values out — build and create the pipeline inside the callback). +'Nothing' when the shader declares no spec constants. + +The supplied values must be in ascending @constant_id@ order and number the +shader's declared constants; a mismatch is a programmer error and 'error's. +-} +withSpecializationInfo + :: (Specialization spec, MonadUnliftIO m) + => Module + -> spec + -> (Maybe Vk.SpecializationInfo -> m a) + -> m a +withSpecializationInfo m spec action = + case packed m spec of + Nothing -> action Nothing + Just (entries, ws) -> + withRunInIO $ \run -> + VS.unsafeWith (VS.fromList ws) $ \p -> + run . action $ Just (specInfo entries (length ws) (castPtr p)) + +{- | As 'withSpecializationInfo', but the backing buffer lives until the +surrounding 'Control.Monad.Trans.Resource.ResourceT' scope ends rather than a +callback — so it survives a later pipeline creation. 'Nothing' when the shader +declares no spec constants. +-} +allocateSpecializationInfo + :: (Specialization spec, MonadResource m) + => Module + -> spec + -> m (Maybe Vk.SpecializationInfo) +allocateSpecializationInfo m spec = + case packed m spec of + Nothing -> pure Nothing + Just (entries, ws) -> do + let n = length ws + -- A C-malloc'd buffer is pointer-stable and freed at scope end; the + -- pointer must stay valid until pipeline creation copies the values. + (_key, ptr) <- allocate (mallocArray n) free + liftIO $ pokeArray ptr ws + pure $ Just (specInfo entries n (castPtr ptr)) + +{- | Reflected map entries paired with the caller's packed values, or 'Nothing' +when there are no spec constants. 'error's on an arity mismatch. +-} +packed + :: (Specialization spec) + => Module + -> spec + -> Maybe (Vector Vk.SpecializationMapEntry, [Word32]) +packed m spec + | V.null entries = Nothing + | length ws /= n = + error $ + "Vulkan.Utils.SpirV.Specialization: shader declares " + <> show n + <> " specialization constant(s) but " + <> show (length ws) + <> " value(s) were supplied" + | otherwise = Just (entries, ws) + where + entries = specializationMapEntries m + n = V.length entries + ws = specializationData spec + +specInfo :: Vector Vk.SpecializationMapEntry -> Int -> Ptr () -> Vk.SpecializationInfo +specInfo entries n p = + Vk.SpecializationInfo + { Vk.mapEntries = entries + , Vk.dataSize = fromIntegral (n * 4) + , Vk.data' = p + } + +-- Field accessors via record patterns (the constructor disambiguates the +-- DuplicateRecordFields names). + +moduleSpecConstants :: Module -> Vector SC.SpecializationConstant +moduleSpecConstants Module.Module{Module.spec_constants = cs} = cs + +scId :: SC.SpecializationConstant -> Word32 +scId SC.SpecializationConstant{SC.constant_id = i} = i + +scName :: SC.SpecializationConstant -> Maybe Text +scName SC.SpecializationConstant{SC.name = n} = n diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs new file mode 100644 index 000000000..9a828d51d --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs @@ -0,0 +1,372 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE NoFieldSelectors #-} + +{-| Per-shader /stage signatures/ and the type-level checks that compose them. + +A 'ShaderSig' captures, at the type level, a shader stage's interface (its @in@ +and @out@ variables, by location) and its resource layouts (descriptor blocks by +@(set, binding)@, and push constants) — each as a normalized 'SigOffsetMap' +("Vulkan.Utils.SpirV.Signature"). 'reflectStageSig' emits one from a compiled +@.spv@; the value-level 'StageInfo' it is promoted from is also returned by +'stageInfoOf' for use as the oracle and for building Vulkan create-infos. + +The composition checks are thin type-level /matchers/ (the heavy work stayed +value-level): 'MatchInterface' requires every fragment input to have a +matching-typed vertex output at the same location. + +'reflectPipelineLayoutSig' goes one step further: it promotes the /merged/ +pipeline-layout signature (descriptor blocks by @(set, binding)@ + push) across a +family of shaders — the type-level counterpart of the runtime merge in +"Vulkan.Utils.SpirV.Descriptors". +-} +module Vulkan.Utils.SpirV.Stage + ( -- * Stage signatures + ShaderSig (..) + , StageKind (..) + + -- * Value-level extraction (the oracle) + , StageInfo (..) + , stageInfoOf + , matchInterface + , Linked (..) + , linkStages + + -- * Type-level checks + , MatchInterface + , CompatibleResources + + -- * Merged pipeline-layout signature + , LayoutSig (..) + , LayoutInfo (..) + , mergeLayout + , KnownLayoutSig (..) + + -- * Generation + , reflectStageSig + , reflectPipelineLayoutSig + ) where + +import Data.Kind (Constraint) +import Data.List (foldl', sortOn) +import Data.Proxy (Proxy (..)) +import Data.Vector qualified as V +import Data.Word (Word32) +import GHC.TypeLits (ErrorMessage (..), Nat, TypeError) +import Language.Haskell.TH + +import Data.SpirV.Enum (BuiltIn (..)) +import Data.SpirV.Reflect.InterfaceVariable qualified as InterfaceVariable +import Data.SpirV.Reflect.Module (Module) +import Data.SpirV.Reflect.Module qualified +import Data.SpirV.Reflect.TypeDescription (TypeDescription) + +import Vulkan.Utils.SpirV.Layout (OffsetMap, fromFields, leafFieldType, normalize) +import Vulkan.Utils.SpirV.Reflect (reflectFileQ) +import Vulkan.Utils.SpirV.Reflect.OffsetMaps (pushOffsetMap, resourceOffsetMaps) +import Vulkan.Utils.SpirV.Signature (KnownKeyedSigs (..), KnownMaybeSig (..), SigOffsetMap, natT, promoteList, promoteMaybe, promoteOffsetMap) +import Vulkan.Utils.SpirV.Types (LayoutMode (..), classifyType) + +-- | Which pipeline stage a shader is. +data StageKind = VertexStage | FragmentStage | ComputeStage + deriving (Eq, Show) + +{- | A stage's type-level signature: stage, inputs and outputs (location ↦ layout), +resources (@(set, binding)@ ↦ block layout) and an optional push-constant layout. +-} +data ShaderSig + = ShaderSig + StageKind + [(Nat, SigOffsetMap)] + [(Nat, SigOffsetMap)] + [((Nat, Nat), SigOffsetMap)] + (Maybe SigOffsetMap) + +-- Value-level extraction. ------------------------------------------------------- + +{- | The value-level form of a 'ShaderSig', reflected from a 'Module'. This is the +oracle the type level is promoted from, and the source for Vulkan create-infos. +-} +data StageInfo = StageInfo + { stage :: StageKind + , inputs :: [(Word32, OffsetMap)] + , outputs :: [(Word32, OffsetMap)] + , resources :: [((Word32, Word32), OffsetMap)] + , push :: Maybe OffsetMap + } + deriving (Eq, Show) + +-- | Reflect a module's stage signature. +stageInfoOf :: Module -> StageInfo +stageInfoOf m = + StageInfo + { stage = stageKindOf m.shader_stage + , inputs = interfaceOffsetMaps m.input_variables + , outputs = interfaceOffsetMaps m.output_variables + , resources = sortOn fst (resourceOffsetMaps m) + , push = + case V.toList m.push_constants of + (pc : _) -> pushOffsetMap pc + [] -> Nothing + } + +-- | Interface variables as @(location, offset map)@, built-ins dropped, ascending. +interfaceOffsetMaps :: V.Vector InterfaceVariable.InterfaceVariable -> [(Word32, OffsetMap)] +interfaceOffsetMaps vs = + sortOn + fst + -- a non-built-in reads as Nothing (ffi) or the @BuiltIn (-1)@ sentinel (yaml) + [ (v.location, g) + | v <- V.toList vs + , v.built_in `elem` [Nothing, Just (BuiltIn (-1))] + , Just td <- [v.type_description] + , Just g <- [interfaceOffsetMap td] + ] + +-- | The offset map of a single interface variable (a scalar/vector at offset 0). +interfaceOffsetMap :: TypeDescription -> Maybe OffsetMap +interfaceOffsetMap td = do + numeric <- classifyType td + pure (normalize (fromFields Std430Layout Nothing [("v", leafFieldType numeric)])) + +stageKindOf :: Int -> StageKind +stageKindOf n + | n == 0x01 = VertexStage + | n == 0x10 = FragmentStage + | otherwise = ComputeStage + +{- | The interface oracle: every input of the second stage must have an +equal-typed output in the first. (Use as @matchInterface vert frag@.) +-} +matchInterface :: StageInfo -> StageInfo -> Either String () +matchInterface producer consumer = + mapM_ check consumer.inputs + where + outs = producer.outputs + check (loc, want) = + case lookup loc outs of + Just have + | have == want -> Right () + | otherwise -> + Left ("interface mismatch at location " <> show loc) + Nothing -> + Left ("input at location " <> show loc <> " has no matching output") + +{- | A linked vertex+fragment pipeline's combined signature (value-level oracle): +the merged resources (shared @(set, binding)@s deduped) and push constants, the +vertex stage's inputs (vertex attributes) and the fragment stage's outputs +(colour attachments). +-} +data Linked = Linked + { resources :: [((Word32, Word32), OffsetMap)] + , push :: Maybe OffsetMap + , inputs :: [(Word32, OffsetMap)] + , outputs :: [(Word32, OffsetMap)] + } + deriving (Eq, Show) + +{- | Link a vertex and a fragment stage: the interface must match, shared bindings +and push constants must agree, and the resources union. Mirrors the type-level +'MatchInterface' + 'CompatibleResources'. +-} +linkStages :: StageInfo -> StageInfo -> Either String Linked +linkStages vert frag = do + matchInterface vert frag + resources <- mergeAssoc showKey vert.resources frag.resources + push <- mergePush vert.push frag.push + pure + Linked + { resources = sortOn fst resources + , push + , inputs = vert.inputs + , outputs = frag.outputs + } + where + showKey (s, b) = "(set " <> show s <> ", binding " <> show b <> ")" + +-- | Union two keyed offset-map lists; a shared key whose maps differ is an error. +mergeAssoc :: (Eq k) => (k -> String) -> [(k, OffsetMap)] -> [(k, OffsetMap)] -> Either String [(k, OffsetMap)] +mergeAssoc shw xs ys = foldr step (Right xs) ys + where + step (k, g) acc = do + m <- acc + case lookup k m of + Nothing -> Right ((k, g) : m) + Just g' + | g' == g -> Right m + | otherwise -> Left ("incompatible layouts at " <> shw k) + +mergePush :: Maybe OffsetMap -> Maybe OffsetMap -> Either String (Maybe OffsetMap) +mergePush Nothing b = Right b +mergePush a Nothing = Right a +mergePush (Just a) (Just b) + | a == b = Right (Just a) + | otherwise = Left "incompatible push-constant layouts between stages" + +-- Merged pipeline-layout signature. --------------------------------------------- + +{- | A merged pipeline-layout signature at the type level: the descriptor blocks by +@(set, binding)@ and the push-constant block, unified across a family of shaders. +The type-level mirror of 'LayoutInfo'. +-} +data LayoutSig = LayoutSig [((Nat, Nat), SigOffsetMap)] (Maybe SigOffsetMap) + +{- | The value-level merged layout 'reflectPipelineLayoutSig' promotes (its oracle): +each descriptor block's 'OffsetMap' by @(set, binding)@ ascending, and the +push-constant block. +-} +data LayoutInfo = LayoutInfo + { resources :: [((Word32, Word32), OffsetMap)] + , push :: Maybe OffsetMap + } + deriving (Eq, Show) + +{- | Merge several stages into one pipeline-layout signature: descriptor blocks are +unioned (a shared @(set, binding)@ whose layouts disagree is a 'Left') and the +push-constant blocks unified. Unlike 'linkStages' there is no interface check — one +layout is shared across pipelines whose vertex\/fragment interfaces differ. +-} +mergeLayout :: [Module] -> Either String LayoutInfo +mergeLayout modules = do + resources <- foldl' addResources (Right []) infos + push <- foldl' addPush (Right Nothing) infos + pure LayoutInfo{resources = sortOn fst resources, push} + where + infos = map stageInfoOf modules + addResources acc info = acc >>= mergeAssoc showKey info.resources + addPush acc info = acc >>= \p -> mergePush p info.push + showKey (s, b) = "(set " <> show s <> ", binding " <> show b <> ")" + +{- | Reflect a type-level 'LayoutSig' back to its value 'LayoutInfo' — the oracle +view, for validating the promotion and (later) driving bind-site checks. +-} +class KnownLayoutSig (sig :: LayoutSig) where + layoutSigVal :: Proxy sig -> LayoutInfo + +instance (KnownKeyedSigs rs, KnownMaybeSig p) => KnownLayoutSig ('LayoutSig rs p) where + layoutSigVal _ = LayoutInfo{resources = keyedSigsVal (Proxy @rs), push = maybeSigVal (Proxy @p)} + +-- Type-level checks. ------------------------------------------------------------ + +{- | Holds when every input of @f@ has a matching-typed output of @v@ at the same +location (extra outputs of @v@ are allowed). A mismatch is a compile error. +-} +type family MatchInterface (v :: ShaderSig) (f :: ShaderSig) :: Constraint where + MatchInterface ('ShaderSig _ _ vouts _ _) ('ShaderSig _ fins _ _ _) = MatchAll fins vouts + +type family MatchAll (ins :: [(Nat, SigOffsetMap)]) (outs :: [(Nat, SigOffsetMap)]) :: Constraint where + MatchAll '[] _ = () + MatchAll ('(loc, g) ': rest) outs = (MatchOne loc g (Lookup loc outs), MatchAll rest outs) + +type family MatchOne (loc :: Nat) (want :: SigOffsetMap) (have :: Maybe SigOffsetMap) :: Constraint where + MatchOne _ g ('Just g) = () + MatchOne loc want ('Just have) = + TypeError + ( 'Text "Interface mismatch at location " + ':<>: 'ShowType loc + ':<>: 'Text ":" + ':$$: 'Text " vertex out: " + ':<>: 'ShowType have + ':$$: 'Text " fragment in: " + ':<>: 'ShowType want + ) + MatchOne loc _ 'Nothing = + TypeError + ('Text "Fragment input at location " ':<>: 'ShowType loc ':<>: 'Text " has no matching vertex output.") + +type family Lookup (k :: Nat) (xs :: [(Nat, SigOffsetMap)]) :: Maybe SigOffsetMap where + Lookup _ '[] = 'Nothing + Lookup k ('(k, v) ': _) = 'Just v + Lookup k ('(_, _) ': rest) = Lookup k rest + +{- | Holds when every descriptor block and push constant the two stages /share/ +(same @(set, binding)@) has the same layout. Disjoint resources are fine; a +shared binding with differing layouts is a compile error. +-} +type family CompatibleResources (v :: ShaderSig) (f :: ShaderSig) :: Constraint where + CompatibleResources ('ShaderSig _ _ _ vres vpush) ('ShaderSig _ _ _ fres fpush) = + (CompatAll vres fres, CompatPush vpush fpush) + +type family CompatAll (xs :: [((Nat, Nat), SigOffsetMap)]) (ys :: [((Nat, Nat), SigOffsetMap)]) :: Constraint where + CompatAll '[] _ = () + CompatAll ('(k, g) ': rest) ys = (CompatOne k g (LookupR k ys), CompatAll rest ys) + +type family CompatOne (k :: (Nat, Nat)) (g :: SigOffsetMap) (m :: Maybe SigOffsetMap) :: Constraint where + CompatOne _ _ 'Nothing = () + CompatOne _ g ('Just g) = () + CompatOne k _ ('Just _) = + TypeError + ( 'Text "Resource layout mismatch at set/binding " + ':<>: 'ShowType k + ':<>: 'Text " between stages." + ) + +type family CompatPush (a :: Maybe SigOffsetMap) (b :: Maybe SigOffsetMap) :: Constraint where + CompatPush 'Nothing _ = () + CompatPush _ 'Nothing = () + CompatPush ('Just g) ('Just g) = () + CompatPush ('Just _) ('Just _) = + TypeError ('Text "Push-constant layouts differ between stages.") + +type family LookupR (k :: (Nat, Nat)) (xs :: [((Nat, Nat), SigOffsetMap)]) :: Maybe SigOffsetMap where + LookupR _ '[] = 'Nothing + LookupR k ('(k, v) ': _) = 'Just v + LookupR k ('(_, _) ': rest) = LookupR k rest + +-- Generation. ------------------------------------------------------------------- + +-- | Reflect a compiled @.spv@ and emit @type \ = \@. +reflectStageSig :: String -> FilePath -> Q [Dec] +reflectStageSig name path = do + m <- reflectFileQ path + pure [TySynD (mkName name) [] (promoteStageInfo (stageInfoOf m))] + +promoteStageInfo :: StageInfo -> Type +promoteStageInfo si = + PromotedT 'ShaderSig + `AppT` promoteStage si.stage + `AppT` promoteList (promoteLocated promoteOffsetMap) si.inputs + `AppT` promoteList (promoteLocated promoteOffsetMap) si.outputs + `AppT` promoteList (promoteKeyed promoteOffsetMap) si.resources + `AppT` promoteMaybe promoteOffsetMap si.push + +promoteStage :: StageKind -> Type +promoteStage = \case + VertexStage -> PromotedT 'VertexStage + FragmentStage -> PromotedT 'FragmentStage + ComputeStage -> PromotedT 'ComputeStage + +promoteLocated :: (a -> Type) -> (Word32, a) -> Type +promoteLocated f (loc, x) = pair (natT loc) (f x) + +promoteKeyed :: (a -> Type) -> ((Word32, Word32), a) -> Type +promoteKeyed f ((s, b), x) = pair (pair (natT s) (natT b)) (f x) + +pair :: Type -> Type -> Type +pair a b = PromotedTupleT 2 `AppT` a `AppT` b + +{- | Reflect the @.spv@ of a pipeline's shaders and emit +@type \ = \@ — the descriptor blocks by +@(set, binding)@ and push constants merged across the stages ('mergeLayout' is the +oracle). A layout disagreement between stages fails the splice. The type-level +counterpart of 'Vulkan.Utils.SpirV.Descriptors.mergedDescriptorSetLayoutInfos'. +-} +reflectPipelineLayoutSig :: String -> [FilePath] -> Q [Dec] +reflectPipelineLayoutSig name paths = do + modules <- traverse reflectFileQ paths + case mergeLayout modules of + Left err -> fail ("reflectPipelineLayoutSig: " <> err) + Right info -> pure [TySynD (mkName name) [] (promoteLayoutSig info)] + +promoteLayoutSig :: LayoutInfo -> Type +promoteLayoutSig info = + PromotedT 'LayoutSig + `AppT` promoteList (promoteKeyed promoteOffsetMap) info.resources + `AppT` promoteMaybe promoteOffsetMap info.push diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/TH.hs b/utils-spirv/src/Vulkan/Utils/SpirV/TH.hs new file mode 100644 index 000000000..49008d6bf --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/TH.hs @@ -0,0 +1,140 @@ +{-| Template Haskell entry points: reflect a compiled @.spv@ at build time and +splice Haskell record types for its uniform / storage / push-constant blocks. + +Types are keyed by their SPIR-V struct name and generated at most once; a name +that already resolves in scope (e.g. generated from another shader that shares +an include) is referenced rather than redefined, so shared GLSL types map to a +single shared Haskell type. +-} +module Vulkan.Utils.SpirV.TH + ( reflectShaderTypes + , reflectShaderTypesWith + , reflectShaderTypesBytes + , reflectShaderTypesBytesWith + , reflectModuleTypes + , reflectModuleTypesWith + ) where + +import Data.ByteString (ByteString) +import Data.Map.Strict qualified as Map +import Data.Vector qualified as V +import Language.Haskell.TH + +import Data.SpirV.Reflect.BlockVariable qualified +import Data.SpirV.Reflect.DescriptorBinding qualified +import Data.SpirV.Reflect.Enums.DescriptorType qualified as R +import Data.SpirV.Reflect.Module (Module) +import Data.SpirV.Reflect.Module qualified +import Data.SpirV.Reflect.TypeDescription (TypeDescription) + +import Vulkan.Utils.SpirV.Block (allMembers16, collectStructs, structRecordDec, structTypeName) +import Vulkan.Utils.SpirV.Reflect (reflectBytes, reflectFileQ) +import Vulkan.Utils.SpirV.Types (LayoutMode (..), TypeMap, geomancyTypeMap) + +{- | Reflect a @.spv@ file and generate record types for its blocks, using the +default 'geomancyTypeMap'. +-} +reflectShaderTypes :: FilePath -> Q [Dec] +reflectShaderTypes = reflectShaderTypesWith geomancyTypeMap + +-- | As 'reflectShaderTypes', with a caller-supplied 'TypeMap'. +reflectShaderTypesWith :: TypeMap -> FilePath -> Q [Dec] +reflectShaderTypesWith tymap path = reflectFileQ path >>= reflectModuleTypesWith tymap + +{- | As 'reflectShaderTypes', but reflecting SPIR-V bytecode already in hand +rather than reading a @.spv@ file — e.g. the 'ByteString' a @[comp|…|]@ +quasiquoter produced in a shader module, referenced from the module that +assembles the pipeline: + +@ +reflectShaderTypesBytes Shaders.compSpirv +@ + +No file is involved. Because the bytes are an ordinary imported binding, GHC's +cross-module recompilation reruns the splice when they change, so (unlike the +file path) there is no 'Language.Haskell.TH.Syntax.addDependentFile' to forget. +The usual Template Haskell stage restriction applies: the bytes must come from a +different module than the splice. +-} +reflectShaderTypesBytes :: ByteString -> Q [Dec] +reflectShaderTypesBytes = reflectShaderTypesBytesWith geomancyTypeMap + +-- | As 'reflectShaderTypesBytes', with a caller-supplied 'TypeMap'. +reflectShaderTypesBytesWith :: TypeMap -> ByteString -> Q [Dec] +reflectShaderTypesBytesWith tymap bytes = runIO (reflectBytes bytes) >>= reflectModuleTypesWith tymap + +{- | Generate record types for every block a reflected 'Module' declares. + +The shared core of 'reflectShaderTypes' and 'reflectShaderTypesBytes', exposed so +a caller who already holds a 'Module' can reach codegen without re-reflecting — +and can reflect once, then splice from the same 'Module' in more than one place. +-} +reflectModuleTypes :: Module -> Q [Dec] +reflectModuleTypes = reflectModuleTypesWith geomancyTypeMap + +-- | As 'reflectModuleTypes', with a caller-supplied 'TypeMap'. +reflectModuleTypesWith :: TypeMap -> Module -> Q [Dec] +reflectModuleTypesWith tymap = genStructs tymap . moduleStructCandidates + +{- | The @OpTypeStruct@ descriptions worth turning into records, each paired with +the layout its 'Storable' should follow. Each block is expanded through +'collectStructs' so nested and array-element structs (e.g. an SSBO's @Sphere[]@ +element) are generated too. +-} +moduleStructCandidates :: Module -> [(LayoutMode, TypeDescription)] +moduleStructCandidates m = + concatMap (uncurry collectStructs) $ + [ (layoutForDescriptor b.descriptor_type, td) + | b <- V.toList m.descriptor_bindings + , Just td <- [b.type_description] + ] + ++ [ (Std430Layout, td) + | pc <- V.toList m.push_constants + , Just td <- [pc.type_description] + ] + +layoutForDescriptor :: R.DescriptorType -> LayoutMode +layoutForDescriptor = \case + R.DESCRIPTOR_TYPE_UNIFORM_BUFFER -> Std140Layout + _ -> Std430Layout + +genStructs :: TypeMap -> [(LayoutMode, TypeDescription)] -> Q [Dec] +genStructs tymap = go Map.empty + where + go _ [] = pure [] + go seen ((layout, td) : rest) = + case structTypeName td of + Nothing -> go seen rest + Just nm -> + case Map.lookup nm seen of + -- Already requested in this splice. + Just prev + | prev == layout -> go seen rest -- same layout; reference it + | allMembers16 td -> go seen rest -- std140 == std430; safe to share + | otherwise -> fail (sharingError nm prev layout) + Nothing -> do + existing <- lookupTypeName nm + case existing of + Just _ -> go (Map.insert nm layout seen) rest -- defined elsewhere; reference it + Nothing -> do + mdec <- structRecordDec tymap layout td + decs <- go (Map.insert nm layout seen) rest + pure $ maybe decs (++ decs) mdec + +{- | A struct used under two layouts that aren't byte-compatible (see +'allMembers16') can't be represented by a single Haskell record. +-} +sharingError :: String -> LayoutMode -> LayoutMode -> String +sharingError nm a b = + "vulkan-utils-spirv: struct '" + <> nm + <> "' is used under both " + <> showLayout a + <> " and " + <> showLayout b + <> " layouts but is not layout-compatible: not every member is 16-byte " + <> "aligned. Make each member 16-byte aligned (e.g. use vec4/mat4) so the " + <> "layouts coincide, or keep the struct to a single layout." + where + showLayout Std140Layout = "std140" + showLayout Std430Layout = "std430" diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Types.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Types.hs new file mode 100644 index 000000000..fbfd6cea1 --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Types.hs @@ -0,0 +1,292 @@ +{-# LANGUAGE NoFieldSelectors #-} + +{-| Classify a reflected block member into a base scalar + shape, and map it to +a Haskell type that carries a 'Graphics.Gl.Block.Block' instance. + +The mapping is pluggable ('TypeMap'). The default is 'geomancyTypeMap', onto +[geomancy](https://hackage.haskell.org/package/geomancy)'s @Vec*@ \/ @IVec*@ \/ +@UVec*@ \/ @Mat4@ (which carry native std140/std430 'Block' instances). +'linearTypeMap' is a worked /example/ of the same recipe for a library whose +types are parametric (it is not usable for codegen yet — see its note). + +A map names its types by /raw qualified name/ against the library's base module +(e.g. @Geomancy.Vec3@) rather than a compile-time @''@ quote, so this package +depends on no vector library. The names resolve at the /splice site/, so a module +that splices these records must @import Geomancy qualified@ — under the base +module's own name, not an @as@ alias — and have a 'Graphics.Gl.Block.Block' +instance in scope for each mapped type. A missing import surfaces as a plain +\"Not in scope: type constructor Geomancy.Vec3\". + +Build a map for any other vector library the same way with 'mkTypeMap' + +'qualType' (+ 'scalarLeaf' for the base components): 'geomancyTypeMap' shows the +monomorphic case (the scalar is baked into the name), 'linearTypeMap' the +parametric case (the scalar is applied as a type argument). +-} +module Vulkan.Utils.SpirV.Types + ( ScalarType (..) + , MemberShape (..) + , NumericType (..) + , classifyType + , isBdaPointer + , arrayDims + , LayoutMode (..) + , scalarWidth + , arrayBaseAlign + , leafAlignment + , structBaseAlign + , TypeMap + , mkTypeMap + , scalarLeaf + , qualType + , geomancyTypeMap + , linearTypeMap + ) where + +import Data.Bits ((.&.)) +import Data.Int (Int32, Int64) +import Data.Vector.Storable qualified as VS +import Data.Word (Word32, Word64) +import Graphics.Gl.Block (roundUp) +import Language.Haskell.TH (Type (AppT, ConT), mkName) + +import Data.SpirV.Reflect.Enums.TypeFlags qualified as TypeFlags +import Data.SpirV.Reflect.Traits qualified as Traits +import Data.SpirV.Reflect.TypeDescription (TypeDescription) +import Data.SpirV.Reflect.TypeDescription qualified as TypeDescription + +{- | The base component type of a block member. + +'STInt64' \/ 'STUInt64' are 64-bit integers (@int64_t@ \/ @uint64_t@, the +@GL_EXT_shader_explicit_arithmetic_types_int64@ scalars): 8-byte slots, like +'STDouble'. 'STAddress' is a 64-bit buffer device address (@buffer_reference@ / +@PhysicalStorageBuffer@ pointer): no GLSL scalar spelling, but one 8-byte slot +like any other scalar. Sub-32-bit scalars (@float16@ \/ @int16@) are out of scope. +-} +data ScalarType = STFloat | STDouble | STInt | STUInt | STInt64 | STUInt64 | STBool | STAddress + deriving (Eq, Ord, Show) + +-- | The shape of a block member. +data MemberShape + = ShScalar + | -- | component count (2..4) + ShVector Int + | -- | columns, rows + ShMatrix Int Int + deriving (Eq, Ord, Show) + +{- | Which gl-block layout a block follows. Uniform buffers use std140; storage +buffers and push constants use std430. +-} +data LayoutMode = Std140Layout | Std430Layout + deriving (Eq, Show) + +-- | The byte width of a scalar component (64-bit scalars are 8, the rest 4). +scalarWidth :: ScalarType -> Int +scalarWidth STDouble = 8 +scalarWidth STInt64 = 8 +scalarWidth STUInt64 = 8 +scalarWidth STAddress = 8 +scalarWidth _ = 4 + +{- | The base alignment of an array or matrix column: std140 rounds the +element\/column alignment up to a multiple of 16, std430 keeps it. +-} +arrayBaseAlign :: LayoutMode -> Int -> Int +arrayBaseAlign Std140Layout a = roundUp a 16 +arrayBaseAlign Std430Layout a = a + +{- | The base alignment of a leaf member (scalar\/vector\/matrix) under the +layout. A matrix aligns as its column vector (rounded up to 16 under std140). +-} +leafAlignment :: LayoutMode -> ScalarType -> MemberShape -> Int +leafAlignment mode s = \case + ShScalar -> w + ShVector 2 -> 2 * w + ShVector _ -> 4 * w + ShMatrix _ r -> arrayBaseAlign mode (if r <= 2 then 2 * w else 4 * w) + where + w = scalarWidth s + +{- | The base alignment of a struct: the largest member alignment (at least 1), +rounded up to 16 under std140. +-} +structBaseAlign :: LayoutMode -> [Int] -> Int +structBaseAlign mode alignments = case mode of + Std140Layout -> roundUp base 16 + Std430Layout -> base + where + base = maximum (1 : alignments) + +{- | A numeric (non-struct) leaf type: a base scalar component, a shape +(scalar\/vector\/matrix), and zero or more array dimensions (outermost first; +empty means not an array). +-} +data NumericType = NumericType + { scalar :: ScalarType + , shape :: MemberShape + , array :: [Word32] + } + deriving (Eq, Ord, Show) + +{- | Classify a reflected struct member (a 'TypeDescription') from its numeric +traits and type flags. +-} +classifyType :: TypeDescription -> Maybe NumericType +classifyType + TypeDescription.TypeDescription + { TypeDescription.type_flags = flags + , TypeDescription.traits = mtraits + } = do + Traits.Numeric + { Traits.scalar = Traits.Scalar{Traits.width = width, Traits.signed = signed} + , Traits.vector = Traits.Vector{Traits.component_count = vecN} + , Traits.matrix = Traits.Matrix{Traits.column_count = cols, Traits.row_count = rows} + } <- + numericOf mtraits + let + shape + | cols > 0 = ShMatrix (fromIntegral cols) (fromIntegral rows) + | vecN > 1 = ShVector (fromIntegral vecN) + | otherwise = ShScalar + wide = width >= 64 + baseScalar + | has TypeFlags.TYPE_FLAG_FLOAT = Just $ if wide then STDouble else STFloat + | has TypeFlags.TYPE_FLAG_INT = Just $ case (signed, wide) of + (True, False) -> STInt + (False, False) -> STUInt + (True, True) -> STInt64 + (False, True) -> STUInt64 + | has TypeFlags.TYPE_FLAG_BOOL = Just STBool + | otherwise = Nothing + scalar <- baseScalar + pure + NumericType + { scalar + , shape + , array = maybe [] dimsOf (arrayOf mtraits) + } + where + has bit = (flags .&. bit) /= TypeFlags.TypeFlagBits 0 + + numericOf t = do + TypeDescription.Traits{TypeDescription.numeric = n} <- t + Just n + + arrayOf t = do + TypeDescription.Traits{TypeDescription.array = a} <- t + Just a + +{- | True when this type is a buffer device address: a @buffer_reference@ \/ +@PhysicalStorageBuffer@ pointer (the @REF@ type flag). Such a member is stored as +an 8-byte address, not its pointee inline. +-} +isBdaPointer :: TypeDescription -> Bool +isBdaPointer td = (td.type_flags .&. TypeFlags.TYPE_FLAG_REF) /= TypeFlags.TypeFlagBits 0 + +{- | The array dimensions of a member (outermost first); @[]@ for a non-array, +@[0]@ for a runtime (unsized) array. +-} +arrayDims :: TypeDescription -> [Word32] +arrayDims td = maybe [] dimsOf (fmap (.array) td.traits) + +dimsOf :: Traits.Array -> [Word32] +dimsOf a + | a.dims_count == 0 = [] + | otherwise = VS.toList a.dims + +{- | A pluggable mapping from a classified member to the Haskell type used for +the generated record field. The returned type must have a +'Graphics.Gl.Block.Block' instance. Returning 'Nothing' rejects the member +(the block record is then not generated). +-} +type TypeMap = NumericType -> Maybe Type + +{- | Assemble a 'TypeMap' from a vector and a matrix spelling — the shared core +of 'geomancyTypeMap' / 'linearTypeMap'. Scalars always map to base types (via +'scalarLeaf'); only the vector/matrix cases differ between libraries. + +This maps an /element/ type only: array dimensions are handled one layer up (in +"Vulkan.Utils.SpirV.Block", which wraps the result in 'Vulkan.Utils.SpirV.Array.Array'), +so a member that still carries array dims here is rejected. +-} +mkTypeMap + :: (ScalarType -> Int -> Maybe Type) + -- ^ vector: component scalar and count (2..4) + -> (ScalarType -> Int -> Int -> Maybe Type) + -- ^ matrix: component scalar, columns, rows + -> TypeMap +mkTypeMap vector matrix NumericType{scalar, shape, array} + | not (null array) = Nothing + | otherwise = case shape of + ShScalar -> scalarLeaf scalar + ShVector n -> vector scalar n + ShMatrix c r -> matrix scalar c r + +{- | The Haskell type for a base scalar component, as a global @''@ quote: 'base' +is always in scope, so a field of this type needs no import at the splice site. +'STAddress' has no scalar spelling — a pointer member is mapped to +'Vulkan.Utils.SpirV.DeviceAddress.DeviceAddress' before it reaches a 'TypeMap'. +-} +scalarLeaf :: ScalarType -> Maybe Type +scalarLeaf = \case + STFloat -> Just (ConT ''Float) + STDouble -> Just (ConT ''Double) + STInt -> Just (ConT ''Int32) + STUInt -> Just (ConT ''Word32) + STInt64 -> Just (ConT ''Int64) + STUInt64 -> Just (ConT ''Word64) + STBool -> Just (ConT ''Bool) + STAddress -> Nothing + +{- | A type referenced by /raw qualified name/ (@Module.Type@), resolved at the +splice site. This is how a default 'TypeMap' names a vector library's types +without this package depending on it: the splice site supplies the import. +-} +qualType :: String -> String -> Type +qualType modName typeName = ConT (mkName (modName <> "." <> typeName)) + +{- | The default mapping, onto [geomancy](https://hackage.haskell.org/package/geomancy)'s +vector/matrix types (which carry native std140/std430 'Block' instances). The +names resolve against geomancy's base module, so the splice site needs only a +single @import Geomancy qualified@. +-} +geomancyTypeMap :: TypeMap +geomancyTypeMap = mkTypeMap vector matrix + where + -- geomancy spells the scalar into the type name: Vec3 / IVec3 / UVec3. + vector STFloat n = geomancy <$> sized "Vec" n + vector STInt n = geomancy <$> sized "IVec" n + vector STUInt n = geomancy <$> sized "UVec" n + vector _ _ = Nothing + matrix STFloat 4 4 = Just (geomancy "Mat4") + matrix _ _ _ = Nothing + geomancy = qualType "Geomancy" + +{- | A worked /example/ of mapping a vector library whose types are /parametric/: +[linear](https://hackage.haskell.org/package/linear)'s @V2@ \/ @V3@ \/ @V4@ \/ +@M44@ take the component scalar as a type argument, so the map applies it +(@V3 Float@, @M44 Float@) instead of spelling it into the name as 'geomancyTypeMap' +does. Names resolve against linear's base module (a single @import Linear qualified@). + +Not usable for record generation yet: neither linear nor gl-block ships a +'Graphics.Gl.Block.Block' instance for @V3 Float@ etc., so a generated record +could not derive its 'Foreign.Storable.Storable'. It stands as the template for +the parametric case until linear gains those instances, at which point it becomes +a real alternative default. +-} +linearTypeMap :: TypeMap +linearTypeMap = mkTypeMap vector matrix + where + -- linear's vectors are parametric (V3 a): apply the component scalar. + vector s n + | s `elem` [STFloat, STInt, STUInt] = AppT . linear <$> sized "V" n <*> scalarLeaf s + | otherwise = Nothing + matrix STFloat 4 4 = AppT (linear "M44") <$> scalarLeaf STFloat + matrix _ _ _ = Nothing + linear = qualType "Linear" + +-- | A library type whose name carries its component count, e.g. @Vec3@ \/ @V4@. +sized :: String -> Int -> Maybe String +sized prefix n + | n >= 2 && n <= 4 = Just (prefix <> show n) + | otherwise = Nothing diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/VertexInput.hs b/utils-spirv/src/Vulkan/Utils/SpirV/VertexInput.hs new file mode 100644 index 000000000..305e01e8b --- /dev/null +++ b/utils-spirv/src/Vulkan/Utils/SpirV/VertexInput.hs @@ -0,0 +1,91 @@ +{-| Build Vulkan vertex-input descriptions from a reflected vertex shader's input +variables. Built-in inputs (e.g. @gl_VertexIndex@) are skipped; the remaining +inputs are packed tightly into a single binding (binding 0), ordered by +@location@, with per-attribute formats taken directly from reflection (SPIR-V +@Format@ shares Vulkan's numeric values). +-} +module Vulkan.Utils.SpirV.VertexInput + ( vertexInputState + , vertexInputAttributes + , vertexInputBinding + ) where + +import Data.List (sortOn) +import Data.Vector qualified as V +import Data.Word (Word32) +import Vulkan.Core10 qualified as Vk +import Vulkan.Zero (zero) + +import Data.SpirV.Enum (BuiltIn (..)) +import Data.SpirV.Reflect.Enums.Format qualified as R +import Data.SpirV.Reflect.InterfaceVariable qualified as InterfaceVariable +import Data.SpirV.Reflect.Module (Module) +import Data.SpirV.Reflect.Module qualified +import Data.SpirV.Reflect.Traits qualified as Traits + +{- | The vertex-input state reflected from a vertex module: the single packed +binding plus its attributes ('vertexInputBinding' + 'vertexInputAttributes'). A +module with no vertex inputs — e.g. one pulling geometry from an SSBO via +@gl_VertexIndex@ — yields 'zero' (no bindings, no attributes). +-} +vertexInputState :: Module -> Vk.PipelineVertexInputStateCreateInfo '[] +vertexInputState m = + case vertexInputAttributes m of + [] -> zero + attrs -> + zero + { Vk.vertexBindingDescriptions = V.fromList [vertexInputBinding m] + , Vk.vertexAttributeDescriptions = V.fromList attrs + } + +-- | One attribute per non-built-in input variable, tightly packed into binding 0. +vertexInputAttributes :: Module -> [Vk.VertexInputAttributeDescription] +vertexInputAttributes m = go 0 (inputs m) + where + go _ [] = [] + go off ((loc, fmt, sz) : rest) = + zero + { Vk.location = loc + , Vk.binding = 0 + , Vk.format = fmt + , Vk.offset = off + } + : go (off + sz) rest + +{- | The single binding (binding 0, per-vertex) covering all inputs, with stride +equal to their packed size. +-} +vertexInputBinding :: Module -> Vk.VertexInputBindingDescription +vertexInputBinding m = + zero + { Vk.binding = 0 + , Vk.stride = sum [sz | (_, _, sz) <- inputs m] + , Vk.inputRate = Vk.VERTEX_INPUT_RATE_VERTEX + } + +-- | @(location, format, byte size)@ for each non-built-in input, ordered by location. +inputs :: Module -> [(Word32, Vk.Format, Word32)] +inputs m = + sortOn + (\(loc, _, _) -> loc) + -- a non-built-in reads as Nothing (ffi) or the @BuiltIn (-1)@ sentinel (yaml) + [ (v.location, ivFormat v, ivSize v) + | v <- V.toList m.input_variables + , v.built_in `elem` [Nothing, Just (BuiltIn (-1))] + ] + +-- Vulkan-side field extractions. + +ivFormat :: InterfaceVariable.InterfaceVariable -> Vk.Format +ivFormat InterfaceVariable.InterfaceVariable{InterfaceVariable.format = R.Format n} = + Vk.Format (fromIntegral n) + +ivSize :: InterfaceVariable.InterfaceVariable -> Word32 +ivSize InterfaceVariable.InterfaceVariable{InterfaceVariable.numeric = num} = + components * (width `div` 8) + where + Traits.Numeric + { Traits.scalar = Traits.Scalar{Traits.width = width} + , Traits.vector = Traits.Vector{Traits.component_count = vecN} + } = num + components = max 1 vecN diff --git a/utils-spirv/test/LayoutSpec.hs b/utils-spirv/test/LayoutSpec.hs new file mode 100644 index 000000000..398849c37 --- /dev/null +++ b/utils-spirv/test/LayoutSpec.hs @@ -0,0 +1,229 @@ +{-# LANGUAGE OverloadedStrings #-} + +{-| Tests for the structured 'Layout' IR, its normalization to a scalar 'OffsetMap', +and the value-level unifier. +-} +module LayoutSpec (tests) where + +import Data.Either (isLeft) +import Data.List (isInfixOf) +import Data.Map.Strict qualified as Map +import Data.Text (unpack) +import Data.Vector qualified as V +import Test.Tasty +import Test.Tasty.HUnit + +import Data.SpirV.Reflect.BlockVariable qualified +import Data.SpirV.Reflect.DescriptorBinding qualified +import Data.SpirV.Reflect.FFI (load) +import Data.SpirV.Reflect.Module (Module) +import Data.SpirV.Reflect.Module qualified +import Data.SpirV.Reflect.TypeDescription (TypeDescription) + +import Vulkan.Utils.SpirV.Layout +import Vulkan.Utils.SpirV.Layout qualified as OffsetMap (OffsetMap (..)) +import Vulkan.Utils.SpirV.Types (LayoutMode (..), ScalarType (..)) + +tests :: TestTree +tests = + testGroup + "layout IR + unification" + [ structuredTests + , normalizeTests + , unifyTests + , runtimeTests + , mergeTests + , reflectionTests + ] + +-- Convenience constructors. ----------------------------------------------------- + +-- | A single-member std430 layout. +field430 :: FieldType -> Layout +field430 ft = fromFields Std430Layout Nothing [("x", ft)] + +-- | A single-member std140 layout. +field140 :: FieldType -> Layout +field140 ft = fromFields Std140Layout Nothing [("x", ft)] + +structuredTests :: TestTree +structuredTests = + testGroup + "structured layout (fromFields)" + [ testCase "std430 packs by member alignment" $ do + -- mat4 (align 16) @0 size 64, vec2 (align 8) @64, float @72, int @76 + let l = + fromFields + Std430Layout + (Just "Push") + [ ("transform", Matrix STFloat 4 4) + , ("offset", Vector STFloat 2) + , ("scale", Scalar STFloat) + , ("count", Scalar STInt) + ] + offsets l @?= [("transform", 0), ("offset", 64), ("scale", 72), ("count", 76)] + l.size @?= Just 80 + l.align @?= 16 + , testCase "std140 rounds a scalar array's stride and the struct align to 16" $ do + let l = field140 (ArrayOf (Sized 4) (Scalar STFloat)) + l.align @?= 16 + l.size @?= Just 64 -- stride 16 * 4 + , testCase "std430 packs a scalar array tightly" $ do + let l = field430 (ArrayOf (Sized 4) (Scalar STFloat)) + l.align @?= 4 + l.size @?= Just 16 + , testCase "vec3 occupies 12 bytes but aligns to 16" $ do + let l = fromFields Std430Layout Nothing [("a", Vector STFloat 3), ("b", Scalar STFloat)] + offsets l @?= [("a", 0), ("b", 12)] + ] + +normalizeTests :: TestTree +normalizeTests = + testGroup + "normalize to scalar offset map" + [ testCase "vec4 flattens to four floats" $ + (normalize (field430 (Vector STFloat 4))).slots + @?= [Slot 0 STFloat, Slot 4 STFloat, Slot 8 STFloat, Slot 12 STFloat] + , testCase "mat4 flattens column-major to a 16-float offset map" $ + map (.offset) (normalize (field430 (Matrix STFloat 4 4))).slots + @?= [0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60] + , testCase "std140 float[4] strides by 16" $ + map (.offset) (normalize (field140 (ArrayOf (Sized 4) (Scalar STFloat)))).slots + @?= [0, 16, 32, 48] + ] + +unifyTests :: TestTree +unifyTests = + testGroup + "unify (layout equivalence)" + [ testCase "mat4 == vec4[4] == float[16] under std430" $ do + let + m = normalize (field430 (Matrix STFloat 4 4)) + v = normalize (field430 (ArrayOf (Sized 4) (Vector STFloat 4))) + f = normalize (field430 (ArrayOf (Sized 16) (Scalar STFloat))) + foldUnify m [v, f] @?= Right m + , testCase "float[16] under std140 is NOT layout-equivalent to mat4" $ do + let + m = normalize (field430 (Matrix STFloat 4 4)) + f = normalize (field140 (ArrayOf (Sized 16) (Scalar STFloat))) + case unify m f of + Left (SlotMismatch off _ _) -> off @?= 4 -- first divergence: mat4 has a float here + other -> assertFailure ("expected a SlotMismatch at 4, got " <> show other) + , testCase "a scalar-kind mismatch is reported with its offset" $ do + let + a = normalize (field430 (Vector STFloat 2)) + b = normalize (field430 (Vector STInt 2)) + unify a b @?= Left (SlotMismatch 0 STFloat STInt) + , testCase "differing total size fails even when slots agree" $ do + -- Same scalar slots, different declared size (e.g. extra trailing padding). + let + a = normalize (field430 (Vector STFloat 4)) + big = a{OffsetMap.size = Just 32} + unify a big @?= Left (SizeMismatch 16 32) + , testCase "renderMismatch is legible" $ + assertBool "mentions offset and types" $ + "offset 4" `isInfixOf` renderMismatch (SlotMismatch 4 STFloat STInt) + && "float" `isInfixOf` renderMismatch (SlotMismatch 4 STFloat STInt) + ] + +runtimeTests :: TestTree +runtimeTests = + testGroup + "runtime arrays (unification with a hole)" + [ testCase "an open vec4[] offset map is open (no size, has a tail)" $ do + let g = normalize (field430 (ArrayOf Runtime (Vector STFloat 4))) + g.size @?= Nothing + fmap (.stride) g.tail @?= Just 16 + , testCase "a runtime vec4[] unifies with a concrete vec4[3], pinning the length" $ do + let + open = normalize (field430 (ArrayOf Runtime (Vector STFloat 4))) + closed = normalize (field430 (ArrayOf (Sized 3) (Vector STFloat 4))) + unify open closed @?= Right closed + unify closed open @?= Right closed -- symmetric + , testCase "a runtime vec4[] rejects an offset map that is not whole elements" $ do + let + open = normalize (field430 (ArrayOf Runtime (Vector STFloat 4))) + ragged = normalize (field430 (ArrayOf (Sized 5) (Scalar STFloat))) + assertBool "should not unify" (isLeft (unify open ragged)) + ] + +mergeTests :: TestTree +mergeTests = + testGroup + "mergeKeyed (row unification across pipelines)" + [ testCase "disjoint keys union; shared keys must agree" $ do + let + camera = normalize (field140 (Vector STFloat 4)) + lights = normalize (field430 (ArrayOf (Sized 2) (Vector STFloat 4))) + merged = + mergeKeyed + [ ((0, 0) :: (Int, Int), camera) -- vertex stage: set 0 binding 0 + , ((0, 1), lights) -- vertex stage: set 0 binding 1 + , ((0, 0), camera) -- fragment stage: same UBO, must agree + ] + fmap Map.keys merged @?= Right [(0, 0), (0, 1)] + , testCase "a conflicting shared key reports the key and the mismatch" $ do + let + a = normalize (field430 (Vector STFloat 4)) + b = normalize (field430 (Vector STInt 4)) + case mergeKeyed [((0, 0) :: (Int, Int), a), ((0, 0), b)] of + Left (k, SlotMismatch{}) -> k @?= (0, 0) + other -> assertFailure ("expected a keyed SlotMismatch, got " <> show other) + ] + +reflectionTests :: TestTree +reflectionTests = + testGroup + "built from reflected SPIR-V" + [ testCase "push.comp Push has std430 offsets 0/64/72/76, size 80" $ do + m <- load "test/fixtures/push.comp.spv" + td <- maybe (assertFailure "no push constant block") pure (pushBlock m) + l <- either (assertFailure . ("layoutOf: " <>)) pure (layoutOf Std430Layout td) + map snd (offsets l) @?= [0, 64, 72, 76] + l.size @?= Just 80 + , testCase "push.comp Output (vec4 data[]) is an open offset map with stride 16" $ do + m <- load "test/fixtures/push.comp.spv" + td <- maybe (assertFailure "no SSBO binding") pure (firstBinding m) + g <- either (assertFailure . ("offsetMapOf: " <>)) pure (offsetMapOf Std430Layout td) + g.size @?= Nothing + fmap (.stride) g.tail @?= Just 16 + , testCase "ssbo-struct Particle[] tail unifies with a concrete Particle[2]" $ do + m <- load "test/fixtures/ssbo-struct.comp.spv" + td <- maybe (assertFailure "no SSBO binding") pure (firstBinding m) + open <- either (assertFailure . ("offsetMapOf: " <>)) pure (offsetMapOf Std430Layout td) + -- Particle = vec3 @0, vec2 @16, float @24, uint @28 (stride 32). + let + particle = + Struct + ( fromFields + Std430Layout + (Just "Particle") + [ ("position", Vector STFloat 3) + , ("velocity", Vector STFloat 2) + , ("mass", Scalar STFloat) + , ("flags", Scalar STUInt) + ] + ) + closed = normalize (fromFields Std430Layout Nothing [("items", ArrayOf (Sized 2) particle)]) + open `unifies` closed + ] + +-- Helpers. ---------------------------------------------------------------------- + +offsets :: Layout -> [(String, Int)] +offsets l = [(unpack m.name, m.offset) | m <- l.members] + +unifies :: OffsetMap -> OffsetMap -> Assertion +unifies a b = case unify a b of + Right _ -> pure () + Left e -> assertFailure (renderMismatch e) + +pushBlock :: Module -> Maybe TypeDescription +pushBlock m = case V.toList m.push_constants of + (pc : _) -> pc.type_description + [] -> Nothing + +firstBinding :: Module -> Maybe TypeDescription +firstBinding m = case V.toList m.descriptor_bindings of + (b : _) -> b.type_description + [] -> Nothing diff --git a/utils-spirv/test/Spec.hs b/utils-spirv/test/Spec.hs new file mode 100644 index 000000000..c42a419ef --- /dev/null +++ b/utils-spirv/test/Spec.hs @@ -0,0 +1,696 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} + +module Main (main) where + +import Data.Bits ((.|.)) +import Data.Either (isLeft) +import Data.Int (Int32, Int64) +import Data.List (sortOn) +import Data.Maybe (isJust) +import Data.Proxy (Proxy (..)) +import Data.Vector qualified as V +import Data.Vector.Storable qualified as VS +import Data.Word (Word32, Word64) +import Foreign.Marshal.Alloc (allocaBytes) +import Foreign.Marshal.Utils (fillBytes) +import Foreign.Ptr (castPtr) +import Foreign.Storable (Storable (..)) +import GHC.Generics (Generic) +import Geomancy qualified +import Geomancy.Mat4 (identity) +import Geomancy.UVec2 (uvec2) +import Geomancy.Vec2 (Vec2, vec2) +import Geomancy.Vec3 (vec3) +import Geomancy.Vec4 (Vec4, vec4) +import Graphics.Gl.Block (Block, Std140 (..), Std430 (..)) +import Language.Haskell.TH (Type (AppT, ConT), mkName) +import Test.Tasty +import Test.Tasty.HUnit + +import Vulkan.Core10 qualified as Vk + +import Data.SpirV.Reflect.FFI (load) +import Vulkan.Utils.SpirV.Array qualified as Array +import Vulkan.Utils.SpirV.Buffer (allocArrayBuffer, newBuffer, readBuffer, readBufferElem, writeBufferElem) +import Vulkan.Utils.SpirV.Descriptors (descriptorSetLayoutInfos, mergedDescriptorSetLayoutInfos, mergedPushConstantRanges, pushConstantRanges) +import Vulkan.Utils.SpirV.DeviceAddress (DeviceAddress (..)) +import Vulkan.Utils.SpirV.Layout (ArraySize (..), FieldType (..), fromFields, normalize) +import Vulkan.Utils.SpirV.Layout qualified +import Vulkan.Utils.SpirV.Signature (ArrayOf, Fits, Sig, knownLayoutInstance, layoutSig) +import Vulkan.Utils.SpirV.Specialization (specializationConstants, specializationMapEntries) +import Vulkan.Utils.SpirV.Stage (CompatibleResources, KnownLayoutSig (..), MatchInterface, linkStages, matchInterface, mergeLayout, reflectPipelineLayoutSig, reflectStageSig, stageInfoOf) +import Vulkan.Utils.SpirV.Stage qualified +import Vulkan.Utils.SpirV.Stage qualified as StageInfo (StageInfo (..)) +import Vulkan.Utils.SpirV.TH (reflectShaderTypes) +import Vulkan.Utils.SpirV.Types (LayoutMode (..), MemberShape (..), NumericType (..), ScalarType (..), geomancyTypeMap, linearTypeMap) +import Vulkan.Utils.SpirV.VertexInput (vertexInputAttributes, vertexInputBinding, vertexInputState) + +import LayoutSpec qualified + +-- Generate the @Params@ record from the committed compute fixture. +reflectShaderTypes "test/fixtures/julia.comp.spv" + +-- Generate the @Push@ push-constant record (std430) from its fixture. +reflectShaderTypes "test/fixtures/push.comp.spv" + +-- Generate the @Particle@ element record of an SSBO array of structs. The +-- wrapping @Particles@ runtime-array block is (correctly) not generated. +reflectShaderTypes "test/fixtures/ssbo-struct.comp.spv" + +-- Generate @Material@ (shared across a std140 UBO field and a std430 SSBO array, +-- safe because all members are 16-byte aligned) and @Scene@, which has the +-- nested @Material@ as a field. +reflectShaderTypes "test/fixtures/nested.comp.spv" + +-- Generate @Kernel140@ / @Kernel430@: records with fixed-size @Array n a@ fields +-- under std140 (element stride 16) and std430 (tight) layouts. +reflectShaderTypes "test/fixtures/array-field.comp.spv" + +-- Generate @Grid430@ / @Grid140@: a 2D array field @float grid[3][4]@ maps to +-- @Array 3 (Array 4 Float)@. +reflectShaderTypes "test/fixtures/array2d.comp.spv" + +-- Generate @Node@ (a self-referential @buffer_reference@ struct) and @Bvh@ (a +-- push-constant block holding a @Node@ device address). The pointer members map +-- to @DeviceAddress Node@; @Node@ is generated once despite the cycle. +reflectShaderTypes "test/fixtures/bda.comp.spv" + +-- Generate @Wide@: a std430 push-constant block with 64-bit integer members +-- (@uint64_t hi@ / @int64_t lo@), which must occupy 8-byte slots like a double. +reflectShaderTypes "test/fixtures/wide.comp.spv" + +-- A std430 element whose array stride (32) exceeds its packed size (20): vec4 @0 +-- + float @16 -> size 20, rounded up to its 16-byte alignment for the stride. +data Pad = Pad Vec4 Float + deriving stock (Generic, Eq, Show) + deriving anyclass (Block) + deriving (Storable) via (Std430 Pad) + +-- Three types that are layout-equivalent under std430 (same scalar offset map): mat4, vec4[4] and +-- float[16]. Their layout signatures are produced from the value-level normal +-- form (the oracle), so they promote to the SAME 'Sig' and 'Fits' accepts them. +data EquivMat4 +data EquivVec4x4 +data EquivFloat16 + +pure + ( knownLayoutInstance + ''EquivMat4 + (normalize (fromFields Std430Layout Nothing [("m", Matrix STFloat 4 4)])) + ) +pure + ( knownLayoutInstance + ''EquivVec4x4 + (normalize (fromFields Std430Layout Nothing [("a", ArrayOf (Sized 4) (Vector STFloat 4))])) + ) +pure + ( knownLayoutInstance + ''EquivFloat16 + (normalize (fromFields Std430Layout Nothing [("a", ArrayOf (Sized 16) (Scalar STFloat))])) + ) + +-- Compile-time witnesses: each body only type-checks if the 'Fits' constraint +-- holds (a mismatch is a compile error). Returned as 'Bool' so a test can run. +mat4FitsVec4x4 :: (Fits (Sig EquivMat4) (Sig EquivVec4x4)) => Bool +mat4FitsVec4x4 = True + +vec4x4FitsFloat16 :: (Fits (Sig EquivVec4x4) (Sig EquivFloat16)) => Bool +vec4x4FitsFloat16 = True + +pushFitsItself :: (Fits (Sig Push) (Sig Push)) => Bool +pushFitsItself = True + +-- Two records that are layout-equivalent under std430: two vec4s vs four vec2s both flatten to +-- eight floats at offsets 0,4,..,28 (size 32). Real 'Storable' records, so a +-- value written as one can be read back through the other. +data PairA = PairA Vec4 Vec4 + deriving stock (Generic, Eq, Show) + deriving anyclass (Block) + deriving (Storable) via (Std430 PairA) + +data PairB = PairB Vec2 Vec2 Vec2 Vec2 + deriving stock (Generic, Eq, Show) + deriving anyclass (Block) + deriving (Storable) via (Std430 PairB) + +pure + ( knownLayoutInstance + ''PairA + (normalize (fromFields Std430Layout Nothing [("a", Vector STFloat 4), ("b", Vector STFloat 4)])) + ) +pure + ( knownLayoutInstance + ''PairB + ( normalize + ( fromFields + Std430Layout + Nothing + [("a", Vector STFloat 2), ("b", Vector STFloat 2), ("c", Vector STFloat 2), ("d", Vector STFloat 2)] + ) + ) + ) + +-- Stage signatures for a matched vertex+fragment pair (shared Scene UBO, vertex +-- push constant, fragment SSBO). Phase 1 exercises only the interface. +reflectStageSig "MeshVert" "test/fixtures/mesh.vert.spv" +reflectStageSig "MeshFrag" "test/fixtures/mesh.frag.spv" + +-- The merged pipeline-layout signature of the same pair, promoted to the type level. +reflectPipelineLayoutSig "MeshLayout" ["test/fixtures/mesh.vert.spv", "test/fixtures/mesh.frag.spv"] + +-- Compile-time witness: only type-checks if the vertex→fragment interface matches. +meshInterfaceOk :: (MatchInterface MeshVert MeshFrag) => Bool +meshInterfaceOk = True + +-- Compile-time witness: only type-checks if the shared resources are compatible +-- (the Scene UBO is declared identically in both stages). +meshResourcesOk :: (CompatibleResources MeshVert MeshFrag) => Bool +meshResourcesOk = True + +main :: IO () +main = + defaultMain $ + testGroup + "vulkan-utils-spirv" + [ testGroup + "block record (std140)" + [ testCase "alignment is std140 vec4 (16)" $ + alignment params @?= 16 + , testCase "fields land at the shader's std140 offsets" $ do + -- shader.comp: center@0, resolution@8, escapeRadius@16, maxIterations@20 + let n = sizeOf params + (resX, esc, maxIt) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) params + (,,) + <$> (peekByteOff ptr 8 :: IO Word32) + <*> (peekByteOff ptr 16 :: IO Float) + <*> (peekByteOff ptr 20 :: IO Int32) + resX @?= 512 + esc @?= 2.0 + maxIt @?= 1000 + , testCase "round-trips through Storable" $ do + let n = sizeOf params + rt <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) params + peek (castPtr ptr) + rt @?= params + ] + , testGroup + "field type spelling (TypeMap)" + -- geomancyTypeMap is also exercised end-to-end by the record fixtures. + -- linearTypeMap is a worked example of the parametric case with no Block + -- instances to splice against yet, so these pure checks pin its spelling. + [ testCase "geomancy: scalar in name, single qualifier" $ do + geomancyTypeMap (NumericType STFloat (ShVector 3) []) @?= Just (ConT (mkName "Geomancy.Vec3")) + geomancyTypeMap (NumericType STUInt (ShVector 2) []) @?= Just (ConT (mkName "Geomancy.UVec2")) + geomancyTypeMap (NumericType STFloat (ShMatrix 4 4) []) @?= Just (ConT (mkName "Geomancy.Mat4")) + , testCase "linear: parametric, scalar applied" $ do + linearTypeMap (NumericType STFloat (ShVector 3) []) @?= Just (AppT (ConT (mkName "Linear.V3")) (ConT ''Float)) + linearTypeMap (NumericType STUInt (ShVector 2) []) @?= Just (AppT (ConT (mkName "Linear.V2")) (ConT ''Word32)) + linearTypeMap (NumericType STFloat (ShMatrix 4 4) []) @?= Just (AppT (ConT (mkName "Linear.M44")) (ConT ''Float)) + , testCase "both reject array members and address scalars" $ do + geomancyTypeMap (NumericType STFloat (ShVector 3) [4]) @?= Nothing + linearTypeMap (NumericType STAddress ShScalar []) @?= Nothing + ] + , testGroup + "push-constant block (std430)" + [ testCase "alignment is mat4 (16)" $ + alignment pushc @?= 16 + , testCase "fields land at the shader's std430 offsets" $ do + -- push.comp: transform@0, offset@64, scale@72, count@76 + let n = sizeOf pushc + (sc, cnt) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) pushc + (,) + <$> (peekByteOff ptr 72 :: IO Float) + <*> (peekByteOff ptr 76 :: IO Int32) + sc @?= 2.5 + cnt @?= 7 + , testCase "size covers the last member (80)" $ + sizeOf pushc @?= 80 + , testCase "round-trips through Storable" $ do + -- @Push@ has a @Mat4@ field (no @Eq@), so compare the comparable + -- fields after a Storable round-trip rather than the whole record. + let n = sizeOf pushc + rt <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) pushc + peek (castPtr ptr) :: IO Push + (rt.offset, rt.scale, rt.count) @?= (pushc.offset, pushc.scale, pushc.count) + ] + , testCase "push-constant range from reflection" $ do + m <- load "test/fixtures/push.comp.spv" + case pushConstantRanges m of + [r] -> do + r.offset @?= 0 + r.size @?= 80 + r.stageFlags @?= Vk.SHADER_STAGE_COMPUTE_BIT + other -> assertFailure ("expected one range, got " <> show (length other)) + , testGroup + "64-bit integer scalars (std430)" + [ testCase "uint64_t/int64_t members land at 8-byte-aligned offsets" $ do + -- wide.comp: hi@0, lo@8, tag@16. A 64-bit int is an 8-byte slot, so + -- tag sits at 16 — under the old 32-bit assumption it would be at 8. + let n = sizeOf wide + (hi, lo, tag) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) wide + (,,) + <$> (peekByteOff ptr 0 :: IO Word64) + <*> (peekByteOff ptr 8 :: IO Int64) + <*> (peekByteOff ptr 16 :: IO Word32) + (hi, lo, tag) @?= (0xDEADBEEFCAFEF00D, -42, 7) + , testCase "record rounds up to its 8-byte alignment (24)" $ + -- gl-block pads the record to a multiple of its base alignment (8); + -- the SPIR-V block extent below is the unpadded 20. + sizeOf wide @?= 24 + , testCase "push-constant range from reflection ends at 20" $ do + -- 16 + 4: tag (a uint) ends at 20, so each int64 before it took 8 + -- bytes — a 4-byte misclassification would put the end at 12. + m <- load "test/fixtures/wide.comp.spv" + case pushConstantRanges m of + [r] -> (r.offset, r.size) @?= (0, 20) + other -> assertFailure ("expected one range, got " <> show (length other)) + , testCase "Wide's std430 Sig has 8-byte int slots at 0/8, uint at 16" $ + layoutSig @Wide + @?= normalize + ( fromFields + Std430Layout + Nothing + [ ("hi", Scalar STUInt64) + , ("lo", Scalar STInt64) + , ("tag", Scalar STUInt) + ] + ) + ] + , testGroup + "buffer_reference (BDA)" + [ testCase "Node's std430 Sig has DeviceAddress slots at 32/40, size 64" $ + -- boundsMin@0, boundsMax@16, left@32, right@40, primCount@48; size 64. + layoutSig @Node + @?= normalize + ( fromFields + Std430Layout + Nothing + [ ("boundsMin", Vector STFloat 4) + , ("boundsMax", Vector STFloat 4) + , ("left", Scalar STAddress) + , ("right", Scalar STAddress) + , ("primCount", Scalar STUInt) + ] + ) + , testCase "DeviceAddress fields land at std430 byte offsets 32 and 40" $ do + let n = Node (vec4 0 0 0 0) (vec4 0 0 0 0) (DeviceAddress 0xCAFE) (DeviceAddress 0xBEEF) 0 + (a32, a40) <- allocaBytes (sizeOf (Std430 n)) $ \p -> do + poke (castPtr p) (Std430 n) + (,) <$> (peekByteOff p 32 :: IO Word64) <*> (peekByteOff p 40 :: IO Word64) + (a32, a40) @?= (0xCAFE, 0xBEEF) + , testCase "push-constant block is a single 8-byte device address" $ do + m <- load "test/fixtures/bda.comp.spv" + case pushConstantRanges m of + [r] -> (r.offset, r.size) @?= (0, 8) + other -> assertFailure ("expected one range, got " <> show (length other)) + ] + , testGroup + "SSBO array-of-struct element (std430)" + [ testCase "element record alignment is vec3 (16)" $ + alignment particle @?= 16 + , testCase "fields land at the shader's std430 offsets" $ do + -- ssbo-struct.comp: position@0, velocity@16, mass@24, flags@28 + let n = sizeOf particle + (vx, m, fl) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) particle + (,,) + <$> (peekByteOff ptr 16 :: IO Float) + <*> (peekByteOff ptr 24 :: IO Float) + <*> (peekByteOff ptr 28 :: IO Word32) + vx @?= 3.0 + m @?= 1.5 + fl @?= 7 + , testCase "element size is std430-padded (32)" $ + sizeOf particle @?= 32 + ] + , testGroup + "nested struct as a field (shared across layouts)" + [ testCase "nested element record offsets (Material)" $ do + let n = sizeOf mat + (al, em) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) mat + (,) <$> (peekByteOff ptr 0 :: IO Float) <*> (peekByteOff ptr 16 :: IO Float) + alignment mat @?= 16 + sizeOf mat @?= 32 + al @?= 1.0 + em @?= 2.0 + , testCase "parent embeds the nested struct at its std140 offsets" $ do + -- nested.comp: sun@0 (albedo@0, emission@16), tint@32 + let n = sizeOf scene + (al, em, ti) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) scene + (,,) + <$> (peekByteOff ptr 0 :: IO Float) + <*> (peekByteOff ptr 16 :: IO Float) + <*> (peekByteOff ptr 32 :: IO Float) + alignment scene @?= 16 + sizeOf scene @?= 48 + al @?= 1.0 + em @?= 2.0 + ti @?= 3.0 + ] + , testGroup + "fixed-size array fields (Array n a)" + [ testCase "std140 rounds element stride up to 16" $ do + -- Kernel140: taps[4] vec4 @0 (stride 16), weights[4] float @64 (stride 16) + let n = sizeOf k140 + (t0, t1, w0, w1, w3) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) k140 + (,,,,) + <$> (peekByteOff ptr 0 :: IO Float) + <*> (peekByteOff ptr 16 :: IO Float) + <*> (peekByteOff ptr 64 :: IO Float) + <*> (peekByteOff ptr 80 :: IO Float) -- std140 stride 16 + <*> (peekByteOff ptr 112 :: IO Float) + alignment k140 @?= 16 + sizeOf k140 @?= 128 + (t0, t1) @?= (1, 2) + (w0, w1, w3) @?= (10, 20, 40) + , testCase "std430 packs the float array tightly (stride 4)" $ do + -- Kernel430: taps[4] vec4 @0, weights[4] float @64 (stride 4) + let n = sizeOf k430 + (w0, w1, w3) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) k430 + (,,) + <$> (peekByteOff ptr 64 :: IO Float) + <*> (peekByteOff ptr 68 :: IO Float) -- std430 stride 4 + <*> (peekByteOff ptr 76 :: IO Float) + sizeOf k430 @?= 80 + (w0, w1, w3) @?= (10, 20, 40) + ] + , testGroup + "multi-dimensional array field (Array h (Array w a))" + [ testCase "std430 nested strides (inner 4, outer 16)" $ do + -- Grid430: head@0, grid[3][4] @16; grid[i][j] @ 16 + i*16 + j*4 + let n = sizeOf g430 + (h, g00, g01, g10, g23) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) g430 + (,,,,) + <$> (peekByteOff ptr 0 :: IO Float) + <*> (peekByteOff ptr 16 :: IO Float) + <*> (peekByteOff ptr 20 :: IO Float) + <*> (peekByteOff ptr 32 :: IO Float) + <*> (peekByteOff ptr 60 :: IO Float) + sizeOf g430 @?= 64 + h @?= 9 + (g00, g01, g10, g23) @?= (1, 2, 5, 12) + , testCase "std140 nested strides (inner 16, outer 64)" $ do + -- Grid140: head@0, grid[3][4] @16; grid[i][j] @ 16 + i*64 + j*16 + let n = sizeOf g140 + (g00, g01, g10) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + poke (castPtr ptr) g140 + (,,) + <$> (peekByteOff ptr 16 :: IO Float) + <*> (peekByteOff ptr 32 :: IO Float) -- inner stride 16 + <*> (peekByteOff ptr 80 :: IO Float) -- outer stride 64 + sizeOf g140 @?= 208 + (g00, g01, g10) @?= (1, 2, 5) + ] + , testGroup + "runtime-sized array (Storable.Vector at the std430 stride)" + [ testCase "elements placed at the array stride (32 > element size 20)" $ do + let + pads = VS.fromList [Pad (vec4 1 0 0 0) 2, Pad (vec4 9 0 0 0) 8] + stride = Array.std430Stride (Proxy :: Proxy Pad) + n = 2 * stride + (a0, b0, a1, b1) <- allocaBytes n $ \ptr -> do + fillBytes ptr 0 n + Array.pokeStd430 ptr pads + (,,,) + <$> (peekByteOff ptr 0 :: IO Float) + <*> (peekByteOff ptr 16 :: IO Float) + <*> (peekByteOff ptr 32 :: IO Float) -- second element at stride 32 + <*> (peekByteOff ptr 48 :: IO Float) + stride @?= 32 + (a0, b0, a1, b1) @?= (1, 2, 9, 8) + , testCase "round-trips through pokeStd430/peekStd430" $ do + let + pads = VS.fromList [Pad (vec4 1 2 3 4) 5, Pad (vec4 6 7 8 9) 10] + stride = Array.std430Stride (Proxy :: Proxy Pad) + rt <- allocaBytes (2 * stride) $ \ptr -> do + fillBytes ptr 0 (2 * stride) + Array.pokeStd430 ptr pads + Array.peekStd430 2 ptr + VS.toList rt @?= VS.toList pads + ] + , testGroup + "specialization constants from reflection" + [ testCase "reflects ids and names (ascending, non-contiguous)" $ do + m <- load "test/fixtures/spec.comp.spv" + specializationConstants m + @?= [(0, Just "count"), (3, Just "scale")] + , testCase "map entries keep real ids but pack tightly" $ do + m <- load "test/fixtures/spec.comp.spv" + let es = toL (specializationMapEntries m) + map (\e -> (e.constantID, e.offset, e.size)) es + @?= [(0, 0, 4), (3, 4, 4)] + ] + , testCase "descriptor set layout from reflection" $ do + m <- load "test/fixtures/julia.comp.spv" + case descriptorSetLayoutInfos m of + [(setNo, info)] -> do + setNo @?= 0 + let bs = toL info.bindings + map (.binding) bs @?= [0, 1] + map (.descriptorType) bs + @?= [Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER, Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER] + all ((== Vk.SHADER_STAGE_COMPUTE_BIT) . (.stageFlags)) bs @? "all compute stage" + other -> assertFailure ("expected one set, got " <> show (length other)) + , testGroup + "vertex input from reflection" + [ testCase "attributes: format + tightly-packed offset" $ do + m <- load "test/fixtures/tri.vert.spv" + let attrs = vertexInputAttributes m + map (\a -> (a.location, a.format, a.offset)) attrs + @?= [ (0, Vk.FORMAT_R32G32B32_SFLOAT, 0) + , (1, Vk.FORMAT_R32G32_SFLOAT, 12) + , (2, Vk.FORMAT_R32G32B32A32_SFLOAT, 20) + ] + , testCase "binding stride is packed size" $ do + m <- load "test/fixtures/tri.vert.spv" + (vertexInputBinding m).stride @?= 36 + , testCase "vertexInputState packs the reflected binding + attributes" $ do + m <- load "test/fixtures/tri.vert.spv" + let vis = vertexInputState m + map (.stride) (toL vis.vertexBindingDescriptions) @?= [36] + map (.offset) (toL vis.vertexAttributeDescriptions) @?= [0, 12, 20] + , testCase "vertexInputState is empty when a module declares no vertex inputs" $ do + m <- load "test/fixtures/julia.comp.spv" + let vis = vertexInputState m + null (toL vis.vertexBindingDescriptions) @? "no bindings" + null (toL vis.vertexAttributeDescriptions) @? "no attributes" + ] + , testGroup + "type-level signatures" + [ testCase "a record's Sig reflects back to its value-level offset map (the oracle)" $ + -- Push: mat4 @0, vec2 @64, float @72, int @76, size 80. + layoutSig @Push + @?= normalize + ( fromFields + Std430Layout + Nothing + [ ("transform", Matrix STFloat 4 4) + , ("offset", Vector STFloat 2) + , ("scale", Scalar STFloat) + , ("count", Scalar STInt) + ] + ) + , testCase "layout-equivalent layouts promote to the SAME Sig (mat4 == vec4[4] == float[16])" $ do + layoutSig @EquivMat4 @?= layoutSig @EquivVec4x4 + layoutSig @EquivVec4x4 @?= layoutSig @EquivFloat16 + , testCase "Fits accepts layout-equivalent layouts at the type level" $ do + -- These only type-check because the 'Fits' constraints are satisfied. + mat4FitsVec4x4 @?= True + vec4x4FitsFloat16 @?= True + pushFitsItself @?= True + ] + , testGroup + "buffer (structural views)" + [ testCase "Storable size matches the layout signature size" $ + (layoutSig @PairA).size @?= Just (sizeOf (undefined :: PairA)) + , testCase "round-trips the same record" $ do + let a = PairA (vec4 9 8 7 6) (vec4 5 4 3 2) + buf <- newBuffer a + a' <- readBuffer buf + a' @?= a + , testCase "write one layout, read a layout-compatible one back (typed view)" $ do + buf <- newBuffer (PairA (vec4 1 2 3 4) (vec4 5 6 7 8)) + -- Fits (Sig PairB) (Sig PairA) holds because the layouts are equivalent. + PairB x y z w <- readBuffer buf + (x, y, z, w) @?= (vec2 1 2, vec2 3 4, vec2 5 6, vec2 7 8) + ] + , testGroup + "buffer (runtime-array element access)" + [ testCase "writes and reads back array elements by index" $ do + -- ArrayOf (Sig PairA): a runtime PairA[] (stride 32, base 0). + buf <- allocArrayBuffer @(ArrayOf (Sig PairA)) 2 + let + p0 = PairA (vec4 1 2 3 4) (vec4 5 6 7 8) + p1 = PairA (vec4 9 10 11 12) (vec4 13 14 15 16) + writeBufferElem buf 0 p0 + writeBufferElem buf 1 p1 + e0 <- readBufferElem buf 0 + e1 <- readBufferElem buf 1 + (e0, e1) @?= (p0, p1) + , testCase "reads an element back through a layout-compatible record (typed view)" $ do + buf <- allocArrayBuffer @(ArrayOf (Sig PairA)) 2 + writeBufferElem buf 0 (PairA (vec4 1 2 3 4) (vec4 5 6 7 8)) + writeBufferElem buf 1 (PairA (vec4 10 20 30 40) (vec4 50 60 70 80)) + -- FitsTail (Sig PairB) (ArrayOf (Sig PairA)) holds: the element layout is equivalent. + PairB x y z w <- readBufferElem buf 1 + (x, y, z, w) @?= (vec2 10 20, vec2 30 40, vec2 50 60, vec2 70 80) + ] + , testGroup + "stage interface matching" + [ testCase "vertex outputs reflect at locations 0 (vec3) and 1 (vec2)" $ do + vm <- load "test/fixtures/mesh.vert.spv" + let outs = (stageInfoOf vm).outputs + map fst outs @?= [0, 1] + map (length . (.slots) . snd) outs @?= [3, 2] + , testCase "vertex outputs match fragment inputs (value oracle)" $ do + vm <- load "test/fixtures/mesh.vert.spv" + fm <- load "test/fixtures/mesh.frag.spv" + matchInterface (stageInfoOf vm) (stageInfoOf fm) @?= Right () + , testCase "a mismatched interface is rejected (value oracle)" $ do + vm <- load "test/fixtures/mesh.vert.spv" + fm <- load "test/fixtures/mesh.frag.spv" + -- frag outputs (vec4 @loc0) cannot satisfy the vertex's own inputs. + assertBool "should not match" (isLeft (matchInterface (stageInfoOf fm) (stageInfoOf vm))) + , testCase "MatchInterface holds at the type level" $ + meshInterfaceOk @?= True + ] + , testGroup + "stage resource linking (shared UBO)" + [ testCase "the Scene UBO reflects identically from both stages" $ do + vm <- load "test/fixtures/mesh.vert.spv" + fm <- load "test/fixtures/mesh.frag.spv" + lookup (0, 0) (stageInfoOf vm).resources + @?= lookup (0, 0) (stageInfoOf fm).resources + , testCase "linking unions resources, push and the external interface" $ do + vm <- load "test/fixtures/mesh.vert.spv" + fm <- load "test/fixtures/mesh.frag.spv" + case linkStages (stageInfoOf vm) (stageInfoOf fm) of + Left e -> assertFailure e + Right linked -> do + map fst linked.resources @?= [(0, 0), (0, 1)] -- Scene (shared) + Materials + isJust linked.push @?= True -- vertex Model push constant + map fst linked.inputs @?= [0, 1, 2] -- vertex attributes + map fst linked.outputs @?= [0] -- fragment colour output + , testCase "a conflicting shared binding is rejected" $ do + vm <- load "test/fixtures/mesh.vert.spv" + fm <- load "test/fixtures/mesh.frag.spv" + let bad = + (stageInfoOf fm) + { StageInfo.resources = [((0, 0), normalize (fromFields Std140Layout Nothing [("x", Vector STFloat 2)]))] + } + assertBool "should conflict" (isLeft (linkStages (stageInfoOf vm) bad)) + , testCase "CompatibleResources holds at the type level" $ + meshResourcesOk @?= True + , testCase "the merged layout signature round-trips to the value-level merge" $ do + vm <- load "test/fixtures/mesh.vert.spv" + fm <- load "test/fixtures/mesh.frag.spv" + let info = layoutSigVal (Proxy @MeshLayout) -- reflected from the promoted type + mergeLayout [vm, fm] @?= Right info -- equals the value-level merge + map fst info.resources @?= [(0, 0), (0, 1)] -- Scene (shared) + Materials + isJust info.push @?= True -- vertex Model push constant + ] + , testGroup + "pipeline layout (depth-only vs depth+color from one vertex shader)" + [ testCase "depth-only: the vertex stage alone" $ do + vm <- load "test/fixtures/mesh.vert.spv" + -- Only the vertex-visible resources: Scene UBO at (0,0), vertex stage. + stageBindings [vm] + @?= [(0, Vk.SHADER_STAGE_VERTEX_BIT)] + fmap length (mergedPushConstantRanges [vm]) @?= Right 1 -- the Model push constant + , testCase "depth+color: vertex+fragment, Scene gains the fragment stage" $ do + vm <- load "test/fixtures/mesh.vert.spv" + fm <- load "test/fixtures/mesh.frag.spv" + stageBindings [vm, fm] + @?= [ (0, Vk.SHADER_STAGE_VERTEX_BIT .|. Vk.SHADER_STAGE_FRAGMENT_BIT) -- Scene, shared + , (1, Vk.SHADER_STAGE_FRAGMENT_BIT) -- Materials, frag-only + ] + ] + , LayoutSpec.tests + ] + where + -- (binding, stageFlags) across all sets, for the merged pipeline layout. + stageBindings modules = + sortOn + fst + [ (b.binding, b.stageFlags) + | (_set, info) <- either error id (mergedDescriptorSetLayoutInfos modules) + , b <- V.toList info.bindings + ] + params = + Params + { center = vec2 (-0.8) 0.156 + , resolution = uvec2 512 512 + , escapeRadius = 2.0 + , maxIterations = 1000 + } + pushc = + Push + { transform = identity + , offset = vec2 1 2 + , scale = 2.5 + , count = 7 + } + wide = + Wide + { hi = 0xDEADBEEFCAFEF00D + , lo = -42 + , tag = 7 + } + particle = + Particle + { position = vec3 (-1) 0 0 + , velocity = vec2 3 4 + , mass = 1.5 + , flags = 7 + } + mat = + Material + { albedo = vec4 1 0 0 0 + , emission = vec4 2 0 0 0 + } + scene = + Scene + { sun = mat + , tint = vec4 3 0 0 0 + } + taps = Array.unsafeFromList [vec4 1 0 0 0, vec4 2 0 0 0, vec4 3 0 0 0, vec4 4 0 0 0] :: Array.Array 4 Vec4 + weights = Array.unsafeFromList [10, 20, 30, 40] :: Array.Array 4 Float + k140 = Kernel140{taps = taps, weights = weights} + k430 = Kernel430{taps = taps, weights = weights} + grid = + Array.unsafeFromList (map Array.unsafeFromList [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) + :: Array.Array 3 (Array.Array 4 Float) + g430 = Grid430{head = vec4 9 0 0 0, grid = grid} + g140 = Grid140{head = vec4 9 0 0 0, grid = grid} + toL = foldr (:) [] diff --git a/utils-spirv/test/fixtures/array-field.comp b/utils-spirv/test/fixtures/array-field.comp new file mode 100644 index 000000000..88014185a --- /dev/null +++ b/utils-spirv/test/fixtures/array-field.comp @@ -0,0 +1,25 @@ +#version 450 + +// Fixture exercising fixed-size array fields, mapped to `Array n a`. The same +// fields appear in a std140 UBO and a std430 SSBO to show the stride difference: +// std140 rounds every element up to 16 bytes, std430 packs tightly. + +layout(local_size_x = 1) in; + +layout(set = 0, binding = 0, std140) uniform Kernel140 { + vec4 taps[4]; // stride 16 in both + float weights[4]; // stride 16 in std140 +} k140; + +layout(set = 0, binding = 1, std430) buffer Kernel430 { + vec4 taps[4]; // stride 16 + float weights[4]; // stride 4 in std430 +} k430; + +layout(set = 0, binding = 2, std430) writeonly buffer O { + vec4 o[]; +}; + +void main() { + o[0] = k140.taps[0] * k140.weights[0] + k430.taps[0] * k430.weights[0]; +} diff --git a/utils-spirv/test/fixtures/array2d.comp b/utils-spirv/test/fixtures/array2d.comp new file mode 100644 index 000000000..c3fbb6401 --- /dev/null +++ b/utils-spirv/test/fixtures/array2d.comp @@ -0,0 +1,25 @@ +#version 450 + +// Fixture exercising a multi-dimensional array field: float grid[3][4] maps to +// Array 3 (Array 4 Float). In std430 the inner stride is 4 (outer 16); in std140 +// the inner stride is 16 (outer 64). + +layout(local_size_x = 1) in; + +layout(set = 0, binding = 0, std430) buffer Grid430 { + vec4 head; // @0 + float grid[3][4]; // @16, inner stride 4, outer stride 16 +} g430; + +layout(set = 0, binding = 1, std140) uniform Grid140 { + vec4 head; // @0 + float grid[3][4]; // @16, inner stride 16, outer stride 64 +} g140; + +layout(set = 0, binding = 2, std430) writeonly buffer O { + vec4 o[]; +}; + +void main() { + o[0] = g430.head + g140.head + vec4(g430.grid[0][0] + g140.grid[0][0]); +} diff --git a/utils-spirv/test/fixtures/bda.comp b/utils-spirv/test/fixtures/bda.comp new file mode 100644 index 000000000..081f51d78 --- /dev/null +++ b/utils-spirv/test/fixtures/bda.comp @@ -0,0 +1,39 @@ +#version 460 +#extension GL_EXT_buffer_reference : require + +// A self-referential buffer_reference (BDA) type: a BVH-ish node whose children +// are 64-bit device addresses back to Node. Reflection-driven codegen maps the +// pointer members to DeviceAddress Node (8-byte address), not an inlined struct, +// and generates Node once despite the cycle. +// +// NOTE: buffer_reference needs SPIR-V 1.3+, so compile with +// glslangValidator -V --target-env vulkan1.2 bda.comp -o bda.comp.spv + +layout(local_size_x = 64) in; + +layout(buffer_reference) buffer Node; // fwd decl enables self-reference +layout(buffer_reference, std430) buffer Node { + vec4 boundsMin; + vec4 boundsMax; + Node left; // BDA pointer -> cycle + Node right; // BDA pointer -> cycle + uint primCount; +}; + +layout(push_constant, std430) uniform Bvh { + Node root; // entry address into the graph +} bvh; + +layout(set = 0, binding = 0, std430) writeonly buffer Out { + uint hits[]; +}; + +void main() { + Node n = bvh.root; + uint count = 0u; + for (int i = 0; i < 8; ++i) { + count += n.primCount; + n = n.left; + } + hits[gl_GlobalInvocationID.x] = count; +} diff --git a/utils-spirv/test/fixtures/build-shaders.sh b/utils-spirv/test/fixtures/build-shaders.sh new file mode 100644 index 000000000..05097aa4e --- /dev/null +++ b/utils-spirv/test/fixtures/build-shaders.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# Compile the test fixture shaders to SPIR-V with a local glslang. The committed +# .spv files are the stable input to reflection in the test suite (no glslang +# dependency at build time). +set -eu +cd "$(dirname "$0")" +for src in *.comp *.vert *.frag; do + [ -e "$src" ] || continue + # buffer_reference (BDA) needs SPIR-V 1.3+, i.e. a Vulkan 1.2 target; the + # Int64 capability (uint64_t/int64_t) needs a Vulkan 1.1 target. + if grep -q buffer_reference "$src"; then + glslangValidator -V --target-env vulkan1.2 "$src" -o "$src.spv" + elif grep -q int64 "$src"; then + glslangValidator -V --target-env vulkan1.1 "$src" -o "$src.spv" + else + glslangValidator -V "$src" -o "$src.spv" + fi + echo "wrote $src.spv" +done diff --git a/utils-spirv/test/fixtures/mesh.frag b/utils-spirv/test/fixtures/mesh.frag new file mode 100644 index 000000000..2cd788a15 --- /dev/null +++ b/utils-spirv/test/fixtures/mesh.frag @@ -0,0 +1,32 @@ +#version 450 + +// Fragment stage paired with mesh.vert. Consumes the vertex outputs (inNormal +// loc 0, inUV loc 1), shares the `Scene` UBO (set 0, binding 0 — using the light +// fields the vertex stage ignores), and reads a fragment-only `Materials` SSBO +// (set 0, binding 1). + +layout(location = 0) in vec3 inNormal; +layout(location = 1) in vec2 inUV; + +layout(set = 0, binding = 0, std140) uniform Scene { + mat4 viewProj; + vec4 lightDir; + vec4 lightColor; +} scene; + +struct Material { + vec4 albedo; + vec4 params; +}; + +layout(set = 0, binding = 1, std430) buffer Materials { + Material materials[]; +}; + +layout(location = 0) out vec4 outColor; + +void main() { + float ndl = max(dot(normalize(inNormal), normalize(scene.lightDir.xyz)), 0.0); + vec4 albedo = materials[0].albedo * vec4(inUV, 1.0, 1.0); + outColor = albedo * scene.lightColor * ndl; +} diff --git a/utils-spirv/test/fixtures/mesh.vert b/utils-spirv/test/fixtures/mesh.vert new file mode 100644 index 000000000..fb88ebd85 --- /dev/null +++ b/utils-spirv/test/fixtures/mesh.vert @@ -0,0 +1,29 @@ +#version 450 + +// Vertex stage for the type-verified pipeline-assembly tests. Shares the `Scene` +// UBO (set 0, binding 0) with mesh.frag (vertex uses viewProj; fragment uses the +// light fields), carries a vertex-only `Model` push constant, and feeds the +// fragment stage `outNormal` (loc 0) + `outUV` (loc 1). + +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec2 inUV; + +layout(set = 0, binding = 0, std140) uniform Scene { + mat4 viewProj; + vec4 lightDir; + vec4 lightColor; +} scene; + +layout(push_constant, std430) uniform Model { + mat4 model; +} model; + +layout(location = 0) out vec3 outNormal; +layout(location = 1) out vec2 outUV; + +void main() { + gl_Position = scene.viewProj * model.model * vec4(inPosition, 1.0); + outNormal = mat3(model.model) * inNormal; + outUV = inUV; +} diff --git a/utils-spirv/test/fixtures/nested.comp b/utils-spirv/test/fixtures/nested.comp new file mode 100644 index 000000000..11c53b280 --- /dev/null +++ b/utils-spirv/test/fixtures/nested.comp @@ -0,0 +1,31 @@ +#version 450 + +// Fixture exercising nested structs as fields, plus cross-layout sharing. The +// `Material` struct is all-vec4 (every member 16-byte aligned), so std140 and +// std430 lay it out identically: it is used both as a field of the std140 +// `Scene` UBO and as the element of the std430 `Mats` SSBO, and a single +// generated record is shared ("promoted") across both. + +layout(local_size_x = 1) in; + +struct Material { + vec4 albedo; + vec4 emission; +}; + +layout(set = 0, binding = 0, std140) uniform Scene { + Material sun; // nested struct field @0 (size 32) + vec4 tint; // @32 +} scene; + +layout(set = 0, binding = 1, std430) buffer Mats { + Material mats[]; +}; + +layout(set = 0, binding = 2, std430) writeonly buffer O { + vec4 o[]; +}; + +void main() { + o[0] = scene.sun.albedo + scene.sun.emission + scene.tint + mats[0].emission; +} diff --git a/utils-spirv/test/fixtures/push.comp b/utils-spirv/test/fixtures/push.comp new file mode 100644 index 000000000..cc905be34 --- /dev/null +++ b/utils-spirv/test/fixtures/push.comp @@ -0,0 +1,28 @@ +#version 450 + +// Fixture exercising push-constant reflection: the `Push` block becomes a +// Haskell record with a gl-block std430 Storable, and `pushConstantRanges` +// derives the VkPushConstantRange. Fields are ordered by non-increasing +// alignment (mat4 = 16, vec2 = 8, float/int = 4) to satisfy the gl-block +// layout guardrail. + +layout(local_size_x = 64) in; + +layout(push_constant, std430) uniform Push { + mat4 transform; + vec2 offset; + float scale; + int count; +} push; + +layout(set = 0, binding = 0, std430) buffer Output { + vec4 data[]; +}; + +void main() { + uint i = gl_GlobalInvocationID.x; + if (i >= uint(push.count)) { + return; + } + data[i] = push.transform * vec4(push.offset * push.scale, 0.0, 1.0); +} diff --git a/utils-spirv/test/fixtures/spec.comp b/utils-spirv/test/fixtures/spec.comp new file mode 100644 index 000000000..36b45958e --- /dev/null +++ b/utils-spirv/test/fixtures/spec.comp @@ -0,0 +1,21 @@ +#version 450 + +// Fixture exercising specialization-constant reflection. The ids are +// deliberately non-contiguous (0 and 3) to show that map entries follow the +// shader's actual constant_ids while values pack tightly (offsets 0, 4). + +layout(local_size_x = 64) in; + +layout(constant_id = 0) const uint count = 1u; +layout(constant_id = 3) const float scale = 1.0; + +layout(set = 0, binding = 0, std430) buffer Output { + float xs[]; +}; + +void main() { + uint i = gl_GlobalInvocationID.x; + if (i < count) { + xs[i] = scale; + } +} diff --git a/utils-spirv/test/fixtures/ssbo-struct.comp b/utils-spirv/test/fixtures/ssbo-struct.comp new file mode 100644 index 000000000..fce08a2ce --- /dev/null +++ b/utils-spirv/test/fixtures/ssbo-struct.comp @@ -0,0 +1,24 @@ +#version 450 + +// Fixture exercising SSBO arrays of structs: the `Particle` element type is +// generated as a std430 record even though the wrapping `Particles` block is a +// runtime array (not itself representable as a flat record). Fields are ordered +// by non-increasing alignment (vec3 = 16, vec2 = 8, float/uint = 4). + +layout(local_size_x = 64) in; + +struct Particle { + vec3 position; // align 16 @0 + vec2 velocity; // align 8 @16 + float mass; // align 4 @24 + uint flags; // align 4 @28 +}; + +layout(set = 0, binding = 0, std430) buffer Particles { + Particle items[]; +}; + +void main() { + uint i = gl_GlobalInvocationID.x; + items[i].position += vec3(items[i].velocity, 0.0) * items[i].mass; +} diff --git a/utils-spirv/test/fixtures/wide.comp b/utils-spirv/test/fixtures/wide.comp new file mode 100644 index 000000000..ac51301f3 --- /dev/null +++ b/utils-spirv/test/fixtures/wide.comp @@ -0,0 +1,27 @@ +#version 460 +#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require + +// 64-bit integer block members. A uint64_t/int64_t occupies an 8-byte std430 +// slot (alignment 8), exactly like a double — the regression fixture for +// scalar-width-faithful classification, so a 64-bit int is never laid out as a +// 4-byte int. Fields are ordered by non-increasing alignment (8, 8, 4) to keep +// gl-block's Generic layout valid. +// +// NOTE: int64 needs the Int64 capability; compile with +// glslangValidator -V --target-env vulkan1.1 wide.comp -o wide.comp.spv + +layout(local_size_x = 1) in; + +layout(push_constant, std430) uniform Wide { + uint64_t hi; // align 8 @0, size 8 + int64_t lo; // align 8 @8, size 8 + uint tag; // align 4 @16, size 4 +} wide; + +layout(set = 0, binding = 0, std430) writeonly buffer Out { + uint sink[]; +}; + +void main() { + sink[0] = uint(wide.hi) + uint(wide.lo) + wide.tag; +} diff --git a/utils-spirv/vulkan-utils-spirv.cabal b/utils-spirv/vulkan-utils-spirv.cabal new file mode 100644 index 000000000..daa5ed223 --- /dev/null +++ b/utils-spirv/vulkan-utils-spirv.cabal @@ -0,0 +1,143 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.39.1. +-- +-- see: https://github.com/sol/hpack + +name: vulkan-utils-spirv +version: 0.1.0.0 +synopsis: Generate Haskell types and Vulkan descriptor/pipeline layouts from SPIR-V reflection +category: Graphics +homepage: https://github.com/expipiplus1/vulkan#readme +bug-reports: https://github.com/expipiplus1/vulkan/issues +maintainer: Ellie Hermaszewska +build-type: Simple +extra-source-files: + README.md + package.yaml + test/fixtures/array-field.comp.spv + test/fixtures/array2d.comp.spv + test/fixtures/julia.comp.spv + test/fixtures/mesh.frag.spv + test/fixtures/mesh.vert.spv + test/fixtures/nested.comp.spv + test/fixtures/push.comp.spv + test/fixtures/spec.comp.spv + test/fixtures/ssbo-struct.comp.spv + test/fixtures/tri.vert.spv + test/fixtures/wide.comp.spv + +source-repository head + type: git + location: https://github.com/expipiplus1/vulkan + +library + exposed-modules: + Vulkan.Utils.SpirV.Array + Vulkan.Utils.SpirV.Block + Vulkan.Utils.SpirV.Buffer + Vulkan.Utils.SpirV.Descriptors + Vulkan.Utils.SpirV.DeviceAddress + Vulkan.Utils.SpirV.Layout + Vulkan.Utils.SpirV.Pipeline + Vulkan.Utils.SpirV.Reflect + Vulkan.Utils.SpirV.Reflect.OffsetMaps + Vulkan.Utils.SpirV.Signature + Vulkan.Utils.SpirV.Specialization + Vulkan.Utils.SpirV.Stage + Vulkan.Utils.SpirV.TH + Vulkan.Utils.SpirV.Types + Vulkan.Utils.SpirV.VertexInput + other-modules: + Paths_vulkan_utils_spirv + hs-source-dirs: + src + default-extensions: + BlockArguments + DataKinds + DeriveAnyClass + DeriveGeneric + DerivingStrategies + DerivingVia + DuplicateRecordFields + FlexibleContexts + FlexibleInstances + ImportQualifiedPost + LambdaCase + NamedFieldPuns + OverloadedRecordDot + OverloadedStrings + PatternSynonyms + RecordWildCards + ScopedTypeVariables + TemplateHaskell + TupleSections + TypeApplications + TypeOperators + ViewPatterns + ghc-options: -Wall + build-depends: + base >=4.16 && <5 + , bytestring + , containers + , gl-block + , ptrdiff + , resourcet + , spirv-enum + , spirv-reflect-ffi + , spirv-reflect-types + , template-haskell + , text + , unliftio-core + , vector + , vulkan ==3.27.* + , vulkan-utils + default-language: Haskell2010 + +test-suite spec + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + LayoutSpec + Paths_vulkan_utils_spirv + hs-source-dirs: + test + default-extensions: + BlockArguments + DataKinds + DeriveAnyClass + DeriveGeneric + DerivingStrategies + DerivingVia + DuplicateRecordFields + FlexibleContexts + FlexibleInstances + ImportQualifiedPost + LambdaCase + NamedFieldPuns + OverloadedRecordDot + OverloadedStrings + PatternSynonyms + RecordWildCards + ScopedTypeVariables + TemplateHaskell + TupleSections + TypeApplications + TypeOperators + ViewPatterns + ghc-options: -Wall + build-depends: + base <5 + , containers + , geomancy + , gl-block + , spirv-reflect-ffi + , spirv-reflect-types + , tasty + , tasty-hunit + , template-haskell + , text + , vector + , vulkan + , vulkan-utils-spirv + default-language: Haskell2010 From b20f7112e48243dc95edc9d34413f7c0d94f45d8 Mon Sep 17 00:00:00 2001 From: IC Rainbow Date: Fri, 3 Jul 2026 23:41:11 +0300 Subject: [PATCH 2/3] Embed reflect-example and fixture shaders with the GLSL quasiquoters The reflect examples and the vulkan-utils-spirv test suite consumed committed .spv files produced by out-of-band build-shaders.sh scripts, which broke CI on a fresh checkout (the .spv were gitignored, and two fixtures had no GLSL source at all). Shaders are now compiled at build time by the vulkan-utils GLSL quasiquoters and reflected from the embedded bytes; the scripts, shader files and file-embed dependency are gone. vulkan-utils-spirv gains the missing Bytes variants for the stage TH: reflectStageSigBytes and reflectPipelineLayoutSigBytes, mirroring reflectShaderTypesBytes. Each example is restructured around per-pipeline modules whose .Shader submodule holds the quasiquoted SPIR-V (satisfying the TH stage restriction) and whose pipeline module reflects it and builds the pipeline; Main does the setup and Render the headless frame: * compute-reflect: Main, Render, Julia(.Shader) * mesh-reflect: Main, Render, Mesh(.Shader) * texture-reflect: Main, Render, Tri(.Shader), Cube(.Shader) * pathtrace-reflect: Main, Render, Scene, Pathtracer(.Shader) The pathtracer and bda shaders use compileShaderQ with a vulkan1.2 target (buffer_reference), wide.comp with vulkan1.1 (Int64). The sourceless julia.comp and tri.vert fixtures are reconstructed from the test suite's layout assertions, which pin them exactly. All four examples render correctly (mesh/texture self-checks pass) and the 75 vulkan-utils-spirv tests pass. Co-Authored-By: Claude Fable 5 --- examples/compute-reflect/Julia.hs | 101 ++++ examples/compute-reflect/Julia/Shader.hs | 75 +++ examples/compute-reflect/Main.hs | 211 +------ examples/compute-reflect/Render.hs | 141 +++++ examples/compute-reflect/build-shaders.sh | 8 - examples/compute-reflect/shader.comp | 63 -- examples/mesh-reflect/Main.hs | 373 +----------- examples/mesh-reflect/Mesh.hs | 93 +++ examples/mesh-reflect/Mesh/Shader.hs | 83 +++ examples/mesh-reflect/Render.hs | 309 ++++++++++ examples/mesh-reflect/build-shaders.sh | 9 - examples/mesh-reflect/shader.frag | 24 - examples/mesh-reflect/shader.vert | 33 -- examples/package.yaml | 4 - examples/pathtrace-reflect/Main.hs | 549 +----------------- examples/pathtrace-reflect/Pathtracer.hs | 119 ++++ .../pathtrace-reflect/Pathtracer/Shader.hs | 282 +++++++++ examples/pathtrace-reflect/Render.hs | 264 +++++++++ examples/pathtrace-reflect/Scene.hs | 208 +++++++ examples/pathtrace-reflect/build-shaders.sh | 9 - examples/pathtrace-reflect/shader.comp | 261 --------- examples/texture-reflect/Cube.hs | 73 +++ examples/texture-reflect/Cube/Shader.hs | 89 +++ examples/texture-reflect/Main.hs | 393 +------------ examples/texture-reflect/Render.hs | 310 ++++++++++ examples/texture-reflect/Tri.hs | 68 +++ examples/texture-reflect/Tri/Shader.hs | 58 ++ examples/texture-reflect/build-shaders.sh | 9 - examples/texture-reflect/cube.frag | 23 - examples/texture-reflect/cube.vert | 44 -- examples/texture-reflect/tri.frag | 17 - examples/texture-reflect/tri.vert | 19 - examples/vulkan-examples.cabal | 19 +- utils-spirv/package.yaml | 13 +- utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs | 22 +- utils-spirv/test/Fixtures.hs | 428 ++++++++++++++ utils-spirv/test/LayoutSpec.hs | 10 +- utils-spirv/test/Spec.hs | 83 +-- utils-spirv/test/fixtures/array-field.comp | 25 - utils-spirv/test/fixtures/array2d.comp | 25 - utils-spirv/test/fixtures/bda.comp | 39 -- utils-spirv/test/fixtures/build-shaders.sh | 19 - utils-spirv/test/fixtures/mesh.frag | 32 - utils-spirv/test/fixtures/mesh.vert | 29 - utils-spirv/test/fixtures/nested.comp | 31 - utils-spirv/test/fixtures/push.comp | 28 - utils-spirv/test/fixtures/spec.comp | 21 - utils-spirv/test/fixtures/ssbo-struct.comp | 24 - utils-spirv/test/fixtures/wide.comp | 27 - utils-spirv/vulkan-utils-spirv.cabal | 14 +- 50 files changed, 2848 insertions(+), 2363 deletions(-) create mode 100644 examples/compute-reflect/Julia.hs create mode 100644 examples/compute-reflect/Julia/Shader.hs create mode 100644 examples/compute-reflect/Render.hs delete mode 100644 examples/compute-reflect/build-shaders.sh delete mode 100644 examples/compute-reflect/shader.comp create mode 100644 examples/mesh-reflect/Mesh.hs create mode 100644 examples/mesh-reflect/Mesh/Shader.hs create mode 100644 examples/mesh-reflect/Render.hs delete mode 100755 examples/mesh-reflect/build-shaders.sh delete mode 100644 examples/mesh-reflect/shader.frag delete mode 100644 examples/mesh-reflect/shader.vert create mode 100644 examples/pathtrace-reflect/Pathtracer.hs create mode 100644 examples/pathtrace-reflect/Pathtracer/Shader.hs create mode 100644 examples/pathtrace-reflect/Render.hs create mode 100644 examples/pathtrace-reflect/Scene.hs delete mode 100644 examples/pathtrace-reflect/build-shaders.sh delete mode 100644 examples/pathtrace-reflect/shader.comp create mode 100644 examples/texture-reflect/Cube.hs create mode 100644 examples/texture-reflect/Cube/Shader.hs create mode 100644 examples/texture-reflect/Render.hs create mode 100644 examples/texture-reflect/Tri.hs create mode 100644 examples/texture-reflect/Tri/Shader.hs delete mode 100755 examples/texture-reflect/build-shaders.sh delete mode 100644 examples/texture-reflect/cube.frag delete mode 100644 examples/texture-reflect/cube.vert delete mode 100644 examples/texture-reflect/tri.frag delete mode 100644 examples/texture-reflect/tri.vert create mode 100644 utils-spirv/test/Fixtures.hs delete mode 100644 utils-spirv/test/fixtures/array-field.comp delete mode 100644 utils-spirv/test/fixtures/array2d.comp delete mode 100644 utils-spirv/test/fixtures/bda.comp delete mode 100644 utils-spirv/test/fixtures/build-shaders.sh delete mode 100644 utils-spirv/test/fixtures/mesh.frag delete mode 100644 utils-spirv/test/fixtures/mesh.vert delete mode 100644 utils-spirv/test/fixtures/nested.comp delete mode 100644 utils-spirv/test/fixtures/push.comp delete mode 100644 utils-spirv/test/fixtures/spec.comp delete mode 100644 utils-spirv/test/fixtures/ssbo-struct.comp delete mode 100644 utils-spirv/test/fixtures/wide.comp diff --git a/examples/compute-reflect/Julia.hs b/examples/compute-reflect/Julia.hs new file mode 100644 index 000000000..8457a7250 --- /dev/null +++ b/examples/compute-reflect/Julia.hs @@ -0,0 +1,101 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +{-| Julia-set compute pipeline, its interface reflected from 'Julia.Shader.code': + + * 'reflectShaderTypesBytes' generates the 'Params' push-constant record (with + a gl-block std430 'Storable'); + * 'singleDescriptorSetLayoutInfo' builds the descriptor set layout for the + output SSBO; + * 'pushConstantRanges' builds the pipeline layout's push-constant range; and + * 'allocateSpecializationInfo' builds the pipeline's specialization info + (@maxIterations@, @escapeRadius@) from the reflected constant ids. + +Compare with the @compute@ example, which hand-writes all of this. +-} +module Julia + ( Params (..) + , Pipeline (..) + , allocatePipeline + , workgroup + ) where + +import Control.Monad.Trans.Resource (ResourceT, allocate) +import qualified Data.Vector as V +import Data.Word (Word32) +import qualified Geomancy +import Graphics.Gl.Block (Std430 (..)) +import Vulkan.CStruct.Extends (SomeStruct (..)) +import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import Vulkan.Utils.Shader (shaderModuleStage) +import Vulkan.Utils.SpirV.Descriptors (pushConstantRanges, singleDescriptorSetLayoutInfo) +import Vulkan.Utils.SpirV.Reflect (reflectBytes) +import Vulkan.Utils.SpirV.Specialization (allocateSpecializationInfo) +import Vulkan.Utils.SpirV.TH (reflectShaderTypesBytes) +import Vulkan.Zero (zero) + +import qualified Julia.Shader as Shader + +-- Generate the @Params@ push-constant record (and its std430 'Storable') from +-- the same SPIR-V the runtime loads. +reflectShaderTypesBytes Shader.code + +-- | Workgroup size on each axis (matches @local_size_x/y@ in the shader). +workgroup :: Int +workgroup = 16 + +data Pipeline = Pipeline + { pipeline :: Vk.Pipeline + , pipelineLayout :: Vk.PipelineLayout + , descriptorSetLayout :: Vk.DescriptorSetLayout + } + +{- | The pipeline, specialized to the given iteration cap and escape radius. + +Descriptor set layout, push-constant range and specialization info all come +from reflecting 'Shader.code'. +-} +allocatePipeline :: Vk.Device -> Word32 -> Float -> ResourceT IO Pipeline +allocatePipeline dev maxIterations escapeRadius = do + -- Reflect the embedded module once; reuse it for the descriptor set layout, + -- the push-constant range and the specialization info. + reflected <- reflectBytes Shader.code + + setLayoutInfo <- either fail pure (singleDescriptorSetLayoutInfo reflected) + (_, descriptorSetLayout) <- Vk.withDescriptorSetLayout dev setLayoutInfo Nothing allocate + + mSpec <- allocateSpecializationInfo reflected (maxIterations, escapeRadius) + (_, shader) <- shaderModuleStage dev Vk.SHADER_STAGE_COMPUTE_BIT mSpec Shader.code + (_, pipelineLayout) <- + Vk.withPipelineLayout + dev + zero + { PipelineLayoutCreateInfo.setLayouts = [descriptorSetLayout] + , PipelineLayoutCreateInfo.pushConstantRanges = + V.fromList (pushConstantRanges reflected) + } + Nothing + allocate + let + pipelineCreateInfo :: Vk.ComputePipelineCreateInfo '[] + pipelineCreateInfo = + zero + { Vk.layout = pipelineLayout + , Vk.stage = shader + , Vk.basePipelineHandle = zero + } + (_, (_, [computePipeline])) <- + Vk.withComputePipelines dev zero [SomeStruct pipelineCreateInfo] Nothing allocate + pure + Pipeline + { pipeline = computePipeline + , pipelineLayout = pipelineLayout + , descriptorSetLayout = descriptorSetLayout + } diff --git a/examples/compute-reflect/Julia/Shader.hs b/examples/compute-reflect/Julia/Shader.hs new file mode 100644 index 000000000..6e3945749 --- /dev/null +++ b/examples/compute-reflect/Julia/Shader.hs @@ -0,0 +1,75 @@ +{-# LANGUAGE QuasiQuotes #-} + +{-| The Julia-set compute shader, compiled to SPIR-V at build time. + +Its interface is reflected by vulkan-utils-spirv in the "Julia" module: +the @Params@ push-constant block, the output SSBO's descriptor set layout, +and the specialization constants. +-} +module Julia.Shader + ( code + ) where + +import Data.ByteString (ByteString) +import Vulkan.Utils.ShaderQQ.GLSL.Glslang (comp) + +code :: ByteString +code = + [comp| + #version 450 + #extension GL_ARB_separate_shader_objects : enable + + layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + + // Specialized at pipeline-creation time (see Vulkan.Utils.SpirV.Specialization). + layout(constant_id = 0) const uint maxIterations = 1000u; + layout(constant_id = 1) const float escapeRadius = 2.0; + + // Fields are ordered by non-increasing alignment (vec2/uvec2 = 8) so gl-block's + // Generic std430 layout matches the shader; see the guardrail in + // Vulkan.Utils.SpirV.Block. + layout(push_constant, std430) uniform Params { + vec2 center; // c in the Julia iteration + uvec2 resolution; // output size in pixels + } params; + + layout(set = 0, binding = 0, std430) buffer OutputBuffer { + vec4 pixels[]; + }; + + // From https://iquilezles.org/articles/palettes/ + vec3 palette(float t) { + const vec3 a = vec3(0.5); + const vec3 b = vec3(0.5); + const vec3 c = vec3(1.0); + const vec3 d = vec3(0.0, 0.10, 0.20); + return a + b * cos(6.28318530718 * (c * t + d)); + } + + void main() { + uvec2 gid = gl_GlobalInvocationID.xy; + if (gid.x >= params.resolution.x || gid.y >= params.resolution.y) { + return; + } + + vec2 uv = + ( vec2(gid) / vec2(params.resolution) * 2.0 - 1.0 ) * escapeRadius; + + vec2 z = uv; + int i = 0; + for (; i < int(maxIterations); ++i) { + vec2 z2 = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y); + z = z2 + params.center; + if (dot(z, z) > escapeRadius * escapeRadius) { + break; + } + } + + uint idx = gid.y * params.resolution.x + gid.x; + if (i == int(maxIterations)) { + pixels[idx] = vec4(0.0, 0.0, 0.0, 1.0); + } else { + pixels[idx] = vec4(palette(float(i) / float(maxIterations)), 1.0); + } + } + |] diff --git a/examples/compute-reflect/Main.hs b/examples/compute-reflect/Main.hs index d382517a5..1e5c351fb 100644 --- a/examples/compute-reflect/Main.hs +++ b/examples/compute-reflect/Main.hs @@ -1,72 +1,21 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DerivingVia #-} -{-# LANGUAGE OverloadedLists #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - -{-| A compute example whose shader interface is derived from the compiled -SPIR-V at build time by @vulkan-utils-spirv@: - - * 'reflectShaderTypes' generates the 'Params' push-constant record (with a - gl-block std430 'Storable') from @shader.comp.spv@; - * 'singleDescriptorSetLayoutInfo' builds the descriptor set layout for the - output SSBO from the same module at runtime; - * 'pushConstantRanges' builds the pipeline layout's push-constant range; and - * 'allocateSpecializationInfo' builds the pipeline's specialization info - (@maxIterations@, @escapeRadius@) from the reflected constant ids. - -Compare with the @compute@ example, which hand-writes all of this. +{-| A compute example whose shader interface is derived from the quasiquoted +shader in "Julia.Shader" at build time by @vulkan-utils-spirv@ — see "Julia" +for the reflected pipeline and "Render" for the frame. -} module Main ( main ) where -import qualified Codec.Picture as JP -import Control.Monad.IO.Class (liftIO) -import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT) -import Data.ByteString (ByteString) -import Data.FileEmbed (embedFile) -import qualified Data.Vector as V -import Data.Word (Word32) -import Foreign.Marshal.Array (peekArray) -import Foreign.Marshal.Utils (with) -import Foreign.Ptr (Ptr, castPtr, plusPtr) -import Foreign.Storable (sizeOf) -import qualified Geomancy -import Geomancy.UVec2 (uvec2) -import Geomancy.Vec2 (vec2) -import Graphics.Gl.Block (Std430 (..)) -import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), submitAndWait, withHeadlessVk) -import ImageReadback (captureImageRGBA8, savePng) -import Vulkan.CStruct.Extends (SomeStruct (..)) -import Vulkan.CStruct.Utils (FixedArray, lowerArrayPtr) -import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) -import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) -import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) +import Control.Monad.Trans.Resource (runResourceT) +import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), withHeadlessVk) +import ImageReadback (savePng) import qualified Vulkan.Core10 as Vk -import Vulkan.Utils.Descriptors (bufferWrite) import Vulkan.Utils.QueueAssignment (QueueFamilyIndex (..)) import Vulkan.Utils.Queues (Queues (..)) -import Vulkan.Utils.Shader (shaderModuleStage) -import Vulkan.Utils.SpirV.Descriptors (pushConstantRanges, singleDescriptorSetLayoutInfo) -import Vulkan.Utils.SpirV.Reflect (reflectBytes) -import Vulkan.Utils.SpirV.Specialization (allocateSpecializationInfo) -import Vulkan.Utils.SpirV.TH (reflectShaderTypes) import Vulkan.Zero (zero) -import qualified VulkanMemoryAllocator as VMA - --- | The compiled shader, embedded for runtime module creation. -compCode :: ByteString -compCode = $(embedFile "compute-reflect/shader.comp.spv") --- Generate the @Params@ push-constant record (and its std430 'Storable') from --- the same SPIR-V the runtime loads. -reflectShaderTypes "compute-reflect/shader.comp.spv" +import qualified Render main :: IO () main = runResourceT $ do @@ -80,150 +29,6 @@ main = runResourceT $ do } let QueueFamilyIndex computeQueueFamilyIndex = fst (qCompute queues) - image <- render allocator device computeQueueFamilyIndex + image <- Render.render allocator device computeQueueFamilyIndex Vk.deviceWaitIdle device savePng "julia-reflect.png" image - -render - :: VMA.Allocator - -> Vk.Device - -> Word32 - -> ResourceT IO (JP.Image JP.PixelRGBA8) -render allocator dev computeQueueFamilyIndex = do - let - width, height, workgroup :: Int - width = 512 - height = width - workgroup = 16 - - -- Push-constant block (reflected as 'Params'). - params :: Params - params = - Params - { center = vec2 (-0.8) 0.156 - , resolution = uvec2 (fromIntegral width) (fromIntegral height) - } - - -- Specialization constants, in ascending constant_id order: - -- id 0 = maxIterations (uint), id 1 = escapeRadius (float). - maxIterations :: Word32 - maxIterations = 1000 - escapeRadius :: Float - escapeRadius = 2.0 - - -- Reflect the embedded module once; reuse it for the descriptor set layout - -- and the push-constant range. - reflected <- reflectBytes compCode - - -- Output storage buffer: one RGBA32F texel per pixel, mapped GPU_TO_CPU. - (_, (outBuffer, outAllocation, outInfo)) <- - VMA.withBuffer - allocator - zero - { Vk.size = fromIntegral $ width * height * 4 * sizeOf (0 :: Float) - , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT - } - zero - { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT - , VMA.usage = VMA.MEMORY_USAGE_GPU_TO_CPU - } - allocate - - -- Descriptor set layout generated from the reflected module (the output SSBO). - setLayoutInfo <- either fail pure (singleDescriptorSetLayoutInfo reflected) - (_, descriptorSetLayout) <- Vk.withDescriptorSetLayout dev setLayoutInfo Nothing allocate - - (_, descriptorPool) <- - Vk.withDescriptorPool - dev - zero - { Vk.maxSets = 1 - , Vk.poolSizes = - [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER 1 - ] - } - Nothing - allocate - [descriptorSet] <- - Vk.allocateDescriptorSets - dev - zero - { Vk.descriptorPool = descriptorPool - , Vk.setLayouts = [descriptorSetLayout] - } - - Vk.updateDescriptorSets - dev - [ bufferWrite descriptorSet 0 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER outBuffer - ] - [] - - -- Pipeline. The specialization info (maxIterations, escapeRadius) is built - -- from the reflected constant ids and lives for this resource scope. - mSpec <- allocateSpecializationInfo reflected (maxIterations, escapeRadius) - (_, shader) <- shaderModuleStage dev Vk.SHADER_STAGE_COMPUTE_BIT mSpec compCode - -- Pipeline layout: descriptor set + push-constant range, both from reflection. - (_, pipelineLayout) <- - Vk.withPipelineLayout - dev - zero - { PipelineLayoutCreateInfo.setLayouts = [descriptorSetLayout] - , PipelineLayoutCreateInfo.pushConstantRanges = - V.fromList (pushConstantRanges reflected) - } - Nothing - allocate - let - pipelineCreateInfo :: Vk.ComputePipelineCreateInfo '[] - pipelineCreateInfo = - zero - { Vk.layout = pipelineLayout - , Vk.stage = shader - , Vk.basePipelineHandle = zero - } - (_, (_, [computePipeline])) <- - Vk.withComputePipelines dev zero [SomeStruct pipelineCreateInfo] Nothing allocate - - (_, commandPool) <- - Vk.withCommandPool dev zero{CommandPoolCreateInfo.queueFamilyIndex = computeQueueFamilyIndex} Nothing allocate - (_, [cb]) <- - Vk.withCommandBuffers - dev - zero - { Vk.commandPool = commandPool - , Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY - , Vk.commandBufferCount = 1 - } - allocate - - Vk.useCommandBuffer cb zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} do - Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_COMPUTE computePipeline - Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_COMPUTE pipelineLayout 0 [descriptorSet] [] - -- Push the reflected 'Params' (std430) for this dispatch. - liftIO $ with params $ \pParams -> - Vk.cmdPushConstants - cb - pipelineLayout - Vk.SHADER_STAGE_COMPUTE_BIT - 0 - (fromIntegral (sizeOf params)) - (castPtr pParams) - Vk.cmdDispatch - cb - (ceiling (realToFrac width / realToFrac @_ @Float workgroup)) - (ceiling (realToFrac height / realToFrac @_ @Float workgroup)) - 1 - - computeQueue <- Vk.getDeviceQueue dev computeQueueFamilyIndex 0 - submitAndWait dev computeQueue cb "Timed out waiting for compute" - - let - pixelAddr :: Int -> Int -> Ptr (FixedArray 4 Float) - pixelAddr x y = - plusPtr - (VMA.mappedData outInfo) - (((y * width) + x) * 4 * sizeOf (0 :: Float)) - captureImageRGBA8 allocator outAllocation width height $ \x y -> do - let ptr = pixelAddr x y - [r, g, b, a] <- fmap (\f -> round (f * 255)) <$> peekArray 4 (lowerArrayPtr ptr) - pure $ JP.PixelRGBA8 r g b a diff --git a/examples/compute-reflect/Render.hs b/examples/compute-reflect/Render.hs new file mode 100644 index 000000000..c1f6b6574 --- /dev/null +++ b/examples/compute-reflect/Render.hs @@ -0,0 +1,141 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedRecordDot #-} + +-- | The single headless frame: dispatch the Julia pipeline and read the image back. +module Render + ( render + ) where + +import qualified Codec.Picture as JP +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Resource (ResourceT, allocate) +import Data.Word (Word32) +import Foreign.Marshal.Array (peekArray) +import Foreign.Marshal.Utils (with) +import Foreign.Ptr (Ptr, castPtr, plusPtr) +import Foreign.Storable (sizeOf) +import Geomancy.UVec2 (uvec2) +import Geomancy.Vec2 (vec2) +import HeadlessBoot (submitAndWait) +import ImageReadback (captureImageRGBA8) +import Vulkan.CStruct.Utils (FixedArray, lowerArrayPtr) +import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) +import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import Vulkan.Utils.Descriptors (bufferWrite) +import Vulkan.Zero (zero) +import qualified VulkanMemoryAllocator as VMA + +import Julia (Params (..)) +import qualified Julia + +render + :: VMA.Allocator + -> Vk.Device + -> Word32 + -> ResourceT IO (JP.Image JP.PixelRGBA8) +render allocator dev computeQueueFamilyIndex = do + let + width, height :: Int + width = 512 + height = width + + -- Push-constant block (reflected as 'Params'). + params :: Params + params = + Params + { center = vec2 (-0.8) 0.156 + , resolution = uvec2 (fromIntegral width) (fromIntegral height) + } + + -- Specialization constants, in ascending constant_id order: + -- id 0 = maxIterations (uint), id 1 = escapeRadius (float). + maxIterations :: Word32 + maxIterations = 1000 + escapeRadius :: Float + escapeRadius = 2.0 + + -- Output storage buffer: one RGBA32F texel per pixel, mapped GPU_TO_CPU. + (_, (outBuffer, outAllocation, outInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral $ width * height * 4 * sizeOf (0 :: Float) + , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_GPU_TO_CPU + } + allocate + + julia <- Julia.allocatePipeline dev maxIterations escapeRadius + + (_, descriptorPool) <- + Vk.withDescriptorPool + dev + zero + { Vk.maxSets = 1 + , Vk.poolSizes = + [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER 1 + ] + } + Nothing + allocate + [descriptorSet] <- + Vk.allocateDescriptorSets + dev + zero + { Vk.descriptorPool = descriptorPool + , Vk.setLayouts = [julia.descriptorSetLayout] + } + + Vk.updateDescriptorSets + dev + [ bufferWrite descriptorSet 0 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER outBuffer + ] + [] + + (_, commandPool) <- + Vk.withCommandPool dev zero{CommandPoolCreateInfo.queueFamilyIndex = computeQueueFamilyIndex} Nothing allocate + (_, [cb]) <- + Vk.withCommandBuffers + dev + zero + { Vk.commandPool = commandPool + , Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY + , Vk.commandBufferCount = 1 + } + allocate + + Vk.useCommandBuffer cb zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_COMPUTE julia.pipeline + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_COMPUTE julia.pipelineLayout 0 [descriptorSet] [] + -- Push the reflected 'Params' (std430) for this dispatch. + liftIO $ with params $ \pParams -> + Vk.cmdPushConstants + cb + julia.pipelineLayout + Vk.SHADER_STAGE_COMPUTE_BIT + 0 + (fromIntegral (sizeOf params)) + (castPtr pParams) + Vk.cmdDispatch + cb + (ceiling (realToFrac width / realToFrac @_ @Float Julia.workgroup)) + (ceiling (realToFrac height / realToFrac @_ @Float Julia.workgroup)) + 1 + + computeQueue <- Vk.getDeviceQueue dev computeQueueFamilyIndex 0 + submitAndWait dev computeQueue cb "Timed out waiting for compute" + + let + pixelAddr :: Int -> Int -> Ptr (FixedArray 4 Float) + pixelAddr x y = + plusPtr + (VMA.mappedData outInfo) + (((y * width) + x) * 4 * sizeOf (0 :: Float)) + captureImageRGBA8 allocator outAllocation width height $ \x y -> do + let ptr = pixelAddr x y + [r, g, b, a] <- fmap (\f -> round (f * 255)) <$> peekArray 4 (lowerArrayPtr ptr) + pure $ JP.PixelRGBA8 r g b a diff --git a/examples/compute-reflect/build-shaders.sh b/examples/compute-reflect/build-shaders.sh deleted file mode 100644 index 9b8277604..000000000 --- a/examples/compute-reflect/build-shaders.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# Compile the GLSL shaders to SPIR-V with a local glslang. The resulting .spv is -# committed and consumed both at runtime (Data.FileEmbed.embedFile) and at -# compile time (Vulkan.Utils.SpirV reflection). -set -eu -cd "$(dirname "$0")" -glslangValidator -V shader.comp -o shader.comp.spv -echo "wrote shader.comp.spv" diff --git a/examples/compute-reflect/shader.comp b/examples/compute-reflect/shader.comp deleted file mode 100644 index 8f07b91a0..000000000 --- a/examples/compute-reflect/shader.comp +++ /dev/null @@ -1,63 +0,0 @@ -#version 450 -#extension GL_ARB_separate_shader_objects : enable - -// A Julia-set compute shader whose entire interface is reflected at build time -// by vulkan-utils-spirv: -// * the `Params` push-constant block -> a Haskell record (gl-block std430 -// Storable) + the pipeline layout's push-constant range; -// * the output SSBO -> the descriptor set layout; and -// * the specialization constants below -> the pipeline's SpecializationInfo. - -layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in; - -// Specialized at pipeline-creation time (see Vulkan.Utils.SpirV.Specialization). -layout(constant_id = 0) const uint maxIterations = 1000u; -layout(constant_id = 1) const float escapeRadius = 2.0; - -// Fields are ordered by non-increasing alignment (vec2/uvec2 = 8) so gl-block's -// Generic std430 layout matches the shader; see the guardrail in -// Vulkan.Utils.SpirV.Block. -layout(push_constant, std430) uniform Params { - vec2 center; // c in the Julia iteration - uvec2 resolution; // output size in pixels -} params; - -layout(set = 0, binding = 0, std430) buffer OutputBuffer { - vec4 pixels[]; -}; - -// From https://iquilezles.org/articles/palettes/ -vec3 palette(float t) { - const vec3 a = vec3(0.5); - const vec3 b = vec3(0.5); - const vec3 c = vec3(1.0); - const vec3 d = vec3(0.0, 0.10, 0.20); - return a + b * cos(6.28318530718 * (c * t + d)); -} - -void main() { - uvec2 gid = gl_GlobalInvocationID.xy; - if (gid.x >= params.resolution.x || gid.y >= params.resolution.y) { - return; - } - - vec2 uv = - ( vec2(gid) / vec2(params.resolution) * 2.0 - 1.0 ) * escapeRadius; - - vec2 z = uv; - int i = 0; - for (; i < int(maxIterations); ++i) { - vec2 z2 = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y); - z = z2 + params.center; - if (dot(z, z) > escapeRadius * escapeRadius) { - break; - } - } - - uint idx = gid.y * params.resolution.x + gid.x; - if (i == int(maxIterations)) { - pixels[idx] = vec4(0.0, 0.0, 0.0, 1.0); - } else { - pixels[idx] = vec4(palette(float(i) / float(maxIterations)), 1.0); - } -} diff --git a/examples/mesh-reflect/Main.hs b/examples/mesh-reflect/Main.hs index 8df1102cd..428710f76 100644 --- a/examples/mesh-reflect/Main.hs +++ b/examples/mesh-reflect/Main.hs @@ -1,20 +1,6 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DerivingVia #-} -{-# LANGUAGE OverloadedLists #-} -{-# LANGUAGE OverloadedRecordDot #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeFamilies #-} - {-| Headless demonstration of type-verified pipeline assembly from reflected -SPIR-V. A single triangle's vertices live in an SSBO; ONE vertex shader pulls -them (indexed by @gl_VertexIndex@) and drives two pipelines built from the same -reflected resources: +SPIR-V — see "Mesh" for the pipeline family (and the compile-time composition +check), "Mesh.Shader" for the quasiquoted shaders, and "Render" for the frame: * a __depth-only__ pipeline (vertex stage alone, no colour attachment) — a z-prepass whose depth buffer is read back and checked (near depth written at @@ -24,113 +10,26 @@ reflected resources: the surface with a Lambert (N·L) light from the shared @Scene@ UBO, producing a top-bright/bottom-dark gradient that is read back and checked. -The fragment stage's compatibility with the vertex stage — matching @out@/@in@ -interface and shared use of the @Scene@ descriptor — is verified __at compile -time__ by 'MatchInterface' / 'CompatibleResources' (see 'pipelineComposes'); the -merged pipeline layout (Scene visible to both stages, the Mesh SSBO to the vertex -stage) and each pipeline come from 'allocateReflectedLayout' / 'allocateGraphicsPipeline'. Exits non-zero on mismatch. -} module Main where import qualified Codec.Picture as JP -import Control.Monad (unless, zipWithM_) +import Control.Monad (unless) import Control.Monad.IO.Class (liftIO) -import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT) -import Data.Bits ((.|.)) -import Data.ByteString (ByteString) -import Data.FileEmbed (embedFile) -import Data.Proxy (Proxy (..)) -import qualified Data.Vector as V -import Data.Word (Word32) -import Foreign.Ptr (castPtr) -import Foreign.Storable (peekElemOff, poke, sizeOf) -import qualified Geomancy -import qualified Geomancy.Mat4 as Mat4 -import Geomancy.Vec3 (vec3) -import Geomancy.Vec4 (vec4) -import Graphics.Gl.Block (Std140 (..), Std430 (..)) -import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), submitAndWait, withHeadlessVk) -import ImageReadback (copyImageToHost, makeReadbackImage, savePng) -import RenderTarget (createColorTarget) +import Control.Monad.Trans.Resource (runResourceT) +import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), withHeadlessVk) +import ImageReadback (savePng) import System.Exit (exitFailure) -import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) -import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) import qualified Vulkan.Core10 as Vk -import qualified Vulkan.Core13 as Vk -import Vulkan.Utils.Barrier (imageBarrier, transitionColorAttachment, transitionDepthAttachment) -import Vulkan.Utils.Descriptors (bufferWrite) import qualified Vulkan.Utils.DynamicRendering as Dynamic -import Vulkan.Utils.DynamicState (DynamicState (..), allDynamicStates, applyDynamicStates, depthOnlyDynamicStates, dynamicStateFor, fullScissor) import Vulkan.Utils.QueueAssignment (QueueFamilyIndex (..)) import Vulkan.Utils.Queues (Queues (..)) import Vulkan.Zero (zero) -import qualified VulkanMemoryAllocator as AllocationCreateInfo (AllocationCreateInfo (..)) -import qualified VulkanMemoryAllocator as VMA - -import Data.SpirV.Reflect.FFI (loadBytes) -import qualified Vulkan.Utils.SpirV.Array as Array -import Vulkan.Utils.SpirV.Buffer (Buffer, unsafeAsBufferPtr, writeBufferElem) -import Vulkan.Utils.SpirV.Pipeline (ReflectedLayout (..), allocateGraphicsPipeline, allocateReflectedLayout) -import Vulkan.Utils.SpirV.Signature (ArrayOf, Sig) -import Vulkan.Utils.SpirV.Stage (CompatibleResources, MatchInterface, reflectStageSig) -import Vulkan.Utils.SpirV.TH (reflectShaderTypes) - --- Reflection at compile time: the @Scene@ UBO record and the @Vertex@ SSBO --- element record, plus a stage signature for each shader. -reflectShaderTypes "mesh-reflect/shader.vert.spv" -reflectStageSig "VertSig" "mesh-reflect/shader.vert.spv" -reflectStageSig "FragSig" "mesh-reflect/shader.frag.spv" - --- Compile-time proof that the fragment stage composes with the vertex stage: --- matching interface and compatible shared resources. Evaluating it forces GHC to --- discharge those constraints. -pipelineComposes :: (MatchInterface VertSig FragSig, CompatibleResources VertSig FragSig) => Bool -pipelineComposes = True - --- Committed SPIR-V, embedded for the runtime shader modules and reflection. -vertSpv :: ByteString -vertSpv = $(embedFile "mesh-reflect/shader.vert.spv") - -fragSpv :: ByteString -fragSpv = $(embedFile "mesh-reflect/shader.frag.spv") - -width, height :: Word32 -width = 256 -height = 256 - -colorFormat :: Vk.Format -colorFormat = Vk.FORMAT_R8G8B8A8_UNORM - -depthFormat :: Vk.Format -depthFormat = Vk.FORMAT_D32_SFLOAT - -floatSize :: Int -floatSize = sizeOf (0 :: Float) - -{- | The triangle, stored in the SSBO. Apex at the top (Vulkan clip space is +Y -down), facing the light; the base faces away — so Lambert shading gives a -top-bright/bottom-dark gradient. Vertices are white; the colour comes from the -light. --} -meshVertices :: [Vertex] -meshVertices = - [ Vertex{position = vec3 0.0 (-0.7) 0.5, normal = vec3 0.0 (-1.0) 0.6, color = vec3 1 1 1} - , Vertex{position = vec3 (-0.7) 0.6 0.5, normal = vec3 0.0 1.0 0.6, color = vec3 1 1 1} - , Vertex{position = vec3 0.7 0.6 0.5, normal = vec3 0.0 1.0 0.6, color = vec3 1 1 1} - ] -vertexCount :: Word32 -vertexCount = 3 - --- | Identity transform, a light from the top-front, and a warm light colour. -sceneValue :: Scene -sceneValue = - Scene - { transform = Mat4.identity - , lightDir = vec4 0.0 (-1.0) 0.6 0.0 - , lightColor = vec4 1.0 0.85 0.6 1.0 - } +import qualified Mesh +import Render (height, width) +import qualified Render main :: IO () main = runResourceT $ do @@ -142,11 +41,11 @@ main = runResourceT $ do , deviceReqs = Dynamic.dynamicRenderingRequirements , vmaFlags = zero } - liftIO $ putStrLn $ "vertex+fragment pipeline composes (compile-time): " <> show pipelineComposes + liftIO $ putStrLn $ "vertex+fragment pipeline composes (compile-time): " <> show Mesh.pipelineComposes let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics queues) (depthCentre, depthCorner, colorImage) <- - render allocator device graphicsQueueFamilyIndex + Render.render allocator device graphicsQueueFamilyIndex Vk.deviceWaitIdle device savePng "mesh-reflect-color.png" colorImage @@ -182,253 +81,3 @@ main = runResourceT $ do mapM_ (\(label, ok) -> putStrLn $ "[" <> (if ok then "PASS" else "FAIL") <> "] " <> label) checks unless (all snd checks) exitFailure putStrLn "All type-verified pipeline-assembly checks passed." - -render - :: VMA.Allocator - -> Vk.Device - -> Word32 - -> ResourceT IO (Float, Float, JP.Image JP.PixelRGBA8) -render allocator dev graphicsQueueFamilyIndex = do - let extent = Vk.Extent2D width height - - -- Reflected modules drive the merged pipeline layout. - vertModule <- loadBytes vertSpv - fragModule <- loadBytes fragSpv - - (_, (image, imageView)) <- createColorTarget allocator dev colorFormat extent - -- Depth target with TRANSFER_SRC so the prepass result can be read back. - (depthImage, depthView) <- createReadableDepthTarget allocator dev depthFormat extent - (cpuImage, readback) <- makeReadbackImage allocator dev colorFormat extent - - -- Geometry SSBO (host-visible, mapped). The mapped pointer is tagged as a typed - -- runtime array of the reflected @Vertex@ element — @'ArrayOf' ('Sig' Vertex)@ — - -- and each vertex is written with 'writeBufferElem', which is gated by - -- 'Vulkan.Utils.SpirV.Signature.FitsTail': a wrong element layout (or indexing a - -- non-array buffer) is a compile error, and the element stride comes from the - -- signature, not a hand-computed offset. - (_, (meshBuffer, _, meshInfo)) <- - VMA.withBuffer - allocator - zero - { Vk.size = fromIntegral (length meshVertices * Array.std430Stride (Proxy @Vertex)) - , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT - } - mappedAlloc - allocate - meshBuf <- unsafeAsBufferPtr (VMA.mappedData meshInfo) :: ResourceT IO (Buffer (ArrayOf (Sig Vertex))) - zipWithM_ (writeBufferElem meshBuf) [0 ..] meshVertices - - -- Scene UBO (host-visible, mapped). - (_, (sceneBuffer, _, sceneInfo)) <- - VMA.withBuffer - allocator - zero - { Vk.size = fromIntegral (sizeOf sceneValue) - , Vk.usage = Vk.BUFFER_USAGE_UNIFORM_BUFFER_BIT - } - mappedAlloc - allocate - liftIO $ poke (castPtr (VMA.mappedData sceneInfo)) sceneValue - - -- Descriptor set layouts + pipeline layout from reflection, merged across both - -- stages: Scene visible to vertex AND fragment (stage-flag union), the Mesh SSBO - -- to the vertex stage. One layout, shared by both pipelines below. - (_, layout) <- allocateReflectedLayout dev [vertModule, fragModule] - - -- One descriptor set: binding 0 -> Scene UBO, binding 1 -> Mesh SSBO. - (_, descriptorPool) <- - Vk.withDescriptorPool - dev - zero - { Vk.maxSets = 1 - , Vk.poolSizes = - [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER 1 - , Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER 1 - ] - } - Nothing - allocate - descriptorSets <- - Vk.allocateDescriptorSets - dev - zero - { Vk.descriptorPool = descriptorPool - , Vk.setLayouts = V.fromList (map snd layout.setLayouts) - } - let descriptorSet = V.head descriptorSets - Vk.updateDescriptorSets - dev - [ bufferWrite descriptorSet 0 Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER sceneBuffer - , bufferWrite descriptorSet 1 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER meshBuffer - ] - [] - - -- Two pipelines from the same vertex shader and the same layout: depth-only (no - -- colour attachment) and depth+colour. Vertex input is empty — geometry comes - -- from the SSBO via gl_VertexIndex. - (_, depthOnlyPipeline) <- - allocateGraphicsPipeline - dev - layout - zero{Dynamic.depthFormat = Just depthFormat} - () - [(vertModule, vertSpv)] - (_, colorPipeline) <- - allocateGraphicsPipeline - dev - layout - zero - { Dynamic.colorFormats = [colorFormat] - , Dynamic.depthFormat = Just depthFormat - } - () - [(vertModule, vertSpv), (fragModule, fragSpv)] - - (_, commandPool) <- - Vk.withCommandPool dev zero{CommandPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex} Nothing allocate - graphicsQueue <- Vk.getDeviceQueue dev graphicsQueueFamilyIndex 0 - - -- Depth buffer readback target (host-visible buffer of width*height floats). - (_, (depthBuffer, depthAllocation, depthBufInfo)) <- - VMA.withBuffer - allocator - zero - { Vk.size = fromIntegral (fromIntegral width * fromIntegral height * floatSize) - , Vk.usage = Vk.BUFFER_USAGE_TRANSFER_DST_BIT - } - readbackAlloc - allocate - - let - oneShot = zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} - -- Apply only the dynamic states the bound pipeline declares (the depth-only - -- pipeline has no colour-blend state). - bindCommon dynStates cb pipeline = do - Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_GRAPHICS pipeline - applyDynamicStates - dynStates - cb - (dynamicStateFor extent){depthTest = True, depthWrite = True, depthCompareOp = Vk.COMPARE_OP_LESS} - Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_GRAPHICS layout.pipelineLayout 0 [descriptorSet] [] - Vk.cmdDraw cb vertexCount 1 0 0 - - -- Pass 1: depth-only z-prepass, then copy the depth image to the host buffer. - cb1 <- oneCommandBuffer dev commandPool - Vk.useCommandBuffer cb1 oneShot do - transitionDepthAttachment cb1 depthImage - Vk.cmdUseRendering cb1 (Dynamic.renderingInfo (fullScissor extent) [] (Just (depthView, 1))) $ - bindCommon depthOnlyDynamicStates cb1 depthOnlyPipeline - depthToTransferSrc cb1 depthImage - Vk.cmdCopyImageToBuffer cb1 depthImage Vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL depthBuffer [depthCopyRegion extent] - submitAndWait dev graphicsQueue cb1 "Timed out in the depth-only prepass" - VMA.invalidateAllocation allocator depthAllocation 0 Vk.WHOLE_SIZE - let depthPtr = castPtr (VMA.mappedData depthBufInfo) - depthCentre <- liftIO $ peekElemOff depthPtr (fromIntegral (height `div` 2) * fromIntegral width + fromIntegral (width `div` 2)) - depthCorner <- liftIO $ peekElemOff depthPtr 0 - - -- Pass 2: depth+colour, then copy the colour image to the host image. - cb2 <- oneCommandBuffer dev commandPool - Vk.useCommandBuffer cb2 oneShot do - transitionColorAttachment cb2 image - transitionDepthAttachment cb2 depthImage - Vk.cmdUseRendering - cb2 - (Dynamic.renderingInfo (fullScissor extent) [(imageView, Vk.Float32 0.05 0.05 0.05 1)] (Just (depthView, 1))) - $ bindCommon allDynamicStates cb2 colorPipeline - copyImageToHost cb2 extent image cpuImage - submitAndWait dev graphicsQueue cb2 "Timed out in the colour pass" - colorImage <- readback - - pure (depthCentre, depthCorner, colorImage) - -mappedAlloc :: VMA.AllocationCreateInfo -mappedAlloc = - zero - { AllocationCreateInfo.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT - , AllocationCreateInfo.usage = VMA.MEMORY_USAGE_CPU_TO_GPU - , AllocationCreateInfo.requiredFlags = Vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT - } - -readbackAlloc :: VMA.AllocationCreateInfo -readbackAlloc = - zero - { AllocationCreateInfo.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT - , AllocationCreateInfo.usage = VMA.MEMORY_USAGE_GPU_TO_CPU - } - -oneCommandBuffer :: Vk.Device -> Vk.CommandPool -> ResourceT IO Vk.CommandBuffer -oneCommandBuffer dev pool = do - (_, cbs) <- - Vk.withCommandBuffers - dev - zero - { Vk.commandPool = pool - , Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY - , Vk.commandBufferCount = 1 - } - allocate - pure (V.head cbs) - -depthCopyRegion :: Vk.Extent2D -> Vk.BufferImageCopy -depthCopyRegion (Vk.Extent2D w h) = - Vk.BufferImageCopy - { Vk.bufferOffset = 0 - , Vk.bufferRowLength = 0 - , Vk.bufferImageHeight = 0 - , Vk.imageSubresource = Vk.ImageSubresourceLayers Vk.IMAGE_ASPECT_DEPTH_BIT 0 0 1 - , Vk.imageOffset = Vk.Offset3D 0 0 0 - , Vk.imageExtent = Vk.Extent3D w h 1 - } - -{- | A GPU-only depth attachment that can also be a transfer source (so the -prepass depth can be copied back). Like 'RenderTarget.createDepthTarget' but -with the extra usage flag. --} -createReadableDepthTarget - :: VMA.Allocator -> Vk.Device -> Vk.Format -> Vk.Extent2D -> ResourceT IO (Vk.Image, Vk.ImageView) -createReadableDepthTarget allocator dev format (Vk.Extent2D w h) = do - (_, (image, _, _)) <- VMA.withImage allocator imageCreateInfo gpuAlloc allocate - (_, view) <- Vk.withImageView dev (viewCreateInfo image) Nothing allocate - pure (image, view) - where - gpuAlloc = zero{AllocationCreateInfo.usage = VMA.MEMORY_USAGE_GPU_ONLY} - imageCreateInfo = - zero - { Vk.imageType = Vk.IMAGE_TYPE_2D - , Vk.format = format - , Vk.extent = Vk.Extent3D w h 1 - , Vk.mipLevels = 1 - , Vk.arrayLayers = 1 - , Vk.samples = Vk.SAMPLE_COUNT_1_BIT - , Vk.tiling = Vk.IMAGE_TILING_OPTIMAL - , Vk.usage = Vk.IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT .|. Vk.IMAGE_USAGE_TRANSFER_SRC_BIT - , Vk.initialLayout = Vk.IMAGE_LAYOUT_UNDEFINED - } - viewCreateInfo image = - zero - { Vk.image = image - , Vk.viewType = Vk.IMAGE_VIEW_TYPE_2D - , Vk.format = format - , Vk.subresourceRange = Vk.ImageSubresourceRange Vk.IMAGE_ASPECT_DEPTH_BIT 0 1 0 1 - } - -{- | Barrier the depth image from a depth attachment (after the prepass) to a -transfer source, so it can be copied out. --} -depthToTransferSrc :: Vk.CommandBuffer -> Vk.Image -> ResourceT IO () -depthToTransferSrc cb img = - Vk.cmdPipelineBarrier - cb - Vk.PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT - Vk.PIPELINE_STAGE_TRANSFER_BIT - zero - [] - [] - [ imageBarrier - Vk.IMAGE_ASPECT_DEPTH_BIT - Vk.ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT - Vk.ACCESS_TRANSFER_READ_BIT - Vk.IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL - Vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL - img - ] diff --git a/examples/mesh-reflect/Mesh.hs b/examples/mesh-reflect/Mesh.hs new file mode 100644 index 000000000..48b1733c6 --- /dev/null +++ b/examples/mesh-reflect/Mesh.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +{-| The mesh pipeline family: one vertex shader, two pipelines. + +Reflection of 'Shader.vertCode' \/ 'Shader.fragCode' provides the @Scene@ UBO +record, the @Vertex@ SSBO element record, and a stage signature for each +shader. The fragment stage's compatibility with the vertex stage — matching +@out@\/@in@ interface and shared use of the @Scene@ descriptor — is verified +__at compile time__ by 'MatchInterface' \/ 'CompatibleResources' (see +'pipelineComposes'); the merged pipeline layout (Scene visible to both stages, +the Mesh SSBO to the vertex stage) and each pipeline come from +'allocateReflectedLayout' \/ 'allocateGraphicsPipeline'. +-} +module Mesh + ( Scene (..) + , Vertex (..) + , pipelineComposes + , Pipelines (..) + , allocatePipelines + ) where + +import Control.Monad.Trans.Resource (ResourceT) +import qualified Geomancy +import Graphics.Gl.Block (Std140 (..), Std430 (..)) +import qualified Vulkan.Core10 as Vk +import qualified Vulkan.Utils.DynamicRendering as Dynamic +import Vulkan.Zero (zero) + +import Data.SpirV.Reflect.FFI (loadBytes) +import Vulkan.Utils.SpirV.Pipeline (ReflectedLayout, allocateGraphicsPipeline, allocateReflectedLayout) +import Vulkan.Utils.SpirV.Stage (CompatibleResources, MatchInterface, reflectStageSigBytes) +import Vulkan.Utils.SpirV.TH (reflectShaderTypesBytes) + +import qualified Mesh.Shader as Shader + +-- Reflection at compile time: the @Scene@ UBO record and the @Vertex@ SSBO +-- element record, plus a stage signature for each shader. +reflectShaderTypesBytes Shader.vertCode +reflectStageSigBytes "VertSig" Shader.vertCode +reflectStageSigBytes "FragSig" Shader.fragCode + +-- Compile-time proof that the fragment stage composes with the vertex stage: +-- matching interface and compatible shared resources. Evaluating it forces GHC to +-- discharge those constraints. +pipelineComposes :: (MatchInterface VertSig FragSig, CompatibleResources VertSig FragSig) => Bool +pipelineComposes = True + +data Pipelines = Pipelines + { layout :: ReflectedLayout + , depthOnly :: Vk.Pipeline + , depthColor :: Vk.Pipeline + } + +{- | Two pipelines from the same vertex shader and the same reflected layout: +depth-only (no colour attachment) and depth+colour. Vertex input is empty — +geometry comes from the SSBO via @gl_VertexIndex@. +-} +allocatePipelines :: Vk.Device -> Vk.Format -> Vk.Format -> ResourceT IO Pipelines +allocatePipelines dev colorFormat depthFormat = do + vertModule <- loadBytes Shader.vertCode + fragModule <- loadBytes Shader.fragCode + + -- Descriptor set layouts + pipeline layout from reflection, merged across both + -- stages: Scene visible to vertex AND fragment (stage-flag union), the Mesh SSBO + -- to the vertex stage. One layout, shared by both pipelines. + (_, layout) <- allocateReflectedLayout dev [vertModule, fragModule] + + (_, depthOnly) <- + allocateGraphicsPipeline + dev + layout + zero{Dynamic.depthFormat = Just depthFormat} + () + [(vertModule, Shader.vertCode)] + (_, depthColor) <- + allocateGraphicsPipeline + dev + layout + zero + { Dynamic.colorFormats = [colorFormat] + , Dynamic.depthFormat = Just depthFormat + } + () + [(vertModule, Shader.vertCode), (fragModule, Shader.fragCode)] + + pure Pipelines{layout = layout, depthOnly = depthOnly, depthColor = depthColor} diff --git a/examples/mesh-reflect/Mesh/Shader.hs b/examples/mesh-reflect/Mesh/Shader.hs new file mode 100644 index 000000000..5dc86d655 --- /dev/null +++ b/examples/mesh-reflect/Mesh/Shader.hs @@ -0,0 +1,83 @@ +{-# LANGUAGE QuasiQuotes #-} + +{-| The mesh vertex/fragment shaders, compiled to SPIR-V at build time. + +Reflected in "Mesh": the @Scene@ UBO and @Vertex@ SSBO-element records, the +per-stage signatures the compile-time composition check runs on, and the +merged pipeline layout. +-} +module Mesh.Shader + ( vertCode + , fragCode + ) where + +import Data.ByteString (ByteString) +import Vulkan.Utils.ShaderQQ.GLSL.Glslang (frag, vert) + +{- | Vertex stage shared by both pipelines (depth-only z-prepass and +depth+color). The geometry is pulled from the @Mesh@ SSBO (set 0, binding 1) +indexed by @gl_VertexIndex@ — no vertex buffer. The Camera\/Scene UBO (set 0, +binding 0) is shared with the fragment stage: the vertex stage uses +@transform@, the fragment stage uses the light fields. +-} +vertCode :: ByteString +vertCode = + [vert| + #version 450 + + struct Vertex { + vec3 position; + vec3 normal; + vec3 color; + }; + + layout(set = 0, binding = 1, std430) readonly buffer Mesh { + Vertex verts[]; + }; + + layout(set = 0, binding = 0, std140) uniform Scene { + mat4 transform; + vec4 lightDir; + vec4 lightColor; + } scene; + + layout(location = 0) out vec3 outNormal; + layout(location = 1) out vec3 outColor; + + void main() { + Vertex v = verts[gl_VertexIndex]; + gl_Position = scene.transform * vec4(v.position, 1.0); + outNormal = v.normal; + outColor = v.color; + } + |] + +{- | Fragment stage of the depth+color pipeline. Its inputs (normal @loc 0@, +color @loc 1@) must match the vertex stage's outputs, and it shares the Scene +UBO (using the light fields the vertex stage ignores) — both checked at +compile time by 'Vulkan.Utils.SpirV.Stage.MatchInterface' \/ +'Vulkan.Utils.SpirV.Stage.CompatibleResources'. Shades the surface with a +simple Lambert (N·L) term plus ambient. +-} +fragCode :: ByteString +fragCode = + [frag| + #version 450 + + layout(location = 0) in vec3 inNormal; + layout(location = 1) in vec3 inColor; + + layout(set = 0, binding = 0, std140) uniform Scene { + mat4 transform; + vec4 lightDir; + vec4 lightColor; + } scene; + + layout(location = 0) out vec4 outColor; + + void main() { + float lambert = max(dot(normalize(inNormal), normalize(scene.lightDir.xyz)), 0.0); + float shade = 0.15 + 0.85 * lambert; + outColor = vec4(inColor * scene.lightColor.rgb * shade, 1.0); + } + |] diff --git a/examples/mesh-reflect/Render.hs b/examples/mesh-reflect/Render.hs new file mode 100644 index 000000000..01d53c4fe --- /dev/null +++ b/examples/mesh-reflect/Render.hs @@ -0,0 +1,309 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE TypeFamilies #-} + +{-| The headless frame: a depth-only z-prepass whose depth buffer is read back, +then a depth+colour pass whose image is read back. +-} +module Render + ( render + , width + , height + ) where + +import qualified Codec.Picture as JP +import Control.Monad (zipWithM_) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Resource (ResourceT, allocate) +import Data.Bits ((.|.)) +import Data.Proxy (Proxy (..)) +import qualified Data.Vector as V +import Data.Word (Word32) +import Foreign.Ptr (castPtr) +import Foreign.Storable (peekElemOff, poke, sizeOf) +import qualified Geomancy.Mat4 as Mat4 +import Geomancy.Vec3 (vec3) +import Geomancy.Vec4 (vec4) +import HeadlessBoot (submitAndWait) +import ImageReadback (copyImageToHost, makeReadbackImage) +import RenderTarget (createColorTarget) +import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) +import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import qualified Vulkan.Core13 as Vk +import Vulkan.Utils.Barrier (imageBarrier, transitionColorAttachment, transitionDepthAttachment) +import Vulkan.Utils.Descriptors (bufferWrite) +import qualified Vulkan.Utils.DynamicRendering as Dynamic +import Vulkan.Utils.DynamicState (DynamicState (..), allDynamicStates, applyDynamicStates, depthOnlyDynamicStates, dynamicStateFor, fullScissor) +import Vulkan.Zero (zero) +import qualified VulkanMemoryAllocator as AllocationCreateInfo (AllocationCreateInfo (..)) +import qualified VulkanMemoryAllocator as VMA + +import qualified Vulkan.Utils.SpirV.Array as Array +import Vulkan.Utils.SpirV.Buffer (Buffer, unsafeAsBufferPtr, writeBufferElem) +import qualified Vulkan.Utils.SpirV.Pipeline +import Vulkan.Utils.SpirV.Signature (ArrayOf, Sig) + +import Mesh (Scene (..), Vertex (..)) +import qualified Mesh + +width, height :: Word32 +width = 256 +height = 256 + +colorFormat :: Vk.Format +colorFormat = Vk.FORMAT_R8G8B8A8_UNORM + +depthFormat :: Vk.Format +depthFormat = Vk.FORMAT_D32_SFLOAT + +floatSize :: Int +floatSize = sizeOf (0 :: Float) + +{- | The triangle, stored in the SSBO. Apex at the top (Vulkan clip space is +Y +down), facing the light; the base faces away — so Lambert shading gives a +top-bright/bottom-dark gradient. Vertices are white; the colour comes from the +light. +-} +meshVertices :: [Vertex] +meshVertices = + [ Vertex{position = vec3 0.0 (-0.7) 0.5, normal = vec3 0.0 (-1.0) 0.6, color = vec3 1 1 1} + , Vertex{position = vec3 (-0.7) 0.6 0.5, normal = vec3 0.0 1.0 0.6, color = vec3 1 1 1} + , Vertex{position = vec3 0.7 0.6 0.5, normal = vec3 0.0 1.0 0.6, color = vec3 1 1 1} + ] + +vertexCount :: Word32 +vertexCount = 3 + +-- | Identity transform, a light from the top-front, and a warm light colour. +sceneValue :: Scene +sceneValue = + Scene + { transform = Mat4.identity + , lightDir = vec4 0.0 (-1.0) 0.6 0.0 + , lightColor = vec4 1.0 0.85 0.6 1.0 + } + +render + :: VMA.Allocator + -> Vk.Device + -> Word32 + -> ResourceT IO (Float, Float, JP.Image JP.PixelRGBA8) +render allocator dev graphicsQueueFamilyIndex = do + let extent = Vk.Extent2D width height + + (_, (image, imageView)) <- createColorTarget allocator dev colorFormat extent + -- Depth target with TRANSFER_SRC so the prepass result can be read back. + (depthImage, depthView) <- createReadableDepthTarget allocator dev depthFormat extent + (cpuImage, readback) <- makeReadbackImage allocator dev colorFormat extent + + -- Geometry SSBO (host-visible, mapped). The mapped pointer is tagged as a typed + -- runtime array of the reflected @Vertex@ element — @'ArrayOf' ('Sig' Vertex)@ — + -- and each vertex is written with 'writeBufferElem', which is gated by + -- 'Vulkan.Utils.SpirV.Signature.FitsTail': a wrong element layout (or indexing a + -- non-array buffer) is a compile error, and the element stride comes from the + -- signature, not a hand-computed offset. + (_, (meshBuffer, _, meshInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (length meshVertices * Array.std430Stride (Proxy @Vertex)) + , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + } + mappedAlloc + allocate + meshBuf <- unsafeAsBufferPtr (VMA.mappedData meshInfo) :: ResourceT IO (Buffer (ArrayOf (Sig Vertex))) + zipWithM_ (writeBufferElem meshBuf) [0 ..] meshVertices + + -- Scene UBO (host-visible, mapped). + (_, (sceneBuffer, _, sceneInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (sizeOf sceneValue) + , Vk.usage = Vk.BUFFER_USAGE_UNIFORM_BUFFER_BIT + } + mappedAlloc + allocate + liftIO $ poke (castPtr (VMA.mappedData sceneInfo)) sceneValue + + pipelines <- Mesh.allocatePipelines dev colorFormat depthFormat + let layout = pipelines.layout + + -- One descriptor set: binding 0 -> Scene UBO, binding 1 -> Mesh SSBO. + (_, descriptorPool) <- + Vk.withDescriptorPool + dev + zero + { Vk.maxSets = 1 + , Vk.poolSizes = + [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER 1 + , Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER 1 + ] + } + Nothing + allocate + descriptorSets <- + Vk.allocateDescriptorSets + dev + zero + { Vk.descriptorPool = descriptorPool + , Vk.setLayouts = V.fromList (map snd layout.setLayouts) + } + let descriptorSet = V.head descriptorSets + Vk.updateDescriptorSets + dev + [ bufferWrite descriptorSet 0 Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER sceneBuffer + , bufferWrite descriptorSet 1 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER meshBuffer + ] + [] + + (_, commandPool) <- + Vk.withCommandPool dev zero{CommandPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex} Nothing allocate + graphicsQueue <- Vk.getDeviceQueue dev graphicsQueueFamilyIndex 0 + + -- Depth buffer readback target (host-visible buffer of width*height floats). + (_, (depthBuffer, depthAllocation, depthBufInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (fromIntegral width * fromIntegral height * floatSize) + , Vk.usage = Vk.BUFFER_USAGE_TRANSFER_DST_BIT + } + readbackAlloc + allocate + + let + oneShot = zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} + -- Apply only the dynamic states the bound pipeline declares (the depth-only + -- pipeline has no colour-blend state). + bindCommon dynStates cb pipeline = do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_GRAPHICS pipeline + applyDynamicStates + dynStates + cb + (dynamicStateFor extent){depthTest = True, depthWrite = True, depthCompareOp = Vk.COMPARE_OP_LESS} + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_GRAPHICS layout.pipelineLayout 0 [descriptorSet] [] + Vk.cmdDraw cb vertexCount 1 0 0 + + -- Pass 1: depth-only z-prepass, then copy the depth image to the host buffer. + cb1 <- oneCommandBuffer dev commandPool + Vk.useCommandBuffer cb1 oneShot do + transitionDepthAttachment cb1 depthImage + Vk.cmdUseRendering cb1 (Dynamic.renderingInfo (fullScissor extent) [] (Just (depthView, 1))) $ + bindCommon depthOnlyDynamicStates cb1 pipelines.depthOnly + depthToTransferSrc cb1 depthImage + Vk.cmdCopyImageToBuffer cb1 depthImage Vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL depthBuffer [depthCopyRegion extent] + submitAndWait dev graphicsQueue cb1 "Timed out in the depth-only prepass" + VMA.invalidateAllocation allocator depthAllocation 0 Vk.WHOLE_SIZE + let depthPtr = castPtr (VMA.mappedData depthBufInfo) + depthCentre <- liftIO $ peekElemOff depthPtr (fromIntegral (height `div` 2) * fromIntegral width + fromIntegral (width `div` 2)) + depthCorner <- liftIO $ peekElemOff depthPtr 0 + + -- Pass 2: depth+colour, then copy the colour image to the host image. + cb2 <- oneCommandBuffer dev commandPool + Vk.useCommandBuffer cb2 oneShot do + transitionColorAttachment cb2 image + transitionDepthAttachment cb2 depthImage + Vk.cmdUseRendering + cb2 + (Dynamic.renderingInfo (fullScissor extent) [(imageView, Vk.Float32 0.05 0.05 0.05 1)] (Just (depthView, 1))) + $ bindCommon allDynamicStates cb2 pipelines.depthColor + copyImageToHost cb2 extent image cpuImage + submitAndWait dev graphicsQueue cb2 "Timed out in the colour pass" + colorImage <- readback + + pure (depthCentre, depthCorner, colorImage) + +mappedAlloc :: VMA.AllocationCreateInfo +mappedAlloc = + zero + { AllocationCreateInfo.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , AllocationCreateInfo.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + , AllocationCreateInfo.requiredFlags = Vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT + } + +readbackAlloc :: VMA.AllocationCreateInfo +readbackAlloc = + zero + { AllocationCreateInfo.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , AllocationCreateInfo.usage = VMA.MEMORY_USAGE_GPU_TO_CPU + } + +oneCommandBuffer :: Vk.Device -> Vk.CommandPool -> ResourceT IO Vk.CommandBuffer +oneCommandBuffer dev pool = do + (_, cbs) <- + Vk.withCommandBuffers + dev + zero + { Vk.commandPool = pool + , Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY + , Vk.commandBufferCount = 1 + } + allocate + pure (V.head cbs) + +depthCopyRegion :: Vk.Extent2D -> Vk.BufferImageCopy +depthCopyRegion (Vk.Extent2D w h) = + Vk.BufferImageCopy + { Vk.bufferOffset = 0 + , Vk.bufferRowLength = 0 + , Vk.bufferImageHeight = 0 + , Vk.imageSubresource = Vk.ImageSubresourceLayers Vk.IMAGE_ASPECT_DEPTH_BIT 0 0 1 + , Vk.imageOffset = Vk.Offset3D 0 0 0 + , Vk.imageExtent = Vk.Extent3D w h 1 + } + +{- | A GPU-only depth attachment that can also be a transfer source (so the +prepass depth can be copied back). Like 'RenderTarget.createDepthTarget' but +with the extra usage flag. +-} +createReadableDepthTarget + :: VMA.Allocator -> Vk.Device -> Vk.Format -> Vk.Extent2D -> ResourceT IO (Vk.Image, Vk.ImageView) +createReadableDepthTarget allocator dev format (Vk.Extent2D w h) = do + (_, (image, _, _)) <- VMA.withImage allocator imageCreateInfo gpuAlloc allocate + (_, view) <- Vk.withImageView dev (viewCreateInfo image) Nothing allocate + pure (image, view) + where + gpuAlloc = zero{AllocationCreateInfo.usage = VMA.MEMORY_USAGE_GPU_ONLY} + imageCreateInfo = + zero + { Vk.imageType = Vk.IMAGE_TYPE_2D + , Vk.format = format + , Vk.extent = Vk.Extent3D w h 1 + , Vk.mipLevels = 1 + , Vk.arrayLayers = 1 + , Vk.samples = Vk.SAMPLE_COUNT_1_BIT + , Vk.tiling = Vk.IMAGE_TILING_OPTIMAL + , Vk.usage = Vk.IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT .|. Vk.IMAGE_USAGE_TRANSFER_SRC_BIT + , Vk.initialLayout = Vk.IMAGE_LAYOUT_UNDEFINED + } + viewCreateInfo image = + zero + { Vk.image = image + , Vk.viewType = Vk.IMAGE_VIEW_TYPE_2D + , Vk.format = format + , Vk.subresourceRange = Vk.ImageSubresourceRange Vk.IMAGE_ASPECT_DEPTH_BIT 0 1 0 1 + } + +{- | Barrier the depth image from a depth attachment (after the prepass) to a +transfer source, so it can be copied out. +-} +depthToTransferSrc :: Vk.CommandBuffer -> Vk.Image -> ResourceT IO () +depthToTransferSrc cb img = + Vk.cmdPipelineBarrier + cb + Vk.PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT + Vk.PIPELINE_STAGE_TRANSFER_BIT + zero + [] + [] + [ imageBarrier + Vk.IMAGE_ASPECT_DEPTH_BIT + Vk.ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT + Vk.ACCESS_TRANSFER_READ_BIT + Vk.IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL + Vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL + img + ] diff --git a/examples/mesh-reflect/build-shaders.sh b/examples/mesh-reflect/build-shaders.sh deleted file mode 100755 index 953f2ebfc..000000000 --- a/examples/mesh-reflect/build-shaders.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -# Compile this example's shaders to committed SPIR-V (input to reflection + runtime). -set -eu -cd "$(dirname "$0")" -for src in *.vert *.frag; do - [ -e "$src" ] || continue - glslangValidator -V "$src" -o "$src.spv" - echo "wrote $src.spv" -done diff --git a/examples/mesh-reflect/shader.frag b/examples/mesh-reflect/shader.frag deleted file mode 100644 index 6f8366113..000000000 --- a/examples/mesh-reflect/shader.frag +++ /dev/null @@ -1,24 +0,0 @@ -#version 450 - -// Fragment stage of the depth+color pipeline. Its inputs (normal @loc 0, color -// @loc 1) must match the vertex stage's outputs, and it shares the Scene UBO -// (using the light fields the vertex stage ignores) — both checked at compile -// time by MatchInterface / CompatibleResources. Shades the surface with a simple -// Lambert (N·L) term plus ambient. - -layout(location = 0) in vec3 inNormal; -layout(location = 1) in vec3 inColor; - -layout(set = 0, binding = 0, std140) uniform Scene { - mat4 transform; - vec4 lightDir; - vec4 lightColor; -} scene; - -layout(location = 0) out vec4 outColor; - -void main() { - float lambert = max(dot(normalize(inNormal), normalize(scene.lightDir.xyz)), 0.0); - float shade = 0.15 + 0.85 * lambert; - outColor = vec4(inColor * scene.lightColor.rgb * shade, 1.0); -} diff --git a/examples/mesh-reflect/shader.vert b/examples/mesh-reflect/shader.vert deleted file mode 100644 index b72fe290f..000000000 --- a/examples/mesh-reflect/shader.vert +++ /dev/null @@ -1,33 +0,0 @@ -#version 450 - -// Vertex stage shared by both pipelines (depth-only z-prepass and depth+color). -// The geometry is pulled from the `Mesh` SSBO (set 0, binding 1) indexed by -// gl_VertexIndex — no vertex buffer. The Camera/Scene UBO (set 0, binding 0) is -// shared with the fragment stage: the vertex stage uses `transform`, the fragment -// stage uses the light fields. - -struct Vertex { - vec3 position; - vec3 normal; - vec3 color; -}; - -layout(set = 0, binding = 1, std430) readonly buffer Mesh { - Vertex verts[]; -}; - -layout(set = 0, binding = 0, std140) uniform Scene { - mat4 transform; - vec4 lightDir; - vec4 lightColor; -} scene; - -layout(location = 0) out vec3 outNormal; -layout(location = 1) out vec3 outColor; - -void main() { - Vertex v = verts[gl_VertexIndex]; - gl_Position = scene.transform * vec4(v.position, 1.0); - outNormal = v.normal; - outColor = v.color; -} diff --git a/examples/package.yaml b/examples/package.yaml index a7b4899ec..5e30fa6bd 100644 --- a/examples/package.yaml +++ b/examples/package.yaml @@ -140,7 +140,6 @@ executables: - vulkan-examples - base <5 - bytestring - - file-embed - geomancy - gl-block - resourcet @@ -162,7 +161,6 @@ executables: - vulkan-examples - base <5 - bytestring - - file-embed - geomancy - gl-block - optparse-applicative @@ -183,7 +181,6 @@ executables: - vulkan-examples - base <5 - bytestring - - file-embed - geomancy - gl-block - resourcet @@ -203,7 +200,6 @@ executables: - vulkan-examples - base <5 - bytestring - - file-embed - gl-block - resourcet - spirv-reflect-ffi diff --git a/examples/pathtrace-reflect/Main.hs b/examples/pathtrace-reflect/Main.hs index 6cdf65a47..80c8e6501 100644 --- a/examples/pathtrace-reflect/Main.hs +++ b/examples/pathtrace-reflect/Main.hs @@ -1,102 +1,33 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DerivingVia #-} -{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedRecordDot #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE QuasiQuotes #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} {-| A single-frame, procedural compute path tracer whose /entire/ shader -interface is derived from the compiled SPIR-V at build time by -@vulkan-utils-spirv@ — exercising every reflected resource kind at once: - - * the @Camera@ uniform block -> a generated record (std140); - * the @Scene@ input storage buffer -> a descriptor binding holding the leaf - sphere geometry; its element record 'Sphere' is generated from the SSBO's - array element type; - * the @Image@ output storage buffer -> a descriptor binding; - * the @BvhNode@ @buffer_reference@ type -> a generated record whose children are - @DeviceAddress BvhNode@; the host builds a BVH, links the nodes by device - address, and the shader hops those addresses to intersect the scene; - * the @Frame@ push-constant block -> a generated record + range, carrying the - root node's device address; and - * the @SAMPLES@ / @MAX_BOUNCES@ specialization constants -> the pipeline's - 'Vk.SpecializationInfo'. - -The scene (a "Ray Tracing in One Weekend" arrangement) is generated on the host -from a command-line seed: the spheres go into the @Scene@ buffer and a BVH over -them into a separate device-address-linked node buffer. +interface is derived from the quasiquoted shader at build time by +@vulkan-utils-spirv@ — see "Pathtracer" for the reflected pipeline, +"Pathtracer.Shader" for the shader, "Scene" for the host-built scene and BVH, +and "Render" for the banded dispatch. + +The scene (a "Ray Tracing in One Weekend" arrangement) is generated on the +host from a command-line seed: the spheres go into the @Scene@ buffer and a +BVH over them into a separate device-address-linked node buffer. -} module Main ( main ) where -import qualified Codec.Picture as JP import Control.Monad.IO.Class (liftIO) -import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT) -import Control.Monad.Trans.State (State, evalState, runState, state) -import Data.Bits ((.|.)) -import Data.ByteString (ByteString) -import Data.FileEmbed (embedFile) -import Data.Foldable (for_) -import Data.List (sortOn) -import Data.Maybe (catMaybes) -import Data.Proxy (Proxy (..)) -import qualified Data.Vector as V -import qualified Data.Vector.Storable as VS -import Data.Word (Word32, Word64) -import Foreign.Marshal.Array (peekArray) -import Foreign.Marshal.Utils (with) -import Foreign.Ptr (Ptr, castPtr, plusPtr) -import Foreign.Storable (poke, sizeOf) -import qualified Geomancy -import Geomancy.UVec2 (uvec2) -import Geomancy.Vec3 (Vec3, emap2, vec3, pattern WithVec3) -import qualified Geomancy.Vec3 as V3 -import Geomancy.Vec4 (Vec4, fromVec3, vec4, pattern WithVec4) -import Graphics.Gl.Block (Std140 (..), Std430 (..)) -import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), submitAndWaitFor, withHeadlessVk) -import ImageReadback (captureImageRGBA8, savePng) +import Control.Monad.Trans.Resource (runResourceT) +import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), withHeadlessVk) +import ImageReadback (savePng) import Options.Applicative -import System.Random (StdGen, mkStdGen, uniformR) -import Vulkan.CStruct.Extends (SomeStruct (..)) -import Vulkan.CStruct.Utils (FixedArray, lowerArrayPtr) -import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) -import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) -import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) import qualified Vulkan.Core10 as Vk -import Vulkan.Core12.Promoted_From_VK_KHR_buffer_device_address (BufferDeviceAddressInfo (..), PhysicalDeviceBufferDeviceAddressFeatures (..), getBufferDeviceAddress) -import Vulkan.Requirement (DeviceRequirement) -import Vulkan.Utils.Descriptors (bufferWrite) import Vulkan.Utils.QueueAssignment (QueueFamilyIndex (..)) import Vulkan.Utils.Queues (Queues (..)) -import Vulkan.Utils.Requirements.TH (reqs) -import Vulkan.Utils.Shader (shaderModuleStage) -import qualified Vulkan.Utils.SpirV.Array as Array -import Vulkan.Utils.SpirV.Descriptors (pushConstantRanges, singleDescriptorSetLayoutInfo) -import Vulkan.Utils.SpirV.DeviceAddress (DeviceAddress (..)) -import Vulkan.Utils.SpirV.Reflect (reflectBytes) -import Vulkan.Utils.SpirV.Specialization (allocateSpecializationInfo) -import Vulkan.Utils.SpirV.TH (reflectShaderTypes) -import Vulkan.Zero (zero) import qualified VulkanMemoryAllocator as VMA --- | The compiled shader, embedded for runtime module creation. -compCode :: ByteString -compCode = $(embedFile "pathtrace-reflect/shader.comp.spv") - --- Generate the records from the same SPIR-V the runtime loads: --- * @Camera@ (std140 UBO); --- * @Frame@ (std430 push constant); and --- * @Sphere@ (std430), the element type of the @Scene@ SSBO's @Sphere[]@ — --- generated even though the runtime-array @Scene@/@Image@ blocks themselves --- aren't (they're @[Sphere]@ / raw @vec4@ texels on the host). -reflectShaderTypes "pathtrace-reflect/shader.comp.spv" +import qualified Pathtracer +import Render (Options (..)) +import qualified Render main :: IO () main = do @@ -107,465 +38,17 @@ main = do HeadlessConfig { appName = "Haskell Vulkan pathtrace-reflect example" , instanceReqs = [] - , deviceReqs = bdaDeviceReqs + , deviceReqs = Pathtracer.deviceRequirements , -- the BVH node buffer is reached by device address vmaFlags = VMA.ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT } let QueueFamilyIndex computeQueueFamilyIndex = fst (qCompute queues) - image <- render allocator device computeQueueFamilyIndex opts + image <- Render.render allocator device computeQueueFamilyIndex opts Vk.deviceWaitIdle device liftIO $ savePng opts.output image liftIO $ putStrLn ("Wrote " <> opts.output) -{- | Enable buffer device addresses so the BVH nodes can be linked on the host -and traversed by 'DeviceAddress' on the GPU. --} -bdaDeviceReqs :: [DeviceRequirement] -bdaDeviceReqs = - [reqs| PhysicalDeviceBufferDeviceAddressFeatures.bufferDeviceAddress |] - -render - :: VMA.Allocator - -> Vk.Device - -> Word32 - -> Options - -> ResourceT IO (JP.Image JP.PixelRGBA8) -render allocator dev computeQueueFamilyIndex opts = do - let - width = opts.width - height = opts.height - workgroup = 16 :: Int - - -- The runtime-sized Scene SSBO is a std430 array of the reflected 'Sphere'; - -- the BVH bounds each sphere from its reflected centerRadius (a 'Vec4'). - spheres :: VS.Vector Sphere - spheres = VS.fromList (buildScene opts) - sphereCount = VS.length spheres - - -- One BVH leaf per sphere, flattened to an array with the root at index 0. - bvhFlats = flattenBvh (buildBvh (zip [0 ..] (map sphereAabb (VS.toList spheres)))) - - aspect = fromIntegral width / fromIntegral height - camera = buildCamera aspect opts.fov (vec3 13 2 3) (vec3 0 0 0) (vec3 0 1 0) - - -- Reflect the embedded module once; reuse it for the descriptor set layout, - -- push-constant range and specialization info. - reflected <- reflectBytes compCode - - -- Output storage buffer: one RGBA32F texel per pixel, mapped GPU_TO_CPU. - (_, (outBuffer, outAllocation, outInfo)) <- - VMA.withBuffer - allocator - zero - { Vk.size = fromIntegral $ width * height * 4 * sizeOf (0 :: Float) - , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT - } - zero - { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT - , VMA.usage = VMA.MEMORY_USAGE_GPU_TO_CPU - } - allocate - - -- Input storage buffer: the scene's spheres, written from the host. - (_, (sceneBuffer, _sceneAllocation, sceneInfo)) <- - VMA.withBuffer - allocator - zero - { Vk.size = fromIntegral (sphereCount * Array.std430Stride (Proxy @Sphere)) - , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT - } - zero - { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT - , VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU - } - allocate - liftIO $ Array.pokeStd430 (VMA.mappedData sceneInfo) spheres - - -- Uniform buffer holding the reflected 'Camera', written from the host. - (_, (camBuffer, _camAllocation, camInfo)) <- - VMA.withBuffer - allocator - zero - { Vk.size = fromIntegral (sizeOf camera) - , Vk.usage = Vk.BUFFER_USAGE_UNIFORM_BUFFER_BIT - } - zero - { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT - , VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU - } - allocate - liftIO $ poke (castPtr (VMA.mappedData camInfo)) camera - - -- BVH node buffer, reached purely by device address (not a descriptor). - -- Allocate it, learn its base address, then write each node with its - -- children linked by address — the generated 'BvhNode' record's - -- @DeviceAddress BvhNode@ fields carry the pointers the shader hops. - let - nodeStride = Array.std430Stride (Proxy @BvhNode) - nodeCount = length bvhFlats - (_, (nodeBuffer, _nodeAllocation, nodeInfo)) <- - VMA.withBuffer - allocator - zero - { Vk.size = fromIntegral (nodeCount * nodeStride) - , Vk.usage = - Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT - .|. Vk.BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT - } - zero - { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT - , VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU - } - allocate - bvhBase <- getBufferDeviceAddress dev zero{buffer = nodeBuffer} - liftIO $ - Array.pokeStd430 - (VMA.mappedData nodeInfo) - (VS.fromList (map (toBvhNode bvhBase nodeStride) bvhFlats)) - - -- The reflected 'Frame' push constant carries the root node's address - -- (the flattened BVH puts the root at index 0, i.e. the buffer base). - let frame = - Frame - { root = DeviceAddress bvhBase - , resolution = uvec2 (fromIntegral width) (fromIntegral height) - , seed = opts.seed - , rowOffset = 0 -- set per band below - } - - -- Descriptor set layout generated from the reflected module (UBO + 2 SSBOs). - setLayoutInfo <- either fail pure (singleDescriptorSetLayoutInfo reflected) - (_, descriptorSetLayout) <- Vk.withDescriptorSetLayout dev setLayoutInfo Nothing allocate - - (_, descriptorPool) <- - Vk.withDescriptorPool - dev - zero - { Vk.maxSets = 1 - , Vk.poolSizes = - [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER 1 - , Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER 2 - ] - } - Nothing - allocate - [descriptorSet] <- - Vk.allocateDescriptorSets - dev - zero - { Vk.descriptorPool = descriptorPool - , Vk.setLayouts = [descriptorSetLayout] - } - - Vk.updateDescriptorSets - dev - [ bufferWrite descriptorSet 0 Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER camBuffer - , bufferWrite descriptorSet 1 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER sceneBuffer - , bufferWrite descriptorSet 2 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER outBuffer - ] - [] - - -- Pipeline. The specialization info (SAMPLES, MAX_BOUNCES) is built from the - -- reflected constant ids and lives for this resource scope. - mSpec <- allocateSpecializationInfo reflected (opts.samples, opts.bounces) - (_, shader) <- shaderModuleStage dev Vk.SHADER_STAGE_COMPUTE_BIT mSpec compCode - (_, pipelineLayout) <- - Vk.withPipelineLayout - dev - zero - { PipelineLayoutCreateInfo.setLayouts = [descriptorSetLayout] - , PipelineLayoutCreateInfo.pushConstantRanges = - V.fromList (pushConstantRanges reflected) - } - Nothing - allocate - let - pipelineCreateInfo :: Vk.ComputePipelineCreateInfo '[] - pipelineCreateInfo = - zero - { Vk.layout = pipelineLayout - , Vk.stage = shader - , Vk.basePipelineHandle = zero - } - (_, (_, [computePipeline])) <- - Vk.withComputePipelines dev zero [SomeStruct pipelineCreateInfo] Nothing allocate - - -- Command buffer, reset and re-recorded per band (hence the pool's - -- RESET_COMMAND_BUFFER flag). - (_, commandPool) <- - Vk.withCommandPool - dev - zero - { CommandPoolCreateInfo.queueFamilyIndex = computeQueueFamilyIndex - , CommandPoolCreateInfo.flags = Vk.COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT - } - Nothing - allocate - (_, [cb]) <- - Vk.withCommandBuffers - dev - zero - { Vk.commandPool = commandPool - , Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY - , Vk.commandBufferCount = 1 - } - allocate - computeQueue <- Vk.getDeviceQueue dev computeQueueFamilyIndex 0 - let timeoutNanos = round (opts.timeout * 1e9) - - -- Split the dispatch into horizontal bands so no single compute submission runs - -- long enough to trip the GPU's hang-recovery watchdog — which resets the queue - -- and drops the unfinished (bottom) workgroups, corrupting that region. Each - -- band is an independent short submission; a low-cost render stays one dispatch. - let - samples = max 1 (fromIntegral opts.samples) :: Int - bounces = max 1 (fromIntegral opts.bounces) :: Int - -- A conservative per-submission budget in ray-bounces (well under what tripped - -- the watchdog here, with margin for heavier scenes). - budget = 6 * 1000 * 1000 * 1000 :: Int - bandRows = max 1 (min height (budget `div` (width * samples * bounces))) - for_ ([0, bandRows .. height - 1] :: [Int]) $ \row0 -> do - let - rowsThis = min bandRows (height - row0) - bandFrame = frame{rowOffset = fromIntegral row0} - Vk.resetCommandBuffer cb zero - Vk.useCommandBuffer cb zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} do - Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_COMPUTE computePipeline - Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_COMPUTE pipelineLayout 0 [descriptorSet] [] - -- Push the reflected 'Frame' (std430) with this band's row offset. - liftIO $ with bandFrame $ \pFrame -> - Vk.cmdPushConstants - cb - pipelineLayout - Vk.SHADER_STAGE_COMPUTE_BIT - 0 - (fromIntegral (sizeOf bandFrame)) - (castPtr pFrame) - Vk.cmdDispatch - cb - (ceiling (realToFrac width / realToFrac @_ @Float workgroup)) - (ceiling (realToFrac rowsThis / realToFrac @_ @Float workgroup)) - 1 - submitAndWaitFor timeoutNanos dev computeQueue cb $ - "Timed out waiting for compute band at row " - <> show row0 - <> " after " - <> show opts.timeout - <> "s (raise --timeout)" - - let - pixelAddr :: Int -> Int -> Ptr (FixedArray 4 Float) - pixelAddr x y = - plusPtr - (VMA.mappedData outInfo) - (((y * width) + x) * 4 * sizeOf (0 :: Float)) - captureImageRGBA8 allocator outAllocation width height $ \x y -> do - let ptr = pixelAddr x y - [r, g, b, a] <- fmap (\f -> round (min 1 f * 255)) <$> peekArray 4 (lowerArrayPtr ptr) - pure $ JP.PixelRGBA8 r g b a - --------------------------------------------------------------------------------- --- Scene --------------------------------------------------------------------------------- - -{- | Build the reflected 'Camera' for a simple look-at camera with unit focal -length (no defocus blur). --} -buildCamera :: Float -> Float -> Vec3 -> Vec3 -> Vec3 -> Camera -buildCamera aspect vfovDeg lookFrom lookAt vup = - Camera - { origin = lookFrom - , lowerLeft = lowerLeft - , horizontal = horizontal - , vertical = vertical - , skyTop = vec3 0.5 0.7 1.0 - , skyBottom = vec3 1.0 1.0 1.0 - } - where - theta = vfovDeg * pi / 180 - viewportH = 2 * tan (theta / 2) - viewportW = aspect * viewportH - w = V3.normalize (lookFrom - lookAt) - u = V3.normalize (V3.cross vup w) - v = V3.cross w u - horizontal = u V3.^* viewportW - vertical = v V3.^* viewportH - lowerLeft = lookFrom - (horizontal V3.^* 0.5) - (vertical V3.^* 0.5) - w - -{- | The full scene: ground, three feature spheres, and @--spheres@ random small -ones laid out on a grid (seeded by @--seed@). --} -buildScene :: Options -> [Sphere] -buildScene opts = ground : features ++ small - where - ground = lambertian (vec3 0 (-1000) 0) 1000 (vec3 0.5 0.5 0.5) - features = - [ dielectric (vec3 0 1 0) 1 1.5 - , lambertian (vec3 (-4) 1 0) 1 (vec3 0.4 0.2 0.1) - , metal (vec3 4 1 0) 1 (vec3 0.7 0.6 0.5) 0 - ] - small = evalState (smallSpheres opts.spheres) (mkStdGen (fromIntegral opts.seed)) - -{- | Up to @n@ small spheres scattered over a grid, skipping any too close to the -feature spheres. Materials are randomly diffuse\/metal\/glass. --} -smallSpheres :: Int -> State StdGen [Sphere] -smallSpheres n = catMaybes <$> mapM gen (take n grid) - where - -- A square field of unit cells centred on the origin, sized to hold @n@ - -- spheres so they expand toward the horizon as the count grows. (@n = 484@ - -- reproduces the classic @[-11 .. 10]^2@ "Ray Tracing in One Weekend" grid.) - side = ceiling (sqrt (fromIntegral (max 1 n) :: Double)) :: Int - lo = negate (side `div` 2) - grid = [(a, b) | a <- [lo .. lo + side - 1], b <- [lo .. lo + side - 1]] - gen (a, b) = do - rx <- rF (0, 0.9) - rz <- rF (0, 0.9) - let - center = vec3 (fromIntegral a + rx) 0.2 (fromIntegral b + rz) - d = center - vec3 4 0.2 0 - -- Skip spheres too close to the metal feature sphere (compare squared). - if V3.dot d d <= 0.9 * 0.9 - then pure Nothing - else do - choose <- rF (0, 1) - fmap Just $ - if choose < (0.8 :: Float) - then do - c1 <- rF (0, 1) - c2 <- rF (0, 1) - c3 <- rF (0, 1) - pure $ lambertian center 0.2 (vec3 (c1 * c1) (c2 * c2) (c3 * c3)) - else - if choose < 0.95 - then do - c1 <- rF (0.5, 1) - c2 <- rF (0.5, 1) - c3 <- rF (0.5, 1) - fz <- rF (0, 0.5) - pure $ metal center 0.2 (vec3 c1 c2 c3) fz - else pure $ dielectric center 0.2 1.5 - rF range = state (uniformR range) - --- Sphere constructors (material encoding matches the shader). The center is a --- 'Vec3' and the albedo a 'Vec3'; @centerRadius@ packs the radius into w. - -mkSphere :: Vec3 -> Float -> Vec4 -> Vec4 -> Sphere -mkSphere center r = Sphere (fromVec3 center r) - -lambertian :: Vec3 -> Float -> Vec3 -> Sphere -lambertian c r albedo = mkSphere c r (fromVec3 albedo 1) (vec4 0 0 0 0) - -metal :: Vec3 -> Float -> Vec3 -> Float -> Sphere -metal c r albedo fuzz = mkSphere c r (fromVec3 albedo 1) (vec4 1 fuzz 0 0) - -dielectric :: Vec3 -> Float -> Float -> Sphere -dielectric c r ior = mkSphere c r (vec4 1 1 1 1) (vec4 2 0 ior 0) - --------------------------------------------------------------------------------- --- BVH (built on the host; linked and traversed on the GPU by device address) --------------------------------------------------------------------------------- - --- | An axis-aligned bounding box (min, max corners). -data Aabb = Aabb !Vec3 !Vec3 - --- | Bound a sphere from its reflected @centerRadius@ (xyz centre, w radius). -sphereAabb :: Sphere -> Aabb -sphereAabb sphere = case centerRadius sphere of - WithVec4 cx cy cz r -> - Aabb (vec3 (cx - r) (cy - r) (cz - r)) (vec3 (cx + r) (cy + r) (cz + r)) - -aabbUnion :: Aabb -> Aabb -> Aabb -aabbUnion (Aabb amin amax) (Aabb bmin bmax) = - Aabb (emap2 min amin bmin) (emap2 max amax bmax) - --- | A binary BVH over sphere indices; each leaf bounds one sphere. -data Bvh = BvhLeaf Int Aabb | BvhSplit Aabb Bvh Bvh - --- | Build a BVH by recursively median-splitting the longest axis. -buildBvh :: [(Int, Aabb)] -> Bvh -buildBvh [] = error "buildBvh: empty scene" -buildBvh [(i, bb)] = BvhLeaf i bb -buildBvh xs = BvhSplit bb (buildBvh l) (buildBvh r) - where - bb = foldr1 aabbUnion (map snd xs) - Aabb lo hi = bb - axis = longestAxis (hi - lo) - (l, r) = splitAt (length xs `div` 2) (sortOn (axisKey axis . snd) xs) - -longestAxis :: Vec3 -> Int -longestAxis (WithVec3 x y z) - | x >= y && x >= z = 0 - | y >= z = 1 - | otherwise = 2 - --- | Twice the box centre on an axis (the constant factor is irrelevant to sorting). -axisKey :: Int -> Aabb -> Float -axisKey axis (Aabb (WithVec3 mnx mny mnz) (WithVec3 mxx mxy mxz)) = case axis of - 0 -> mnx + mxx - 1 -> mny + mxy - _ -> mnz + mxz - -{- | A flattened node: bounds, child array indices (-1 if none), and the leaf's -sphere index (-1 for an internal node). --} -data BvhFlat = BvhFlat Vec3 Vec3 Int Int Int - -{- | Flatten the tree to an array in pre-order so the root lands at index 0; -children are referenced by array index, later resolved to device addresses. --} -flattenBvh :: Bvh -> [BvhFlat] -flattenBvh tree = map snd (sortOn fst nodes) - where - (_, (_, nodes)) = runState (go tree) (0 :: Int, []) - fresh = state (\(n, acc) -> (n, (n + 1, acc))) - emit i f = state (\(n, acc) -> ((), (n, (i, f) : acc))) - go (BvhLeaf i (Aabb mn mx)) = do - idx <- fresh - emit idx (BvhFlat mn mx (-1) (-1) i) - pure idx - go (BvhSplit (Aabb mn mx) l r) = do - idx <- fresh - li <- go l - ri <- go r - emit idx (BvhFlat mn mx li ri (-1)) - pure idx - -{- | Realise a flattened node as the reflected 'BvhNode' record, resolving child -indices to device addresses within the node buffer (base + index * stride). --} -toBvhNode :: Word64 -> Int -> BvhFlat -> BvhNode -toBvhNode base stride (BvhFlat mn mx li ri si) = - BvhNode - { boundsMin = fromVec3 mn 0 - , boundsMax = fromVec3 mx 0 - , left = childAddr li - , right = childAddr ri - , sphereIndex = fromIntegral si - } - where - childAddr i - | i < 0 = DeviceAddress 0 - | otherwise = DeviceAddress (base + fromIntegral (i * stride)) - --------------------------------------------------------------------------------- --- Command line --------------------------------------------------------------------------------- - -data Options = Options - { width :: Int - , height :: Int - , samples :: Word32 - , bounces :: Word32 - , spheres :: Int - , seed :: Word32 - , fov :: Float - , timeout :: Double - -- ^ GPU wait budget, seconds - , output :: FilePath - } - optionsInfo :: ParserInfo Options optionsInfo = info diff --git a/examples/pathtrace-reflect/Pathtracer.hs b/examples/pathtrace-reflect/Pathtracer.hs new file mode 100644 index 000000000..b584f2212 --- /dev/null +++ b/examples/pathtrace-reflect/Pathtracer.hs @@ -0,0 +1,119 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +{-| The path-tracing compute pipeline, its /entire/ interface derived from +'Shader.code' — exercising every reflected resource kind at once: + + * the @Camera@ uniform block -> a generated record (std140); + * the @Scene@ input storage buffer -> a descriptor binding holding the leaf + sphere geometry; its element record 'Sphere' is generated from the SSBO's + array element type; + * the @Image@ output storage buffer -> a descriptor binding; + * the @BvhNode@ @buffer_reference@ type -> a generated record whose children + are @DeviceAddress BvhNode@; + * the @Frame@ push-constant block -> a generated record + range, carrying + the root node's device address; and + * the @SAMPLES@ \/ @MAX_BOUNCES@ specialization constants -> the pipeline's + 'Vk.SpecializationInfo'. +-} +module Pathtracer + ( Camera (..) + , Frame (..) + , Sphere (..) + , BvhNode (..) + , deviceRequirements + , Pipeline (..) + , allocatePipeline + ) where + +import Control.Monad.Trans.Resource (ResourceT, allocate) +import qualified Data.Vector as V +import Data.Word (Word32) +import qualified Geomancy +import Graphics.Gl.Block (Std140 (..), Std430 (..)) +import Vulkan.CStruct.Extends (SomeStruct (..)) +import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import Vulkan.Core12.Promoted_From_VK_KHR_buffer_device_address (PhysicalDeviceBufferDeviceAddressFeatures (..)) +import Vulkan.Requirement (DeviceRequirement) +import Vulkan.Utils.Requirements.TH (reqs) +import Vulkan.Utils.Shader (shaderModuleStage) +import Vulkan.Utils.SpirV.Descriptors (pushConstantRanges, singleDescriptorSetLayoutInfo) +import Vulkan.Utils.SpirV.Reflect (reflectBytes) +import Vulkan.Utils.SpirV.Specialization (allocateSpecializationInfo) +import Vulkan.Utils.SpirV.TH (reflectShaderTypesBytes) +import Vulkan.Zero (zero) + +import qualified Pathtracer.Shader as Shader + +-- Generate the records from the same SPIR-V the runtime loads: +-- * @Camera@ (std140 UBO); +-- * @Frame@ (std430 push constant); +-- * @Sphere@ (std430), the element type of the @Scene@ SSBO's @Sphere[]@ — +-- generated even though the runtime-array @Scene@/@Image@ blocks themselves +-- aren't (they're @[Sphere]@ / raw @vec4@ texels on the host); and +-- * @BvhNode@ (std430 buffer_reference) with @DeviceAddress BvhNode@ children. +reflectShaderTypesBytes Shader.code + +{- | Enable buffer device addresses so the BVH nodes can be linked on the host +and traversed by 'DeviceAddress' on the GPU. +-} +deviceRequirements :: [DeviceRequirement] +deviceRequirements = + [reqs| PhysicalDeviceBufferDeviceAddressFeatures.bufferDeviceAddress |] + +data Pipeline = Pipeline + { pipeline :: Vk.Pipeline + , pipelineLayout :: Vk.PipelineLayout + , descriptorSetLayout :: Vk.DescriptorSetLayout + } + +{- | The pipeline, specialized to the given samples-per-pixel and bounce cap. + +Descriptor set layout (UBO + 2 SSBOs), push-constant range and specialization +info all come from reflecting 'Shader.code'. +-} +allocatePipeline :: Vk.Device -> Word32 -> Word32 -> ResourceT IO Pipeline +allocatePipeline dev samples bounces = do + -- Reflect the embedded module once; reuse it for the descriptor set layout, + -- the push-constant range and the specialization info. + reflected <- reflectBytes Shader.code + + setLayoutInfo <- either fail pure (singleDescriptorSetLayoutInfo reflected) + (_, descriptorSetLayout) <- Vk.withDescriptorSetLayout dev setLayoutInfo Nothing allocate + + mSpec <- allocateSpecializationInfo reflected (samples, bounces) + (_, shader) <- shaderModuleStage dev Vk.SHADER_STAGE_COMPUTE_BIT mSpec Shader.code + (_, pipelineLayout) <- + Vk.withPipelineLayout + dev + zero + { PipelineLayoutCreateInfo.setLayouts = [descriptorSetLayout] + , PipelineLayoutCreateInfo.pushConstantRanges = + V.fromList (pushConstantRanges reflected) + } + Nothing + allocate + let + pipelineCreateInfo :: Vk.ComputePipelineCreateInfo '[] + pipelineCreateInfo = + zero + { Vk.layout = pipelineLayout + , Vk.stage = shader + , Vk.basePipelineHandle = zero + } + (_, (_, [computePipeline])) <- + Vk.withComputePipelines dev zero [SomeStruct pipelineCreateInfo] Nothing allocate + pure + Pipeline + { pipeline = computePipeline + , pipelineLayout = pipelineLayout + , descriptorSetLayout = descriptorSetLayout + } diff --git a/examples/pathtrace-reflect/Pathtracer/Shader.hs b/examples/pathtrace-reflect/Pathtracer/Shader.hs new file mode 100644 index 000000000..40365f9b3 --- /dev/null +++ b/examples/pathtrace-reflect/Pathtracer/Shader.hs @@ -0,0 +1,282 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} + +{-| The path-tracing compute shader, compiled to SPIR-V at build time. + +@buffer_reference@ needs SPIR-V 1.3+, so this is compiled with +@--target-env vulkan1.2@ ('compileShaderQ' rather than the plain @comp@ +quasiquoter). The whole interface is reflected in "Pathtracer". +-} +module Pathtracer.Shader + ( code + ) where + +import Data.ByteString (ByteString) +import Vulkan.Utils.ShaderQQ.GLSL.Glslang (compileShaderQ, glsl) + +{- | Scene intersection walks a BVH whose nodes are linked by 64-bit +@buffer_reference@ device addresses (BDA): + + * @Camera@ UBO (std140) -> generated Haskell record (Geomancy.Vec3); + * @Scene@ input SSBO (binding 1, readonly) -> leaf sphere geometry, indexed + by a BVH leaf's @sphereIndex@; + * @Image@ output SSBO (binding 2, writeonly) -> descriptor set layout; + * @BvhNode@ (buffer_reference) -> generated record with DeviceAddress children; + * @Frame@ push constants (std430) carry the root BvhNode device address; + * @SAMPLES@ \/ @MAX_BOUNCES@ spec constants -> the pipeline's SpecializationInfo. +-} +code :: ByteString +code = + $( compileShaderQ + (Just "vulkan1.2") + "comp" + Nothing + [glsl| + #version 460 + #extension GL_EXT_buffer_reference : require + + layout(local_size_x = 16, local_size_y = 16) in; + + // Compile-time tunables, specialized at pipeline creation. + layout(constant_id = 0) const uint SAMPLES = 16u; + layout(constant_id = 1) const uint MAX_BOUNCES = 8u; + + // All members are vec3, so they share std140 base alignment 16 (the gl-block + // layout guardrail wants non-increasing alignments). + layout(set = 0, binding = 0, std140) uniform Camera { + vec3 origin; + vec3 lowerLeft; + vec3 horizontal; + vec3 vertical; + vec3 skyTop; + vec3 skyBottom; + } cam; + + struct Sphere { + vec4 centerRadius; // xyz: center, w: radius + vec4 albedo; // rgb: albedo + vec4 material; // x: type (0 lambertian, 1 metal, 2 dielectric), y: fuzz, z: ior + }; + + layout(set = 0, binding = 1, std430) readonly buffer Scene { + Sphere spheres[]; + }; + + layout(set = 0, binding = 2, std430) writeonly buffer Image { + vec4 pixels[]; + }; + + // A BVH node reached by device address. Internal nodes have child addresses and + // sphereIndex < 0; leaves have sphereIndex >= 0 (into the Scene buffer) and null + // children. Fields are ordered by non-increasing alignment (16,16,8,8,4). + layout(buffer_reference) buffer BvhNode; // fwd decl enables self-reference + layout(buffer_reference, std430) buffer BvhNode { + vec4 boundsMin; // xyz AABB min + vec4 boundsMax; // xyz AABB max + BvhNode left; // child device address (null for a leaf) + BvhNode right; // child device address (null for a leaf) + int sphereIndex; // >= 0: leaf sphere index; < 0: internal node + }; + + layout(push_constant, std430) uniform Frame { + BvhNode root; // entry address into the BVH + uvec2 resolution; // image size in pixels + uint seed; // varies sampling between runs + uint rowOffset; // first image row this dispatch (band) covers + } frame; + + struct Ray { + vec3 o; + vec3 d; + }; + + struct Hit { + float t; + vec3 p; + vec3 n; + bool front; + vec4 albedo; + vec4 material; + }; + + // PCG hash RNG, advanced in place. + uint pcg(inout uint s) { + s = s * 747796405u + 2891336453u; + uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u; + return (w >> 22u) ^ w; + } + + float rnd(inout uint s) { + return float(pcg(s)) * (1.0 / 4294967296.0); + } + + // Uniformly distributed point on the unit sphere. + vec3 rndUnit(inout uint s) { + float z = rnd(s) * 2.0 - 1.0; + float a = rnd(s) * 6.28318530718; + float r = sqrt(max(0.0, 1.0 - z * z)); + return vec3(r * cos(a), r * sin(a), z); + } + + bool hitSphere(Sphere sp, Ray ray, float tmin, float tmax, inout Hit h) { + vec3 center = sp.centerRadius.xyz; + float radius = sp.centerRadius.w; + vec3 oc = ray.o - center; + float a = dot(ray.d, ray.d); + float halfB = dot(oc, ray.d); + float c = dot(oc, oc) - radius * radius; + float disc = halfB * halfB - a * c; + if (disc < 0.0) { + return false; + } + float sq = sqrt(disc); + float t = (-halfB - sq) / a; + if (t < tmin || t > tmax) { + t = (-halfB + sq) / a; + if (t < tmin || t > tmax) { + return false; + } + } + h.t = t; + h.p = ray.o + t * ray.d; + vec3 outwardN = (h.p - center) / radius; + h.front = dot(ray.d, outwardN) < 0.0; + h.n = h.front ? outwardN : -outwardN; + h.albedo = sp.albedo; + h.material = sp.material; + return true; + } + + // Ray vs AABB slab test, gated to [tmin, tmax]. + bool aabbHit(vec3 bmin, vec3 bmax, Ray ray, float tmin, float tmax) { + vec3 inv = 1.0 / ray.d; + vec3 t0 = (bmin - ray.o) * inv; + vec3 t1 = (bmax - ray.o) * inv; + vec3 tsmall = min(t0, t1); + vec3 tbig = max(t0, t1); + float lo = max(max(tsmall.x, tsmall.y), max(tsmall.z, tmin)); + float hi = min(min(tbig.x, tbig.y), min(tbig.z, tmax)); + return hi >= lo; + } + + // Max BVH traversal depth. A balanced median-split BVH of N leaves is ~log2(N) + // deep, so this covers far more spheres than the host will ever generate; the + // bound guards against a pathologically deep tree (a subtree is skipped rather + // than overflowing the stack). + const int BVH_STACK = 64; + + // Walk the BVH from the root device address, hopping pointers to find the + // closest sphere hit. An explicit stack stands in for recursion. + bool worldHit(Ray ray, float tmin, float tmax, out Hit h) { + bool hitAny = false; + float closest = tmax; + + BvhNode stack[BVH_STACK]; + int sp = 0; + stack[sp++] = frame.root; + + while (sp > 0) { + BvhNode node = stack[--sp]; + if (!aabbHit(node.boundsMin.xyz, node.boundsMax.xyz, ray, tmin, closest)) { + continue; + } + if (node.sphereIndex >= 0) { + Hit tmp; + if (hitSphere(spheres[node.sphereIndex], ray, tmin, closest, tmp)) { + hitAny = true; + closest = tmp.t; + h = tmp; + } + } else if (sp + 2 <= BVH_STACK) { + stack[sp++] = node.left; + stack[sp++] = node.right; + } + } + return hitAny; + } + + vec3 skyColor(vec3 dir) { + float t = 0.5 * (normalize(dir).y + 1.0); + return mix(cam.skyBottom, cam.skyTop, t); + } + + float schlick(float cosine, float ref) { + float r0 = (1.0 - ref) / (1.0 + ref); + r0 = r0 * r0; + return r0 + (1.0 - r0) * pow(1.0 - cosine, 5.0); + } + + vec3 rayColor(Ray ray, inout uint s) { + vec3 attenuation = vec3(1.0); + for (uint bounce = 0u; bounce < MAX_BOUNCES; ++bounce) { + Hit h; + if (!worldHit(ray, 0.001, 1e30, h)) { + return attenuation * skyColor(ray.d); + } + + int mat = int(h.material.x + 0.5); + if (mat == 0) { + // Lambertian: scatter about the normal. + vec3 dir = h.n + rndUnit(s); + if (dot(dir, dir) < 1e-8) { + dir = h.n; + } + ray = Ray(h.p, dir); + attenuation *= h.albedo.rgb; + } else if (mat == 1) { + // Metal: reflect, perturbed by fuzz. + vec3 refl = reflect(normalize(ray.d), h.n); + vec3 dir = refl + h.material.y * rndUnit(s); + if (dot(dir, h.n) <= 0.0) { + return vec3(0.0); + } + ray = Ray(h.p, dir); + attenuation *= h.albedo.rgb; + } else { + // Dielectric: refract or reflect (Schlick). + float ior = h.material.z; + float ratio = h.front ? (1.0 / ior) : ior; + vec3 ud = normalize(ray.d); + float cosT = min(dot(-ud, h.n), 1.0); + float sinT = sqrt(max(0.0, 1.0 - cosT * cosT)); + vec3 dir; + if (ratio * sinT > 1.0 || schlick(cosT, ratio) > rnd(s)) { + dir = reflect(ud, h.n); + } else { + dir = refract(ud, h.n, ratio); + } + ray = Ray(h.p, dir); + } + } + return vec3(0.0); // exceeded the bounce budget + } + + void main() { + // gl_GlobalInvocationID.y is relative to this band; rowOffset places it into + // the full image. The host splits the render into horizontal bands so no single + // compute submission runs long enough to trip the GPU's hang-recovery watchdog. + uvec2 gid = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y + frame.rowOffset); + if (gid.x >= frame.resolution.x || gid.y >= frame.resolution.y) { + return; + } + + uint s = (gid.x * 1973u + gid.y * 9277u + frame.seed * 26699u) | 1u; + + vec3 color = vec3(0.0); + for (uint i = 0u; i < SAMPLES; ++i) { + float u = (float(gid.x) + rnd(s)) / float(frame.resolution.x); + float v = (float(gid.y) + rnd(s)) / float(frame.resolution.y); + // Image rows grow downward; flip v so the top row maps to the top of the + // viewport. + vec3 dir = + cam.lowerLeft + u * cam.horizontal + (1.0 - v) * cam.vertical - cam.origin; + color += rayColor(Ray(cam.origin, dir), s); + } + color /= float(SAMPLES); + color = sqrt(color); // gamma 2.0 + + uint idx = gid.y * frame.resolution.x + gid.x; + pixels[idx] = vec4(color, 1.0); + } + |] + ) diff --git a/examples/pathtrace-reflect/Render.hs b/examples/pathtrace-reflect/Render.hs new file mode 100644 index 000000000..669d493f7 --- /dev/null +++ b/examples/pathtrace-reflect/Render.hs @@ -0,0 +1,264 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedRecordDot #-} + +{-| The headless frame: upload the scene, camera and device-address-linked BVH, +then path-trace in horizontal bands and read the image back. +-} +module Render + ( Options (..) + , render + ) where + +import qualified Codec.Picture as JP +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Resource (ResourceT, allocate) +import Data.Bits ((.|.)) +import Data.Foldable (for_) +import Data.Proxy (Proxy (..)) +import qualified Data.Vector.Storable as VS +import Data.Word (Word32) +import Foreign.Marshal.Array (peekArray) +import Foreign.Marshal.Utils (with) +import Foreign.Ptr (Ptr, castPtr, plusPtr) +import Foreign.Storable (poke, sizeOf) +import Geomancy.UVec2 (uvec2) +import Geomancy.Vec3 (vec3) +import HeadlessBoot (submitAndWaitFor) +import ImageReadback (captureImageRGBA8) +import Vulkan.CStruct.Utils (FixedArray, lowerArrayPtr) +import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) +import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import Vulkan.Core12.Promoted_From_VK_KHR_buffer_device_address (BufferDeviceAddressInfo (..), getBufferDeviceAddress) +import Vulkan.Utils.Descriptors (bufferWrite) +import Vulkan.Zero (zero) +import qualified VulkanMemoryAllocator as VMA + +import qualified Vulkan.Utils.SpirV.Array as Array +import Vulkan.Utils.SpirV.DeviceAddress (DeviceAddress (..)) + +import Pathtracer (BvhNode, Frame (..), Sphere) +import qualified Pathtracer +import qualified Scene + +data Options = Options + { width :: Int + , height :: Int + , samples :: Word32 + , bounces :: Word32 + , spheres :: Int + , seed :: Word32 + , fov :: Float + , timeout :: Double + -- ^ GPU wait budget, seconds + , output :: FilePath + } + +render + :: VMA.Allocator + -> Vk.Device + -> Word32 + -> Options + -> ResourceT IO (JP.Image JP.PixelRGBA8) +render allocator dev computeQueueFamilyIndex opts = do + let + width = opts.width + height = opts.height + workgroup = 16 :: Int + + -- The runtime-sized Scene SSBO is a std430 array of the reflected 'Sphere'; + -- the BVH bounds each sphere from its reflected centerRadius (a 'Vec4'). + spheres :: VS.Vector Sphere + spheres = VS.fromList (Scene.buildScene opts.spheres opts.seed) + sphereCount = VS.length spheres + + -- One BVH leaf per sphere, flattened to an array with the root at index 0. + bvhFlats = Scene.flattenBvh (Scene.buildBvh (zip [0 ..] (map Scene.sphereAabb (VS.toList spheres)))) + + aspect = fromIntegral width / fromIntegral height + camera = Scene.buildCamera aspect opts.fov (vec3 13 2 3) (vec3 0 0 0) (vec3 0 1 0) + + -- Output storage buffer: one RGBA32F texel per pixel, mapped GPU_TO_CPU. + (_, (outBuffer, outAllocation, outInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral $ width * height * 4 * sizeOf (0 :: Float) + , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_GPU_TO_CPU + } + allocate + + -- Input storage buffer: the scene's spheres, written from the host. + (_, (sceneBuffer, _sceneAllocation, sceneInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (sphereCount * Array.std430Stride (Proxy @Sphere)) + , Vk.usage = Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + } + allocate + liftIO $ Array.pokeStd430 (VMA.mappedData sceneInfo) spheres + + -- Uniform buffer holding the reflected 'Camera', written from the host. + (_, (camBuffer, _camAllocation, camInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (sizeOf camera) + , Vk.usage = Vk.BUFFER_USAGE_UNIFORM_BUFFER_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + } + allocate + liftIO $ poke (castPtr (VMA.mappedData camInfo)) camera + + -- BVH node buffer, reached purely by device address (not a descriptor). + -- Allocate it, learn its base address, then write each node with its + -- children linked by address — the generated 'BvhNode' record's + -- @DeviceAddress BvhNode@ fields carry the pointers the shader hops. + let + nodeStride = Array.std430Stride (Proxy @BvhNode) + nodeCount = length bvhFlats + (_, (nodeBuffer, _nodeAllocation, nodeInfo)) <- + VMA.withBuffer + allocator + zero + { Vk.size = fromIntegral (nodeCount * nodeStride) + , Vk.usage = + Vk.BUFFER_USAGE_STORAGE_BUFFER_BIT + .|. Vk.BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT + } + zero + { VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + } + allocate + bvhBase <- getBufferDeviceAddress dev zero{buffer = nodeBuffer} + liftIO $ + Array.pokeStd430 + (VMA.mappedData nodeInfo) + (VS.fromList (map (Scene.toBvhNode bvhBase nodeStride) bvhFlats)) + + -- The reflected 'Frame' push constant carries the root node's address + -- (the flattened BVH puts the root at index 0, i.e. the buffer base). + let frame = + Frame + { root = DeviceAddress bvhBase + , resolution = uvec2 (fromIntegral width) (fromIntegral height) + , seed = opts.seed + , rowOffset = 0 -- set per band below + } + + pathtracer <- Pathtracer.allocatePipeline dev opts.samples opts.bounces + + (_, descriptorPool) <- + Vk.withDescriptorPool + dev + zero + { Vk.maxSets = 1 + , Vk.poolSizes = + [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER 1 + , Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER 2 + ] + } + Nothing + allocate + [descriptorSet] <- + Vk.allocateDescriptorSets + dev + zero + { Vk.descriptorPool = descriptorPool + , Vk.setLayouts = [pathtracer.descriptorSetLayout] + } + + Vk.updateDescriptorSets + dev + [ bufferWrite descriptorSet 0 Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER camBuffer + , bufferWrite descriptorSet 1 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER sceneBuffer + , bufferWrite descriptorSet 2 Vk.DESCRIPTOR_TYPE_STORAGE_BUFFER outBuffer + ] + [] + + -- Command buffer, reset and re-recorded per band (hence the pool's + -- RESET_COMMAND_BUFFER flag). + (_, commandPool) <- + Vk.withCommandPool + dev + zero + { CommandPoolCreateInfo.queueFamilyIndex = computeQueueFamilyIndex + , CommandPoolCreateInfo.flags = Vk.COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT + } + Nothing + allocate + (_, [cb]) <- + Vk.withCommandBuffers + dev + zero + { Vk.commandPool = commandPool + , Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY + , Vk.commandBufferCount = 1 + } + allocate + computeQueue <- Vk.getDeviceQueue dev computeQueueFamilyIndex 0 + let timeoutNanos = round (opts.timeout * 1e9) + + -- Split the dispatch into horizontal bands so no single compute submission runs + -- long enough to trip the GPU's hang-recovery watchdog — which resets the queue + -- and drops the unfinished (bottom) workgroups, corrupting that region. Each + -- band is an independent short submission; a low-cost render stays one dispatch. + let + samples = max 1 (fromIntegral opts.samples) :: Int + bounces = max 1 (fromIntegral opts.bounces) :: Int + -- A conservative per-submission budget in ray-bounces (well under what tripped + -- the watchdog here, with margin for heavier scenes). + budget = 6 * 1000 * 1000 * 1000 :: Int + bandRows = max 1 (min height (budget `div` (width * samples * bounces))) + for_ ([0, bandRows .. height - 1] :: [Int]) $ \row0 -> do + let + rowsThis = min bandRows (height - row0) + bandFrame = frame{rowOffset = fromIntegral row0} + Vk.resetCommandBuffer cb zero + Vk.useCommandBuffer cb zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_COMPUTE pathtracer.pipeline + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_COMPUTE pathtracer.pipelineLayout 0 [descriptorSet] [] + -- Push the reflected 'Frame' (std430) with this band's row offset. + liftIO $ with bandFrame $ \pFrame -> + Vk.cmdPushConstants + cb + pathtracer.pipelineLayout + Vk.SHADER_STAGE_COMPUTE_BIT + 0 + (fromIntegral (sizeOf bandFrame)) + (castPtr pFrame) + Vk.cmdDispatch + cb + (ceiling (realToFrac width / realToFrac @_ @Float workgroup)) + (ceiling (realToFrac rowsThis / realToFrac @_ @Float workgroup)) + 1 + submitAndWaitFor timeoutNanos dev computeQueue cb $ + "Timed out waiting for compute band at row " + <> show row0 + <> " after " + <> show opts.timeout + <> "s (raise --timeout)" + + let + pixelAddr :: Int -> Int -> Ptr (FixedArray 4 Float) + pixelAddr x y = + plusPtr + (VMA.mappedData outInfo) + (((y * width) + x) * 4 * sizeOf (0 :: Float)) + captureImageRGBA8 allocator outAllocation width height $ \x y -> do + let ptr = pixelAddr x y + [r, g, b, a] <- fmap (\f -> round (min 1 f * 255)) <$> peekArray 4 (lowerArrayPtr ptr) + pure $ JP.PixelRGBA8 r g b a diff --git a/examples/pathtrace-reflect/Scene.hs b/examples/pathtrace-reflect/Scene.hs new file mode 100644 index 000000000..c36c6503c --- /dev/null +++ b/examples/pathtrace-reflect/Scene.hs @@ -0,0 +1,208 @@ +{-# LANGUAGE OverloadedRecordDot #-} + +{-| The procedural sphere scene (a "Ray Tracing in One Weekend" arrangement) +and the BVH built over it on the host — linked and traversed on the GPU by +device address. +-} +module Scene + ( buildCamera + , buildScene + , sphereAabb + , buildBvh + , flattenBvh + , toBvhNode + , BvhFlat + ) where + +import Control.Monad.Trans.State (State, evalState, runState, state) +import Data.List (sortOn) +import Data.Maybe (catMaybes) +import Data.Word (Word32, Word64) +import Geomancy.Vec3 (Vec3, emap2, vec3, pattern WithVec3) +import qualified Geomancy.Vec3 as V3 +import Geomancy.Vec4 (Vec4, fromVec3, vec4, pattern WithVec4) +import System.Random (StdGen, mkStdGen, uniformR) + +import Vulkan.Utils.SpirV.DeviceAddress (DeviceAddress (..)) + +import Pathtracer (BvhNode (..), Camera (..), Sphere (..)) + +{- | Build the reflected 'Camera' for a simple look-at camera with unit focal +length (no defocus blur). +-} +buildCamera :: Float -> Float -> Vec3 -> Vec3 -> Vec3 -> Camera +buildCamera aspect vfovDeg lookFrom lookAt vup = + Camera + { origin = lookFrom + , lowerLeft = lowerLeft + , horizontal = horizontal + , vertical = vertical + , skyTop = vec3 0.5 0.7 1.0 + , skyBottom = vec3 1.0 1.0 1.0 + } + where + theta = vfovDeg * pi / 180 + viewportH = 2 * tan (theta / 2) + viewportW = aspect * viewportH + w = V3.normalize (lookFrom - lookAt) + u = V3.normalize (V3.cross vup w) + v = V3.cross w u + horizontal = u V3.^* viewportW + vertical = v V3.^* viewportH + lowerLeft = lookFrom - (horizontal V3.^* 0.5) - (vertical V3.^* 0.5) - w + +{- | The full scene: ground, three feature spheres, and @count@ random small +ones laid out on a grid (from @seed@). +-} +buildScene :: Int -> Word32 -> [Sphere] +buildScene count seed = ground : features ++ small + where + ground = lambertian (vec3 0 (-1000) 0) 1000 (vec3 0.5 0.5 0.5) + features = + [ dielectric (vec3 0 1 0) 1 1.5 + , lambertian (vec3 (-4) 1 0) 1 (vec3 0.4 0.2 0.1) + , metal (vec3 4 1 0) 1 (vec3 0.7 0.6 0.5) 0 + ] + small = evalState (smallSpheres count) (mkStdGen (fromIntegral seed)) + +{- | Up to @n@ small spheres scattered over a grid, skipping any too close to the +feature spheres. Materials are randomly diffuse\/metal\/glass. +-} +smallSpheres :: Int -> State StdGen [Sphere] +smallSpheres n = catMaybes <$> mapM gen (take n grid) + where + -- A square field of unit cells centred on the origin, sized to hold @n@ + -- spheres so they expand toward the horizon as the count grows. (@n = 484@ + -- reproduces the classic @[-11 .. 10]^2@ "Ray Tracing in One Weekend" grid.) + side = ceiling (sqrt (fromIntegral (max 1 n) :: Double)) :: Int + lo = negate (side `div` 2) + grid = [(a, b) | a <- [lo .. lo + side - 1], b <- [lo .. lo + side - 1]] + gen (a, b) = do + rx <- rF (0, 0.9) + rz <- rF (0, 0.9) + let + center = vec3 (fromIntegral a + rx) 0.2 (fromIntegral b + rz) + d = center - vec3 4 0.2 0 + -- Skip spheres too close to the metal feature sphere (compare squared). + if V3.dot d d <= 0.9 * 0.9 + then pure Nothing + else do + choose <- rF (0, 1) + fmap Just $ + if choose < (0.8 :: Float) + then do + c1 <- rF (0, 1) + c2 <- rF (0, 1) + c3 <- rF (0, 1) + pure $ lambertian center 0.2 (vec3 (c1 * c1) (c2 * c2) (c3 * c3)) + else + if choose < 0.95 + then do + c1 <- rF (0.5, 1) + c2 <- rF (0.5, 1) + c3 <- rF (0.5, 1) + fz <- rF (0, 0.5) + pure $ metal center 0.2 (vec3 c1 c2 c3) fz + else pure $ dielectric center 0.2 1.5 + rF range = state (uniformR range) + +-- Sphere constructors (material encoding matches the shader). The center is a +-- 'Vec3' and the albedo a 'Vec3'; @centerRadius@ packs the radius into w. + +mkSphere :: Vec3 -> Float -> Vec4 -> Vec4 -> Sphere +mkSphere center r = Sphere (fromVec3 center r) + +lambertian :: Vec3 -> Float -> Vec3 -> Sphere +lambertian c r albedo = mkSphere c r (fromVec3 albedo 1) (vec4 0 0 0 0) + +metal :: Vec3 -> Float -> Vec3 -> Float -> Sphere +metal c r albedo fuzz = mkSphere c r (fromVec3 albedo 1) (vec4 1 fuzz 0 0) + +dielectric :: Vec3 -> Float -> Float -> Sphere +dielectric c r ior = mkSphere c r (vec4 1 1 1 1) (vec4 2 0 ior 0) + +-------------------------------------------------------------------------------- +-- BVH (built on the host; linked and traversed on the GPU by device address) +-------------------------------------------------------------------------------- + +-- | An axis-aligned bounding box (min, max corners). +data Aabb = Aabb !Vec3 !Vec3 + +-- | Bound a sphere from its reflected @centerRadius@ (xyz centre, w radius). +sphereAabb :: Sphere -> Aabb +sphereAabb sphere = case sphere.centerRadius of + WithVec4 cx cy cz r -> + Aabb (vec3 (cx - r) (cy - r) (cz - r)) (vec3 (cx + r) (cy + r) (cz + r)) + +aabbUnion :: Aabb -> Aabb -> Aabb +aabbUnion (Aabb amin amax) (Aabb bmin bmax) = + Aabb (emap2 min amin bmin) (emap2 max amax bmax) + +-- | A binary BVH over sphere indices; each leaf bounds one sphere. +data Bvh = BvhLeaf Int Aabb | BvhSplit Aabb Bvh Bvh + +-- | Build a BVH by recursively median-splitting the longest axis. +buildBvh :: [(Int, Aabb)] -> Bvh +buildBvh [] = error "buildBvh: empty scene" +buildBvh [(i, bb)] = BvhLeaf i bb +buildBvh xs = BvhSplit bb (buildBvh l) (buildBvh r) + where + bb = foldr1 aabbUnion (map snd xs) + Aabb lo hi = bb + axis = longestAxis (hi - lo) + (l, r) = splitAt (length xs `div` 2) (sortOn (axisKey axis . snd) xs) + +longestAxis :: Vec3 -> Int +longestAxis (WithVec3 x y z) + | x >= y && x >= z = 0 + | y >= z = 1 + | otherwise = 2 + +-- | Twice the box centre on an axis (the constant factor is irrelevant to sorting). +axisKey :: Int -> Aabb -> Float +axisKey axis (Aabb (WithVec3 mnx mny mnz) (WithVec3 mxx mxy mxz)) = case axis of + 0 -> mnx + mxx + 1 -> mny + mxy + _ -> mnz + mxz + +{- | A flattened node: bounds, child array indices (-1 if none), and the leaf's +sphere index (-1 for an internal node). +-} +data BvhFlat = BvhFlat Vec3 Vec3 Int Int Int + +{- | Flatten the tree to an array in pre-order so the root lands at index 0; +children are referenced by array index, later resolved to device addresses. +-} +flattenBvh :: Bvh -> [BvhFlat] +flattenBvh tree = map snd (sortOn fst nodes) + where + (_, (_, nodes)) = runState (go tree) (0 :: Int, []) + fresh = state (\(n, acc) -> (n, (n + 1, acc))) + emit i f = state (\(n, acc) -> ((), (n, (i, f) : acc))) + go (BvhLeaf i (Aabb mn mx)) = do + idx <- fresh + emit idx (BvhFlat mn mx (-1) (-1) i) + pure idx + go (BvhSplit (Aabb mn mx) l r) = do + idx <- fresh + li <- go l + ri <- go r + emit idx (BvhFlat mn mx li ri (-1)) + pure idx + +{- | Realise a flattened node as the reflected 'BvhNode' record, resolving child +indices to device addresses within the node buffer (base + index * stride). +-} +toBvhNode :: Word64 -> Int -> BvhFlat -> BvhNode +toBvhNode base stride (BvhFlat mn mx li ri si) = + BvhNode + { boundsMin = fromVec3 mn 0 + , boundsMax = fromVec3 mx 0 + , left = childAddr li + , right = childAddr ri + , sphereIndex = fromIntegral si + } + where + childAddr i + | i < 0 = DeviceAddress 0 + | otherwise = DeviceAddress (base + fromIntegral (i * stride)) diff --git a/examples/pathtrace-reflect/build-shaders.sh b/examples/pathtrace-reflect/build-shaders.sh deleted file mode 100644 index cbbfe5788..000000000 --- a/examples/pathtrace-reflect/build-shaders.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -# Compile the GLSL shader to SPIR-V with a local glslang. The resulting .spv is -# committed and consumed both at runtime (Data.FileEmbed.embedFile) and at -# compile time (Vulkan.Utils.SpirV reflection). -set -eu -cd "$(dirname "$0")" -# buffer_reference (BDA) needs SPIR-V 1.3+, i.e. a Vulkan 1.2 target. -glslangValidator -V --target-env vulkan1.2 shader.comp -o shader.comp.spv -echo "wrote shader.comp.spv" diff --git a/examples/pathtrace-reflect/shader.comp b/examples/pathtrace-reflect/shader.comp deleted file mode 100644 index a60adb90a..000000000 --- a/examples/pathtrace-reflect/shader.comp +++ /dev/null @@ -1,261 +0,0 @@ -#version 460 -#extension GL_EXT_buffer_reference : require - -// A single-frame procedural compute path tracer whose *entire* shader interface -// is reflected at build time by vulkan-utils-spirv. Scene intersection walks a -// BVH whose nodes are linked by 64-bit buffer_reference device addresses (BDA): -// -// * Camera UBO (std140) -> generated Haskell record (Geomancy.Vec3); -// * Scene input SSBO (binding 1, readonly) -> leaf sphere geometry, indexed -// by a BVH leaf's `sphereIndex`. Its element `Sphere` is hand-written; -// * Image output SSBO (binding 2, writeonly) -> descriptor set layout; -// * BvhNode (buffer_reference) -> generated record with DeviceAddress children; -// * Frame push constants (std430) carry the root BvhNode device address; -// * SAMPLES / MAX_BOUNCES spec constants -> the pipeline's SpecializationInfo. -// -// NOTE: buffer_reference needs SPIR-V 1.3+, so build-shaders.sh compiles this -// with --target-env vulkan1.2. - -layout(local_size_x = 16, local_size_y = 16) in; - -// Compile-time tunables, specialized at pipeline creation. -layout(constant_id = 0) const uint SAMPLES = 16u; -layout(constant_id = 1) const uint MAX_BOUNCES = 8u; - -// All members are vec3, so they share std140 base alignment 16 (the gl-block -// layout guardrail wants non-increasing alignments). -layout(set = 0, binding = 0, std140) uniform Camera { - vec3 origin; - vec3 lowerLeft; - vec3 horizontal; - vec3 vertical; - vec3 skyTop; - vec3 skyBottom; -} cam; - -struct Sphere { - vec4 centerRadius; // xyz: center, w: radius - vec4 albedo; // rgb: albedo - vec4 material; // x: type (0 lambertian, 1 metal, 2 dielectric), y: fuzz, z: ior -}; - -layout(set = 0, binding = 1, std430) readonly buffer Scene { - Sphere spheres[]; -}; - -layout(set = 0, binding = 2, std430) writeonly buffer Image { - vec4 pixels[]; -}; - -// A BVH node reached by device address. Internal nodes have child addresses and -// sphereIndex < 0; leaves have sphereIndex >= 0 (into the Scene buffer) and null -// children. Fields are ordered by non-increasing alignment (16,16,8,8,4). -layout(buffer_reference) buffer BvhNode; // fwd decl enables self-reference -layout(buffer_reference, std430) buffer BvhNode { - vec4 boundsMin; // xyz AABB min - vec4 boundsMax; // xyz AABB max - BvhNode left; // child device address (null for a leaf) - BvhNode right; // child device address (null for a leaf) - int sphereIndex; // >= 0: leaf sphere index; < 0: internal node -}; - -layout(push_constant, std430) uniform Frame { - BvhNode root; // entry address into the BVH - uvec2 resolution; // image size in pixels - uint seed; // varies sampling between runs - uint rowOffset; // first image row this dispatch (band) covers -} frame; - -struct Ray { - vec3 o; - vec3 d; -}; - -struct Hit { - float t; - vec3 p; - vec3 n; - bool front; - vec4 albedo; - vec4 material; -}; - -// PCG hash RNG, advanced in place. -uint pcg(inout uint s) { - s = s * 747796405u + 2891336453u; - uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u; - return (w >> 22u) ^ w; -} - -float rnd(inout uint s) { - return float(pcg(s)) * (1.0 / 4294967296.0); -} - -// Uniformly distributed point on the unit sphere. -vec3 rndUnit(inout uint s) { - float z = rnd(s) * 2.0 - 1.0; - float a = rnd(s) * 6.28318530718; - float r = sqrt(max(0.0, 1.0 - z * z)); - return vec3(r * cos(a), r * sin(a), z); -} - -bool hitSphere(Sphere sp, Ray ray, float tmin, float tmax, inout Hit h) { - vec3 center = sp.centerRadius.xyz; - float radius = sp.centerRadius.w; - vec3 oc = ray.o - center; - float a = dot(ray.d, ray.d); - float halfB = dot(oc, ray.d); - float c = dot(oc, oc) - radius * radius; - float disc = halfB * halfB - a * c; - if (disc < 0.0) { - return false; - } - float sq = sqrt(disc); - float t = (-halfB - sq) / a; - if (t < tmin || t > tmax) { - t = (-halfB + sq) / a; - if (t < tmin || t > tmax) { - return false; - } - } - h.t = t; - h.p = ray.o + t * ray.d; - vec3 outwardN = (h.p - center) / radius; - h.front = dot(ray.d, outwardN) < 0.0; - h.n = h.front ? outwardN : -outwardN; - h.albedo = sp.albedo; - h.material = sp.material; - return true; -} - -// Ray vs AABB slab test, gated to [tmin, tmax]. -bool aabbHit(vec3 bmin, vec3 bmax, Ray ray, float tmin, float tmax) { - vec3 inv = 1.0 / ray.d; - vec3 t0 = (bmin - ray.o) * inv; - vec3 t1 = (bmax - ray.o) * inv; - vec3 tsmall = min(t0, t1); - vec3 tbig = max(t0, t1); - float lo = max(max(tsmall.x, tsmall.y), max(tsmall.z, tmin)); - float hi = min(min(tbig.x, tbig.y), min(tbig.z, tmax)); - return hi >= lo; -} - -// Max BVH traversal depth. A balanced median-split BVH of N leaves is ~log2(N) -// deep, so this covers far more spheres than the host will ever generate; the -// bound guards against a pathologically deep tree (a subtree is skipped rather -// than overflowing the stack). -const int BVH_STACK = 64; - -// Walk the BVH from the root device address, hopping pointers to find the -// closest sphere hit. An explicit stack stands in for recursion. -bool worldHit(Ray ray, float tmin, float tmax, out Hit h) { - bool hitAny = false; - float closest = tmax; - - BvhNode stack[BVH_STACK]; - int sp = 0; - stack[sp++] = frame.root; - - while (sp > 0) { - BvhNode node = stack[--sp]; - if (!aabbHit(node.boundsMin.xyz, node.boundsMax.xyz, ray, tmin, closest)) { - continue; - } - if (node.sphereIndex >= 0) { - Hit tmp; - if (hitSphere(spheres[node.sphereIndex], ray, tmin, closest, tmp)) { - hitAny = true; - closest = tmp.t; - h = tmp; - } - } else if (sp + 2 <= BVH_STACK) { - stack[sp++] = node.left; - stack[sp++] = node.right; - } - } - return hitAny; -} - -vec3 skyColor(vec3 dir) { - float t = 0.5 * (normalize(dir).y + 1.0); - return mix(cam.skyBottom, cam.skyTop, t); -} - -float schlick(float cosine, float ref) { - float r0 = (1.0 - ref) / (1.0 + ref); - r0 = r0 * r0; - return r0 + (1.0 - r0) * pow(1.0 - cosine, 5.0); -} - -vec3 rayColor(Ray ray, inout uint s) { - vec3 attenuation = vec3(1.0); - for (uint bounce = 0u; bounce < MAX_BOUNCES; ++bounce) { - Hit h; - if (!worldHit(ray, 0.001, 1e30, h)) { - return attenuation * skyColor(ray.d); - } - - int mat = int(h.material.x + 0.5); - if (mat == 0) { - // Lambertian: scatter about the normal. - vec3 dir = h.n + rndUnit(s); - if (dot(dir, dir) < 1e-8) { - dir = h.n; - } - ray = Ray(h.p, dir); - attenuation *= h.albedo.rgb; - } else if (mat == 1) { - // Metal: reflect, perturbed by fuzz. - vec3 refl = reflect(normalize(ray.d), h.n); - vec3 dir = refl + h.material.y * rndUnit(s); - if (dot(dir, h.n) <= 0.0) { - return vec3(0.0); - } - ray = Ray(h.p, dir); - attenuation *= h.albedo.rgb; - } else { - // Dielectric: refract or reflect (Schlick). - float ior = h.material.z; - float ratio = h.front ? (1.0 / ior) : ior; - vec3 ud = normalize(ray.d); - float cosT = min(dot(-ud, h.n), 1.0); - float sinT = sqrt(max(0.0, 1.0 - cosT * cosT)); - vec3 dir; - if (ratio * sinT > 1.0 || schlick(cosT, ratio) > rnd(s)) { - dir = reflect(ud, h.n); - } else { - dir = refract(ud, h.n, ratio); - } - ray = Ray(h.p, dir); - } - } - return vec3(0.0); // exceeded the bounce budget -} - -void main() { - // gl_GlobalInvocationID.y is relative to this band; rowOffset places it into - // the full image. The host splits the render into horizontal bands so no single - // compute submission runs long enough to trip the GPU's hang-recovery watchdog. - uvec2 gid = uvec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y + frame.rowOffset); - if (gid.x >= frame.resolution.x || gid.y >= frame.resolution.y) { - return; - } - - uint s = (gid.x * 1973u + gid.y * 9277u + frame.seed * 26699u) | 1u; - - vec3 color = vec3(0.0); - for (uint i = 0u; i < SAMPLES; ++i) { - float u = (float(gid.x) + rnd(s)) / float(frame.resolution.x); - float v = (float(gid.y) + rnd(s)) / float(frame.resolution.y); - // Image rows grow downward; flip v so the top row maps to the top of the - // viewport. - vec3 dir = - cam.lowerLeft + u * cam.horizontal + (1.0 - v) * cam.vertical - cam.origin; - color += rayColor(Ray(cam.origin, dir), s); - } - color /= float(SAMPLES); - color = sqrt(color); // gamma 2.0 - - uint idx = gid.y * frame.resolution.x + gid.x; - pixels[idx] = vec4(color, 1.0); -} diff --git a/examples/texture-reflect/Cube.hs b/examples/texture-reflect/Cube.hs new file mode 100644 index 000000000..cf18199a5 --- /dev/null +++ b/examples/texture-reflect/Cube.hs @@ -0,0 +1,73 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +{-| The cube pipeline: a spinning cube sampling the offscreen image through a +@sampler2D@ at set 1, binding 0. + +Its @position@ + @uv@ vertex attributes are described from reflection +("Vulkan.Utils.SpirV.VertexInput"); the per-stage signatures prove the +vertex\/fragment composition at compile time ('composes'). The @Globals@ +record its shaders share with the offscreen pass is generated once, in "Tri". +-} +module Cube + ( composes + , Pipeline (..) + , allocatePipeline + ) where + +import Control.Monad.Trans.Resource (ResourceT, allocate) +import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import qualified Vulkan.Utils.DynamicRendering as Dynamic +import Vulkan.Zero (zero) + +import Data.SpirV.Reflect.FFI (loadBytes) +import Vulkan.Utils.SpirV.Stage (CompatibleResources, MatchInterface, reflectStageSigBytes) +import Vulkan.Utils.SpirV.VertexInput (vertexInputState) + +import qualified Cube.Shader as Shader + +reflectStageSigBytes "VertSig" Shader.vertCode +reflectStageSigBytes "FragSig" Shader.fragCode + +-- | This pipeline's vertex↔fragment composition, proved at compile time. +composes :: (MatchInterface VertSig FragSig, CompatibleResources VertSig FragSig) => Bool +composes = True + +data Pipeline = Pipeline + { pipeline :: Vk.Pipeline + , pipelineLayout :: Vk.PipelineLayout + } + +{- | Colour + depth, vertex attributes from reflection. Set 0 is the layout +shared with the offscreen pipeline; set 1 holds this pipeline's sampler. +-} +allocatePipeline + :: Vk.Device + -> Vk.Format + -> Vk.Format + -> Vk.DescriptorSetLayout + -> Vk.DescriptorSetLayout + -> ResourceT IO Pipeline +allocatePipeline dev colorFormat depthFormat set0Layout set1Layout = do + vertModule <- loadBytes Shader.vertCode + (_, pipelineLayout) <- + Vk.withPipelineLayout + dev + zero{PipelineLayoutCreateInfo.setLayouts = [set0Layout, set1Layout]} + Nothing + allocate + (_, pipeline) <- + Dynamic.allocatePipelineFromShaders + dev + zero + { Dynamic.colorFormats = [colorFormat] + , Dynamic.depthFormat = Just depthFormat + , Dynamic.vertexInput = vertexInputState vertModule + , Dynamic.layout = Just pipelineLayout + } + () + [(Vk.SHADER_STAGE_VERTEX_BIT, Shader.vertCode), (Vk.SHADER_STAGE_FRAGMENT_BIT, Shader.fragCode)] + pure Pipeline{pipeline = pipeline, pipelineLayout = pipelineLayout} diff --git a/examples/texture-reflect/Cube/Shader.hs b/examples/texture-reflect/Cube/Shader.hs new file mode 100644 index 000000000..da8e15a51 --- /dev/null +++ b/examples/texture-reflect/Cube/Shader.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE QuasiQuotes #-} + +{-| The cube-pass shaders: a spinning cube sampling the offscreen image, +compiled to SPIR-V at build time and reflected in "Cube". +-} +module Cube.Shader + ( vertCode + , fragCode + ) where + +import Data.ByteString (ByteString) +import Vulkan.Utils.ShaderQQ.GLSL.Glslang (frag, vert) + +{- | Geometry comes from a vertex buffer with per-vertex position + uv +attributes — the binding\/attribute descriptions are built from reflection +("Vulkan.Utils.SpirV.VertexInput"), not hand-written. The cube spins by +@Globals.time@ (the same shared UBO at set 0, binding 0 the offscreen pass +used). A perspective projection is built inline. +-} +vertCode :: ByteString +vertCode = + [vert| + #version 450 + + layout(set = 0, binding = 0, std140) uniform Globals { + float time; + } g; + + layout(location = 0) in vec3 position; + layout(location = 1) in vec2 uv; + + layout(location = 0) out vec2 vUv; + + mat4 rotY(float a) { + float c = cos(a), s = sin(a); + return mat4(c, 0.0, -s, 0.0, 0.0, 1.0, 0.0, 0.0, s, 0.0, c, 0.0, 0.0, 0.0, 0.0, 1.0); + } + + mat4 rotX(float a) { + float c = cos(a), s = sin(a); + return mat4(1.0, 0.0, 0.0, 0.0, 0.0, c, s, 0.0, 0.0, -s, c, 0.0, 0.0, 0.0, 0.0, 1.0); + } + + // Vulkan-clip perspective (z in [0,1], y flipped), column-major. + mat4 perspective(float fovy, float aspect, float near, float far) { + float f = 1.0 / tan(fovy * 0.5); + return mat4( + f / aspect, 0.0, 0.0, 0.0, + 0.0, -f, 0.0, 0.0, + 0.0, 0.0, far / (near - far), -1.0, + 0.0, 0.0, (near * far) / (near - far), 0.0); + } + + void main() { + // Scale the unit cube down so the framed image has padding around it. + vec4 world = rotY(g.time) * rotX(0.5) * vec4(position * 0.62, 1.0); + world.z -= 3.0; + gl_Position = perspective(radians(50.0), 1.0, 0.1, 10.0) * world; + vUv = uv; + } + |] + +{- | Samples the offscreen RGB triangle — produced by the "Tri" pipeline and +barriered into a shader-read layout — through a combined image sampler at +set 1, binding 0. The shared Globals UBO (set 0, binding 0) is read again here +for a time-based tint; it is the SAME descriptor the offscreen pass bound, +never rebound between the two draws. +-} +fragCode :: ByteString +fragCode = + [frag| + #version 450 + + layout(set = 0, binding = 0, std140) uniform Globals { + float time; + } g; + + layout(set = 1, binding = 0) uniform sampler2D offscreen; + + layout(location = 0) in vec2 vUv; + + layout(location = 0) out vec4 outColor; + + void main() { + vec3 c = texture(offscreen, vUv).rgb; + c *= 0.85 + 0.15 * sin(g.time); + outColor = vec4(c, 1.0); + } + |] diff --git a/examples/texture-reflect/Main.hs b/examples/texture-reflect/Main.hs index 54e3cb82c..5e0e4ff70 100644 --- a/examples/texture-reflect/Main.hs +++ b/examples/texture-reflect/Main.hs @@ -1,154 +1,40 @@ {-# LANGUAGE BangPatterns #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE DerivingVia #-} -{-# LANGUAGE OverloadedLists #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeFamilies #-} {-| Headless demonstration of __using a colour attachment as a texture__ -(render-to-texture), driven by reflected SPIR-V. Two pipelines run back to back -in one command buffer: - - * __offscreen pass__: a full-screen triangle with red/green/blue corners is - drawn into an offscreen colour image (@COLOR_ATTACHMENT | SAMPLED@). A barrier - then moves that image from @COLOR_ATTACHMENT_OPTIMAL@ to - @SHADER_READ_ONLY_OPTIMAL@. - - * __cube pass__: a spinning cube — its @position@ + @uv@ vertex attributes - described from reflection ("Vulkan.Utils.SpirV.VertexInput") — samples the - offscreen image through a @sampler2D@ at set 1, binding 0. - -Both pipelines share a @Globals@ UBO at __set 0, binding 0__ (a @time@). The set 0 -descriptor-set layout is a single object reused by both pipeline layouts, so the -layouts are /compatible for set 0/: the UBO is bound __once__ before the offscreen -pass and is never rebound — the cube pass only binds its sampler at set 1. (The -cross-pipeline layout match is not type-enforced here; each pipeline's own -vertex↔fragment composition still is, via 'MatchInterface' / 'CompatibleResources'.) +(render-to-texture), driven by reflected SPIR-V. Two pipelines run back to +back in one command buffer — see "Tri" (the offscreen RGB triangle), "Cube" +(the spinning cube sampling it) and "Render" (the frame and the shared-set-0 +wiring). + +Both pipelines share a @Globals@ UBO at __set 0, binding 0__ (a @time@). The +set 0 descriptor-set layout is a single object reused by both pipeline +layouts, so the layouts are /compatible for set 0/: the UBO is bound __once__ +before the offscreen pass and is never rebound — the cube pass only binds its +sampler at set 1. (The cross-pipeline layout match is not type-enforced here; +each pipeline's own vertex↔fragment composition still is, via +'Vulkan.Utils.SpirV.Stage.MatchInterface' \/ +'Vulkan.Utils.SpirV.Stage.CompatibleResources'.) -} module Main where import qualified Codec.Picture as JP import Control.Monad (unless) import Control.Monad.IO.Class (liftIO) -import Control.Monad.Trans.Resource (ResourceT, allocate, runResourceT) -import Data.Bits ((.|.)) -import Data.ByteString (ByteString) -import Data.FileEmbed (embedFile) +import Control.Monad.Trans.Resource (runResourceT) import Data.List (foldl') -import qualified Data.Vector as V -import Data.Word (Word32) -import Foreign.Marshal.Array (pokeArray) -import Foreign.Ptr (castPtr) -import Foreign.Storable (poke, sizeOf) -import Graphics.Gl.Block (Std140 (..)) -import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), submitAndWait, withHeadlessVk) -import ImageReadback (copyImageToHost, makeReadbackImage, savePng) -import RenderTarget (createColorTarget, createDepthTarget) +import HeadlessBoot (HeadlessConfig (..), HeadlessVk (..), withHeadlessVk) +import ImageReadback (savePng) import System.Exit (exitFailure) -import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) -import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) -import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) -import qualified Vulkan.Core10 as SamplerCreateInfo (SamplerCreateInfo (..)) import qualified Vulkan.Core10 as Vk -import qualified Vulkan.Core13 as Vk -import Vulkan.Utils.Barrier (imageBarrier, transitionColorAttachment, transitionDepthAttachment) -import Vulkan.Utils.Descriptors (bufferWrite, combinedImageSamplerWrite) import qualified Vulkan.Utils.DynamicRendering as Dynamic -import Vulkan.Utils.DynamicState (DynamicState (..), allDynamicStates, applyDynamicStates, dynamicStateFor, fullScissor) import Vulkan.Utils.QueueAssignment (QueueFamilyIndex (..)) import Vulkan.Utils.Queues (Queues (..)) import Vulkan.Zero (zero) -import qualified VulkanMemoryAllocator as AllocationCreateInfo (AllocationCreateInfo (..)) -import qualified VulkanMemoryAllocator as VMA - -import Data.SpirV.Reflect.FFI (loadBytes) -import Vulkan.Utils.SpirV.Descriptors (mergedDescriptorSetLayoutInfos) -import Vulkan.Utils.SpirV.Stage (CompatibleResources, MatchInterface, reflectStageSig) -import Vulkan.Utils.SpirV.TH (reflectShaderTypes) -import Vulkan.Utils.SpirV.VertexInput (vertexInputState) - --- Compile-time reflection: the shared @Globals@ UBO record (from any shader that --- declares it) and a stage signature per shader. -reflectShaderTypes "texture-reflect/tri.frag.spv" -reflectStageSig "TriVert" "texture-reflect/tri.vert.spv" -reflectStageSig "TriFrag" "texture-reflect/tri.frag.spv" -reflectStageSig "CubeVert" "texture-reflect/cube.vert.spv" -reflectStageSig "CubeFrag" "texture-reflect/cube.frag.spv" - --- Each pipeline's own vertex↔fragment composition, proved at compile time. -triComposes :: (MatchInterface TriVert TriFrag, CompatibleResources TriVert TriFrag) => Bool -triComposes = True - -cubeComposes :: (MatchInterface CubeVert CubeFrag, CompatibleResources CubeVert CubeFrag) => Bool -cubeComposes = True - -triVertSpv, triFragSpv, cubeVertSpv, cubeFragSpv :: ByteString -triVertSpv = $(embedFile "texture-reflect/tri.vert.spv") -triFragSpv = $(embedFile "texture-reflect/tri.frag.spv") -cubeVertSpv = $(embedFile "texture-reflect/cube.vert.spv") -cubeFragSpv = $(embedFile "texture-reflect/cube.frag.spv") - -width, height :: Word32 -width = 256 -height = 256 - -colorFormat :: Vk.Format -colorFormat = Vk.FORMAT_R8G8B8A8_UNORM - -depthFormat :: Vk.Format -depthFormat = Vk.FORMAT_D32_SFLOAT - -floatSize :: Int -floatSize = sizeOf (0 :: Float) --- | The shared uniform: a single @time@ both pipelines read. -globalsValue :: Globals -globalsValue = Globals{time = 0.7} - -{- | A unit cube centred at the origin: 6 faces × 2 triangles, each vertex five -floats @px py pz u v@ (tightly packed, matching the reflected vertex input). --} -cubeVertices :: [Float] -cubeVertices = concatMap faceVerts faces - where - h = 0.5 - add :: (Float, Float, Float) -> (Float, Float, Float) -> (Float, Float, Float) - add (a, b, c) (d, e, f) = (a + d, b + e, c + f) - -- Each face: an origin corner and two edge vectors (length 1). - faces :: [((Float, Float, Float), (Float, Float, Float), (Float, Float, Float))] - faces = - [ ((-h, -h, h), (1, 0, 0), (0, 1, 0)) -- +Z - , ((h, -h, -h), (-1, 0, 0), (0, 1, 0)) -- -Z - , ((h, -h, h), (0, 0, -1), (0, 1, 0)) -- +X - , ((-h, -h, -h), (0, 0, 1), (0, 1, 0)) -- -X - , ((-h, h, h), (1, 0, 0), (0, 0, -1)) -- +Y - , ((-h, -h, -h), (1, 0, 0), (0, 0, 1)) -- -Y - ] - faceVerts :: ((Float, Float, Float), (Float, Float, Float), (Float, Float, Float)) -> [Float] - faceVerts (o, du, dv) = - let - c00 = o - c10 = add o du - c11 = add (add o du) dv - c01 = add o dv - -- V is flipped (top edge -> v = 0): Vulkan's texture origin is top-left, - -- so v = 0 is the top row of the sampled offscreen image. - quad :: [((Float, Float, Float), (Float, Float))] - quad = [(c00, (0, 1)), (c10, (1, 1)), (c11, (1, 0)), (c00, (0, 1)), (c11, (1, 0)), (c01, (0, 0))] - in - concatMap (\((x, y, z), (u, v)) -> [x, y, z, u, v]) quad - -{- | Five floats per vertex (@px py pz u v@), so the draw count is derived from -the geometry rather than asserted separately. --} -cubeVertexCount :: Word32 -cubeVertexCount = fromIntegral (length cubeVertices `div` 5) +import qualified Cube +import Render (height, width) +import qualified Render +import qualified Tri main :: IO () main = runResourceT $ do @@ -161,11 +47,11 @@ main = runResourceT $ do , vmaFlags = zero } liftIO $ do - putStrLn $ "offscreen pipeline composes (compile-time): " <> show triComposes - putStrLn $ "cube pipeline composes (compile-time): " <> show cubeComposes + putStrLn $ "offscreen pipeline composes (compile-time): " <> show Tri.composes + putStrLn $ "cube pipeline composes (compile-time): " <> show Cube.composes let QueueFamilyIndex graphicsQueueFamilyIndex = fst (qGraphics queues) - colorImage <- render allocator device graphicsQueueFamilyIndex + colorImage <- Render.render allocator device graphicsQueueFamilyIndex Vk.deviceWaitIdle device savePng "texture-reflect.png" colorImage @@ -207,236 +93,3 @@ main = runResourceT $ do mapM_ (\(label, ok) -> putStrLn $ "[" <> (if ok then "PASS" else "FAIL") <> "] " <> label) checks unless (all snd checks) exitFailure putStrLn "All render-to-texture checks passed." - -render - :: VMA.Allocator - -> Vk.Device - -> Word32 - -> ResourceT IO (JP.Image JP.PixelRGBA8) -render allocator dev graphicsQueueFamilyIndex = do - let extent = Vk.Extent2D width height - - -- Reflected modules drive the merged set layouts and the cube's vertex input. - triVertModule <- loadBytes triVertSpv - triFragModule <- loadBytes triFragSpv - cubeVertModule <- loadBytes cubeVertSpv - cubeFragModule <- loadBytes cubeFragSpv - - -- Offscreen target (sampled in the cube pass), final colour target (read back), - -- depth for the cube, and the host readback image. - (offscreenImage, offscreenView) <- createSampledColorTarget allocator dev colorFormat extent - (_, (sceneImage, sceneView)) <- createColorTarget allocator dev colorFormat extent - (_, (depthImage, depthView)) <- createDepthTarget allocator dev depthFormat extent - (cpuImage, readback) <- makeReadbackImage allocator dev colorFormat extent - - -- Shared Globals UBO (host-visible, mapped). - (_, (uboBuffer, _, uboInfo)) <- - VMA.withBuffer - allocator - zero{Vk.size = fromIntegral (sizeOf globalsValue), Vk.usage = Vk.BUFFER_USAGE_UNIFORM_BUFFER_BIT} - mappedAlloc - allocate - liftIO $ poke (castPtr (VMA.mappedData uboInfo)) globalsValue - - -- Cube vertex buffer (host-visible, mapped) — plain floats, interpreted by the - -- reflected attribute descriptions. - (_, (cubeBuffer, _, cubeBufInfo)) <- - VMA.withBuffer - allocator - zero{Vk.size = fromIntegral (length cubeVertices * floatSize), Vk.usage = Vk.BUFFER_USAGE_VERTEX_BUFFER_BIT} - mappedAlloc - allocate - liftIO $ pokeArray (castPtr (VMA.mappedData cubeBufInfo)) cubeVertices - - (_, sampler) <- Vk.withSampler dev samplerInfo Nothing allocate - - -- ONE set 0 layout (the Globals UBO, visible to all stages that read it) and one - -- set 1 layout (the sampler), merged across all four shaders. Reusing the set 0 - -- layout object in both pipeline layouts makes them compatible for set 0. - setInfos <- orDie (mergedDescriptorSetLayoutInfos [triVertModule, triFragModule, cubeVertModule, cubeFragModule]) - setLayouts <- - mapM - (\(setNo, info) -> do (_, l) <- Vk.withDescriptorSetLayout dev info Nothing allocate; pure (setNo, l)) - setInfos - let - layoutFor n = maybe (error ("missing descriptor set " <> show n)) id (lookup n setLayouts) - set0Layout = layoutFor 0 - set1Layout = layoutFor 1 - (_, triPipelineLayout) <- - Vk.withPipelineLayout dev zero{PipelineLayoutCreateInfo.setLayouts = [set0Layout]} Nothing allocate - (_, cubePipelineLayout) <- - Vk.withPipelineLayout dev zero{PipelineLayoutCreateInfo.setLayouts = [set0Layout, set1Layout]} Nothing allocate - - -- Descriptor sets: set 0 = Globals UBO, set 1 = the offscreen image + sampler. - (_, descriptorPool) <- - Vk.withDescriptorPool - dev - zero - { Vk.maxSets = 2 - , Vk.poolSizes = - [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER 1 - , Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER 1 - ] - } - Nothing - allocate - descriptorSets <- - Vk.allocateDescriptorSets - dev - zero{Vk.descriptorPool = descriptorPool, Vk.setLayouts = [set0Layout, set1Layout]} - let - globalsSet = descriptorSets V.! 0 - samplerSet = descriptorSets V.! 1 - Vk.updateDescriptorSets - dev - [ bufferWrite globalsSet 0 Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER uboBuffer - , combinedImageSamplerWrite samplerSet 0 sampler offscreenView Vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL - ] - [] - - -- Offscreen triangle pipeline (colour only, no vertex input) and cube pipeline - -- (colour + depth, vertex attributes from reflection). - let cubeVertexInput = vertexInputState cubeVertModule - (_, triPipeline) <- - Dynamic.allocatePipelineFromShaders - dev - zero - { Dynamic.colorFormats = [colorFormat] - , Dynamic.layout = Just triPipelineLayout - } - () - [(Vk.SHADER_STAGE_VERTEX_BIT, triVertSpv), (Vk.SHADER_STAGE_FRAGMENT_BIT, triFragSpv)] - (_, cubePipeline) <- - Dynamic.allocatePipelineFromShaders - dev - zero - { Dynamic.colorFormats = [colorFormat] - , Dynamic.depthFormat = Just depthFormat - , Dynamic.vertexInput = cubeVertexInput - , Dynamic.layout = Just cubePipelineLayout - } - () - [(Vk.SHADER_STAGE_VERTEX_BIT, cubeVertSpv), (Vk.SHADER_STAGE_FRAGMENT_BIT, cubeFragSpv)] - - (_, commandPool) <- - Vk.withCommandPool dev zero{CommandPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex} Nothing allocate - graphicsQueue <- Vk.getDeviceQueue dev graphicsQueueFamilyIndex 0 - cb <- oneCommandBuffer dev commandPool - - let oneShot = zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} - Vk.useCommandBuffer cb oneShot $ do - transitionColorAttachment cb offscreenImage - transitionColorAttachment cb sceneImage - transitionDepthAttachment cb depthImage - - -- Pass 1: draw the RGB triangle into the offscreen image. The shared Globals - -- descriptor is bound here, ONCE, under the (compatible) triangle layout. - Vk.cmdUseRendering cb (Dynamic.renderingInfo (fullScissor extent) [(offscreenView, Vk.Float32 0 0 0 1)] Nothing) $ do - Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_GRAPHICS triPipeline - applyDynamicStates allDynamicStates cb (dynamicStateFor extent) - Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_GRAPHICS triPipelineLayout 0 [globalsSet] [] - Vk.cmdDraw cb 3 1 0 0 - - -- Make the offscreen colour image readable by the cube's fragment shader. - colorToSampled cb offscreenImage - - -- Pass 2: draw the cube sampling the offscreen image. Set 0 (Globals) is still - -- bound — only the sampler at set 1 is bound now. - Vk.cmdUseRendering - cb - (Dynamic.renderingInfo (fullScissor extent) [(sceneView, Vk.Float32 0.30 0.32 0.38 1)] (Just (depthView, 1))) - $ do - Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_GRAPHICS cubePipeline - applyDynamicStates - allDynamicStates - cb - (dynamicStateFor extent){depthTest = True, depthWrite = True, depthCompareOp = Vk.COMPARE_OP_LESS} - Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_GRAPHICS cubePipelineLayout 1 [samplerSet] [] - Vk.cmdBindVertexBuffers cb 0 [cubeBuffer] [0] - Vk.cmdDraw cb cubeVertexCount 1 0 0 - - copyImageToHost cb extent sceneImage cpuImage - submitAndWait dev graphicsQueue cb "Timed out in the render-to-texture passes" - readback - where - samplerInfo = - zero - { SamplerCreateInfo.magFilter = Vk.FILTER_LINEAR - , SamplerCreateInfo.minFilter = Vk.FILTER_LINEAR - , SamplerCreateInfo.addressModeU = Vk.SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE - , SamplerCreateInfo.addressModeV = Vk.SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE - } - --- | Print a merged-layout conflict and exit non-zero. -orDie :: Either String a -> ResourceT IO a -orDie = either (\e -> liftIO (putStrLn ("merged layout error: " <> e) >> exitFailure)) pure - -mappedAlloc :: VMA.AllocationCreateInfo -mappedAlloc = - zero - { AllocationCreateInfo.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT - , AllocationCreateInfo.usage = VMA.MEMORY_USAGE_CPU_TO_GPU - , AllocationCreateInfo.requiredFlags = Vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT - } - -oneCommandBuffer :: Vk.Device -> Vk.CommandPool -> ResourceT IO Vk.CommandBuffer -oneCommandBuffer dev pool = do - (_, cbs) <- - Vk.withCommandBuffers - dev - zero{Vk.commandPool = pool, Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY, Vk.commandBufferCount = 1} - allocate - pure (V.head cbs) - -{- | A GPU-only colour target that can be a colour attachment AND be sampled (the -offscreen render-to-texture target). Like 'RenderTarget.createColorTarget' but -with @SAMPLED@ instead of @TRANSFER_SRC@. --} -createSampledColorTarget - :: VMA.Allocator -> Vk.Device -> Vk.Format -> Vk.Extent2D -> ResourceT IO (Vk.Image, Vk.ImageView) -createSampledColorTarget allocator dev format (Vk.Extent2D w h) = do - (_, (image, _, _)) <- VMA.withImage allocator imageCreateInfo gpuAlloc allocate - (_, view) <- Vk.withImageView dev (viewCreateInfo image) Nothing allocate - pure (image, view) - where - gpuAlloc = zero{AllocationCreateInfo.usage = VMA.MEMORY_USAGE_GPU_ONLY} - imageCreateInfo = - zero - { Vk.imageType = Vk.IMAGE_TYPE_2D - , Vk.format = format - , Vk.extent = Vk.Extent3D w h 1 - , Vk.mipLevels = 1 - , Vk.arrayLayers = 1 - , Vk.samples = Vk.SAMPLE_COUNT_1_BIT - , Vk.tiling = Vk.IMAGE_TILING_OPTIMAL - , Vk.usage = Vk.IMAGE_USAGE_COLOR_ATTACHMENT_BIT .|. Vk.IMAGE_USAGE_SAMPLED_BIT - , Vk.initialLayout = Vk.IMAGE_LAYOUT_UNDEFINED - } - viewCreateInfo image = - zero - { Vk.image = image - , Vk.viewType = Vk.IMAGE_VIEW_TYPE_2D - , Vk.format = format - , Vk.subresourceRange = Vk.ImageSubresourceRange Vk.IMAGE_ASPECT_COLOR_BIT 0 1 0 1 - } - -{- | Barrier a colour image from a colour attachment (after the offscreen pass) to -a shader-readable texture for the next pass's fragment sampling. --} -colorToSampled :: Vk.CommandBuffer -> Vk.Image -> ResourceT IO () -colorToSampled cb img = - Vk.cmdPipelineBarrier - cb - Vk.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT - Vk.PIPELINE_STAGE_FRAGMENT_SHADER_BIT - zero - [] - [] - [ imageBarrier - Vk.IMAGE_ASPECT_COLOR_BIT - Vk.ACCESS_COLOR_ATTACHMENT_WRITE_BIT - Vk.ACCESS_SHADER_READ_BIT - Vk.IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL - Vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL - img - ] diff --git a/examples/texture-reflect/Render.hs b/examples/texture-reflect/Render.hs new file mode 100644 index 000000000..3e341b339 --- /dev/null +++ b/examples/texture-reflect/Render.hs @@ -0,0 +1,310 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedRecordDot #-} + +{-| The headless frame: the offscreen RGB triangle, a barrier to a sampleable +layout, then the cube pass sampling it — both in one command buffer. The +shared set 0 (Globals) is bound once, before the offscreen pass, and never +rebound. +-} +module Render + ( render + , width + , height + ) where + +import qualified Codec.Picture as JP +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Resource (ResourceT, allocate) +import Data.Bits ((.|.)) +import qualified Data.Vector as V +import Data.Word (Word32) +import Foreign.Marshal.Array (pokeArray) +import Foreign.Ptr (castPtr) +import Foreign.Storable (poke, sizeOf) +import HeadlessBoot (submitAndWait) +import ImageReadback (copyImageToHost, makeReadbackImage) +import RenderTarget (createColorTarget, createDepthTarget) +import System.Exit (exitFailure) +import qualified Vulkan.Core10 as CommandBufferBeginInfo (CommandBufferBeginInfo (..)) +import qualified Vulkan.Core10 as CommandPoolCreateInfo (CommandPoolCreateInfo (..)) +import qualified Vulkan.Core10 as SamplerCreateInfo (SamplerCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import qualified Vulkan.Core13 as Vk +import Vulkan.Utils.Barrier (imageBarrier, transitionColorAttachment, transitionDepthAttachment) +import Vulkan.Utils.Descriptors (bufferWrite, combinedImageSamplerWrite) +import qualified Vulkan.Utils.DynamicRendering as Dynamic +import Vulkan.Utils.DynamicState (DynamicState (..), allDynamicStates, applyDynamicStates, dynamicStateFor, fullScissor) +import Vulkan.Zero (zero) +import qualified VulkanMemoryAllocator as AllocationCreateInfo (AllocationCreateInfo (..)) +import qualified VulkanMemoryAllocator as VMA + +import Data.SpirV.Reflect.FFI (loadBytes) +import Vulkan.Utils.SpirV.Descriptors (mergedDescriptorSetLayoutInfos) + +import qualified Cube +import qualified Cube.Shader +import Tri (Globals (..)) +import qualified Tri +import qualified Tri.Shader + +width, height :: Word32 +width = 256 +height = 256 + +colorFormat :: Vk.Format +colorFormat = Vk.FORMAT_R8G8B8A8_UNORM + +depthFormat :: Vk.Format +depthFormat = Vk.FORMAT_D32_SFLOAT + +floatSize :: Int +floatSize = sizeOf (0 :: Float) + +-- | The shared uniform: a single @time@ both pipelines read. +globalsValue :: Globals +globalsValue = Globals{time = 0.7} + +{- | A unit cube centred at the origin: 6 faces × 2 triangles, each vertex five +floats @px py pz u v@ (tightly packed, matching the reflected vertex input). +-} +cubeVertices :: [Float] +cubeVertices = concatMap faceVerts faces + where + h = 0.5 + add :: (Float, Float, Float) -> (Float, Float, Float) -> (Float, Float, Float) + add (a, b, c) (d, e, f) = (a + d, b + e, c + f) + -- Each face: an origin corner and two edge vectors (length 1). + faces :: [((Float, Float, Float), (Float, Float, Float), (Float, Float, Float))] + faces = + [ ((-h, -h, h), (1, 0, 0), (0, 1, 0)) -- +Z + , ((h, -h, -h), (-1, 0, 0), (0, 1, 0)) -- -Z + , ((h, -h, h), (0, 0, -1), (0, 1, 0)) -- +X + , ((-h, -h, -h), (0, 0, 1), (0, 1, 0)) -- -X + , ((-h, h, h), (1, 0, 0), (0, 0, -1)) -- +Y + , ((-h, -h, -h), (1, 0, 0), (0, 0, 1)) -- -Y + ] + faceVerts :: ((Float, Float, Float), (Float, Float, Float), (Float, Float, Float)) -> [Float] + faceVerts (o, du, dv) = + let + c00 = o + c10 = add o du + c11 = add (add o du) dv + c01 = add o dv + -- V is flipped (top edge -> v = 0): Vulkan's texture origin is top-left, + -- so v = 0 is the top row of the sampled offscreen image. + quad :: [((Float, Float, Float), (Float, Float))] + quad = [(c00, (0, 1)), (c10, (1, 1)), (c11, (1, 0)), (c00, (0, 1)), (c11, (1, 0)), (c01, (0, 0))] + in + concatMap (\((x, y, z), (u, v)) -> [x, y, z, u, v]) quad + +{- | Five floats per vertex (@px py pz u v@), so the draw count is derived from +the geometry rather than asserted separately. +-} +cubeVertexCount :: Word32 +cubeVertexCount = fromIntegral (length cubeVertices `div` 5) + +render + :: VMA.Allocator + -> Vk.Device + -> Word32 + -> ResourceT IO (JP.Image JP.PixelRGBA8) +render allocator dev graphicsQueueFamilyIndex = do + let extent = Vk.Extent2D width height + + -- Offscreen target (sampled in the cube pass), final colour target (read back), + -- depth for the cube, and the host readback image. + (offscreenImage, offscreenView) <- createSampledColorTarget allocator dev colorFormat extent + (_, (sceneImage, sceneView)) <- createColorTarget allocator dev colorFormat extent + (_, (depthImage, depthView)) <- createDepthTarget allocator dev depthFormat extent + (cpuImage, readback) <- makeReadbackImage allocator dev colorFormat extent + + -- Shared Globals UBO (host-visible, mapped). + (_, (uboBuffer, _, uboInfo)) <- + VMA.withBuffer + allocator + zero{Vk.size = fromIntegral (sizeOf globalsValue), Vk.usage = Vk.BUFFER_USAGE_UNIFORM_BUFFER_BIT} + mappedAlloc + allocate + liftIO $ poke (castPtr (VMA.mappedData uboInfo)) globalsValue + + -- Cube vertex buffer (host-visible, mapped) — plain floats, interpreted by the + -- reflected attribute descriptions. + (_, (cubeBuffer, _, cubeBufInfo)) <- + VMA.withBuffer + allocator + zero{Vk.size = fromIntegral (length cubeVertices * floatSize), Vk.usage = Vk.BUFFER_USAGE_VERTEX_BUFFER_BIT} + mappedAlloc + allocate + liftIO $ pokeArray (castPtr (VMA.mappedData cubeBufInfo)) cubeVertices + + (_, sampler) <- Vk.withSampler dev samplerInfo Nothing allocate + + -- ONE set 0 layout (the Globals UBO, visible to all stages that read it) and one + -- set 1 layout (the sampler), merged across all four shaders. Reusing the set 0 + -- layout object in both pipeline layouts makes them compatible for set 0. + modules <- + traverse + loadBytes + [Tri.Shader.vertCode, Tri.Shader.fragCode, Cube.Shader.vertCode, Cube.Shader.fragCode] + setInfos <- orDie (mergedDescriptorSetLayoutInfos modules) + setLayouts <- + mapM + (\(setNo, info) -> do (_, l) <- Vk.withDescriptorSetLayout dev info Nothing allocate; pure (setNo, l)) + setInfos + let + layoutFor n = maybe (error ("missing descriptor set " <> show n)) id (lookup n setLayouts) + set0Layout = layoutFor 0 + set1Layout = layoutFor 1 + + tri <- Tri.allocatePipeline dev colorFormat set0Layout + cube <- Cube.allocatePipeline dev colorFormat depthFormat set0Layout set1Layout + + -- Descriptor sets: set 0 = Globals UBO, set 1 = the offscreen image + sampler. + (_, descriptorPool) <- + Vk.withDescriptorPool + dev + zero + { Vk.maxSets = 2 + , Vk.poolSizes = + [ Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER 1 + , Vk.DescriptorPoolSize Vk.DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER 1 + ] + } + Nothing + allocate + descriptorSets <- + Vk.allocateDescriptorSets + dev + zero{Vk.descriptorPool = descriptorPool, Vk.setLayouts = [set0Layout, set1Layout]} + let + globalsSet = descriptorSets V.! 0 + samplerSet = descriptorSets V.! 1 + Vk.updateDescriptorSets + dev + [ bufferWrite globalsSet 0 Vk.DESCRIPTOR_TYPE_UNIFORM_BUFFER uboBuffer + , combinedImageSamplerWrite samplerSet 0 sampler offscreenView Vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + ] + [] + + (_, commandPool) <- + Vk.withCommandPool dev zero{CommandPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex} Nothing allocate + graphicsQueue <- Vk.getDeviceQueue dev graphicsQueueFamilyIndex 0 + cb <- oneCommandBuffer dev commandPool + + let oneShot = zero{CommandBufferBeginInfo.flags = Vk.COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT} + Vk.useCommandBuffer cb oneShot $ do + transitionColorAttachment cb offscreenImage + transitionColorAttachment cb sceneImage + transitionDepthAttachment cb depthImage + + -- Pass 1: draw the RGB triangle into the offscreen image. The shared Globals + -- descriptor is bound here, ONCE, under the (compatible) triangle layout. + Vk.cmdUseRendering cb (Dynamic.renderingInfo (fullScissor extent) [(offscreenView, Vk.Float32 0 0 0 1)] Nothing) $ do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_GRAPHICS tri.pipeline + applyDynamicStates allDynamicStates cb (dynamicStateFor extent) + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_GRAPHICS tri.pipelineLayout 0 [globalsSet] [] + Vk.cmdDraw cb 3 1 0 0 + + -- Make the offscreen colour image readable by the cube's fragment shader. + colorToSampled cb offscreenImage + + -- Pass 2: draw the cube sampling the offscreen image. Set 0 (Globals) is still + -- bound — only the sampler at set 1 is bound now. + Vk.cmdUseRendering + cb + (Dynamic.renderingInfo (fullScissor extent) [(sceneView, Vk.Float32 0.30 0.32 0.38 1)] (Just (depthView, 1))) + $ do + Vk.cmdBindPipeline cb Vk.PIPELINE_BIND_POINT_GRAPHICS cube.pipeline + applyDynamicStates + allDynamicStates + cb + (dynamicStateFor extent){depthTest = True, depthWrite = True, depthCompareOp = Vk.COMPARE_OP_LESS} + Vk.cmdBindDescriptorSets cb Vk.PIPELINE_BIND_POINT_GRAPHICS cube.pipelineLayout 1 [samplerSet] [] + Vk.cmdBindVertexBuffers cb 0 [cubeBuffer] [0] + Vk.cmdDraw cb cubeVertexCount 1 0 0 + + copyImageToHost cb extent sceneImage cpuImage + submitAndWait dev graphicsQueue cb "Timed out in the render-to-texture passes" + readback + where + samplerInfo = + zero + { SamplerCreateInfo.magFilter = Vk.FILTER_LINEAR + , SamplerCreateInfo.minFilter = Vk.FILTER_LINEAR + , SamplerCreateInfo.addressModeU = Vk.SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE + , SamplerCreateInfo.addressModeV = Vk.SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE + } + +-- | Print a merged-layout conflict and exit non-zero. +orDie :: Either String a -> ResourceT IO a +orDie = either (\e -> liftIO (putStrLn ("merged layout error: " <> e) >> exitFailure)) pure + +mappedAlloc :: VMA.AllocationCreateInfo +mappedAlloc = + zero + { AllocationCreateInfo.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT + , AllocationCreateInfo.usage = VMA.MEMORY_USAGE_CPU_TO_GPU + , AllocationCreateInfo.requiredFlags = Vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT + } + +oneCommandBuffer :: Vk.Device -> Vk.CommandPool -> ResourceT IO Vk.CommandBuffer +oneCommandBuffer dev pool = do + (_, cbs) <- + Vk.withCommandBuffers + dev + zero{Vk.commandPool = pool, Vk.level = Vk.COMMAND_BUFFER_LEVEL_PRIMARY, Vk.commandBufferCount = 1} + allocate + pure (V.head cbs) + +{- | A GPU-only colour target that can be a colour attachment AND be sampled (the +offscreen render-to-texture target). Like 'RenderTarget.createColorTarget' but +with @SAMPLED@ instead of @TRANSFER_SRC@. +-} +createSampledColorTarget + :: VMA.Allocator -> Vk.Device -> Vk.Format -> Vk.Extent2D -> ResourceT IO (Vk.Image, Vk.ImageView) +createSampledColorTarget allocator dev format (Vk.Extent2D w h) = do + (_, (image, _, _)) <- VMA.withImage allocator imageCreateInfo gpuAlloc allocate + (_, view) <- Vk.withImageView dev (viewCreateInfo image) Nothing allocate + pure (image, view) + where + gpuAlloc = zero{AllocationCreateInfo.usage = VMA.MEMORY_USAGE_GPU_ONLY} + imageCreateInfo = + zero + { Vk.imageType = Vk.IMAGE_TYPE_2D + , Vk.format = format + , Vk.extent = Vk.Extent3D w h 1 + , Vk.mipLevels = 1 + , Vk.arrayLayers = 1 + , Vk.samples = Vk.SAMPLE_COUNT_1_BIT + , Vk.tiling = Vk.IMAGE_TILING_OPTIMAL + , Vk.usage = Vk.IMAGE_USAGE_COLOR_ATTACHMENT_BIT .|. Vk.IMAGE_USAGE_SAMPLED_BIT + , Vk.initialLayout = Vk.IMAGE_LAYOUT_UNDEFINED + } + viewCreateInfo image = + zero + { Vk.image = image + , Vk.viewType = Vk.IMAGE_VIEW_TYPE_2D + , Vk.format = format + , Vk.subresourceRange = Vk.ImageSubresourceRange Vk.IMAGE_ASPECT_COLOR_BIT 0 1 0 1 + } + +{- | Barrier a colour image from a colour attachment (after the offscreen pass) to +a shader-readable texture for the next pass's fragment sampling. +-} +colorToSampled :: Vk.CommandBuffer -> Vk.Image -> ResourceT IO () +colorToSampled cb img = + Vk.cmdPipelineBarrier + cb + Vk.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT + Vk.PIPELINE_STAGE_FRAGMENT_SHADER_BIT + zero + [] + [] + [ imageBarrier + Vk.IMAGE_ASPECT_COLOR_BIT + Vk.ACCESS_COLOR_ATTACHMENT_WRITE_BIT + Vk.ACCESS_SHADER_READ_BIT + Vk.IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL + Vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + img + ] diff --git a/examples/texture-reflect/Tri.hs b/examples/texture-reflect/Tri.hs new file mode 100644 index 000000000..67576c727 --- /dev/null +++ b/examples/texture-reflect/Tri.hs @@ -0,0 +1,68 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} + +{-| The offscreen pipeline: the RGB triangle drawn into the image the cube +samples. + +Reflection of 'Shader.fragCode' generates the shared @Globals@ UBO record +(the cube pass references the same struct, so it is generated once, here); the +per-stage signatures prove the vertex\/fragment composition at compile time +('composes'). +-} +module Tri + ( Globals (..) + , composes + , Pipeline (..) + , allocatePipeline + ) where + +import Control.Monad.Trans.Resource (ResourceT, allocate) +import Graphics.Gl.Block (Std140 (..)) +import qualified Vulkan.Core10 as PipelineLayoutCreateInfo (PipelineLayoutCreateInfo (..)) +import qualified Vulkan.Core10 as Vk +import qualified Vulkan.Utils.DynamicRendering as Dynamic +import Vulkan.Zero (zero) + +import Vulkan.Utils.SpirV.Stage (CompatibleResources, MatchInterface, reflectStageSigBytes) +import Vulkan.Utils.SpirV.TH (reflectShaderTypesBytes) + +import qualified Tri.Shader as Shader + +-- Compile-time reflection: the shared @Globals@ UBO record and a stage +-- signature per shader. +reflectShaderTypesBytes Shader.fragCode +reflectStageSigBytes "VertSig" Shader.vertCode +reflectStageSigBytes "FragSig" Shader.fragCode + +-- | This pipeline's vertex↔fragment composition, proved at compile time. +composes :: (MatchInterface VertSig FragSig, CompatibleResources VertSig FragSig) => Bool +composes = True + +data Pipeline = Pipeline + { pipeline :: Vk.Pipeline + , pipelineLayout :: Vk.PipelineLayout + } + +{- | Colour only, no vertex input. The set 0 layout is shared with the cube +pipeline, making the two pipeline layouts compatible for set 0. +-} +allocatePipeline :: Vk.Device -> Vk.Format -> Vk.DescriptorSetLayout -> ResourceT IO Pipeline +allocatePipeline dev colorFormat set0Layout = do + (_, pipelineLayout) <- + Vk.withPipelineLayout dev zero{PipelineLayoutCreateInfo.setLayouts = [set0Layout]} Nothing allocate + (_, pipeline) <- + Dynamic.allocatePipelineFromShaders + dev + zero + { Dynamic.colorFormats = [colorFormat] + , Dynamic.layout = Just pipelineLayout + } + () + [(Vk.SHADER_STAGE_VERTEX_BIT, Shader.vertCode), (Vk.SHADER_STAGE_FRAGMENT_BIT, Shader.fragCode)] + pure Pipeline{pipeline = pipeline, pipelineLayout = pipelineLayout} diff --git a/examples/texture-reflect/Tri/Shader.hs b/examples/texture-reflect/Tri/Shader.hs new file mode 100644 index 000000000..b335c303c --- /dev/null +++ b/examples/texture-reflect/Tri/Shader.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE QuasiQuotes #-} + +{-| The offscreen-pass shaders: a screen-filling RGB triangle, compiled to +SPIR-V at build time and reflected in "Tri". +-} +module Tri.Shader + ( vertCode + , fragCode + ) where + +import Data.ByteString (ByteString) +import Vulkan.Utils.ShaderQQ.GLSL.Glslang (frag, vert) + +{- | A screen-filling triangle with saturated red \/ green \/ blue corners, so +the offscreen colour image holds the full RGB range (each primary saturated +near a vertex). That image is then sampled as a texture by the cube pass. The +geometry is generated from @gl_VertexIndex@ (no vertex buffer). The shared +Globals UBO (set 0, binding 0) is read by the fragment stage; this pipeline +and the cube pipeline keep set 0 compatible so the descriptor need not be +rebound between them. +-} +vertCode :: ByteString +vertCode = + [vert| + #version 450 + + layout(location = 0) out vec3 vColor; + + void main() { + // Vulkan clip space is +Y down: y = +1 is the bottom, y = -1 the top. + vec2 base[3] = vec2[](vec2(-1.0, 1.0), vec2(1.0, 1.0), vec2(0.0, -1.0)); + vec3 cols[3] = vec3[](vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0)); + gl_Position = vec4(base[gl_VertexIndex], 0.0, 1.0); + vColor = cols[gl_VertexIndex]; + } + |] + +{- | Writes the interpolated RGB gradient, pulsed by @Globals.time@ — the first +read of the shared UBO (set 0, binding 0). +-} +fragCode :: ByteString +fragCode = + [frag| + #version 450 + + layout(set = 0, binding = 0, std140) uniform Globals { + float time; + } g; + + layout(location = 0) in vec3 vColor; + + layout(location = 0) out vec4 outColor; + + void main() { + float pulse = 0.7 + 0.3 * abs(sin(g.time)); + outColor = vec4(vColor * pulse, 1.0); + } + |] diff --git a/examples/texture-reflect/build-shaders.sh b/examples/texture-reflect/build-shaders.sh deleted file mode 100755 index 953f2ebfc..000000000 --- a/examples/texture-reflect/build-shaders.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -# Compile this example's shaders to committed SPIR-V (input to reflection + runtime). -set -eu -cd "$(dirname "$0")" -for src in *.vert *.frag; do - [ -e "$src" ] || continue - glslangValidator -V "$src" -o "$src.spv" - echo "wrote $src.spv" -done diff --git a/examples/texture-reflect/cube.frag b/examples/texture-reflect/cube.frag deleted file mode 100644 index a63480a46..000000000 --- a/examples/texture-reflect/cube.frag +++ /dev/null @@ -1,23 +0,0 @@ -#version 450 - -// Cube pass, fragment stage. Samples the offscreen RGB triangle — produced by -// the first pipeline and barriered into a shader-read layout — through a combined -// image sampler at set 1, binding 0. The shared Globals UBO (set 0, binding 0) -// is read again here for a time-based tint; it is the SAME descriptor the -// offscreen pass bound, never rebound between the two draws. - -layout(set = 0, binding = 0, std140) uniform Globals { - float time; -} g; - -layout(set = 1, binding = 0) uniform sampler2D offscreen; - -layout(location = 0) in vec2 vUv; - -layout(location = 0) out vec4 outColor; - -void main() { - vec3 c = texture(offscreen, vUv).rgb; - c *= 0.85 + 0.15 * sin(g.time); - outColor = vec4(c, 1.0); -} diff --git a/examples/texture-reflect/cube.vert b/examples/texture-reflect/cube.vert deleted file mode 100644 index c8c1fc29d..000000000 --- a/examples/texture-reflect/cube.vert +++ /dev/null @@ -1,44 +0,0 @@ -#version 450 - -// Cube pass, vertex stage. Geometry comes from a vertex buffer with per-vertex -// position + uv attributes — the binding/attribute descriptions are built from -// reflection (Vulkan.Utils.SpirV.VertexInput), not hand-written. The cube spins -// by Globals.time (the same shared UBO at set 0, binding 0 the offscreen pass -// used). A perspective projection is built inline. - -layout(set = 0, binding = 0, std140) uniform Globals { - float time; -} g; - -layout(location = 0) in vec3 position; -layout(location = 1) in vec2 uv; - -layout(location = 0) out vec2 vUv; - -mat4 rotY(float a) { - float c = cos(a), s = sin(a); - return mat4(c, 0.0, -s, 0.0, 0.0, 1.0, 0.0, 0.0, s, 0.0, c, 0.0, 0.0, 0.0, 0.0, 1.0); -} - -mat4 rotX(float a) { - float c = cos(a), s = sin(a); - return mat4(1.0, 0.0, 0.0, 0.0, 0.0, c, s, 0.0, 0.0, -s, c, 0.0, 0.0, 0.0, 0.0, 1.0); -} - -// Vulkan-clip perspective (z in [0,1], y flipped), column-major. -mat4 perspective(float fovy, float aspect, float near, float far) { - float f = 1.0 / tan(fovy * 0.5); - return mat4( - f / aspect, 0.0, 0.0, 0.0, - 0.0, -f, 0.0, 0.0, - 0.0, 0.0, far / (near - far), -1.0, - 0.0, 0.0, (near * far) / (near - far), 0.0); -} - -void main() { - // Scale the unit cube down so the framed image has padding around it. - vec4 world = rotY(g.time) * rotX(0.5) * vec4(position * 0.62, 1.0); - world.z -= 3.0; - gl_Position = perspective(radians(50.0), 1.0, 0.1, 10.0) * world; - vUv = uv; -} diff --git a/examples/texture-reflect/tri.frag b/examples/texture-reflect/tri.frag deleted file mode 100644 index b6c39f0aa..000000000 --- a/examples/texture-reflect/tri.frag +++ /dev/null @@ -1,17 +0,0 @@ -#version 450 - -// Offscreen pass, fragment stage. Writes the interpolated RGB gradient, pulsed -// by Globals.time — the first read of the shared UBO (set 0, binding 0). - -layout(set = 0, binding = 0, std140) uniform Globals { - float time; -} g; - -layout(location = 0) in vec3 vColor; - -layout(location = 0) out vec4 outColor; - -void main() { - float pulse = 0.7 + 0.3 * abs(sin(g.time)); - outColor = vec4(vColor * pulse, 1.0); -} diff --git a/examples/texture-reflect/tri.vert b/examples/texture-reflect/tri.vert deleted file mode 100644 index 27fc00079..000000000 --- a/examples/texture-reflect/tri.vert +++ /dev/null @@ -1,19 +0,0 @@ -#version 450 - -// Offscreen pass, vertex stage. A screen-filling triangle with saturated -// red / green / blue corners, so the offscreen colour image holds the full RGB -// range (each primary saturated near a vertex). That image is then sampled as a -// texture by the cube pass. The geometry is generated from gl_VertexIndex (no -// vertex buffer). The shared Globals UBO (set 0, binding 0) is read by the -// fragment stage; this pipeline and the cube pipeline keep set 0 compatible so -// the descriptor need not be rebound between them. - -layout(location = 0) out vec3 vColor; - -void main() { - // Vulkan clip space is +Y down: y = +1 is the bottom, y = -1 the top. - vec2 base[3] = vec2[](vec2(-1.0, 1.0), vec2(1.0, 1.0), vec2(0.0, -1.0)); - vec3 cols[3] = vec3[](vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0)); - gl_Position = vec4(base[gl_VertexIndex], 0.0, 1.0); - vColor = cols[gl_VertexIndex]; -} diff --git a/examples/vulkan-examples.cabal b/examples/vulkan-examples.cabal index 94f45234e..ce261f9ae 100644 --- a/examples/vulkan-examples.cabal +++ b/examples/vulkan-examples.cabal @@ -180,6 +180,9 @@ executable compute executable compute-reflect main-is: Main.hs other-modules: + Julia + Julia.Shader + Render Paths_vulkan_examples autogen-modules: Paths_vulkan_examples @@ -226,7 +229,6 @@ executable compute-reflect , VulkanMemoryAllocator , base <5 , bytestring - , file-embed , geomancy , gl-block , resourcet @@ -366,6 +368,9 @@ executable info executable mesh-reflect main-is: Main.hs other-modules: + Mesh + Mesh.Shader + Render Paths_vulkan_examples autogen-modules: Paths_vulkan_examples @@ -412,7 +417,6 @@ executable mesh-reflect , VulkanMemoryAllocator , base <5 , bytestring - , file-embed , geomancy , gl-block , resourcet @@ -430,6 +434,10 @@ executable mesh-reflect executable pathtrace-reflect main-is: Main.hs other-modules: + Pathtracer + Pathtracer.Shader + Render + Scene Paths_vulkan_examples autogen-modules: Paths_vulkan_examples @@ -476,7 +484,6 @@ executable pathtrace-reflect , VulkanMemoryAllocator , base <5 , bytestring - , file-embed , geomancy , gl-block , optparse-applicative @@ -639,6 +646,11 @@ executable resize executable texture-reflect main-is: Main.hs other-modules: + Cube + Cube.Shader + Render + Tri + Tri.Shader Paths_vulkan_examples autogen-modules: Paths_vulkan_examples @@ -685,7 +697,6 @@ executable texture-reflect , VulkanMemoryAllocator , base <5 , bytestring - , file-embed , gl-block , resourcet , spirv-reflect-ffi diff --git a/utils-spirv/package.yaml b/utils-spirv/package.yaml index 4c3af9d61..eab6592f5 100644 --- a/utils-spirv/package.yaml +++ b/utils-spirv/package.yaml @@ -7,17 +7,6 @@ github: expipiplus1/vulkan extra-source-files: - README.md - package.yaml -- test/fixtures/array-field.comp.spv -- test/fixtures/array2d.comp.spv -- test/fixtures/julia.comp.spv -- test/fixtures/mesh.frag.spv -- test/fixtures/mesh.vert.spv -- test/fixtures/nested.comp.spv -- test/fixtures/push.comp.spv -- test/fixtures/spec.comp.spv -- test/fixtures/ssbo-struct.comp.spv -- test/fixtures/tri.vert.spv -- test/fixtures/wide.comp.spv library: source-dirs: src @@ -44,6 +33,7 @@ tests: source-dirs: test dependencies: - base <5 + - bytestring - containers - geomancy - gl-block @@ -55,6 +45,7 @@ tests: - text - vector - vulkan + - vulkan-utils - vulkan-utils-spirv ghc-options: diff --git a/utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs b/utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs index 9a828d51d..0142a00b7 100644 --- a/utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs +++ b/utils-spirv/src/Vulkan/Utils/SpirV/Stage.hs @@ -51,9 +51,12 @@ module Vulkan.Utils.SpirV.Stage -- * Generation , reflectStageSig + , reflectStageSigBytes , reflectPipelineLayoutSig + , reflectPipelineLayoutSigBytes ) where +import Data.ByteString (ByteString) import Data.Kind (Constraint) import Data.List (foldl', sortOn) import Data.Proxy (Proxy (..)) @@ -69,7 +72,7 @@ import Data.SpirV.Reflect.Module qualified import Data.SpirV.Reflect.TypeDescription (TypeDescription) import Vulkan.Utils.SpirV.Layout (OffsetMap, fromFields, leafFieldType, normalize) -import Vulkan.Utils.SpirV.Reflect (reflectFileQ) +import Vulkan.Utils.SpirV.Reflect (reflectBytes, reflectFileQ) import Vulkan.Utils.SpirV.Reflect.OffsetMaps (pushOffsetMap, resourceOffsetMaps) import Vulkan.Utils.SpirV.Signature (KnownKeyedSigs (..), KnownMaybeSig (..), SigOffsetMap, natT, promoteList, promoteMaybe, promoteOffsetMap) import Vulkan.Utils.SpirV.Types (LayoutMode (..), classifyType) @@ -328,6 +331,15 @@ reflectStageSig name path = do m <- reflectFileQ path pure [TySynD (mkName name) [] (promoteStageInfo (stageInfoOf m))] +{- | As 'reflectStageSig', but from SPIR-V bytecode already in hand — e.g. a +quasiquoted shader imported from another module (the Template Haskell stage +restriction applies). +-} +reflectStageSigBytes :: String -> ByteString -> Q [Dec] +reflectStageSigBytes name bytes = do + m <- runIO (reflectBytes bytes) + pure [TySynD (mkName name) [] (promoteStageInfo (stageInfoOf m))] + promoteStageInfo :: StageInfo -> Type promoteStageInfo si = PromotedT 'ShaderSig @@ -365,6 +377,14 @@ reflectPipelineLayoutSig name paths = do Left err -> fail ("reflectPipelineLayoutSig: " <> err) Right info -> pure [TySynD (mkName name) [] (promoteLayoutSig info)] +-- | As 'reflectPipelineLayoutSig', but from SPIR-V bytecode already in hand. +reflectPipelineLayoutSigBytes :: String -> [ByteString] -> Q [Dec] +reflectPipelineLayoutSigBytes name bytess = do + modules <- runIO (traverse reflectBytes bytess) + case mergeLayout modules of + Left err -> fail ("reflectPipelineLayoutSigBytes: " <> err) + Right info -> pure [TySynD (mkName name) [] (promoteLayoutSig info)] + promoteLayoutSig :: LayoutInfo -> Type promoteLayoutSig info = PromotedT 'LayoutSig diff --git a/utils-spirv/test/Fixtures.hs b/utils-spirv/test/Fixtures.hs new file mode 100644 index 000000000..5b82ece02 --- /dev/null +++ b/utils-spirv/test/Fixtures.hs @@ -0,0 +1,428 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} + +{-| The test fixture shaders, compiled to SPIR-V at build time by the +vulkan-utils GLSL quasiquoters. + +The reflection splices in "Spec" and "LayoutSpec" consume these bytes; the +Template Haskell stage restriction is why they live in their own module. +-} +module Fixtures + ( juliaComp + , pushComp + , ssboStructComp + , nestedComp + , arrayFieldComp + , array2dComp + , bdaComp + , wideComp + , specComp + , meshVert + , meshFrag + , triVert + ) where + +import Data.ByteString (ByteString) +import Vulkan.Utils.ShaderQQ.GLSL.Glslang (comp, compileShaderQ, frag, glsl, vert) + +{- | A Julia-set compute shader whose @Params@ UBO (set 0, binding 0, std140) +becomes the generated record, next to an @OutputBuffer@ runtime-array SSBO +(binding 1, not generated). Fields are ordered by non-increasing alignment +(vec2\/uvec2 = 8, float\/uint = 4). +-} +juliaComp :: ByteString +juliaComp = + [comp| + #version 450 + + layout(local_size_x = 16, local_size_y = 16) in; + + layout(set = 0, binding = 0, std140) uniform Params { + vec2 center; // align 8 @0 + uvec2 resolution; // align 8 @8 + float escapeRadius; // align 4 @16 + uint maxIterations; // align 4 @20 + } params; + + layout(set = 0, binding = 1, std430) buffer OutputBuffer { + vec4 pixels[]; + }; + + void main() { + uvec2 gid = gl_GlobalInvocationID.xy; + if (gid.x >= params.resolution.x || gid.y >= params.resolution.y) { + return; + } + vec2 z = (vec2(gid) / vec2(params.resolution) * 2.0 - 1.0) * params.escapeRadius; + uint i = 0u; + for (; i < params.maxIterations; ++i) { + z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + params.center; + if (dot(z, z) > params.escapeRadius * params.escapeRadius) { + break; + } + } + pixels[gid.y * params.resolution.x + gid.x] = + vec4(vec3(float(i) / float(params.maxIterations)), 1.0); + } + |] + +{- | Push-constant reflection: the @Push@ block becomes a Haskell record with a +gl-block std430 Storable, and @pushConstantRanges@ derives the +VkPushConstantRange. Fields are ordered by non-increasing alignment (mat4 = +16, vec2 = 8, float\/int = 4) to satisfy the gl-block layout guardrail. +-} +pushComp :: ByteString +pushComp = + [comp| + #version 450 + + layout(local_size_x = 64) in; + + layout(push_constant, std430) uniform Push { + mat4 transform; + vec2 offset; + float scale; + int count; + } push; + + layout(set = 0, binding = 0, std430) buffer Output { + vec4 data[]; + }; + + void main() { + uint i = gl_GlobalInvocationID.x; + if (i >= uint(push.count)) { + return; + } + data[i] = push.transform * vec4(push.offset * push.scale, 0.0, 1.0); + } + |] + +{- | SSBO arrays of structs: the @Particle@ element type is generated as a +std430 record even though the wrapping @Particles@ block is a runtime array +(not itself representable as a flat record). Fields are ordered by +non-increasing alignment (vec3 = 16, vec2 = 8, float\/uint = 4). +-} +ssboStructComp :: ByteString +ssboStructComp = + [comp| + #version 450 + + layout(local_size_x = 64) in; + + struct Particle { + vec3 position; // align 16 @0 + vec2 velocity; // align 8 @16 + float mass; // align 4 @24 + uint flags; // align 4 @28 + }; + + layout(set = 0, binding = 0, std430) buffer Particles { + Particle items[]; + }; + + void main() { + uint i = gl_GlobalInvocationID.x; + items[i].position += vec3(items[i].velocity, 0.0) * items[i].mass; + } + |] + +{- | Nested structs as fields, plus cross-layout sharing. The @Material@ struct +is all-vec4 (every member 16-byte aligned), so std140 and std430 lay it out +identically: it is used both as a field of the std140 @Scene@ UBO and as the +element of the std430 @Mats@ SSBO, and a single generated record is shared +("promoted") across both. +-} +nestedComp :: ByteString +nestedComp = + [comp| + #version 450 + + layout(local_size_x = 1) in; + + struct Material { + vec4 albedo; + vec4 emission; + }; + + layout(set = 0, binding = 0, std140) uniform Scene { + Material sun; // nested struct field @0 (size 32) + vec4 tint; // @32 + } scene; + + layout(set = 0, binding = 1, std430) buffer Mats { + Material mats[]; + }; + + layout(set = 0, binding = 2, std430) writeonly buffer O { + vec4 o[]; + }; + + void main() { + o[0] = scene.sun.albedo + scene.sun.emission + scene.tint + mats[0].emission; + } + |] + +{- | Fixed-size array fields, mapped to @Array n a@. The same fields appear in a +std140 UBO and a std430 SSBO to show the stride difference: std140 rounds +every element up to 16 bytes, std430 packs tightly. +-} +arrayFieldComp :: ByteString +arrayFieldComp = + [comp| + #version 450 + + layout(local_size_x = 1) in; + + layout(set = 0, binding = 0, std140) uniform Kernel140 { + vec4 taps[4]; // stride 16 in both + float weights[4]; // stride 16 in std140 + } k140; + + layout(set = 0, binding = 1, std430) buffer Kernel430 { + vec4 taps[4]; // stride 16 + float weights[4]; // stride 4 in std430 + } k430; + + layout(set = 0, binding = 2, std430) writeonly buffer O { + vec4 o[]; + }; + + void main() { + o[0] = k140.taps[0] * k140.weights[0] + k430.taps[0] * k430.weights[0]; + } + |] + +{- | A multi-dimensional array field: @float grid[3][4]@ maps to +@Array 3 (Array 4 Float)@. In std430 the inner stride is 4 (outer 16); in +std140 the inner stride is 16 (outer 64). +-} +array2dComp :: ByteString +array2dComp = + [comp| + #version 450 + + layout(local_size_x = 1) in; + + layout(set = 0, binding = 0, std430) buffer Grid430 { + vec4 head; // @0 + float grid[3][4]; // @16, inner stride 4, outer stride 16 + } g430; + + layout(set = 0, binding = 1, std140) uniform Grid140 { + vec4 head; // @0 + float grid[3][4]; // @16, inner stride 16, outer stride 64 + } g140; + + layout(set = 0, binding = 2, std430) writeonly buffer O { + vec4 o[]; + }; + + void main() { + o[0] = g430.head + g140.head + vec4(g430.grid[0][0] + g140.grid[0][0]); + } + |] + +{- | A self-referential buffer_reference (BDA) type: a BVH-ish node whose +children are 64-bit device addresses back to @Node@. Reflection-driven codegen +maps the pointer members to @DeviceAddress Node@ (8-byte address), not an +inlined struct, and generates @Node@ once despite the cycle. + +buffer_reference needs SPIR-V 1.3+, hence the vulkan1.2 target. +-} +bdaComp :: ByteString +bdaComp = + $( compileShaderQ + (Just "vulkan1.2") + "comp" + Nothing + [glsl| + #version 460 + #extension GL_EXT_buffer_reference : require + + layout(local_size_x = 64) in; + + layout(buffer_reference) buffer Node; // fwd decl enables self-reference + layout(buffer_reference, std430) buffer Node { + vec4 boundsMin; + vec4 boundsMax; + Node left; // BDA pointer -> cycle + Node right; // BDA pointer -> cycle + uint primCount; + }; + + layout(push_constant, std430) uniform Bvh { + Node root; // entry address into the graph + } bvh; + + layout(set = 0, binding = 0, std430) writeonly buffer Out { + uint hits[]; + }; + + void main() { + Node n = bvh.root; + uint count = 0u; + for (int i = 0; i < 8; ++i) { + count += n.primCount; + n = n.left; + } + hits[gl_GlobalInvocationID.x] = count; + } + |] + ) + +{- | 64-bit integer block members. A uint64_t\/int64_t occupies an 8-byte std430 +slot (alignment 8), exactly like a double — the regression fixture for +scalar-width-faithful classification, so a 64-bit int is never laid out as a +4-byte int. Fields are ordered by non-increasing alignment (8, 8, 4) to keep +gl-block's Generic layout valid. + +int64 needs the Int64 capability, hence the vulkan1.1 target. +-} +wideComp :: ByteString +wideComp = + $( compileShaderQ + (Just "vulkan1.1") + "comp" + Nothing + [glsl| + #version 460 + #extension GL_EXT_shader_explicit_arithmetic_types_int64 : require + + layout(local_size_x = 1) in; + + layout(push_constant, std430) uniform Wide { + uint64_t hi; // align 8 @0, size 8 + int64_t lo; // align 8 @8, size 8 + uint tag; // align 4 @16, size 4 + } wide; + + layout(set = 0, binding = 0, std430) writeonly buffer Out { + uint sink[]; + }; + + void main() { + sink[0] = uint(wide.hi) + uint(wide.lo) + wide.tag; + } + |] + ) + +{- | Specialization-constant reflection. The ids are deliberately non-contiguous +(0 and 3) to show that map entries follow the shader's actual constant_ids +while values pack tightly (offsets 0, 4). +-} +specComp :: ByteString +specComp = + [comp| + #version 450 + + layout(local_size_x = 64) in; + + layout(constant_id = 0) const uint count = 1u; + layout(constant_id = 3) const float scale = 1.0; + + layout(set = 0, binding = 0, std430) buffer Output { + float xs[]; + }; + + void main() { + uint i = gl_GlobalInvocationID.x; + if (i < count) { + xs[i] = scale; + } + } + |] + +{- | Vertex stage for the type-verified pipeline-assembly tests. Shares the +@Scene@ UBO (set 0, binding 0) with 'meshFrag' (vertex uses viewProj; fragment +uses the light fields), carries a vertex-only @Model@ push constant, and feeds +the fragment stage @outNormal@ (loc 0) + @outUV@ (loc 1). +-} +meshVert :: ByteString +meshVert = + [vert| + #version 450 + + layout(location = 0) in vec3 inPosition; + layout(location = 1) in vec3 inNormal; + layout(location = 2) in vec2 inUV; + + layout(set = 0, binding = 0, std140) uniform Scene { + mat4 viewProj; + vec4 lightDir; + vec4 lightColor; + } scene; + + layout(push_constant, std430) uniform Model { + mat4 model; + } model; + + layout(location = 0) out vec3 outNormal; + layout(location = 1) out vec2 outUV; + + void main() { + gl_Position = scene.viewProj * model.model * vec4(inPosition, 1.0); + outNormal = mat3(model.model) * inNormal; + outUV = inUV; + } + |] + +{- | Fragment stage paired with 'meshVert'. Consumes the vertex outputs +(inNormal loc 0, inUV loc 1), shares the @Scene@ UBO (set 0, binding 0 — using +the light fields the vertex stage ignores), and reads a fragment-only +@Materials@ SSBO (set 0, binding 1). +-} +meshFrag :: ByteString +meshFrag = + [frag| + #version 450 + + layout(location = 0) in vec3 inNormal; + layout(location = 1) in vec2 inUV; + + layout(set = 0, binding = 0, std140) uniform Scene { + mat4 viewProj; + vec4 lightDir; + vec4 lightColor; + } scene; + + struct Material { + vec4 albedo; + vec4 params; + }; + + layout(set = 0, binding = 1, std430) buffer Materials { + Material materials[]; + }; + + layout(location = 0) out vec4 outColor; + + void main() { + float ndl = max(dot(normalize(inNormal), normalize(scene.lightDir.xyz)), 0.0); + vec4 albedo = materials[0].albedo * vec4(inUV, 1.0, 1.0); + outColor = albedo * scene.lightColor * ndl; + } + |] + +{- | Vertex-input reflection: three attributes (vec3 \/ vec2 \/ vec4 at +locations 0\/1\/2) that pack tightly to offsets 0\/12\/20 and a 36-byte binding +stride. +-} +triVert :: ByteString +triVert = + [vert| + #version 450 + + layout(location = 0) in vec3 inPosition; + layout(location = 1) in vec2 inUV; + layout(location = 2) in vec4 inColor; + + layout(location = 0) out vec2 outUV; + layout(location = 1) out vec4 outColor; + + void main() { + gl_Position = vec4(inPosition, 1.0); + outUV = inUV; + outColor = inColor; + } + |] diff --git a/utils-spirv/test/LayoutSpec.hs b/utils-spirv/test/LayoutSpec.hs index 398849c37..7560eed3a 100644 --- a/utils-spirv/test/LayoutSpec.hs +++ b/utils-spirv/test/LayoutSpec.hs @@ -15,7 +15,7 @@ import Test.Tasty.HUnit import Data.SpirV.Reflect.BlockVariable qualified import Data.SpirV.Reflect.DescriptorBinding qualified -import Data.SpirV.Reflect.FFI (load) +import Data.SpirV.Reflect.FFI (loadBytes) import Data.SpirV.Reflect.Module (Module) import Data.SpirV.Reflect.Module qualified import Data.SpirV.Reflect.TypeDescription (TypeDescription) @@ -24,6 +24,8 @@ import Vulkan.Utils.SpirV.Layout import Vulkan.Utils.SpirV.Layout qualified as OffsetMap (OffsetMap (..)) import Vulkan.Utils.SpirV.Types (LayoutMode (..), ScalarType (..)) +import Fixtures qualified + tests :: TestTree tests = testGroup @@ -176,19 +178,19 @@ reflectionTests = testGroup "built from reflected SPIR-V" [ testCase "push.comp Push has std430 offsets 0/64/72/76, size 80" $ do - m <- load "test/fixtures/push.comp.spv" + m <- loadBytes Fixtures.pushComp td <- maybe (assertFailure "no push constant block") pure (pushBlock m) l <- either (assertFailure . ("layoutOf: " <>)) pure (layoutOf Std430Layout td) map snd (offsets l) @?= [0, 64, 72, 76] l.size @?= Just 80 , testCase "push.comp Output (vec4 data[]) is an open offset map with stride 16" $ do - m <- load "test/fixtures/push.comp.spv" + m <- loadBytes Fixtures.pushComp td <- maybe (assertFailure "no SSBO binding") pure (firstBinding m) g <- either (assertFailure . ("offsetMapOf: " <>)) pure (offsetMapOf Std430Layout td) g.size @?= Nothing fmap (.stride) g.tail @?= Just 16 , testCase "ssbo-struct Particle[] tail unifies with a concrete Particle[2]" $ do - m <- load "test/fixtures/ssbo-struct.comp.spv" + m <- loadBytes Fixtures.ssboStructComp td <- maybe (assertFailure "no SSBO binding") pure (firstBinding m) open <- either (assertFailure . ("offsetMapOf: " <>)) pure (offsetMapOf Std430Layout td) -- Particle = vec3 @0, vec2 @16, float @24, uint @28 (stride 32). diff --git a/utils-spirv/test/Spec.hs b/utils-spirv/test/Spec.hs index c42a419ef..709208619 100644 --- a/utils-spirv/test/Spec.hs +++ b/utils-spirv/test/Spec.hs @@ -38,7 +38,7 @@ import Test.Tasty.HUnit import Vulkan.Core10 qualified as Vk -import Data.SpirV.Reflect.FFI (load) +import Data.SpirV.Reflect.FFI (loadBytes) import Vulkan.Utils.SpirV.Array qualified as Array import Vulkan.Utils.SpirV.Buffer (allocArrayBuffer, newBuffer, readBuffer, readBufferElem, writeBufferElem) import Vulkan.Utils.SpirV.Descriptors (descriptorSetLayoutInfos, mergedDescriptorSetLayoutInfos, mergedPushConstantRanges, pushConstantRanges) @@ -47,46 +47,47 @@ import Vulkan.Utils.SpirV.Layout (ArraySize (..), FieldType (..), fromFields, no import Vulkan.Utils.SpirV.Layout qualified import Vulkan.Utils.SpirV.Signature (ArrayOf, Fits, Sig, knownLayoutInstance, layoutSig) import Vulkan.Utils.SpirV.Specialization (specializationConstants, specializationMapEntries) -import Vulkan.Utils.SpirV.Stage (CompatibleResources, KnownLayoutSig (..), MatchInterface, linkStages, matchInterface, mergeLayout, reflectPipelineLayoutSig, reflectStageSig, stageInfoOf) +import Vulkan.Utils.SpirV.Stage (CompatibleResources, KnownLayoutSig (..), MatchInterface, linkStages, matchInterface, mergeLayout, reflectPipelineLayoutSigBytes, reflectStageSigBytes, stageInfoOf) import Vulkan.Utils.SpirV.Stage qualified import Vulkan.Utils.SpirV.Stage qualified as StageInfo (StageInfo (..)) -import Vulkan.Utils.SpirV.TH (reflectShaderTypes) +import Vulkan.Utils.SpirV.TH (reflectShaderTypesBytes) import Vulkan.Utils.SpirV.Types (LayoutMode (..), MemberShape (..), NumericType (..), ScalarType (..), geomancyTypeMap, linearTypeMap) import Vulkan.Utils.SpirV.VertexInput (vertexInputAttributes, vertexInputBinding, vertexInputState) +import Fixtures qualified import LayoutSpec qualified --- Generate the @Params@ record from the committed compute fixture. -reflectShaderTypes "test/fixtures/julia.comp.spv" +-- Generate the @Params@ record from the quasiquoted compute fixture. +reflectShaderTypesBytes Fixtures.juliaComp -- Generate the @Push@ push-constant record (std430) from its fixture. -reflectShaderTypes "test/fixtures/push.comp.spv" +reflectShaderTypesBytes Fixtures.pushComp -- Generate the @Particle@ element record of an SSBO array of structs. The -- wrapping @Particles@ runtime-array block is (correctly) not generated. -reflectShaderTypes "test/fixtures/ssbo-struct.comp.spv" +reflectShaderTypesBytes Fixtures.ssboStructComp -- Generate @Material@ (shared across a std140 UBO field and a std430 SSBO array, -- safe because all members are 16-byte aligned) and @Scene@, which has the -- nested @Material@ as a field. -reflectShaderTypes "test/fixtures/nested.comp.spv" +reflectShaderTypesBytes Fixtures.nestedComp -- Generate @Kernel140@ / @Kernel430@: records with fixed-size @Array n a@ fields -- under std140 (element stride 16) and std430 (tight) layouts. -reflectShaderTypes "test/fixtures/array-field.comp.spv" +reflectShaderTypesBytes Fixtures.arrayFieldComp -- Generate @Grid430@ / @Grid140@: a 2D array field @float grid[3][4]@ maps to -- @Array 3 (Array 4 Float)@. -reflectShaderTypes "test/fixtures/array2d.comp.spv" +reflectShaderTypesBytes Fixtures.array2dComp -- Generate @Node@ (a self-referential @buffer_reference@ struct) and @Bvh@ (a -- push-constant block holding a @Node@ device address). The pointer members map -- to @DeviceAddress Node@; @Node@ is generated once despite the cycle. -reflectShaderTypes "test/fixtures/bda.comp.spv" +reflectShaderTypesBytes Fixtures.bdaComp -- Generate @Wide@: a std430 push-constant block with 64-bit integer members -- (@uint64_t hi@ / @int64_t lo@), which must occupy 8-byte slots like a double. -reflectShaderTypes "test/fixtures/wide.comp.spv" +reflectShaderTypesBytes Fixtures.wideComp -- A std430 element whose array stride (32) exceeds its packed size (20): vec4 @0 -- + float @16 -> size 20, rounded up to its 16-byte alignment for the stride. @@ -161,11 +162,11 @@ pure -- Stage signatures for a matched vertex+fragment pair (shared Scene UBO, vertex -- push constant, fragment SSBO). Phase 1 exercises only the interface. -reflectStageSig "MeshVert" "test/fixtures/mesh.vert.spv" -reflectStageSig "MeshFrag" "test/fixtures/mesh.frag.spv" +reflectStageSigBytes "MeshVert" Fixtures.meshVert +reflectStageSigBytes "MeshFrag" Fixtures.meshFrag -- The merged pipeline-layout signature of the same pair, promoted to the type level. -reflectPipelineLayoutSig "MeshLayout" ["test/fixtures/mesh.vert.spv", "test/fixtures/mesh.frag.spv"] +reflectPipelineLayoutSigBytes "MeshLayout" [Fixtures.meshVert, Fixtures.meshFrag] -- Compile-time witness: only type-checks if the vertex→fragment interface matches. meshInterfaceOk :: (MatchInterface MeshVert MeshFrag) => Bool @@ -251,7 +252,7 @@ main = (rt.offset, rt.scale, rt.count) @?= (pushc.offset, pushc.scale, pushc.count) ] , testCase "push-constant range from reflection" $ do - m <- load "test/fixtures/push.comp.spv" + m <- loadBytes Fixtures.pushComp case pushConstantRanges m of [r] -> do r.offset @?= 0 @@ -279,7 +280,7 @@ main = , testCase "push-constant range from reflection ends at 20" $ do -- 16 + 4: tag (a uint) ends at 20, so each int64 before it took 8 -- bytes — a 4-byte misclassification would put the end at 12. - m <- load "test/fixtures/wide.comp.spv" + m <- loadBytes Fixtures.wideComp case pushConstantRanges m of [r] -> (r.offset, r.size) @?= (0, 20) other -> assertFailure ("expected one range, got " <> show (length other)) @@ -318,7 +319,7 @@ main = (,) <$> (peekByteOff p 32 :: IO Word64) <*> (peekByteOff p 40 :: IO Word64) (a32, a40) @?= (0xCAFE, 0xBEEF) , testCase "push-constant block is a single 8-byte device address" $ do - m <- load "test/fixtures/bda.comp.spv" + m <- loadBytes Fixtures.bdaComp case pushConstantRanges m of [r] -> (r.offset, r.size) @?= (0, 8) other -> assertFailure ("expected one range, got " <> show (length other)) @@ -462,17 +463,17 @@ main = , testGroup "specialization constants from reflection" [ testCase "reflects ids and names (ascending, non-contiguous)" $ do - m <- load "test/fixtures/spec.comp.spv" + m <- loadBytes Fixtures.specComp specializationConstants m @?= [(0, Just "count"), (3, Just "scale")] , testCase "map entries keep real ids but pack tightly" $ do - m <- load "test/fixtures/spec.comp.spv" + m <- loadBytes Fixtures.specComp let es = toL (specializationMapEntries m) map (\e -> (e.constantID, e.offset, e.size)) es @?= [(0, 0, 4), (3, 4, 4)] ] , testCase "descriptor set layout from reflection" $ do - m <- load "test/fixtures/julia.comp.spv" + m <- loadBytes Fixtures.juliaComp case descriptorSetLayoutInfos m of [(setNo, info)] -> do setNo @?= 0 @@ -485,7 +486,7 @@ main = , testGroup "vertex input from reflection" [ testCase "attributes: format + tightly-packed offset" $ do - m <- load "test/fixtures/tri.vert.spv" + m <- loadBytes Fixtures.triVert let attrs = vertexInputAttributes m map (\a -> (a.location, a.format, a.offset)) attrs @?= [ (0, Vk.FORMAT_R32G32B32_SFLOAT, 0) @@ -493,15 +494,15 @@ main = , (2, Vk.FORMAT_R32G32B32A32_SFLOAT, 20) ] , testCase "binding stride is packed size" $ do - m <- load "test/fixtures/tri.vert.spv" + m <- loadBytes Fixtures.triVert (vertexInputBinding m).stride @?= 36 , testCase "vertexInputState packs the reflected binding + attributes" $ do - m <- load "test/fixtures/tri.vert.spv" + m <- loadBytes Fixtures.triVert let vis = vertexInputState m map (.stride) (toL vis.vertexBindingDescriptions) @?= [36] map (.offset) (toL vis.vertexAttributeDescriptions) @?= [0, 12, 20] , testCase "vertexInputState is empty when a module declares no vertex inputs" $ do - m <- load "test/fixtures/julia.comp.spv" + m <- loadBytes Fixtures.juliaComp let vis = vertexInputState m null (toL vis.vertexBindingDescriptions) @? "no bindings" null (toL vis.vertexAttributeDescriptions) @? "no attributes" @@ -569,17 +570,17 @@ main = , testGroup "stage interface matching" [ testCase "vertex outputs reflect at locations 0 (vec3) and 1 (vec2)" $ do - vm <- load "test/fixtures/mesh.vert.spv" + vm <- loadBytes Fixtures.meshVert let outs = (stageInfoOf vm).outputs map fst outs @?= [0, 1] map (length . (.slots) . snd) outs @?= [3, 2] , testCase "vertex outputs match fragment inputs (value oracle)" $ do - vm <- load "test/fixtures/mesh.vert.spv" - fm <- load "test/fixtures/mesh.frag.spv" + vm <- loadBytes Fixtures.meshVert + fm <- loadBytes Fixtures.meshFrag matchInterface (stageInfoOf vm) (stageInfoOf fm) @?= Right () , testCase "a mismatched interface is rejected (value oracle)" $ do - vm <- load "test/fixtures/mesh.vert.spv" - fm <- load "test/fixtures/mesh.frag.spv" + vm <- loadBytes Fixtures.meshVert + fm <- loadBytes Fixtures.meshFrag -- frag outputs (vec4 @loc0) cannot satisfy the vertex's own inputs. assertBool "should not match" (isLeft (matchInterface (stageInfoOf fm) (stageInfoOf vm))) , testCase "MatchInterface holds at the type level" $ @@ -588,13 +589,13 @@ main = , testGroup "stage resource linking (shared UBO)" [ testCase "the Scene UBO reflects identically from both stages" $ do - vm <- load "test/fixtures/mesh.vert.spv" - fm <- load "test/fixtures/mesh.frag.spv" + vm <- loadBytes Fixtures.meshVert + fm <- loadBytes Fixtures.meshFrag lookup (0, 0) (stageInfoOf vm).resources @?= lookup (0, 0) (stageInfoOf fm).resources , testCase "linking unions resources, push and the external interface" $ do - vm <- load "test/fixtures/mesh.vert.spv" - fm <- load "test/fixtures/mesh.frag.spv" + vm <- loadBytes Fixtures.meshVert + fm <- loadBytes Fixtures.meshFrag case linkStages (stageInfoOf vm) (stageInfoOf fm) of Left e -> assertFailure e Right linked -> do @@ -603,8 +604,8 @@ main = map fst linked.inputs @?= [0, 1, 2] -- vertex attributes map fst linked.outputs @?= [0] -- fragment colour output , testCase "a conflicting shared binding is rejected" $ do - vm <- load "test/fixtures/mesh.vert.spv" - fm <- load "test/fixtures/mesh.frag.spv" + vm <- loadBytes Fixtures.meshVert + fm <- loadBytes Fixtures.meshFrag let bad = (stageInfoOf fm) { StageInfo.resources = [((0, 0), normalize (fromFields Std140Layout Nothing [("x", Vector STFloat 2)]))] @@ -613,8 +614,8 @@ main = , testCase "CompatibleResources holds at the type level" $ meshResourcesOk @?= True , testCase "the merged layout signature round-trips to the value-level merge" $ do - vm <- load "test/fixtures/mesh.vert.spv" - fm <- load "test/fixtures/mesh.frag.spv" + vm <- loadBytes Fixtures.meshVert + fm <- loadBytes Fixtures.meshFrag let info = layoutSigVal (Proxy @MeshLayout) -- reflected from the promoted type mergeLayout [vm, fm] @?= Right info -- equals the value-level merge map fst info.resources @?= [(0, 0), (0, 1)] -- Scene (shared) + Materials @@ -623,14 +624,14 @@ main = , testGroup "pipeline layout (depth-only vs depth+color from one vertex shader)" [ testCase "depth-only: the vertex stage alone" $ do - vm <- load "test/fixtures/mesh.vert.spv" + vm <- loadBytes Fixtures.meshVert -- Only the vertex-visible resources: Scene UBO at (0,0), vertex stage. stageBindings [vm] @?= [(0, Vk.SHADER_STAGE_VERTEX_BIT)] fmap length (mergedPushConstantRanges [vm]) @?= Right 1 -- the Model push constant , testCase "depth+color: vertex+fragment, Scene gains the fragment stage" $ do - vm <- load "test/fixtures/mesh.vert.spv" - fm <- load "test/fixtures/mesh.frag.spv" + vm <- loadBytes Fixtures.meshVert + fm <- loadBytes Fixtures.meshFrag stageBindings [vm, fm] @?= [ (0, Vk.SHADER_STAGE_VERTEX_BIT .|. Vk.SHADER_STAGE_FRAGMENT_BIT) -- Scene, shared , (1, Vk.SHADER_STAGE_FRAGMENT_BIT) -- Materials, frag-only diff --git a/utils-spirv/test/fixtures/array-field.comp b/utils-spirv/test/fixtures/array-field.comp deleted file mode 100644 index 88014185a..000000000 --- a/utils-spirv/test/fixtures/array-field.comp +++ /dev/null @@ -1,25 +0,0 @@ -#version 450 - -// Fixture exercising fixed-size array fields, mapped to `Array n a`. The same -// fields appear in a std140 UBO and a std430 SSBO to show the stride difference: -// std140 rounds every element up to 16 bytes, std430 packs tightly. - -layout(local_size_x = 1) in; - -layout(set = 0, binding = 0, std140) uniform Kernel140 { - vec4 taps[4]; // stride 16 in both - float weights[4]; // stride 16 in std140 -} k140; - -layout(set = 0, binding = 1, std430) buffer Kernel430 { - vec4 taps[4]; // stride 16 - float weights[4]; // stride 4 in std430 -} k430; - -layout(set = 0, binding = 2, std430) writeonly buffer O { - vec4 o[]; -}; - -void main() { - o[0] = k140.taps[0] * k140.weights[0] + k430.taps[0] * k430.weights[0]; -} diff --git a/utils-spirv/test/fixtures/array2d.comp b/utils-spirv/test/fixtures/array2d.comp deleted file mode 100644 index c3fbb6401..000000000 --- a/utils-spirv/test/fixtures/array2d.comp +++ /dev/null @@ -1,25 +0,0 @@ -#version 450 - -// Fixture exercising a multi-dimensional array field: float grid[3][4] maps to -// Array 3 (Array 4 Float). In std430 the inner stride is 4 (outer 16); in std140 -// the inner stride is 16 (outer 64). - -layout(local_size_x = 1) in; - -layout(set = 0, binding = 0, std430) buffer Grid430 { - vec4 head; // @0 - float grid[3][4]; // @16, inner stride 4, outer stride 16 -} g430; - -layout(set = 0, binding = 1, std140) uniform Grid140 { - vec4 head; // @0 - float grid[3][4]; // @16, inner stride 16, outer stride 64 -} g140; - -layout(set = 0, binding = 2, std430) writeonly buffer O { - vec4 o[]; -}; - -void main() { - o[0] = g430.head + g140.head + vec4(g430.grid[0][0] + g140.grid[0][0]); -} diff --git a/utils-spirv/test/fixtures/bda.comp b/utils-spirv/test/fixtures/bda.comp deleted file mode 100644 index 081f51d78..000000000 --- a/utils-spirv/test/fixtures/bda.comp +++ /dev/null @@ -1,39 +0,0 @@ -#version 460 -#extension GL_EXT_buffer_reference : require - -// A self-referential buffer_reference (BDA) type: a BVH-ish node whose children -// are 64-bit device addresses back to Node. Reflection-driven codegen maps the -// pointer members to DeviceAddress Node (8-byte address), not an inlined struct, -// and generates Node once despite the cycle. -// -// NOTE: buffer_reference needs SPIR-V 1.3+, so compile with -// glslangValidator -V --target-env vulkan1.2 bda.comp -o bda.comp.spv - -layout(local_size_x = 64) in; - -layout(buffer_reference) buffer Node; // fwd decl enables self-reference -layout(buffer_reference, std430) buffer Node { - vec4 boundsMin; - vec4 boundsMax; - Node left; // BDA pointer -> cycle - Node right; // BDA pointer -> cycle - uint primCount; -}; - -layout(push_constant, std430) uniform Bvh { - Node root; // entry address into the graph -} bvh; - -layout(set = 0, binding = 0, std430) writeonly buffer Out { - uint hits[]; -}; - -void main() { - Node n = bvh.root; - uint count = 0u; - for (int i = 0; i < 8; ++i) { - count += n.primCount; - n = n.left; - } - hits[gl_GlobalInvocationID.x] = count; -} diff --git a/utils-spirv/test/fixtures/build-shaders.sh b/utils-spirv/test/fixtures/build-shaders.sh deleted file mode 100644 index 05097aa4e..000000000 --- a/utils-spirv/test/fixtures/build-shaders.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -# Compile the test fixture shaders to SPIR-V with a local glslang. The committed -# .spv files are the stable input to reflection in the test suite (no glslang -# dependency at build time). -set -eu -cd "$(dirname "$0")" -for src in *.comp *.vert *.frag; do - [ -e "$src" ] || continue - # buffer_reference (BDA) needs SPIR-V 1.3+, i.e. a Vulkan 1.2 target; the - # Int64 capability (uint64_t/int64_t) needs a Vulkan 1.1 target. - if grep -q buffer_reference "$src"; then - glslangValidator -V --target-env vulkan1.2 "$src" -o "$src.spv" - elif grep -q int64 "$src"; then - glslangValidator -V --target-env vulkan1.1 "$src" -o "$src.spv" - else - glslangValidator -V "$src" -o "$src.spv" - fi - echo "wrote $src.spv" -done diff --git a/utils-spirv/test/fixtures/mesh.frag b/utils-spirv/test/fixtures/mesh.frag deleted file mode 100644 index 2cd788a15..000000000 --- a/utils-spirv/test/fixtures/mesh.frag +++ /dev/null @@ -1,32 +0,0 @@ -#version 450 - -// Fragment stage paired with mesh.vert. Consumes the vertex outputs (inNormal -// loc 0, inUV loc 1), shares the `Scene` UBO (set 0, binding 0 — using the light -// fields the vertex stage ignores), and reads a fragment-only `Materials` SSBO -// (set 0, binding 1). - -layout(location = 0) in vec3 inNormal; -layout(location = 1) in vec2 inUV; - -layout(set = 0, binding = 0, std140) uniform Scene { - mat4 viewProj; - vec4 lightDir; - vec4 lightColor; -} scene; - -struct Material { - vec4 albedo; - vec4 params; -}; - -layout(set = 0, binding = 1, std430) buffer Materials { - Material materials[]; -}; - -layout(location = 0) out vec4 outColor; - -void main() { - float ndl = max(dot(normalize(inNormal), normalize(scene.lightDir.xyz)), 0.0); - vec4 albedo = materials[0].albedo * vec4(inUV, 1.0, 1.0); - outColor = albedo * scene.lightColor * ndl; -} diff --git a/utils-spirv/test/fixtures/mesh.vert b/utils-spirv/test/fixtures/mesh.vert deleted file mode 100644 index fb88ebd85..000000000 --- a/utils-spirv/test/fixtures/mesh.vert +++ /dev/null @@ -1,29 +0,0 @@ -#version 450 - -// Vertex stage for the type-verified pipeline-assembly tests. Shares the `Scene` -// UBO (set 0, binding 0) with mesh.frag (vertex uses viewProj; fragment uses the -// light fields), carries a vertex-only `Model` push constant, and feeds the -// fragment stage `outNormal` (loc 0) + `outUV` (loc 1). - -layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec3 inNormal; -layout(location = 2) in vec2 inUV; - -layout(set = 0, binding = 0, std140) uniform Scene { - mat4 viewProj; - vec4 lightDir; - vec4 lightColor; -} scene; - -layout(push_constant, std430) uniform Model { - mat4 model; -} model; - -layout(location = 0) out vec3 outNormal; -layout(location = 1) out vec2 outUV; - -void main() { - gl_Position = scene.viewProj * model.model * vec4(inPosition, 1.0); - outNormal = mat3(model.model) * inNormal; - outUV = inUV; -} diff --git a/utils-spirv/test/fixtures/nested.comp b/utils-spirv/test/fixtures/nested.comp deleted file mode 100644 index 11c53b280..000000000 --- a/utils-spirv/test/fixtures/nested.comp +++ /dev/null @@ -1,31 +0,0 @@ -#version 450 - -// Fixture exercising nested structs as fields, plus cross-layout sharing. The -// `Material` struct is all-vec4 (every member 16-byte aligned), so std140 and -// std430 lay it out identically: it is used both as a field of the std140 -// `Scene` UBO and as the element of the std430 `Mats` SSBO, and a single -// generated record is shared ("promoted") across both. - -layout(local_size_x = 1) in; - -struct Material { - vec4 albedo; - vec4 emission; -}; - -layout(set = 0, binding = 0, std140) uniform Scene { - Material sun; // nested struct field @0 (size 32) - vec4 tint; // @32 -} scene; - -layout(set = 0, binding = 1, std430) buffer Mats { - Material mats[]; -}; - -layout(set = 0, binding = 2, std430) writeonly buffer O { - vec4 o[]; -}; - -void main() { - o[0] = scene.sun.albedo + scene.sun.emission + scene.tint + mats[0].emission; -} diff --git a/utils-spirv/test/fixtures/push.comp b/utils-spirv/test/fixtures/push.comp deleted file mode 100644 index cc905be34..000000000 --- a/utils-spirv/test/fixtures/push.comp +++ /dev/null @@ -1,28 +0,0 @@ -#version 450 - -// Fixture exercising push-constant reflection: the `Push` block becomes a -// Haskell record with a gl-block std430 Storable, and `pushConstantRanges` -// derives the VkPushConstantRange. Fields are ordered by non-increasing -// alignment (mat4 = 16, vec2 = 8, float/int = 4) to satisfy the gl-block -// layout guardrail. - -layout(local_size_x = 64) in; - -layout(push_constant, std430) uniform Push { - mat4 transform; - vec2 offset; - float scale; - int count; -} push; - -layout(set = 0, binding = 0, std430) buffer Output { - vec4 data[]; -}; - -void main() { - uint i = gl_GlobalInvocationID.x; - if (i >= uint(push.count)) { - return; - } - data[i] = push.transform * vec4(push.offset * push.scale, 0.0, 1.0); -} diff --git a/utils-spirv/test/fixtures/spec.comp b/utils-spirv/test/fixtures/spec.comp deleted file mode 100644 index 36b45958e..000000000 --- a/utils-spirv/test/fixtures/spec.comp +++ /dev/null @@ -1,21 +0,0 @@ -#version 450 - -// Fixture exercising specialization-constant reflection. The ids are -// deliberately non-contiguous (0 and 3) to show that map entries follow the -// shader's actual constant_ids while values pack tightly (offsets 0, 4). - -layout(local_size_x = 64) in; - -layout(constant_id = 0) const uint count = 1u; -layout(constant_id = 3) const float scale = 1.0; - -layout(set = 0, binding = 0, std430) buffer Output { - float xs[]; -}; - -void main() { - uint i = gl_GlobalInvocationID.x; - if (i < count) { - xs[i] = scale; - } -} diff --git a/utils-spirv/test/fixtures/ssbo-struct.comp b/utils-spirv/test/fixtures/ssbo-struct.comp deleted file mode 100644 index fce08a2ce..000000000 --- a/utils-spirv/test/fixtures/ssbo-struct.comp +++ /dev/null @@ -1,24 +0,0 @@ -#version 450 - -// Fixture exercising SSBO arrays of structs: the `Particle` element type is -// generated as a std430 record even though the wrapping `Particles` block is a -// runtime array (not itself representable as a flat record). Fields are ordered -// by non-increasing alignment (vec3 = 16, vec2 = 8, float/uint = 4). - -layout(local_size_x = 64) in; - -struct Particle { - vec3 position; // align 16 @0 - vec2 velocity; // align 8 @16 - float mass; // align 4 @24 - uint flags; // align 4 @28 -}; - -layout(set = 0, binding = 0, std430) buffer Particles { - Particle items[]; -}; - -void main() { - uint i = gl_GlobalInvocationID.x; - items[i].position += vec3(items[i].velocity, 0.0) * items[i].mass; -} diff --git a/utils-spirv/test/fixtures/wide.comp b/utils-spirv/test/fixtures/wide.comp deleted file mode 100644 index ac51301f3..000000000 --- a/utils-spirv/test/fixtures/wide.comp +++ /dev/null @@ -1,27 +0,0 @@ -#version 460 -#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require - -// 64-bit integer block members. A uint64_t/int64_t occupies an 8-byte std430 -// slot (alignment 8), exactly like a double — the regression fixture for -// scalar-width-faithful classification, so a 64-bit int is never laid out as a -// 4-byte int. Fields are ordered by non-increasing alignment (8, 8, 4) to keep -// gl-block's Generic layout valid. -// -// NOTE: int64 needs the Int64 capability; compile with -// glslangValidator -V --target-env vulkan1.1 wide.comp -o wide.comp.spv - -layout(local_size_x = 1) in; - -layout(push_constant, std430) uniform Wide { - uint64_t hi; // align 8 @0, size 8 - int64_t lo; // align 8 @8, size 8 - uint tag; // align 4 @16, size 4 -} wide; - -layout(set = 0, binding = 0, std430) writeonly buffer Out { - uint sink[]; -}; - -void main() { - sink[0] = uint(wide.hi) + uint(wide.lo) + wide.tag; -} diff --git a/utils-spirv/vulkan-utils-spirv.cabal b/utils-spirv/vulkan-utils-spirv.cabal index daa5ed223..b566547e3 100644 --- a/utils-spirv/vulkan-utils-spirv.cabal +++ b/utils-spirv/vulkan-utils-spirv.cabal @@ -15,17 +15,6 @@ build-type: Simple extra-source-files: README.md package.yaml - test/fixtures/array-field.comp.spv - test/fixtures/array2d.comp.spv - test/fixtures/julia.comp.spv - test/fixtures/mesh.frag.spv - test/fixtures/mesh.vert.spv - test/fixtures/nested.comp.spv - test/fixtures/push.comp.spv - test/fixtures/spec.comp.spv - test/fixtures/ssbo-struct.comp.spv - test/fixtures/tri.vert.spv - test/fixtures/wide.comp.spv source-repository head type: git @@ -98,6 +87,7 @@ test-suite spec type: exitcode-stdio-1.0 main-is: Spec.hs other-modules: + Fixtures LayoutSpec Paths_vulkan_utils_spirv hs-source-dirs: @@ -128,6 +118,7 @@ test-suite spec ghc-options: -Wall build-depends: base <5 + , bytestring , containers , geomancy , gl-block @@ -139,5 +130,6 @@ test-suite spec , text , vector , vulkan + , vulkan-utils , vulkan-utils-spirv default-language: Haskell2010 From 5ba11b24d0289920f680fec87968335cdeae4bc9 Mon Sep 17 00:00:00 2001 From: IC Rainbow Date: Sat, 4 Jul 2026 21:08:44 +0300 Subject: [PATCH 3/3] Fix CI --- .github/workflows/ci.yml | 4 +++- default.nix | 2 +- nix/haskell-packages.nix | 43 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc7a0baba..95ca305fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -263,6 +263,7 @@ jobs: - run: nix-build --arg compiler '${{ matrix.ghc }}' -A vulkan - run: nix-build --arg compiler '${{ matrix.ghc }}' -A VulkanMemoryAllocator - run: nix-build --arg compiler '${{ matrix.ghc }}' -A vulkan-utils + - run: nix-build --arg compiler '${{ matrix.ghc }}' -A vulkan-utils-spirv - run: nix-build --arg compiler '${{ matrix.ghc }}' -A openxr - run: nix-build --arg compiler '${{ matrix.ghc }}' ./nix/haskell-packages.nix --arg openxrNoVulkan true -A openxr - run: nix-build --arg compiler '${{ matrix.ghc }}' -A vulkan-examples @@ -328,7 +329,8 @@ jobs: sudo apt-get install fd-find - name: Check scripts with shellcheck run: | - fdfind .sh$ . \ + fdfind '\.sh$' . \ + --type f \ --exclude VulkanMemoryAllocator/VulkanMemoryAllocator \ --exclude generate-new/Vulkan-Docs \ --exclude generate-new/OpenXR-Docs \ diff --git a/default.nix b/default.nix index 0f25677ac..b51d90032 100644 --- a/default.nix +++ b/default.nix @@ -21,7 +21,7 @@ let (pkgs.haskell.lib.dontCheck vulkan) (pkgs.haskell.lib.dontCheck VulkanMemoryAllocator) ] else - [ vulkan vulkan-utils vulkan-init-sdl2 vulkan-init-glfw VulkanMemoryAllocator vulkan-examples openxr ] + [ vulkan vulkan-utils vulkan-utils-spirv vulkan-init-sdl2 vulkan-init-glfw VulkanMemoryAllocator vulkan-examples openxr ] ++ pkgs.lib.optional (p.ghc.version == generator-ghc-version) generate-new; in if forShell then diff --git a/nix/haskell-packages.nix b/nix/haskell-packages.nix index 128e32a5f..de3daf9fa 100644 --- a/nix/haskell-packages.nix +++ b/nix/haskell-packages.nix @@ -53,6 +53,13 @@ let modifier = drv: addExtraLibrary pkgs.vulkan-headers (mod drv); returnShellEnv = false; }; + vulkan-utils-spirv = self.developPackage { + name = "vulkan-utils-spirv"; + root = gitignore ../utils-spirv; + # tests compile GLSL fixtures with the glslang quasiquoters + modifier = drv: addBuildTools [ pkgs.glslang ] (mod drv); + returnShellEnv = false; + }; vulkan-init-sdl2 = self.developPackage { name = "vulkan-init-sdl2"; root = gitignore ../utils-init/vulkan-init-sdl2; @@ -116,6 +123,42 @@ let binary-orphans = addBuildDepend self.OneTuple super.binary-orphans; + # + # Dependencies for vulkan-utils-spirv, absent or too old in nixpkgs + # + # dontCheck: hedgehog property gives up on discards + geomancy = dontCheck (self.callHackageDirect { + pkg = "geomancy"; + ver = "0.3.0.1"; + sha256 = "0c0mm70hg1zv6di4y8z2j7a8xbrdxccqqv2v826jis7cyfznppgp"; + } { }); + gl-block = self.callHackageDirect { + pkg = "gl-block"; + ver = "1.1"; + sha256 = "0d6gi2hi96r7yzwg7vmrz936d2wmn88qs09pqhzk1ph7qwjbaqx0"; + } { }; + spirv-enum = self.callHackageDirect { + pkg = "spirv-enum"; + ver = "1.104.350.0"; + sha256 = "0pmj7a0sqvx2nlk7fl1yqlljxdrf91l0rzaa42fwjb39qg9hxzcf"; + } { }; + spirv-reflect-ffi = self.callHackageDirect { + pkg = "spirv-reflect-ffi"; + ver = "0.4"; + sha256 = "1861z86hb5pdi2x9fagl9yqrsxvkcm0pr11il5mklhs99xzkh49x"; + } { }; + spirv-reflect-types = self.callHackageDirect { + pkg = "spirv-reflect-types"; + ver = "0.4"; + sha256 = "1v5bip44jmzmcnlhjf8ckx8zz05f7fsx4d2idlkqs6q09zd7cb6n"; + } { }; + # dontCheck: test suite wants base >= 4.18 + webcolor-labels = dontCheck (self.callHackageDirect { + pkg = "webcolor-labels"; + ver = "0.1.0.0"; + sha256 = "1cwjz31xiya9wmy0h0srn4f6brd4xh4hi0rh5ry80vkwqz7f3fhm"; + } { }); + # # Overrides for generate #