packages feed

vulkan-utils-0.3: src/Vulkan/Utils/CommandCheck.hs

{-# language TemplateHaskell #-}
{-# language NoMonadComprehensions #-}
{-# language MultiWayIf #-}
{-# language QuasiQuotes #-}

module Vulkan.Utils.CommandCheck
  ( checkCommandsExp
  ) where

import           Control.Applicative            ( (<|>) )
import           Control.Arrow                  ( (&&&) )
import           Data.Char
import           Data.Functor                   ( (<&>) )
import           Data.List                      ( isPrefixOf
                                                , isSuffixOf
                                                , nub
                                                )
import           Data.List.Extra                ( dropEnd )
import           Data.Maybe                     ( catMaybes )
import           Foreign.Ptr
import           Language.Haskell.TH
import           Language.Haskell.TH.Syntax
import           Vulkan.Core10 (Instance(..), Device(..))
import           Vulkan.Dynamic

-- | Create an expression which checks the function pointers for all the Vulkan
-- commands depended upon by the specified list of function names.
--
-- It returns a list of function names corresponding to those functions with
-- null pointers.
--
-- Your program can use this function to fail early if a command couldn't be
-- loaded for some reason (missing extension or layer for example).
--
-- One can create a function called @checkCommands@ with the following:
-- @
-- [d| checkCommands = $(checkCommandsExp ['withInstance, 'cmdDraw, ...]) |]
-- @
--
-- It has the type @IsString a => Instance -> Device -> [a]@
--
-- It looks basically like
--
-- @
-- \inst dev ->
--   [ name
--   | True <- [ nullFunPtr == pVkCreateDevice inst
--             , nullFunPtr == pVkCreateFence dev
--               ..
--             ]
--   | name <- [ "vkCreateDevice"
--             , "vkCreateFence"
--               ..
--             ]
--   ]
-- @
checkCommandsExp
  :: [Name]
  -- ^ The names of functions from the @vulkan@ package. Unknown commands are
  -- ignored
  -> Q Exp
checkCommandsExp requestedCommands = do
  instAccessors   <- accessorNames ''InstanceCmds
  deviceAccessors <- accessorNames ''DeviceCmds
  let vkCommandNames =
        nub . commandNames instAccessors deviceAccessors =<< requestedCommands
  inst   <- newName "inst"
  device <- newName "device"
  let isNull = \case
        InstanceCmd i -> [|nullFunPtr == $(varE i) $(varE inst)|]
        DeviceCmd   i -> [|nullFunPtr == $(varE i) $(varE device)|]
  [| \(Instance _ $(varP inst)) (Device _ $(varP device)) ->
      [ name
      | (True, name) <- zip
          $(listE (isNull <$> vkCommandNames))
          $(lift (commandString <$> vkCommandNames))
      ]
    |]

-- | Given instance and device accessors and a function, find the function
-- pointer accessor names which it depends on
--
-- >>> commandNames ['pVkCreateDevice, 'pVkDestroyDevice] ['pVkCreateFence] (mkName "withDevice")
-- [InstanceCmd Vulkan.Dynamic.pVkCreateDevice,InstanceCmd Vulkan.Dynamic.pVkDestroyDevice]
commandNames :: [Name] -> [Name] -> Name -> [DeviceOrInstanceCommand]
commandNames instAccessors deviceAccessors =
  let instNames   = (nameBase &&& id) <$> instAccessors
      deviceNames = (nameBase &&& id) <$> deviceAccessors
      findCommand :: String -> Maybe DeviceOrInstanceCommand
      findCommand command =
        (InstanceCmd <$> lookup command instNames)
          <|> (DeviceCmd <$> lookup command deviceNames)
  in  \n ->
        let candidates = commandCandidates (nameBase n)
        in  catMaybes $ findCommand <$> candidates

data DeviceOrInstanceCommand
  = DeviceCmd Name
  | InstanceCmd Name
  deriving (Eq, Show)

-- | Get the C name of a function
--
-- >>> commandString (DeviceCmd (mkName "pVkCreateInstance"))
-- "vkCreateInstance"
commandString :: DeviceOrInstanceCommand -> String
commandString = unPtrName . nameBase . \case
  InstanceCmd n -> n
  DeviceCmd   n -> n

-- | A list of potential sets of vulkan commands this name depends on, not all
-- of them will be valid names.
--
-- >>> commandCandidates "withDevice"
-- ["pVkAllocateDevice","pVkFreeDevice","pVkCreateDevice","pVkDestroyDevice"]
--
-- >>> commandCandidates "waitSemaphoresSafe"
-- ["pVkWaitSemaphores"]
--
-- >>> commandCandidates "useCmdBuffer"
-- ["pVkBeginCmdBuffer","pVkEndCmdBuffer"]
--
-- >>> commandCandidates "withSemaphore"
-- ["pVkAllocateSemaphore","pVkFreeSemaphore","pVkCreateSemaphore","pVkDestroySemaphore"]
commandCandidates :: String -> [String]
commandCandidates n = if
  | "Safe" `isSuffixOf` n
  -> commandCandidates (dropEnd 4 n)
  | Just u <- stripPrefix "with"
  -> (<> u) <$> ["pVkAllocate", "pVkFree", "pVkCreate", "pVkDestroy"]
  | Just u <- stripPrefix "withMapped"
  -> (<> u) <$> ["pVkMap", "pVkUnmap"]
  | Just u <- stripPrefix "use"
  -> (<> u) <$> ["pVkBegin", "pVkEnd"]
  | Just u <- stripPrefix "cmdUse"
  -> (<> u) <$> ["pVkCmdBegin", "pVkCmdEnd"]
  | otherwise
  -> ["pVk" <> upperCaseFirst n]
 where
  stripPrefix p = if p `isPrefixOf` n
    then Just (upperCaseFirst (drop (length p) n))
    else Nothing

-- | Get the record accessors of a type
--
-- >>> $(lift . fmap show =<< accessorNames ''Device)
-- ["Vulkan.Core10.Handles.deviceHandle","Vulkan.Core10.Handles.deviceCmds"]
accessorNames :: Name -> Q [Name]
accessorNames record = reify record <&> \case
  TyConI (DataD _ _ _ _ [con] _)
    | RecC _ vars <- con       -> firstOfThree <$> vars
    | RecGadtC _ vars _ <- con -> firstOfThree <$> vars
  _ -> fail "Name wasn't a TyConI"
  where firstOfThree (a, _, _) = a

unPtrName :: String -> String
unPtrName = \case
  'p' : 'V' : xs -> 'v' : xs
  s              -> s

upperCaseFirst :: String -> String
upperCaseFirst = \case
  x:xs -> toUpper x : xs
  [] -> []