packages feed

immutaball-core-0.1.0.4.1: Immutaball/Share/ImmutaballIO/SDLIO.hs

{-# OPTIONS_GHC -fno-warn-tabs #-}  -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .
-- Enable warnings:
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}

-- ImmutaballIO.hs.

{-# LANGUAGE Haskell2010 #-}
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, InstanceSigs, ScopedTypeVariables, ExistentialQuantification #-}

module Immutaball.Share.ImmutaballIO.SDLIO
	(
		-- * SDLIO
		SDLIO,
		SDLIOF(..),
		runSDLIO,

		-- * mfix
		FixSDLIOException(..),
		fixSDLIOExceptionToException,
		fixSDLIOExceptionFromException,
		PrematureEvaluationFixSDLIOException(..),
		EmptyFixSDLIOException(..),
		fixSDLIOF,
		unsafeFixSDLIOFTo,

		-- * Runners
		runSDLIOIO,
		hsdlttfRender,
		hsdlcaptureMouse,
		hsdlsetRelativeMouseMode,

		-- * SDLIO aliases that apply the Fixed wrapper
		mkSDLWithInit,
		mkSDLWithTTFInit,
		mkSDLPollEvent,
		mkSDLPollEventSync,
		mkSDLPollEvents,
		mkSDLPollEventsSync,
		mkSDLWithWindow,
		mkSDLWithGLContext,
		mkSDLGLSwapWindow,
		mkSDLTTFLoad,
		mkSDLTTFRender,
		mkSDLTTFRenderSync,
		mkSDLCaptureMouse,
		mkSDLSetRelativeMouseMode,

		-- * types
		SDLData,
		sdlDataToBS,
		sdlDataToBL,
		bsToSDLData,
		blToSDLData
	) where

import Prelude ()
import Immutaball.Prelude

import Data.Word
import Foreign.ForeignPtr
import Foreign.Ptr
import Foreign.Storable

import Control.Concurrent.Async
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import Graphics.GL.Core45
import qualified SDL.Event
import qualified SDL.Font
import qualified SDL.Init
import qualified SDL.Raw.Enum
import qualified SDL.Raw.Event
import qualified SDL.Raw.Types
import qualified SDL.Vect
import qualified SDL.Video
import qualified SDL.Video.OpenGL
import qualified SDL.Video.Renderer

import Immutaball.Share.Math
import Immutaball.Share.Utils

-- (mfix imports.)
import Control.Concurrent.MVar
import Control.Exception
import Data.Typeable
import GHC.IO.Unsafe (unsafeDupableInterleaveIO)
import System.IO.Unsafe (unsafePerformIO)

-- * SDLIO

type SDLIO = Fixed SDLIOF
data SDLIOF me =
	  SDLWithInit [SDL.Init.InitFlag] me
	| SDLWithTTFInit me
	-- | WARNING: do not call directly to avoid undefined behavior, but only
	-- with OS thread management compliant with the requirements of
	-- 'SDL.Event.pollEvent':
	-- > You can only call this function in the OS thread that set the video mode.
	-- SDLManager can handle this.
	| SDLPollEvent (Async (Maybe SDL.Event.Event) -> me)
	| SDLPollEventSync (Maybe SDL.Event.Event -> me)
	| SDLPollEvents (Async [SDL.Event.Event] -> me)
	| SDLPollEventsSync ([SDL.Event.Event] -> me)
	-- | Automatically handles destruction after lifetime.
	| SDLWithWindow T.Text SDL.Video.WindowConfig (SDL.Video.Window -> me)
	-- _| SDLDestroyWindow SDL.Video.Window  -- We can already manage the lifetime with WithCreate.
	-- | Automatically calls 'glMakeCurrent'.  NOTE: automatically calls
	-- 'glFinish' upon destruction.
	| SDLWithGLContext SDL.Video.Window (SDL.Video.OpenGL.GLContext -> me)
	-- | SDLGLMakeCurrent SDL.Video.Window SDL.Video.OpenGL.GLContext  -- We can automatically call this.
	-- -- _| See notes on 'glDeleleteContext' and 'glFinish' before using.
	-- _| SDLGLDeleteContext SDL.Video.OpenGL.GLContext
	| SDLGLSwapWindow SDL.Video.Window me
	| SDLTTFLoad FilePath SDL.Font.PointSize (SDL.Font.Font -> me)
	-- | Tight RGBA encoding.  Caching recommended.
	| SDLTTFRender SDL.Font.Font T.Text (Async (WidthHeightI, SDLData) -> me)
	| SDLTTFRenderSync SDL.Font.Font T.Text ((WidthHeightI, SDLData) -> me)

	| SDLCaptureMouse Bool (Integer -> me)
	| SDLSetRelativeMouseMode Bool (Integer -> me)
instance Functor SDLIOF where
	fmap :: (a -> b) -> (SDLIOF a -> SDLIOF b)
	fmap f (SDLWithInit subsystems sdlio)         = SDLWithInit subsystems (f sdlio)
	fmap f (SDLWithTTFInit sdlio)                 = SDLWithTTFInit (f sdlio)
	fmap f (SDLPollEvent withMEvent)              = SDLPollEvent (f . withMEvent)
	fmap f (SDLPollEventSync withMEvent)          = SDLPollEventSync (f . withMEvent)
	fmap f (SDLPollEvents withEvents)             = SDLPollEvents (f . withEvents)
	fmap f (SDLPollEventsSync withEvents)         = SDLPollEventsSync (f .  withEvents)
	fmap f (SDLWithWindow title cfg withWindow  ) = SDLWithWindow title cfg (f . withWindow)
	fmap f (SDLWithGLContext window withCxt)      = SDLWithGLContext window (f . withCxt)
	fmap f (SDLGLSwapWindow window withUnit)      = SDLGLSwapWindow window (f withUnit)
	fmap f (SDLTTFLoad path size withFont)        = SDLTTFLoad path size (f . withFont)
	fmap f (SDLTTFRender font text withImage)     = SDLTTFRender font text (f . withImage)
	fmap f (SDLTTFRenderSync font text withImage) = SDLTTFRenderSync font text (f . withImage)
	fmap f (SDLCaptureMouse enable withSuccess)   = SDLCaptureMouse enable (f . withSuccess)
	fmap f (SDLSetRelativeMouseMode enable withSuccess) = SDLSetRelativeMouseMode enable (f . withSuccess)

runSDLIO :: SDLIO -> IO ()
runSDLIO sdlio = cata runSDLIOIO sdlio

-- TODO: revisit:
{-
instance Foldable SDLIOF where
	foldr :: (a -> b -> b) -> b -> SDLIOF a -> b
	foldr reduce reduction0 (SDLWithInit _subsystems sdlio) = reduce sdlio reduction0
instance Traversable SDLIOF where
	traverse :: Applicative f => (a -> f b) -> SDLIOF a -> f (SDLIOF b)
	traverse traversal (SDLWithInit subsystems sdlio) = pure SDLWithInit <*> pure subsystems <*> traversal sdlio
-}

-- * mfix

data FixSDLIOException = forall e. Exception e => FixSDLIOException e
instance Show FixSDLIOException where
	show (FixSDLIOException e) = show e
instance Exception FixSDLIOException
fixSDLIOExceptionToException :: Exception e => e -> SomeException
fixSDLIOExceptionToException = toException . FixSDLIOException
fixSDLIOExceptionFromException :: Exception e => SomeException -> Maybe e
fixSDLIOExceptionFromException x = do
	FixSDLIOException a <- fromException x
	cast a

data PrematureEvaluationFixSDLIOException = PrematureEvaluationFixSDLIOException
	deriving (Show)
instance Exception PrematureEvaluationFixSDLIOException where
	toException = fixSDLIOExceptionToException
	fromException = fixSDLIOExceptionFromException

data EmptyFixSDLIOException = EmptyFixSDLIOException
	deriving (Show)
instance Exception EmptyFixSDLIOException where
	toException = fixSDLIOExceptionToException
	fromException = fixSDLIOExceptionFromException

--    mfix f = mfix f >>= f
-- => mfix f = join $ f <$> mfix f
-- Incorrect: runs f twice.
	--x -> f undefined >>= mfix f
{-
fixSDLIOF :: (me -> SDLIOF me) -> SDLIOF me
fixSDLIOF f = case f (error "Error: fixSDLIOF: premature evaluation of result before we could start it!") of
	x -> joinSDLIOF $ f <$> x
-}
-- Do it like fixIO and fixST (see also their notes; it's a little tricky).
-- Use a lazily read MVar.
fixSDLIOF :: (me -> SDLIOF me) -> SDLIOF me
fixSDLIOF f = unsafePerformIO $ do
	mme <- newEmptyMVar
	return $ unsafeFixSDLIOFTo mme f

-- | Helper for fixSDLIOF.
unsafeFixSDLIOFTo :: MVar me -> (me -> SDLIOF me) -> SDLIOF me
unsafeFixSDLIOFTo mme f = unsafePerformIO $ do
	me_ <- unsafeDupableInterleaveIO (readMVar mme `catch` \BlockedIndefinitelyOnMVar -> throwIO PrematureEvaluationFixSDLIOException)
	case f me_ of
		y@( SDLWithInit _subsystems me)           -> putMVar mme me >> return y
		y@( SDLWithTTFInit me)                    -> putMVar mme me >> return y
		_y@(SDLPollEvent withMEvent)              -> return $ SDLPollEvent               ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withMEvent)
		_y@(SDLPollEventSync withMEvent)          -> return $ SDLPollEventSync           ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withMEvent)
		_y@(SDLPollEvents withEvents)             -> return $ SDLPollEvents              ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withEvents)
		_y@(SDLPollEventsSync withEvents)         -> return $ SDLPollEventsSync          ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withEvents)
		_y@(SDLWithWindow title cfg withWindow)   -> return $ SDLWithWindow    title cfg ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withWindow)
		_y@(SDLWithGLContext window withCxt)      -> return $ SDLWithGLContext window    ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withCxt)
		y@( SDLGLSwapWindow _window me)           -> putMVar mme me >> return y
		_y@(SDLTTFLoad path size withFont)        -> return $ SDLTTFLoad       path size ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withFont)
		_y@(SDLTTFRender font text withImage)     -> return $ SDLTTFRender     font text ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withImage)
		_y@(SDLTTFRenderSync font text withImage) -> return $ SDLTTFRenderSync font text ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withImage)
		_y@(SDLCaptureMouse enable withSuccess)   -> return $ SDLCaptureMouse  enable    ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withSuccess)
		_y@(SDLSetRelativeMouseMode enable withSuccess) -> return $ SDLSetRelativeMouseMode enable ((\me -> unsafePerformIO $ putMVar mme me >> return me) . withSuccess)

