packages feed

vulkan-3.1.0.0: examples/sdl-triangle/ShaderQQ.hs

module ShaderQQ
  ( comp
  , frag
  , geom
  , tesc
  , tese
  , vert
  , compileShader
  ) where

import           Data.ByteString                ( ByteString )
import qualified Data.ByteString               as BS
import           Data.FileEmbed
import           Language.Haskell.TH
import           Language.Haskell.TH.Syntax
import           Language.Haskell.TH.Quote
import           System.IO
import           System.IO.Temp
import           System.Process.Typed
import           Data.List.Extra
import           Data.Char

comp = shaderQQ "comp"
frag = shaderQQ "frag"
geom = shaderQQ "geom"
tesc = shaderQQ "tesc"
tese = shaderQQ "tese"
vert = shaderQQ "vert"

shaderQQ :: String -> QuasiQuoter
shaderQQ stage = QuasiQuoter
  { quoteExp  = \code -> do
                  loc <- location
                  bs  <- runIO $ compileShader (Just loc) stage code
                  bsToExp bs
  , quotePat  = bad "pattern"
  , quoteType = bad "type"
  , quoteDec  = bad "declaration"
  }
 where
  bad :: String -> a
  bad s = error $ "Can't use " <> stage <> " quote in a " <> s <> " context"

-- | Compile a glsl shader to spir-v using glslangValidator
compileShader
  :: Maybe Loc
  -- ^ Source location
  -> String
  -- ^ stage
  -> String
  -- ^ glsl code
  -> IO ByteString
  -- ^ Spir-V bytecode
compileShader loc stage code = withSystemTempDirectory "th-shader" $ \dir -> do
  let codeWithLineDirective = maybe code (`insertLineDirective` code) loc
  let shader = dir <> "/shader." <> stage
      spirv  = dir <> "/shader.spv"
  writeFile shader codeWithLineDirective
  let -- TODO: writing to stdout here breaks HIE
      p =
        setStderr inherit
          . setStdout (useHandleOpen stderr)
          . setStdin closed
          $ proc "glslangValidator" ["-S", stage, "-V", shader, "-o", spirv]
  runProcess_ p
  -- 'runIO' suggests flushing as GHC may not
  hFlush stderr
  BS.readFile spirv

-- If possible, insert a #line directive after the #version directive (as well
-- as the extension which allows filenames in line directives.
insertLineDirective :: Loc -> String -> String
insertLineDirective Loc {..} code =
  let isVersionDirective = ("#version" `isPrefixOf`) . dropWhile isSpace
      codeLines = lines code
      (beforeVersion, afterVersion) = break isVersionDirective codeLines
      lineDirective =
        [ "#extension GL_GOOGLE_cpp_style_line_directive : enable"
        , "#line "
          <> show (fst loc_start + length beforeVersion + 1)
          <> " \""
          <> loc_filename
          <> "\""
        ]
  in  case afterVersion of
        []     -> code
        v : xs -> unlines $ beforeVersion <> [v] <> lineDirective <> xs