packages feed

vulkan-utils-spirv-0.1.0.0: src/Vulkan/Utils/SpirV/TH.hs

{-| 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"