flaw-graphics/Flaw/Graphics.hs

Summary

Maintainability
Test Coverage
{-|
Module: Flaw.Graphics
Description: Graphics abstraction.
License: MIT

Graphics system is mostly abstracted from backend. Almost everything from
uploading textures to writing shader programs can be done with zero amount
of backend-dependent code.

Initialization of graphics subsystem is still backend-dependent though.
Initialization includes creating instances of 'System', 'Device' and 'Presenter'
classes.

* 'System' instance allows to enumerate graphics hardware supported by backend,
get information about displays and display modes.

* 'Device' instance creates graphics resources such as render targets, textures,
shader programs, etc.

* 'Context' instance performs actual drawing. Draw operations run in 'Render' monad.

* 'Presenter' instance shows rendering results onto screen or window.

Abstraction is mostly follows DirectX 11 / OpenGL 3 model.
In a few places the lowest common denominator was choosen for certain features,
to allow one-to-one mapping from abstraction to implementation. Examples:

* Textures accept default sampling parameters, used when no sampler is bound;

* Program objects are created explicitly;

* There's no separate texture and sampler slots. Both a texture and a sampler are bound
into combined slot indexed by numeric index;

* Default framebuffer is bound automatically as part of initial state inside 'present'.

-}

{-# LANGUAGE DeriveGeneric, FlexibleContexts, FunctionalDependencies, MultiParamTypeClasses, RankNTypes, TypeFamilies #-}

module Flaw.Graphics
  ( System(..)
  , Device(..)
  , Context(..)
  , Presenter(..)
  , DeviceInfo(..)
  , DisplayInfo(..)
  , DisplayModeInfo(..)
  , IndexTopology(..)
  , IndexStride(..)
  , DepthTestFunc(..)
  , Render
  , renderScope
  , renderFrameBuffer
  , renderViewport
  , renderGetViewport
  , renderScissor
  , renderGetScissor
  , renderIntersectScissor
  , renderVertexBuffer
  , renderIndexBuffer
  , renderUniformBuffer
  , renderSampler
  , renderBlendState
  , renderDepthTestFunc
  , renderDepthWrite
  , renderProgram
  , renderClearColor
  , renderClearDepth
  , renderClearStencil
  , renderClearDepthStencil
  , renderUploadUniformBuffer
  , renderUploadVertexBuffer
  , renderDraw
  , renderDrawInstanced
  , renderPlay
  , render
  , present
  ) where

import Control.Exception
import Control.Monad.Trans.Class
import Control.Monad.Trans.Reader
import qualified Data.ByteString as B
import qualified Data.Serialize as S
import qualified Data.Text as T
import GHC.Generics(Generic)

import Flaw.Exception
import Flaw.Graphics.Blend
import Flaw.Graphics.Program.Internal
import Flaw.Graphics.Sampler
import Flaw.Graphics.Texture
import Flaw.Math
import Flaw.Stack

-- | Class of graphics system.
{- Initialization of graphics system is backend-dependent.
There're just a few general functions.
-}
class System s where
  -- | Type for id of graphics device.
  data DeviceId s :: *
  -- | Type for id of display.
  data DisplayId s :: *
  -- | Type for id of display mode.
  data DisplayModeId s :: *
  -- | Get list of graphics devices installed in system.
  getInstalledDevices :: s -> IO ([(DeviceId s, DeviceInfo s)], IO ())
  -- | Create custom display mode (with specified width and height) for specified display.
  createDisplayMode
    :: s
    -> DisplayId s -- ^ Display id.
    -> Int -- ^ Width.
    -> Int -- ^ Height.
    -> IO ((DisplayModeId s, DisplayModeInfo), IO ())

-- | Class of graphics device.
-- Graphics device performs managing of resources.
-- Also it works as a primary context for the device.
class Device d where
  -- | Type for deferred contexts.
  type DeferredContext d :: *
  -- | Type for texture id.
  data TextureId d :: *
  -- | Type for sampler state id.
  data SamplerStateId d :: *
  -- | Type for blend state id.
  data BlendStateId d :: *
  -- | Type for render target id.
  data RenderTargetId d :: *
  -- | Type for depth stencil target id.
  data DepthStencilTargetId d :: *
  -- | Type for framebuffer id.
  data FrameBufferId d :: *
  -- | Type for vertex buffer id.
  data VertexBufferId d :: *
  -- | Type for index buffer id.
  data IndexBufferId d :: *
  -- | Type for program id.
  data ProgramId d :: *
  -- | Type for uniform buffer id.
  data UniformBufferId d :: *

  -- | Null texture.
  nullTexture :: TextureId d
  -- | Null sampler state.
  nullSamplerState :: SamplerStateId d
  -- | Null blend state.
  nullBlendState :: BlendStateId d
  -- | Null depth stencil target.
  nullDepthStencilTarget :: DepthStencilTargetId d
  -- | Null vertex buffer.
  nullVertexBuffer :: VertexBufferId d
  -- | Null index buffer.
  nullIndexBuffer :: IndexBufferId d
  -- | Null uniform buffer.
  nullUniformBuffer :: UniformBufferId d

  -- | Create deferred context.
  createDeferredContext :: d -> IO (DeferredContext d, IO ())
  createDeferredContext _ = throwIO $ DescribeFirstException "creating deferred context is not supported"
  -- | Create static texture.
  createStaticTexture :: d -> TextureInfo -> SamplerStateInfo -> B.ByteString -> IO (TextureId d, IO ())
  -- | Create texture from image packed in any format natively supported by device.
  createNativeTexture :: d -> SamplerStateInfo -> B.ByteString -> IO (TextureId d, IO ())
  createNativeTexture _ _ _ = throwIO $ DescribeFirstException "creating native texture is not supported"
  -- | Create sampler state.
  createSamplerState :: d -> SamplerStateInfo -> IO (SamplerStateId d, IO ())
  -- | Create blend state.
  createBlendState :: d -> BlendStateInfo -> IO (BlendStateId d, IO ())
  -- | Create readable render target.
  createReadableRenderTarget :: d -> Int -> Int -> TextureFormat -> SamplerStateInfo -> IO ((RenderTargetId d, TextureId d), IO ())
  -- | Create depth stencil target.
  createDepthStencilTarget :: d -> Int -> Int -> IO (DepthStencilTargetId d, IO ())
  -- | Create readable depth stencil target.
  createReadableDepthStencilTarget :: d -> Int -> Int -> SamplerStateInfo -> IO ((DepthStencilTargetId d, TextureId d), IO ())
  -- | Create framebuffer.
  createFrameBuffer :: d -> [RenderTargetId d] -> DepthStencilTargetId d -> IO (FrameBufferId d, IO ())
  -- | Create static vertex buffer.
  createStaticVertexBuffer
    :: d -- ^ Device.
    -> B.ByteString -- ^ Buffer.
    -> Int -- ^ Stride in bytes.
    -> IO (VertexBufferId d, IO ())
  -- | Create dynamic vertex buffer.
  createDynamicVertexBuffer
    :: d -- ^ Device.
    -> Int -- ^ Size in bytes.
    -> Int -- ^ Stride in bytes.
    -> IO (VertexBufferId d, IO ())
  -- | Create index buffer.
  createStaticIndexBuffer :: d -> B.ByteString -> IndexTopology -> IndexStride -> IO (IndexBufferId d, IO ())
  -- | Create program.
  createProgram
    :: d -- ^ Device.
    -> Program () -- ^ Program contents.
    -> IO (ProgramId d, IO ())
  -- | Create uniform buffer.
  createUniformBuffer :: d -> Int -> IO (UniformBufferId d, IO ())

-- | Class of graphics context.
-- Performs actual render operations.
class Device d => Context c d | c -> d where
  ------- Immediate commands.
  -- | Clear render target.
  contextClearColor :: c -> Int -> Float4 -> IO ()
  -- | Clear depth.
  contextClearDepth :: c -> Float -> IO ()
  -- | Clear stencil.
  contextClearStencil :: c -> Int -> IO ()
  -- | Clear depth and stencil.
  contextClearDepthStencil :: c -> Float -> Int -> IO ()
  -- | Upload data to uniform buffer.
  contextUploadUniformBuffer :: c -> UniformBufferId d -> B.ByteString -> IO ()
  -- | Upload data to dynamic vertex buffer.
  contextUploadVertexBuffer :: c -> VertexBufferId d -> B.ByteString -> IO ()
  -- | Draw (instanced).
  contextDraw :: c
    -> Int -- ^ Instances count (1 for non-instanced).
    -> Int -- ^ Indices count.
    -> IO ()
  -- | Replay deferred context on immediate context.
  contextPlay :: Context dc d => c -> dc -> IO ()
  contextPlay _ _ = throwIO $ DescribeFirstException "playing deferred context is not supported"
  -- | Perform rendering. Initial state is context's default state.
  contextRender :: c -> IO a -> IO a
  ------- Setup commands.
  -- | Set framebuffer.
  contextSetFrameBuffer :: c -> FrameBufferId d -> IO a -> IO a
  -- | Set viewport (left, top, right, bottom).
  contextSetViewport :: c -> Int4 -> IO a -> IO a
  -- | Get current viewport.
  contextGetViewport :: c -> IO Int4
  -- | Set scissor (left, top, right, bottom).
  contextSetScissor :: c -> Maybe Int4 -> IO a -> IO a
  -- | Get current scissor.
  contextGetScissor :: c -> IO (Maybe Int4)
  -- | Set vertex buffer.
  contextSetVertexBuffer :: c -> Int -> VertexBufferId d -> IO a -> IO a
  -- | Set index buffer.
  contextSetIndexBuffer :: c -> IndexBufferId d -> IO a -> IO a
  -- | Set uniform buffer.
  contextSetUniformBuffer :: c -> Int -> UniformBufferId d -> IO a -> IO a
  -- | Set sampler.
  contextSetSampler :: c -> Int -> TextureId d -> SamplerStateId d -> IO a -> IO a
  -- | Set blend state.
  contextSetBlendState :: c -> BlendStateId d -> IO a -> IO a
  -- | Set depth-test function.
  contextSetDepthTestFunc :: c -> DepthTestFunc -> IO a -> IO a
  -- | Set depth write flag.
  contextSetDepthWrite :: c -> Bool -> IO a -> IO a
  -- | Set program.
  contextSetProgram :: c -> ProgramId d -> IO a -> IO a

-- | Presenter class.
class (System s, Context c d) => Presenter p s c d | p -> s c d where
  setPresenterMode :: p -> Maybe (DisplayModeId s) -> IO ()
  -- | Perform rendering on presenter's surface.
  -- Presenter's framebuffer, viewport, etc will be automatically set
  -- as an initial state.
  presenterRender :: p -> c -> IO a -> IO a

-- | Device information structure.
data DeviceInfo device = DeviceInfo
  { deviceName :: !T.Text
  , deviceDisplays :: [(DisplayId device, DisplayInfo device)]
  }

-- | Display information structure.
data DisplayInfo device = DisplayInfo
  { displayName :: !T.Text
  , displayModes :: [(DisplayModeId device, DisplayModeInfo)]
  }

-- | Display mode information structure.
data DisplayModeInfo = DisplayModeInfo
  { displayModeName :: !T.Text
  , displayModeWidth :: !Int
  , displayModeHeight :: !Int
  , displayModeRefreshRate :: !Rational
  } deriving Show

-- | Index topology.
data IndexTopology
  = IndexTopologyPoints
  | IndexTopologyLines
  | IndexTopologyLineStrip
  | IndexTopologyTriangles
  | IndexTopologyTriangleStrip
  | IndexTopologyPatches {-# UNPACK #-} !Int
  deriving (Eq, Generic)
instance S.Serialize IndexTopology

-- | Index stride.
data IndexStride
  = IndexStride32Bit
  | IndexStride16Bit
  deriving (Eq, Generic)
instance S.Serialize IndexStride

-- | Depth test function.
data DepthTestFunc
  = DepthTestFuncNever
  | DepthTestFuncLess
  | DepthTestFuncLessOrEqual
  | DepthTestFuncEqual
  | DepthTestFuncNonEqual
  | DepthTestFuncGreaterOrEqual
  | DepthTestFuncGreater
  | DepthTestFuncAlways
  deriving Eq

-- | Rendering monad.
type Render c = StackT (ReaderT c IO)

renderSetup :: (forall a. c -> IO a -> IO a) -> Render c ()
renderSetup setup = StackT $ \q -> do
  c <- ask
  mapReaderT (setup c) $ q ()

renderAction :: (c -> IO a) -> Render c a
renderAction action = StackT $ \q -> do
  c <- ask
  lift (action c) >>= q

-- | Scope for rendering state.
-- Context state will be restored after the scope.
renderScope :: Render c a -> Render c a
renderScope = scope

-- | Set current framebuffer.
renderFrameBuffer :: Context c d => FrameBufferId d -> Render c ()
renderFrameBuffer fb = renderSetup $ \c q -> contextSetFrameBuffer c fb q

-- | Set current viewport (vector with left, top, right, bottom).
renderViewport :: Context c d => Int4 -> Render c ()
renderViewport viewport = renderSetup $ \c q -> contextSetViewport c viewport q

-- | Get current viewport.
renderGetViewport :: Context c d => Render c Int4
renderGetViewport = renderAction contextGetViewport

-- | Set current scissor (vector with left, top, right, bottom).
renderScissor :: Context c d => Maybe Int4 -> Render c ()
renderScissor scissor = renderSetup $ \c q -> contextSetScissor c scissor q

-- | Get current scissor.
renderGetScissor :: Context c d => Render c (Maybe Int4)
renderGetScissor = renderAction contextGetScissor

-- | Set intersection between specified and current scissor as scissor.
renderIntersectScissor :: Context c d => Int4 -> Render c ()
renderIntersectScissor scissor@(Vec4 left top right bottom) = do
  currentScissor <- renderGetScissor
  renderScissor $ Just $ case currentScissor of
    Just (Vec4 currentLeft currentTop currentRight currentBottom) ->
      Vec4 (max left currentLeft) (max top currentTop) (min right currentRight) (min bottom currentBottom)
    Nothing -> scissor

-- | Set vertex buffer.
renderVertexBuffer :: Context c d => Int -> VertexBufferId d -> Render c ()
renderVertexBuffer i vb = renderSetup $ \c q -> contextSetVertexBuffer c i vb q

-- | Set current index buffer.
renderIndexBuffer :: Context c d => IndexBufferId d -> Render c ()
renderIndexBuffer ib = renderSetup $ \c q -> contextSetIndexBuffer c ib q

-- | Set uniform buffer.
renderUniformBuffer :: Context c d => Int -> UniformBufferId d -> Render c ()
renderUniformBuffer i ub = renderSetup $ \c q -> contextSetUniformBuffer c i ub q

-- | Set sampler.
renderSampler :: Context c d => Int -> TextureId d -> SamplerStateId d -> Render c ()
renderSampler i t s = renderSetup $ \c q -> contextSetSampler c i t s q

-- | Set blend state.
renderBlendState :: Context c d => BlendStateId d -> Render c ()
renderBlendState b = renderSetup $ \c q -> contextSetBlendState c b q

-- | Set depth test function.
renderDepthTestFunc :: Context c d => DepthTestFunc -> Render c ()
renderDepthTestFunc f = renderSetup $ \c q -> contextSetDepthTestFunc c f q

-- | Set depth write flag.
renderDepthWrite :: Context c d => Bool -> Render c ()
renderDepthWrite f = renderSetup $ \c q -> contextSetDepthWrite c f q

-- | Set current program.
renderProgram :: Context c d => ProgramId d -> Render c ()
renderProgram p = renderSetup $ \c q -> contextSetProgram c p q

-- | Clear render target.
renderClearColor :: Context c d => Int -> Float4 -> Render c ()
renderClearColor i color = renderAction $ \c -> contextClearColor c i color

-- | Clear depth.
renderClearDepth :: Context c d => Float -> Render c ()
renderClearDepth depth = renderAction $ \c -> contextClearDepth c depth

-- | Clear stencil.
renderClearStencil :: Context c d => Int -> Render c ()
renderClearStencil stencil = renderAction $ \c -> contextClearStencil c stencil

-- | Clear depth and stencil.
renderClearDepthStencil :: Context c d => Float -> Int -> Render c ()
renderClearDepthStencil depth stencil = renderAction $ \c -> contextClearDepthStencil c depth stencil

-- | Upload data to uniform buffer.
renderUploadUniformBuffer :: Context c d => UniformBufferId d -> B.ByteString -> Render c ()
renderUploadUniformBuffer ub bytes = renderAction $ \c -> contextUploadUniformBuffer c ub bytes

-- | Upload data to dynamic vertex buffer.
renderUploadVertexBuffer :: Context c d => VertexBufferId d -> B.ByteString -> Render c ()
renderUploadVertexBuffer vb bytes = renderAction $ \c -> contextUploadVertexBuffer c vb bytes

-- | Draw.
renderDraw :: Context c d
  => Int -- ^ Indices count.
  -> Render c ()
renderDraw = renderDrawInstanced 1

-- | Draw instanced.
renderDrawInstanced :: Context c d
  => Int -- ^ Instances count.
  -> Int -- ^ Indices count.
  -> Render c ()
renderDrawInstanced instancesCount indicesCount = renderAction $ \c -> contextDraw c instancesCount indicesCount

-- | Play deferred context on immediate context.
renderPlay :: (Context c d, Context dc d) => dc -> Render c ()
renderPlay deferredContext = renderAction $ \c -> contextPlay c deferredContext

-- | Perform offscreen rendering.
render :: Context c d => c -> Render c a -> IO a
render c f = contextRender c $ runReaderT (runStackT f) c

-- | Perform rendering on presenter.
present :: Presenter p s c d => p -> Render c a -> Render c a
present p f = renderAction $ \c -> presenterRender p c $ runReaderT (runStackT f) c