-- * Runners

runSDLIOIO :: SDLIOF (IO ()) -> IO ()
runSDLIOIO (SDLWithInit subsystems sdlioio) = do
	SDL.Init.initialize subsystems
	sdlioio
	SDL.Init.quit
runSDLIOIO (SDLWithTTFInit sdlioio) = do
	SDL.Font.initialize
	sdlioio
	SDL.Font.quit
runSDLIOIO (SDLPollEvent withMEvent) = withAsync SDL.Event.pollEvent withMEvent
runSDLIOIO (SDLPollEventSync withMEvent) = SDL.Event.pollEvent >>= withMEvent
runSDLIOIO (SDLPollEvents withEvents) = withAsync SDL.Event.pollEvents withEvents
runSDLIOIO (SDLPollEventsSync withEvents) = SDL.Event.pollEvents >>= withEvents
runSDLIOIO (SDLWithWindow title cfg withWindow) = do
	window <- SDL.Video.createWindow title cfg
	withWindow window
	SDL.Video.destroyWindow window
runSDLIOIO (SDLWithGLContext window withCxt) = do
	cxt <- SDL.Video.OpenGL.glCreateContext window
	withCxt cxt
	glFinish
	SDL.Video.OpenGL.glDeleteContext cxt
runSDLIOIO (SDLGLSwapWindow window withUnit) = do
	SDL.Video.OpenGL.glSwapWindow window
	withUnit
