packages feed

wgpu-hs-0.1.0.0: src-internal/WGPU/Internal/Instance.hs

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- |
-- Module      : WGPU.Internal.Instance
-- Description : Instance.
--
-- Instance of the WGPU API Haskell bindings.
module WGPU.Internal.Instance
  ( -- * Instance

    --
    -- $instance
    Instance (..),
    withInstance,

    -- * Logging
    LogCallback,
    LogLevel (..),
    setLogLevel,
    logLevelToText,
    logStdout,

    -- * Version
    Version (..),
    getVersion,
    versionToText,
  )
where

import Data.Bits (shiftR, (.&.))
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as TextIO
import Data.Word (Word32, Word8)
import Foreign (Ptr, freeHaskellFunPtr, nullFunPtr)
import Foreign.C (CChar, peekCString)
import WGPU.Internal.Memory (ToRaw, raw)
import WGPU.Raw.Dynamic (withWGPU)
import WGPU.Raw.Generated.Enum.WGPULogLevel (WGPULogLevel (WGPULogLevel))
import qualified WGPU.Raw.Generated.Enum.WGPULogLevel as WGPULogLevel
import WGPU.Raw.Generated.Fun (WGPUHsInstance)
import qualified WGPU.Raw.Generated.Fun as RawFun
import WGPU.Raw.Types (WGPULogCallback)

-- $instance
--
-- The Haskell bindings to WGPU use a value of type 'Instance' as a handle to
-- the rest of the API. An 'Instance' value is obtained by loading a dynamic
-- library at runtime, using the 'withInstance' function. A typical invocation
-- might look like this:
--
-- @
-- 'withInstance' "libwgpu_native.dylib" (Just 'logStdOut') $ \inst -> do
--   -- set the logging level for the instance
--   'setLogLevel' inst 'Warn'
--   -- run the rest of the program ...
-- @
--
-- The dynamic library @libwgpu_native.dylib@ is obtained by compiling the
-- Rust project <https://github.com/gfx-rs/wgpu-native wgpu-native>. Care
-- should be take to compile a version of @libwgpu_native.dylib@ which is
-- compatible with the API in these bindings.

-------------------------------------------------------------------------------

-- | Instance of the WGPU API.
--
-- An instance is loaded from a dynamic library using the 'withInstance'
-- function.
newtype Instance = Instance {wgpuHsInstance :: WGPUHsInstance}

instance Show Instance where show _ = "<Instance>"

instance ToRaw Instance WGPUHsInstance where
  raw = pure . wgpuHsInstance

-------------------------------------------------------------------------------

-- | Logging level.
data LogLevel
  = Trace
  | Debug
  | Info
  | Warn
  | Error
  deriving (Eq, Show)

-- | Logging callback function.
type LogCallback = LogLevel -> Text -> IO ()

-- | Load the WGPU API from a dynamic library and supply an 'Instance' to a
-- program.
withInstance ::
  -- | Name of the @wgpu-native@ dynamic library, or a complete path to it.
  FilePath ->
  -- | Optional logging callback. @'Just' 'logStdout'@ can be supplied here to
  --   print log messages to @stdout@ for debugging purposes.
  Maybe LogCallback ->
  -- | The Program. A function which takes an 'Instance' and returns an IO
  --   action that uses the instance.
  (Instance -> IO a) ->
  -- | IO action which loads the WGPU 'Instance', passes it to the program, and
  --   returns the result of running the program.
  IO a
withInstance dylibPath mLog program =
  withWGPU dylibPath $
    \winst -> do
      -- create the logging callback if necessary
      logCallback_c <- case mLog of
        Nothing -> pure nullFunPtr
        Just logFn -> mkLogCallback . toRawLogCallback $ logFn
      RawFun.wgpuSetLogCallback winst logCallback_c

      -- run the program
      result <- program (Instance winst)

      -- free the logging callback
      case mLog of
        Nothing -> pure ()
        Just _ -> do
          RawFun.wgpuSetLogCallback winst nullFunPtr
          freeHaskellFunPtr logCallback_c

      pure result

