packages feed

GPipe-GLFW4-2.0.0: src/Graphics/GPipe/Context/GLFW/Handler.hs

{-# LANGUAGE TypeFamilies #-}
-- | Internal module defining handler and its ContextHandler instance as well as some methods
module Graphics.GPipe.Context.GLFW.Handler where

-- stdlib
import           Control.Concurrent                   (MVar, ThreadId,
                                                       modifyMVar_, myThreadId,
                                                       newMVar, withMVar)
import           Control.Concurrent.Async             (withAsync)
import           Control.Concurrent.STM               (atomically)
import           Control.Concurrent.STM.TVar          (TVar, modifyTVar,
                                                       newTVarIO, readTVarIO,
                                                       writeTVar)
import           Control.Exception                    (Exception, throwIO)
import           Control.Monad                        (forM, forM_, unless,
                                                       void, when)
import           Control.Monad.IO.Class               (MonadIO, liftIO)
import           Data.List                            (delete, partition)
import           Data.Maybe                           (fromMaybe)
import           Text.Printf                          (printf)
-- thirdparty
import qualified Graphics.GPipe                       as GPipe (ContextHandler (..),
                                                                ContextT,
                                                                Window,
                                                                WindowBits,
                                                                withContextWindow)
import qualified Graphics.UI.GLFW                     as GLFW (Error, Window)
-- local
import qualified Graphics.GPipe.Context.GLFW.Calls    as Call
import qualified Graphics.GPipe.Context.GLFW.Format   as Format
import qualified Graphics.GPipe.Context.GLFW.Logger   as Log
import qualified Graphics.GPipe.Context.GLFW.RPC      as RPC
import           Graphics.GPipe.Context.GLFW.Resource (defaultWindowConfig)
import qualified Graphics.GPipe.Context.GLFW.Resource as Resource

-- | Internal handle for a GPipe-created GLFW window/context
newtype Context = Context
    { contextRaw :: GLFW.Window
--  , contextComm :: RPC.Handle
--  , contextAsync :: Async ()
    }
-- | Closeable internal handle for 'Context'.
type MMContext = MVar (Maybe Context)

-- | Opaque handle representing the initialized GLFW library.
--
-- To get started quickly try 'defaultHandleConfig' and 'defaultWindowConfig'.
--
-- @
--      import Graphics.GPipe
--      import qualified Graphics.GPipe.Context.GLFW as GLFW
--
--      runContextT GLFW.defaultHandleConfig $ do
--          win <- newWindow (WindowFormatColorDepth RGB8 Depth16) (GLFW.defaultWindowConfig "OpenGL Graphics")
--          -- Do GPipe things here
-- @
data Handle = Handle
    { handleTid         :: ThreadId
    , handleComm        :: RPC.Handle
    , handleRaw         :: GLFW.Window
    , handleCtxs        :: TVar [MMContext]
    , handleEventPolicy :: Maybe EventPolicy
    , handleLogger      :: Log.Logger
    }

-- | Opaque handle representing a, possibly closed, internal 'Context'. You'll
-- typically deal with GPipe's @Window@ instead of this one.
newtype GLFWWindow = WWindow (MMContext, Handle)

-- | Run the action with the context /if the context is still open/.
withContext :: String -> MMContext -> (Context -> IO a) -> IO (Maybe a)
withContext callerTag mmContext action = withMVar mmContext go
    where
        go Nothing = printf "WARNING %s: GPipe-GLFW context already closed" callerTag >> return Nothing
        go (Just context) = pure <$> action context

-- | Template for "Run the action with XYZ /if the gpipe window still exists and ABC/."
unwrappingGPipeWindow :: MonadIO m
    => (String -> action -> Handle -> MMContext -> IO (Maybe a)) -- ^ Specialize use of unwrappingGPipeWindow
    -> String -> GPipe.Window os c ds -> action -> GPipe.ContextT Handle os m (Maybe a)
unwrappingGPipeWindow specialize callerTag wid action = GPipe.withContextWindow wid go
    where
        go Nothing = printf "WARNING %s: GPipe had no such window" callerTag >> return Nothing
        go (Just (WWindow (mmContext, handle))) = specialize callerTag action handle mmContext

-- | Run the action with the context __handle__ /if the gpipe window still exists/.
withHandleFromGPipe :: MonadIO m => String -> GPipe.Window os c ds -> (Handle -> IO a) -> GPipe.ContextT Handle os m (Maybe a)
withHandleFromGPipe = unwrappingGPipeWindow $ \_callerTag action handle _mmContext ->
    Just <$> action handle

---- | Run the action with the __context__ /if the gpipe window still exists and corresponding context is still open/.
withContextFromGPipe :: MonadIO m => String -> GPipe.Window os c ds -> (Context -> IO a) -> GPipe.ContextT Handle os m (Maybe a)
withContextFromGPipe = unwrappingGPipeWindow $ \callerTag action _handle mmContext ->
    withContext callerTag mmContext action

withBothFromGPipe :: MonadIO m => String -> GPipe.Window os c ds -> (Handle -> Context -> IO a) -> GPipe.ContextT Handle os m (Maybe a)
withBothFromGPipe = unwrappingGPipeWindow $ \callerTag action handle mmContext ->
    withContext callerTag mmContext (action handle)

-- | Route an effect to the main thread.
effectMain :: Handle -> Call.EffectMain
effectMain handle = RPC.sendEffect (handleComm handle)

-- | Route an action with a result to the main thread.
onMain :: Handle -> Call.OnMain a
onMain handle = RPC.fetchResult (handleComm handle)

-- | Default GLFW handle configuration.
--
-- * Print any errors that GLFW emits.
-- * Automatically process GLFW events after every buffer swap.
-- * Log only context handling activity which represents undesired conditions.
defaultHandleConfig :: GPipe.ContextHandlerParameters Handle
defaultHandleConfig = HandleConfig
    { configErrorCallback = printf "%s: %s\n" . show
    , configEventPolicy = pure Poll
    , configLogger = Log.Logger
        { Log.loggerLevel = Log.WARNING
        , Log.loggerSink = Log.stderrSink
        }
    }

instance GPipe.ContextHandler Handle where

    -- | Configuration for the GLFW handle.
    data ContextHandlerParameters Handle = HandleConfig
        { -- | Specify a callback to handle errors emitted by GLFW.
          configErrorCallback :: GLFW.Error -> String -> IO ()
          -- | Specify the 'EventPolicy' to use for automatic GLFW event
          -- processing. Set to 'Nothing' to disable automatic event processing
          -- (you'll need to call 'mainloop' or 'mainstep').
        , configEventPolicy :: Maybe EventPolicy
          -- | Configuration for emitting messages.
        , configLogger :: Log.Logger
        }

    type ContextWindow Handle = GLFWWindow
    type WindowParameters Handle = Resource.WindowConfig

    -- Thread assumption: any thread
    --
    -- Create a context which shares objects with the contexts created by this
    -- handle, if any.
    createContext handle settings = do
        window <- createWindow (handleLogger handle) (Just $ handleRaw handle) settings
        mmContext <- newMVar . pure $ Context window
        atomically $ modifyTVar (handleCtxs handle) (mmContext :)
        return $ WWindow (mmContext, handle)

    -- Threading assumption: any thread
    --
    -- Do work with the specified context by making it current. If no context
    -- is specified, then any context being current is sufficient.
    --
    -- XXX: If there's a lot of context swapping, change this to RPC to a
    -- context-private thread running a mainloop.
    contextDoAsync handle Nothing action = RPC.sendEffect (handleComm handle) $ do
        -- (on main thread) Make the ancestor current if nothing else already is
        -- FIXME: these two bodies could be combined, perhaps.. the RPC is only necessary if the current thread lacks a context
        ccHuh <- Call.getCurrentContext
        maybe (Call.makeContextCurrent (handleLogger handle) "contextDoAsync required some context" . pure $ handleRaw handle)
            (const $ return ())
            ccHuh
        action
    contextDoAsync _ (Just (WWindow (mmContext, handle))) action =
        void $ withContext "contextDoAsync" mmContext $ \context -> do
            Call.makeContextCurrent (handleLogger handle) "contextDoAsync required a specific context" . pure . contextRaw $ context
            action

    -- Threading assumption: main thread
    --
    -- Swap buffers for the specified context. If an event policy is set,
    -- process events.
    contextSwap _ (WWindow (mmContext, handle)) = do
        void $ withContext "contextSwap" mmContext $ Call.swapBuffers . contextRaw
        mapM_ (mainstepInternal handle) $ handleEventPolicy handle

    -- Threading assumption: same thread as contextCreate for the given context
    --
    -- Fetch framebuffer size for the specified context by RPCing the main thread.
    contextFrameBufferSize _ (WWindow (mmContext, handle)) = do
        result <- withContext "contextFrameBufferSize" mmContext $ \context -> do
            Call.getFramebufferSize (onMain handle) $ contextRaw context
        maybe failure return result
        where
            failure = do
                Call.say (handleLogger handle) Log.ERROR $ printf "contextFrameBufferSize could not access context"
                return (0, 0)

    -- Threading assumption: same thread as contextCreate for the given context
    --
    -- Destroy the given context by making it current on the main thread and
    -- then destroying it there.
    --
    -- Note: See the restrictions for Call.destroyWindow
    contextDelete _ (WWindow (mmContext, handle)) = do
        -- close the context mvar
        modifyMVar_ mmContext $ \mContext -> do
            Call.say (handleLogger handle) Log.INFO $ printf "contextDelete of %s" (show $ contextRaw <$> mContext)
            forM_ mContext $ \context -> RPC.sendEffect (handleComm handle) $ do
                Call.makeContextCurrent (handleLogger handle) "contextDelete" . pure . contextRaw $ context
                Call.destroyWindow id (contextRaw context) -- id RPC because this is in a mainthread RPC
            return Nothing
        -- remove the context from the handle
        atomically $ modifyTVar (handleCtxs handle) (delete mmContext)

    -- Threading assumption: main thread
    contextHandlerCreate config = do
        Call.say (configLogger config) Log.DEBUG "contextHandlerCreate"
        -- make handle resources
        tid <- myThreadId
        comm <- RPC.newBound
        ctxs <- newTVarIO []
        -- initialize glfw
        Call.setErrorCallback id . pure $ configErrorCallback config -- id RPC because contextHandlerCreate is called only on mainthread
        ok <- Call.init id -- id RPC because contextHandlerCreate is called only on mainthread
        unless ok $ throwIO InitException
        -- wrap up handle
        ancestor <- createWindow (configLogger config) Nothing Nothing
        return $ Handle
            { handleTid = tid
            , handleComm = comm
            , handleRaw = ancestor
            , handleCtxs = ctxs
            , handleEventPolicy = configEventPolicy config
            , handleLogger = configLogger config
            }

    -- Threading: main thread
    contextHandlerDelete handle = do
        Call.say (handleLogger handle) Log.DEBUG "contextHandlerDelete"
        ctxs <- readTVarIO $ handleCtxs handle
        forM_ ctxs $ \mmContext -> GPipe.contextDelete handle (WWindow (mmContext, handle))
        atomically $ writeTVar (handleCtxs handle) []
        -- all resources are released
        Call.terminate id -- id RPC because contextHandlerDelete is called only on mainthread
        Call.setErrorCallback id Nothing -- id RPC because contextHandlerDelete is called only on mainthread

-- Create a raw GLFW window for use by contextHandlerCreate & createContext
createWindow :: Log.Logger -> Maybe GLFW.Window -> Maybe (GPipe.WindowBits, Resource.WindowConfig) -> IO GLFW.Window
createWindow logger parentHuh settings = do
    unless (null disallowedHints) $
        throwIO $ Format.UnsafeWindowHintsException disallowedHints
    -- make a context
    windowHuh <- Call.createWindow id width height title monitor hints parentHuh -- id RPC because contextHandlerCreate & createContext are called only on mainthread
    Call.say logger Log.DEBUG $ printf "made context %s -> parent %s" (show windowHuh) (show parentHuh)
    window <- maybe exc return windowHuh
    -- set up context
    forM_ intervalHuh $ \interval -> do
        Call.makeContextCurrent logger "apply vsync setting" $ pure window
        Call.swapInterval interval
    -- done
    return window
    where
        config = maybe (defaultWindowConfig "") snd settings
        Resource.WindowConfig {Resource.configWidth=width, Resource.configHeight=height} = config
        Resource.WindowConfig _ _ title monitor _ intervalHuh = config
        (userHints, disallowedHints) = partition Format.allowedHint $ Resource.configHints config
        hints = userHints ++ Format.bitsToHints (fst <$> settings) ++ Format.unconditionalHints
        exc = throwIO . CreateSharedWindowException . show $ config {Resource.configHints = hints}

-- | Type to describe the waiting or polling style of event processing
-- supported by GLFW.
--
-- * Recommended reading: /Event Processing/ section of the GLFW /Input Guide/
-- at <http://www.glfw.org/docs/latest/input_guide.html#events>.
data EventPolicy
    = Poll
    | Wait
    | WaitTimeout Double
    deriving
    ( Show
    )

-- | Process GLFW and GPipe events according to the given 'EventPolicy'.
--
-- __Use case:__ Call 'mainstep' as part of a custom engine loop in multithreaded
-- applications which do GPipe rendering off of the main thread. Use 'mainloop'
-- for less complex applications.
--
-- * Must be called on the main thread.
-- * Can be called with /any/ window you've created and not yet deleted.
-- * If GPipe can't find the window you passed in, returns 'Nothing'.
mainstep :: MonadIO m
    => GPipe.Window os c ds
    -> EventPolicy -- ^ 'Poll' will process events and return immediately while 'Wait' will sleep until events are received.
    -> GPipe.ContextT Handle os m (Maybe ())
mainstep win eventPolicy = withHandleFromGPipe "mainstep" win $ liftIO . flip mainstepInternal eventPolicy

mainstepInternal :: Handle -> EventPolicy -> IO ()
mainstepInternal handle eventPolicy = do
    tid <- myThreadId
    when (tid /= handleTid handle) $
        throwIO $ MainstepOffMainException "mainstep must be called from main thread"
    case eventPolicy of
        Poll -> Call.pollEvents id -- id RPC because mainstepInternal is called only on mainthread
        Wait -> withAsync
                    -- Async sleeps on RPC chan, waking op main when RPC received
                    (RPC.awaitActions (handleComm handle) >> Call.postEmptyEvent)
                    -- Main sleeps on waitEvents
                    (const $ Call.waitEvents id) -- id RPC because mainstepInternal is called only on mainthread
        WaitTimeout timeout -> withAsync
                    -- Async sleeps on RPC chan, waking op main when RPC received
                    (RPC.awaitActions (handleComm handle) >> Call.postEmptyEvent)
                    -- Main sleeps on waitEventsTimeout
                    (const $ Call.waitEventsTimeout id timeout) -- id RPC because mainstepInternal is called only on mainthread
    RPC.processActions $ handleComm handle

-- | Process GLFW and GPipe events according to the given 'EventPolicy' in a
-- loop.
--
-- __Use case:__ Call 'mainloop' in multithreaded applications which do GPipe
-- rendering off of the main thread, but which do not otherwise need additional
-- control over the main thread. For less complex applications use automatic
-- event processing configured via 'HandleConfig'.
--
-- * Must be called on the main thread.
-- * The loop will run until 'windowShouldClose' is true for the all 'Window's
-- created by the same 'ContextHandler', or all the 'Window's have been
-- deleted.
-- * To indicate a window should close use 'setWindowShouldClose' in "Graphics.GPipe.Context.GLFW.Wrapped".
mainloop :: MonadIO m
    => GPipe.Window os c ds
    -> EventPolicy -- ^ A 'Poll' loop runs continuously while a 'Wait' loop sleeps until events or user input occur.
    -> GPipe.ContextT Handle os m (Maybe ())
mainloop win eventPolicy = withHandleFromGPipe "mainloop" win $ liftIO . flip mainloopInternal eventPolicy

mainloopInternal :: Handle -> EventPolicy -> IO ()
mainloopInternal handle eventPolicy = do
    mainstepInternal handle eventPolicy
    ctxs <- readTVarIO $ handleCtxs handle
    allShouldClose <- and <$> forM ctxs oneShouldClose
    unless allShouldClose $
        mainloopInternal handle eventPolicy
    where
        oneShouldClose mmContext = do
            shouldCloseHuh <- withContext "oneShouldClose" mmContext $ Call.windowShouldClose . contextRaw
            return $ fromMaybe True shouldCloseHuh

-- | IO exception thrown when GLFW library initialization fails.
data InitException = InitException
    deriving Show
instance Exception InitException

-- | IO Exception thrown when GLFW window creation fails.
data CreateWindowException
    = CreateWindowException String
    | CreateSharedWindowException String
    deriving Show
instance Exception CreateWindowException

-- | IO Exception thrown when application code calls a GPipe-GLFW incorrectly
-- (eg. on the wrong thread).
newtype UsageException
    = MainstepOffMainException String
    deriving Show
instance Exception UsageException