runSDLIOIO (SDLTTFLoad path size withFont) = do
	font <- SDL.Font.load path size
	withFont font
runSDLIOIO (SDLTTFRender font text withImage) = withAsync (hsdlttfRender font text) withImage
runSDLIOIO (SDLTTFRenderSync font text withImage) = hsdlttfRender font text >>= withImage
runSDLIOIO (SDLCaptureMouse enable withSuccess) = hsdlcaptureMouse enable >>= withSuccess
runSDLIOIO (SDLSetRelativeMouseMode enable withSuccess) = hsdlsetRelativeMouseMode enable >>= withSuccess

-- | Render to a tight RGBA image.
--
-- We need some low-level C-like processing to interface with the SDL
-- libraries.
hsdlttfRender :: SDL.Font.Font -> T.Text -> IO (WidthHeightI, SDLData)
hsdlttfRender font text = do
	let maxColorComponent = 255  :: Word8
	let mcc = maxColorComponent
	fmt0Surface <- SDL.Font.blended font (SDL.Vect.V4 mcc mcc mcc mcc) text

	-- Get a couple fields from fmt0Surface's pixel format.
	(fmt0SurfaceFmtFmt, fmt0SurfaceFmtPalette) <- do
		let (SDL.Video.Renderer.Surface fmt0SurfaceSurfacePtr _) = fmt0Surface
		(SDL.Raw.Types.Surface fmt0SurfacePixelFormatPtr _w0' _h0' _pixels0' _userdata0 _cliprect0 _refcount0) <- peek fmt0SurfaceSurfacePtr
		(SDL.Raw.Types.PixelFormat {SDL.Raw.Types.pixelFormatFormat = fmt0SurfaceFmtFmt, SDL.Raw.Types.pixelFormatPalette = fmt0SurfaceFmtPalette}) <- peek fmt0SurfacePixelFormatPtr
		return (fmt0SurfaceFmtFmt, fmt0SurfaceFmtPalette)

	let
		tightRGBA :: SDL.Raw.Types.PixelFormat
		tightRGBA = SDL.Raw.Types.PixelFormat {
			-- The documentation doesn't say what this field is.  I'll just copy it.
			SDL.Raw.Types.pixelFormatFormat        = fmt0SurfaceFmtFmt :: Word32,
			-- I may as well copy this field too.
			SDL.Raw.Types.pixelFormatPalette       = fmt0SurfaceFmtPalette :: (Ptr SDL.Raw.Types.Palette),
			SDL.Raw.Types.pixelFormatBitsPerPixel  = 32  :: Word8,
			SDL.Raw.Types.pixelFormatBytesPerPixel = 4  :: Word8,
			SDL.Raw.Types.pixelFormatRMask         = if' (SDL.Raw.Enum.SDL_BYTEORDER == SDL.Raw.Enum.SDL_BIG_ENDIAN) 0xFF000000 0x000000FF  :: Word32,
			SDL.Raw.Types.pixelFormatGMask         = if' (SDL.Raw.Enum.SDL_BYTEORDER == SDL.Raw.Enum.SDL_BIG_ENDIAN) 0x00FF0000 0x0000FF00  :: Word32,
			SDL.Raw.Types.pixelFormatBMask         = if' (SDL.Raw.Enum.SDL_BYTEORDER == SDL.Raw.Enum.SDL_BIG_ENDIAN) 0x0000FF00 0x00FF0000  :: Word32,
			SDL.Raw.Types.pixelFormatAMask         = if' (SDL.Raw.Enum.SDL_BYTEORDER == SDL.Raw.Enum.SDL_BIG_ENDIAN) 0x000000FF 0xFF000000  :: Word32
		}
	tightRGBAFmtFPtr <- mallocForeignPtrArray 1
	fmt1Surface <- withForeignPtr tightRGBAFmtFPtr $ \tightRGBAFmtPtr -> do
		poke tightRGBAFmtPtr tightRGBA
		let
			tightRGBA' :: SDL.Video.Renderer.SurfacePixelFormat
			tightRGBA' = SDL.Video.Renderer.SurfacePixelFormat tightRGBAFmtPtr
		fmt1Surface <- SDL.Video.Renderer.convertSurface fmt0Surface tightRGBA'
		return fmt1Surface
	--SDL.Video.Renderer.freeSurface fmt0Surface  -- We copied a pointer from fmt0; hold on until we free fmt1 just in case.
	let (SDL.Video.Renderer.Surface rawSurfacePtr _) = fmt1Surface
	(SDL.Raw.Types.Surface _fmt w' h' pixels' _userdata _cliprect _refcount) <- peek rawSurfacePtr
	let (w, h) = (fromIntegral w', fromIntegral h')
	w `seq` h `seq` pixels' `seq` return ()
	pixelsBs <- BS.packCStringLen $ (castPtr pixels', fromIntegral $ 4*w*h)
	--let pixelsBl = BL.fromStrict pixelsBs
	--let pixels = pixelsBl `seq` pixelsBl
	let pixels = pixelsBs `seq` pixelsBs
	let result = w `seq` h `seq` pixels `seq` ((w, h), pixels)
	result `seq` return ()
	SDL.Video.Renderer.freeSurface fmt0Surface
	SDL.Video.Renderer.freeSurface fmt1Surface
	return result

hsdlcaptureMouse :: Bool -> IO Integer
hsdlcaptureMouse enable = fromIntegral <$> SDL.Raw.Event.captureMouse enable

hsdlsetRelativeMouseMode :: Bool -> IO Integer
hsdlsetRelativeMouseMode enable = fromIntegral <$> SDL.Raw.Event.setRelativeMouseMode enable

-- * SDLIO aliases that apply the Fixed wrapper

mkSDLWithInit :: [SDL.Init.InitFlag] -> SDLIO -> SDLIO
mkSDLWithInit subsystems sdlio = Fixed $ SDLWithInit subsystems sdlio

mkSDLWithTTFInit :: SDLIO -> SDLIO
mkSDLWithTTFInit sdlio = Fixed $ SDLWithTTFInit sdlio

mkSDLPollEvent :: (Async (Maybe SDL.Event.Event) -> SDLIO) -> SDLIO
mkSDLPollEvent withMEvent = Fixed $ SDLPollEvent withMEvent

mkSDLPollEventSync :: (Maybe SDL.Event.Event -> SDLIO) -> SDLIO
mkSDLPollEventSync withMEvent = Fixed $ SDLPollEventSync withMEvent

mkSDLPollEvents :: (Async [SDL.Event.Event] -> SDLIO) -> SDLIO
mkSDLPollEvents withEvents = Fixed $ SDLPollEvents withEvents

mkSDLPollEventsSync :: ([SDL.Event.Event] -> SDLIO) -> SDLIO
mkSDLPollEventsSync withEvents = Fixed $ SDLPollEventsSync withEvents

mkSDLWithWindow :: T.Text -> SDL.Video.WindowConfig -> (SDL.Video.Window -> SDLIO) -> SDLIO
mkSDLWithWindow title cfg withWindow = Fixed $ SDLWithWindow title cfg withWindow

mkSDLWithGLContext :: SDL.Video.Window -> (SDL.Video.OpenGL.GLContext -> SDLIO) -> SDLIO
mkSDLWithGLContext window withCxt = Fixed $ SDLWithGLContext window withCxt

mkSDLGLSwapWindow :: SDL.Video.Window -> SDLIO -> SDLIO
mkSDLGLSwapWindow window withUnit = Fixed $ SDLGLSwapWindow window withUnit

mkSDLTTFLoad :: FilePath -> SDL.Font.PointSize -> (SDL.Font.Font -> SDLIO) -> SDLIO
mkSDLTTFLoad path size withFont = Fixed $ SDLTTFLoad path size withFont

mkSDLTTFRender :: SDL.Font.Font -> T.Text -> (Async (WidthHeightI, SDLData) -> SDLIO) -> SDLIO
mkSDLTTFRender font text withImage = Fixed $ SDLTTFRender font text withImage

mkSDLTTFRenderSync :: SDL.Font.Font -> T.Text -> ((WidthHeightI, SDLData) -> SDLIO) -> SDLIO
mkSDLTTFRenderSync font text withImage = Fixed $ SDLTTFRenderSync font text withImage

mkSDLCaptureMouse :: Bool -> (Integer -> SDLIO) -> SDLIO
mkSDLCaptureMouse enable withSuccess = Fixed $ SDLCaptureMouse enable withSuccess

mkSDLSetRelativeMouseMode :: Bool -> (Integer -> SDLIO) -> SDLIO
mkSDLSetRelativeMouseMode enable withSuccess = Fixed $ SDLSetRelativeMouseMode enable withSuccess

-- * types

type SDLData = BS.ByteString

sdlDataToBS :: SDLData -> BS.ByteString
sdlDataToBS = id

sdlDataToBL :: SDLData -> BL.ByteString
sdlDataToBL = BL.fromStrict

bsToSDLData :: BS.ByteString -> SDLData
bsToSDLData = id

blToSDLData :: BL.ByteString -> SDLData
blToSDLData = BL.toStrict