-- | Set the current logging level for the instance.
setLogLevel :: Instance -> LogLevel -> IO ()
setLogLevel inst lvl =
  RawFun.wgpuSetLogLevel (wgpuHsInstance inst) (logLevelToWLogLevel lvl)

-- | Create a C callback from a Haskell logging function.
foreign import ccall "wrapper"
  mkLogCallback ::
    (WGPULogLevel -> Ptr CChar -> IO ()) ->
    IO WGPULogCallback

-- | Convert a logging callback function to the form required by the Raw API.
toRawLogCallback :: LogCallback -> (WGPULogLevel -> Ptr CChar -> IO ())
toRawLogCallback logFn wLogLevel cMsg = do
  msg <- Text.pack <$> peekCString cMsg
  logFn (wLogLevelToLogLevel wLogLevel) msg

-- | Convert a raw API logging level into a 'LogLevel'.
--
-- Any unknown log levels become an 'Error'.
wLogLevelToLogLevel :: WGPULogLevel -> LogLevel
wLogLevelToLogLevel wLvl =
  case wLvl of
    WGPULogLevel.Trace -> Trace
    WGPULogLevel.Debug -> Debug
    WGPULogLevel.Info -> Info
    WGPULogLevel.Warn -> Warn
    WGPULogLevel.Error -> Error
    _ -> Error

-- | Convert a 'LogLevel' value into the type required by the raw API.
logLevelToWLogLevel :: LogLevel -> WGPULogLevel
logLevelToWLogLevel lvl =
  case lvl of
    Trace -> WGPULogLevel.Trace
    Debug -> WGPULogLevel.Debug
    Info -> WGPULogLevel.Info
    Warn -> WGPULogLevel.Warn
    Error -> WGPULogLevel.Error

-- | Convert a 'LogLevel' to a text string.
logLevelToText :: LogLevel -> Text
logLevelToText lvl =
  case lvl of
    Trace -> "Trace"
    Debug -> "Debug"
    Info -> "Info"
    Warn -> "Warn"
    Error -> "Error"

-- | A logging function which prints to @stdout@.
--
-- This logging function can be supplied to 'withInstance' to print logging
-- messages to @stdout@ for debugging purposes.
logStdout :: LogLevel -> Text -> IO ()
logStdout lvl msg = TextIO.putStrLn $ "[" <> logLevelToText lvl <> "]: " <> msg

-------------------------------------------------------------------------------

-- | Version of WGPU native.
data Version = Version
  { major :: !Word8,
    minor :: !Word8,
    patch :: !Word8,
    subPatch :: !Word8
  }
  deriving (Eq, Show)

-- | Return the exact version of the WGPU native instance.
getVersion :: Instance -> IO Version
getVersion inst = w32ToVersion <$> RawFun.wgpuGetVersion (wgpuHsInstance inst)
  where
    w32ToVersion :: Word32 -> Version
    w32ToVersion w =
      let major = fromIntegral $ (w `shiftR` 24) .&. 0xFF
          minor = fromIntegral $ (w `shiftR` 16) .&. 0xFF
          patch = fromIntegral $ (w `shiftR` 8) .&. 0xFF
          subPatch = fromIntegral $ w .&. 0xFF
       in Version {..}

-- | Convert a 'Version' value to a text string.
--
-- >>> versionToText (Version 0 9 2 2)
-- "v0.9.2.2"
versionToText :: Version -> Text
versionToText ver =
  "" <> showt (major ver)
    <> "."
    <> showt (minor ver)
    <> "."
    <> showt (patch ver)
    <> "."
    <> showt (subPatch ver)

-- | Show a value as a 'Text' string.
showt :: Show a => a -> Text
showt = Text.pack . show