packages feed

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

{-# 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' <name> where type 'Sig' <name> = <promoted
offset map>@, 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