packages feed

immutaball-core-0.1.0.4.1: Immutaball/Share/Video.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 #-}

-- CLI.hs.

{-# LANGUAGE Haskell2010 #-}
{-# LANGUAGE TemplateHaskell, ExistentialQuantification #-}

module Immutaball.Share.Video
	(
		reverseRowsImageReallySlow,
		reverseRowsImage,

		-- * Shader: high level
		withImmutaballShader,
		sdlCreateImmutaballShader,
		sdlCreateImmutaballShaderWith,

		shaderEnableSceneDataLocation,
		shaderEnableBallDataLocation,
		shaderBallRadiusLocation,
		shaderBallPosLocation,
		shaderBallRotLocation,
		shaderBallNumTrianglesLocation,
		shaderSSBOVertexDataLocation,
		shaderSSBOGeomDataLocation,
		shaderSSBOLumpDataLocation,
		shaderSSBOPathDoublesDataLocation,
		shaderSSBOPathInt32sDataLocation,
		shaderSSBOBodyDataLocation,
		shaderSSBOGcDataLocation,
		shaderSSBOAllGeomPassMvDataLocation,
		shaderSSBOAllGeomPassTexturesDataLocation,
		shaderSSBOAllGeomPassGisDataLocation,
		shaderSSBOGeomPassMvRangesDataLocation,
		shaderSSBOGeomPassTexturesRangesDataLocation,
		shaderSSBOGeomPassGisRangesDataLocation,
		shaderSSBOGeomPassBisDataLocation,
		shaderSSBOTransformationLocation,
		shaderSceneGeomPassIdxLocation,
		shaderSSBOTexcoordsDoubleDataLocation,

		ShaderDoubleType,
		toShaderDoubleType,

		-- * Shader: low level
		ImmutaballShaderHandle(..), ibshVertexShader, ibshFragmentShader,
			ibshProgram, ibshPipeline,
		initImmutaballShader,
		freeImmutaballShader,
		rawInitializeImmutaballShaderContinue,

		-- * MtrlMeta
		MtrlMeta(..),

		-- * Errors
		VideoException(..),
		videoExceptionToException,
		videoExceptionFromException,
		GLErrorVideoException(..),

		-- * Utils
		checkGLErrorsIB,
		glErrType,
		glChecked,
		numToGL_TEXTUREi,
		gpuEncodeArray
	) where

import Prelude ()
import Immutaball.Prelude

import Control.Exception
import Control.Monad
import Data.Bits
import Data.List
import Data.Typeable (cast)
import Data.Word

import Control.Concurrent.STM.TMVar
import Control.Lens
import qualified Data.ByteString as BS
--import qualified Data.ByteString.Lazy as BL
import qualified Data.Map as M
import Graphics.GL.Compatibility45
--import Graphics.GL.Core45
import Graphics.GL.Types

import Immutaball.Share.Math
import Immutaball.Share.ImmutaballIO
import Immutaball.Share.ImmutaballIO.BasicIO
import Immutaball.Share.ImmutaballIO.GLIO
import Immutaball.Share.SDLManager
import Immutaball.Share.Utils
import Immutaball.Share.Video.LowLevel
import Immutaball.Share.Video.Shaders

-- | Replaced by a newer 'reverseRowsImage' that performs better.
reverseRowsImageReallySlow :: (WidthHeightI, BS.ByteString) -> BS.ByteString
reverseRowsImageReallySlow ((w, _h), image) = glImage
	where
		glImage = BS.pack glBytes
		bytes :: [Word8]
		bytes = BS.unpack image
		glBytes :: [Word8]
		glBytes = concat glRows
		glRows :: [[Word8]]
		glRows = reverse rows
		rows :: [[Word8]]
		rows = withRemaining bytes
		withRemaining :: [Word8] -> [[Word8]]
		withRemaining [] = []
		withRemaining xs = genericTake row xs : withRemaining (genericDrop row xs)
		row = 4 * w

-- Moved to avoid Template Haskell errors.

data ImmutaballShaderHandle = ImmutaballShaderHandle {
	_ibshVertexShader   :: GLuint,
	_ibshFragmentShader :: GLuint,
	_ibshProgram        :: GLuint,
	_ibshPipeline       :: GLuint
}
makeLenses ''ImmutaballShaderHandle

-- * Shader: high level

-- | Directly initialize the shader, attaching the lifetime to the caller; does
-- not asynchronously attach the lifetime to the SDL manager thread, which can be done with
-- 'sdlCreateImmutaballShader'.
withImmutaballShader :: SDLManagerHandle -> (ImmutaballShaderHandle -> ImmutaballIOF me) -> ImmutaballIOF me
withImmutaballShader sdlMgr withShader = do
	shader <- initImmutaballShader sdlMgr
	me <- withShader shader
	freeImmutaballShader sdlMgr shader
	return me

-- | Use the SDL Manager thread to manage the immutaball shader resource.
--
-- When the SDL Manager thread exits, it will deallocate the resource.
--
-- The TMVar is set with the resource once the initializer is done.
--
-- The SDLManager initializes this resource concurrently, so it does not block
-- the SDLManager thread.
sdlCreateImmutaballShader :: SDLManagerHandle -> ImmutaballIOF (TMVar ImmutaballShaderHandle)
sdlCreateImmutaballShader sdlMgr =
	JoinIBIOF .
	Atomically (newEmptyTMVar) $ \to_ ->
	attachLifetime sdlMgr (initImmutaballShader sdlMgr) (freeImmutaballShader sdlMgr) to_ to_

-- | 'sdlCreateImmutaballShader' variant that uses a supplied TMVar for storage.
sdlCreateImmutaballShaderWith :: SDLManagerHandle -> TMVar ImmutaballShaderHandle -> ImmutaballIOF ()
sdlCreateImmutaballShaderWith sdlMgr to_ =
	attachLifetime sdlMgr (initImmutaballShader sdlMgr) (freeImmutaballShader sdlMgr) to_ ()

shaderEnableSceneDataLocation :: GLint
shaderEnableSceneDataLocation = fromIntegral (16 :: Integer)

shaderEnableBallDataLocation :: GLint
shaderEnableBallDataLocation = fromIntegral (34 :: Integer)

shaderBallRadiusLocation :: GLint
shaderBallRadiusLocation = fromIntegral (35 :: Integer)

shaderBallNumTrianglesLocation :: GLint
shaderBallNumTrianglesLocation = fromIntegral (36 :: Integer)

shaderBallPosLocation :: GLint
shaderBallPosLocation = fromIntegral (37 :: Integer)

shaderBallRotLocation :: GLint
shaderBallRotLocation = fromIntegral (38 :: Integer)

shaderSSBOVertexDataLocation :: GLuint
shaderSSBOVertexDataLocation = fromIntegral (17 :: Integer)

shaderSSBOGeomDataLocation :: GLuint
shaderSSBOGeomDataLocation = fromIntegral (18 :: Integer)

shaderSSBOLumpDataLocation :: GLuint
shaderSSBOLumpDataLocation = fromIntegral (19 :: Integer)

shaderSSBOPathDoublesDataLocation :: GLuint
shaderSSBOPathDoublesDataLocation = fromIntegral (20 :: Integer)

shaderSSBOPathInt32sDataLocation :: GLuint
shaderSSBOPathInt32sDataLocation = fromIntegral (21 :: Integer)

shaderSSBOBodyDataLocation :: GLuint
shaderSSBOBodyDataLocation = fromIntegral (22 :: Integer)

shaderSSBOGcDataLocation :: GLuint
shaderSSBOGcDataLocation = fromIntegral (23 :: Integer)

shaderSSBOAllGeomPassMvDataLocation :: GLuint
shaderSSBOAllGeomPassMvDataLocation = fromIntegral (24 :: Integer)

shaderSSBOAllGeomPassTexturesDataLocation :: GLuint
shaderSSBOAllGeomPassTexturesDataLocation = fromIntegral (25 :: Integer)

shaderSSBOAllGeomPassGisDataLocation :: GLuint
shaderSSBOAllGeomPassGisDataLocation = fromIntegral (26 :: Integer)

shaderSSBOGeomPassMvRangesDataLocation :: GLuint
shaderSSBOGeomPassMvRangesDataLocation = fromIntegral (27 :: Integer)

shaderSSBOGeomPassTexturesRangesDataLocation :: GLuint
shaderSSBOGeomPassTexturesRangesDataLocation = fromIntegral (28 :: Integer)

shaderSSBOGeomPassGisRangesDataLocation :: GLuint
shaderSSBOGeomPassGisRangesDataLocation = fromIntegral (29 :: Integer)

shaderSSBOGeomPassBisDataLocation :: GLuint
shaderSSBOGeomPassBisDataLocation = fromIntegral (30 :: Integer)

shaderSSBOTransformationLocation :: GLuint
shaderSSBOTransformationLocation = fromIntegral (31 :: Integer)

shaderSSBOTexcoordsDoubleDataLocation :: GLuint
shaderSSBOTexcoordsDoubleDataLocation = fromIntegral (32 :: Integer)

shaderSceneGeomPassIdxLocation :: GLint
shaderSceneGeomPassIdxLocation = fromIntegral (33 :: Integer)

-- | Whether the shaders use doubles or floats for the SSBOs.
--type ShaderDoubleType = Double
type ShaderDoubleType = Float

-- | Convert Doubles to the Double type the shaders use for SSBOs.
toShaderDoubleType :: Double -> ShaderDoubleType
--toShaderDoubleType = id
toShaderDoubleType = realToFrac

-- * Shader: low level

-- ImmutaballShaderHandle moved to avoid Template Haskell errors.

-- | Allocate an immutaball shader.
--
-- (The SDLManager runs the initializer concurrently.  If it didn't, it would
-- deadlock because we also dispatch to the SDL Manager to run commands.)
initImmutaballShader :: SDLManagerHandle -> ImmutaballIOF ImmutaballShaderHandle
initImmutaballShader sdlMgr =
	JoinIBIOF .
	Atomically (newEmptyTMVar) $ \mibsh ->
	(>>= \() -> Atomically (readTMVar mibsh) id) . sdlIBIO sdlMgr $ do
		-- We're the only shader manager in this application, so we don't need
		-- to worry about exclusion here currently.  But regardless, exclusion
		-- is already present because sdlMgr blocks if we, the caller, don't
		-- fork a thread for a general IBIO dispatch.
		vertexShader_   <- (BasicIBIOF . GLIO $ GLCreateShader GL_VERTEX_SHADER id  ) <* checkGLErrorsIB
		fragmentShader_ <- (BasicIBIOF . GLIO $ GLCreateShader GL_FRAGMENT_SHADER id) <* checkGLErrorsIB
		program         <- (BasicIBIOF . GLIO $ GLCreateProgram id                  ) <* checkGLErrorsIB
		mpipeline       <- (BasicIBIOF . GLIO $ GLGenProgramPipelines 1 id          ) <* checkGLErrorsIB
		let pipeline = unSingleton mpipeline
		let ibsh = ImmutaballShaderHandle {
			_ibshVertexShader   = vertexShader_,
			_ibshFragmentShader = fragmentShader_,
			_ibshProgram        = program,
			_ibshPipeline       = pipeline
		}
		warnIf (vertexShader_ == 0 && fragmentShader_ == 0) $ "Warning: initImmutaballShader: vertex and fragment shaders both have names 0!"
		-- Don't write it until we finish initialization, to keep things synchronized.
		rawInitializeImmutaballShaderContinue ibsh
		Atomically (writeTMVar mibsh ibsh) id
	where
		unSingleton [me] = me
		unSingleton _    = error "Internal error: initImmutaballShader expected a single result from GLGenProgramPipelines."
		warnIf :: Bool -> String -> ImmutaballIOF ()
		warnIf condition msg = do
			() <- if' (not condition) (pure ()) . BasicIBIOF $ PutStrLn msg ()
			return ()

-- | Deallocate an immutaball shader.
freeImmutaballShader :: SDLManagerHandle -> ImmutaballShaderHandle -> ImmutaballIOF ()
freeImmutaballShader sdlMgr ibsh = do
	let sdlGL1' = sdlGL1 sdlMgr
	sdlGL1' $ do
		GLDeleteProgramPipelines [(ibsh^.ibshPipeline)] ()
		GLDeleteProgram (ibsh^.ibshProgram) ()
		GLDeleteShader (ibsh^.ibshFragmentShader) ()
		GLDeleteShader (ibsh^.ibshVertexShader) ()

-- | (Note: we are already in the SDL Manager thread.)
rawInitializeImmutaballShaderContinue :: ImmutaballShaderHandle -> ImmutaballIOF ()
rawInitializeImmutaballShaderContinue ibsh = do
	glChecked $ GLShaderSource (ibsh^.ibshVertexShader) [vertexShader] ()
	glChecked $ GLShaderSource (ibsh^.ibshFragmentShader) [fragmentShader] ()

	glChecked $ GLCompileShader (ibsh^.ibshVertexShader) ()
	successV <- ((/= 0) <$>) . glChecked $ GLGetShaderiv (ibsh^.ibshVertexShader) GL_COMPILE_STATUS id
	when (not successV) $ do
		compileError <- BasicIBIOF . GLIO $ GLGetShaderInfoLog (ibsh^.ibshVertexShader) id
		() <- BasicIBIOF $ PutStrLn ("Error: the vertex shader failed to compile!  OpenGL error: " ++ compileError) ()
		checkGLErrorsIB
		() <- BasicIBIOF $ ExitFailureBasicIOF
		return ()

	glChecked $ GLCompileShader (ibsh^.ibshFragmentShader) ()
	successF <- ((/= 0) <$>) . glChecked $ GLGetShaderiv (ibsh^.ibshFragmentShader) GL_COMPILE_STATUS id
	when (not successF) $ do
		compileError <- glChecked $ GLGetShaderInfoLog (ibsh^.ibshFragmentShader) id
		() <- BasicIBIOF $ PutStrLn ("Error: the fragment shader failed to compile!  OpenGL error: " ++ compileError) ()
		checkGLErrorsIB
		() <- BasicIBIOF $ ExitFailureBasicIOF
		return ()

	when setupProgramPipeline $ do
		glChecked $ GLProgramParameteri (ibsh^.ibshProgram) GL_PROGRAM_SEPARABLE GL_TRUE ()

	glChecked $ GLAttachShader (ibsh^.ibshProgram) (ibsh^.ibshVertexShader) ()
	glChecked $ GLAttachShader (ibsh^.ibshProgram) (ibsh^.ibshFragmentShader) ()
	glChecked $ GLLinkProgram (ibsh^.ibshProgram) ()

	successL <- ((/= 0) <$>) . glChecked $ GLGetProgramiv (ibsh^.ibshProgram) GL_LINK_STATUS id
	when (not successL) $ do
		linkError <- BasicIBIOF . GLIO $ GLGetProgramInfoLog (ibsh^.ibshProgram) id
		() <- BasicIBIOF $ PutStrLn ("Error: the OpenGL GLSL shaders failed to link!  OpenGL error: " ++ linkError) ()
		checkGLErrorsIB
		() <- BasicIBIOF $ ExitFailureBasicIOF
		return ()

	when (not useProgramPipeline) $ do
		glChecked $ GLUseProgram (ibsh^.ibshProgram) ()
	when setupProgramPipeline $ do
		let stages = foldr (.|.) 0 $
			[
				GL_VERTEX_SHADER_BIT,
				GL_FRAGMENT_SHADER_BIT
			]
		-- TODO FIXME: when 'useProgramPipeline' is True, this fails with GL_INVALID_OPERATION for me.
		-- We're not using it anyway, though, so this isn't particularly urgent.
		glChecked $ GLUseProgramStages (ibsh^.ibshPipeline) stages (ibsh^.ibshProgram) ()
	when useProgramPipeline $ do
		glChecked $ GLUseProgram 0 ()
		glChecked $ GLBindProgramPipeline (ibsh^.ibshPipeline) ()
	where
		setupProgramPipeline :: Bool
		setupProgramPipeline = useProgramPipeline
		-- We only need the program.
		useProgramPipeline :: Bool
		useProgramPipeline = False

-- * MtrlMeta

-- | TODO:
data MtrlMeta = MtrlMeta {
}
	deriving (Eq, Ord, Show)
--makeLenses ''MtrlMeta

-- * Errors

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

data GLErrorVideoException = GLErrorVideoException String
instance Exception GLErrorVideoException where
	toException = videoExceptionToException
	fromException = videoExceptionFromException
instance Show GLErrorVideoException where
	show (GLErrorVideoException msg) = msg

-- * Utils

checkGLErrorsIB :: ImmutaballIOF ()
checkGLErrorsIB = do
	error_ <- BasicIBIOF . GLIO $ GLGetError id
	case error_ of
		GL_NO_ERROR -> return ()
		err -> do
			let msg = "Error: an OpenGL error occurred (" ++ show err ++ "): " ++ glErrType err
			() <- BasicIBIOF $ PutStrLn msg ()
			() <- ThrowIO (GLErrorVideoException msg)
			() <- BasicIBIOF $ ExitFailureBasicIOF
			return ()

glErrType :: GLenum -> String
glErrType GL_NO_ERROR                      = "GL_NO_ERROR"
glErrType GL_INVALID_ENUM                  = "GL_INVALID_ENUM"
glErrType GL_INVALID_VALUE                 = "GL_INVALID_VALUE"
glErrType GL_INVALID_OPERATION             = "GL_INVALID_OPERATION"
glErrType GL_INVALID_FRAMEBUFFER_OPERATION = "GL_INVALID_FRAMEBUFFER_OPERATION"
glErrType GL_OUT_OF_MEMORY                 = "GL_OUT_OF_MEMORY"
glErrType GL_STACK_OVERFLOW                = "GL_STACK_OVERFLOW"
glErrType GL_STACK_UNDERFLOW               = "GL_STACK_UNDERLOW"
glErrType _                                = "unknown error type"

glChecked :: GLIOF me -> ImmutaballIOF me
glChecked m = (BasicIBIOF . GLIO $ m) <* checkGLErrorsIB

numToGL_TEXTUREi :: M.Map Integer GLenum
numToGL_TEXTUREi = M.fromList $
	[
		(0,  GL_TEXTURE0),
		(1,  GL_TEXTURE1),
		(2,  GL_TEXTURE2),
		(3,  GL_TEXTURE3),
		(4,  GL_TEXTURE4),
		(5,  GL_TEXTURE5),
		(6,  GL_TEXTURE6),
		(7,  GL_TEXTURE7),
		(8,  GL_TEXTURE8),
		(9,  GL_TEXTURE9),
		(10, GL_TEXTURE10),
		(11, GL_TEXTURE11),
		(12, GL_TEXTURE12),
		(13, GL_TEXTURE13),
		(14, GL_TEXTURE14),
		(15, GL_TEXTURE15)
	]

makeLenses ''MtrlMeta