packages feed

gruff-0.2: src/Browser.hs

module Browser (Browser(..), browserNew, browserRenders) where

import Prelude hiding (log)
import Control.Concurrent
  ( forkIO, MVar, newMVar, takeMVar, putMVar, tryTakeMVar, threadDelay )
import Control.Monad (foldM, forever, forM_, replicateM, unless, when)
import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef)
import Data.List (sortBy)
import qualified Data.Map as M
import Data.Map (Map)
import Data.Maybe (fromMaybe)
import Data.Ord (comparing)
import qualified Data.Set as S
import Data.Set ((\\))
import Foreign (Ptr, malloc, alloca, peek, poke, nullPtr)
import Foreign.C (CFloat, CInt)
import GHC.Conc (numCapabilities)
import Graphics.Rendering.OpenGL hiding (viewport, Angle, Color, Point, Position, Size)
import qualified Graphics.Rendering.OpenGL as GL
import Graphics.Rendering.OpenGL.Raw
  ( glTexImage2D, gl_TEXTURE_2D, gl_R32F, gl_RGBA, gl_LUMINANCE
  , gl_FLOAT, gl_UNSIGNED_BYTE, gl_FALSE, glClampColor
  , gl_CLAMP_VERTEX_COLOR, gl_CLAMP_READ_COLOR, gl_CLAMP_FRAGMENT_COLOR
  , glGenFramebuffers, glBindFramebuffer, glFramebufferTexture2D
  , gl_FRAMEBUFFER, gl_COLOR_ATTACHMENT0, glGenerateMipmap
  , glGetError
  )
import Graphics.UI.Gtk hiding (get, Window, Viewport, Region, Size, Action, Image)
import qualified Graphics.UI.Gtk as GTK

import Fractal.RUFF.Types.Complex (Complex((:+)))

import Number (R)
import GLUTGtk
  (  GLUTGtk, Size(Size), Key(MouseButton), KeyState(Down), Position(Position)
  , postRedisplay, widget, realizeCallback, reshapeCallback
  , displayCallback, keyboardMouseCallback
  )
import Shader (shader)
import QuadTree (Quad(..))
import Tile (Tile(Tile), getTile, freeTile)
import Logger (Logger, LogLevel(Debug))
import qualified Logger as Log
import Snapshot (writeSnapshot)
import View
  ( Image(..), Location(..), Window(..), Viewport(..)
  , BufferSize(..), bufferSize, tileSize, delta
  , Colours(..), Colour(..), defColours
  , pixelLocation, originQuad, visibleQuads
  , defLocation, defWindow, defViewport
  )
import Paths_gruff (getDataFileName)

type TextureObject3 = (TextureObject, TextureObject, TextureObject)
type QuadList = [(Complex Int, Quad)]
data GruffImage = GruffImage
  { location :: Location
  , window :: Window
  , viewport :: Viewport
  , tiles :: Map Quad TextureObject3
  , queue :: MVar [Tile]
  , jobs :: MVar [Quad]
  , viewQuads :: (QuadList, QuadList)
  , workers :: [Ptr CInt]
  , progress :: Map (Ptr CInt) Quad
  , gl :: GLUTGtk
  , cacheDir :: FilePath
  , log :: LogLevel -> String -> IO ()
  , colours :: Colours
  , prog :: Program
  , combineProg :: Program
  , tsheet0 :: TextureObject
  , tsheet1 :: TextureObject
  , fbo :: GLuint
  , cacheSizeMin :: Int
  , cacheSizeMax :: Int
  -- callbacks
  , exitCallback :: Maybe (IO ())
  , reshapeCallback :: Maybe (Int -> Int -> IO ())
  , mouseCallback :: Maybe (Complex R -> Double -> IO ())
  , doneCallback :: Maybe (IO ())
  , abortCallback :: Maybe (IO ())
  , doClear :: Bool
  }

sheetSize :: GruffImage -> Size
sheetSize g = let s = texels . bufferSize . window $ g in Size s s

zoomPhase :: GruffImage -> Double
zoomPhase = delta . location

rotationA :: GruffImage -> Double
rotationA = orient . viewport

iDisplay :: IORef GruffImage -> IO ()
iDisplay iR = do
  prune iR
  s0 <- readIORef iR
  mtls <- tryTakeMVar (queue s0)
  case mtls of
    Just tls -> do
      putMVar (queue s0) []
      forM_ tls $ \tile@(Tile q ns ds ts) -> do
        tde <- upload ds
        tit <- upload ns
        ttt <- upload ts
        freeTile tile
        atomicModifyIORef iR $ \s' ->
          ( s'{ tiles = M.insert q (tde, tit, ttt) (tiles s')
              , progress = M.filter (/= q) (progress s') }
          , ())
    Nothing -> return ()
  s <- readIORef iR
  log s Debug $ "displayCallback " ++ show (location s)
  c <- atomicModifyIORef iR $ \s' -> (s'{ doClear = False }, doClear s')
  when c $ do
    clearSheet s False
    clearSheet s True
  todo0 <- renderSheet s False
  todo1 <- renderSheet s True
  atomicModifyIORef iR $ \s' -> (s'{ viewQuads = (todo0, todo1) }, ())
  let complete = null todo0 && null todo1
      w = width (window s)
      h = height (window s)
  GL.viewport $=
    (GL.Position 0 0, GL.Size (fromIntegral w) (fromIntegral h))
  loadIdentity
  ortho2D 0 (fromIntegral w) 0 (fromIntegral h)
  clearColor $= Color4 0.5 0.5 0.5 1
  clear [ColorBuffer]
  currentProgram $= Just (combineProg s)
  lsheet0 <- get $ uniformLocation (combineProg s) "sheet0"
  lsheet1 <- get $ uniformLocation (combineProg s) "sheet1"
  lblend  <- get $ uniformLocation (combineProg s) "blend"
  uniform lsheet0 $= TexCoord1 (0 :: GLint)
  uniform lsheet1 $= TexCoord1 (1 :: GLint)
  uniform lblend $= TexCoord1 (realToFrac (
    let p = negate $ zoomPhase s
        q = 4 -- realToFrac $ supersamples (window s) -- FIXME figure out correct blend
    in  1 - (1 - q**p) / (q**(p + 1) - q**p) ) :: GLfloat)
  activeTexture $= TextureUnit 0
  textureBinding Texture2D $= Just (tsheet0 s)
  activeTexture $= TextureUnit 1
  textureBinding Texture2D $= Just (tsheet1 s)
  let t x0 y0 = texCoord $ TexCoord2 (0.5 + x' :: GLdouble) (0.5 + y' :: GLdouble)
        where
          p = realToFrac . sqrt . aspect . viewport $ s
          x = k * x0 * p
          y = k * y0 / p
          a = - rotationA s
          co = realToFrac $ cos a
          si = realToFrac $ sin a
          x' =  co * x + si * y
          y' = -si * x + co * y
      v :: Int -> Int -> IO ()
      v x y = vertex $ Vertex2
        (fromIntegral x :: GLdouble) (fromIntegral y :: GLdouble)
      k = 0.25 * realToFrac (0.5 ** zoomPhase s) :: GLdouble
  renderPrimitive Quads $ do
    t (-1) 1 >> v 0 h
    t (-1) (-1) >> v 0 0
    t 1 (-1) >> v w 0
    t 1 1 >> v w h
  textureBinding Texture2D $= Nothing
  activeTexture $= TextureUnit 0
  textureBinding Texture2D $= Nothing
  currentProgram $= Nothing
  when complete $ case doneCallback s of
    Nothing -> return ()
    Just act -> do
      atomicModifyIORef iR $ \s' -> (s'{ doneCallback = Nothing, abortCallback = Nothing }, ())
      act
  err <- glGetError
  when (err /= 0) $ print ("error: " ++ show err)

clearSheet :: GruffImage -> Bool -> IO ()
clearSheet s b = do
  let Size tw' th' = sheetSize s
      tw = if b then tw' * 2 else tw'
      th = if b then th' * 2 else th'
      tsheet = (if b then tsheet1 else tsheet0) s
  bindFBO (fbo s) tsheet
  GL.viewport $=
    (GL.Position 0 0, GL.Size (fromIntegral tw) (fromIntegral th))
  loadIdentity
  clearColor $= Color4 0 0 1 1
  clear [ColorBuffer]
  unbindFBO
  textureBinding Texture2D $= Just tsheet
  glGenerateMipmap gl_TEXTURE_2D
  textureBinding Texture2D $= Nothing

renderSheet :: GruffImage -> Bool -> IO [(Complex Int, Quad)]
renderSheet s b = do
  let Size tw' th' = sheetSize s
      tw = if b then tw' * 2 else tw'
      th = if b then th' * 2 else th'
      tsheet = (if b then tsheet1 else tsheet0) s
      vquads = (if b then snd else fst) (viewQuads s)
  if null vquads then return [] else do
    bindFBO (fbo s) tsheet
    GL.viewport $=
      (GL.Position 0 0, GL.Size (fromIntegral tw) (fromIntegral th))
    loadIdentity
    ortho2D 0 (fromIntegral tw) 0 (fromIntegral th)
    currentProgram $= Just (prog s)
    lde <- get $ uniformLocation (prog s) "de"
    lit <- get $ uniformLocation (prog s) "it"
    ltt <- get $ uniformLocation (prog s) "tt"
    lint <- get $ uniformLocation (prog s) "interior"
    lbrd <- get $ uniformLocation (prog s) "border"
    lext <- get $ uniformLocation (prog s) "exterior"
    uniform lde $= TexCoord1 (0 :: GLint)
    uniform lit $= TexCoord1 (1 :: GLint)
    uniform ltt $= TexCoord1 (2 :: GLint)
    let (ci, cb, ce) = fromColours $ colours s
    uniform lint $= ci
    uniform lbrd $= cb
    uniform lext $= ce
    todo <- foldM (\a t -> do a' <- drawQuad (tiles s) t ; return (a' ++ a)) [] vquads
    currentProgram $= Nothing
    unbindFBO
    when (length todo < length vquads) $ do
      textureBinding Texture2D $= Just tsheet
      glGenerateMipmap gl_TEXTURE_2D
      textureBinding Texture2D $= Nothing
    return todo

drawQuad :: Map Quad TextureObject3 -> (Complex Int, Quad) -> IO [(Complex Int, Quad)]
drawQuad m ijq@(i :+ j, q) = case q `M.lookup` m of
  Nothing -> return [ijq]
  Just (tde, tit, ttt) -> do
    let t x y = texCoord $ TexCoord2 (x :: GLdouble) (y :: GLdouble)
        v x y = vertex $ Vertex2 (x :: GLdouble) (y :: GLdouble)
        x0 = fromIntegral i
        y0 = fromIntegral j
        x1 = x0 + fromIntegral tileSize
        y1 = y0 + fromIntegral tileSize
    activeTexture $= TextureUnit 0
    textureBinding Texture2D $= Just tde
    activeTexture $= TextureUnit 1
    textureBinding Texture2D $= Just tit
    activeTexture $= TextureUnit 2
    textureBinding Texture2D $= Just ttt
    renderPrimitive Quads $ do
      color $ Color3 1 1 (1::GLdouble)
      t 0 1 >> v x0 y1
      t 0 0 >> v x0 y0
      t 1 0 >> v x1 y0
      t 1 1 >> v x1 y1
    textureBinding Texture2D $= Nothing
    activeTexture $= TextureUnit 1
    textureBinding Texture2D $= Nothing
    activeTexture $= TextureUnit 0
    textureBinding Texture2D $= Nothing
    return []

iReshape :: IORef GruffImage -> Maybe Double -> Size -> IO ()
iReshape iR ms size'@(Size w h) = do
  s' <- readIORef iR
  log s' Debug $ "reshapeCallback " ++ show size'
  let ss = case ms of Nothing -> supersamples (window s') ; Just r -> r
  atomicModifyIORef iR $ \s -> (s{ window = (window s){ width = w, height = h, supersamples = ss }, viewport = (viewport s){ aspect = fromIntegral w / fromIntegral h } }, ())
  s'' <- readIORef iR
  unless (sheetSize s' == sheetSize s'') (reallocateBuffers iR)
  case Browser.reshapeCallback s' of
    Nothing -> return ()
    Just act -> act w h

iMouse :: IORef GruffImage -> Key -> KeyState -> [Modifier] -> Position -> IO ()
iMouse sR (MouseButton LeftButton  ) Down _ p@(Position x y) = do
  s <- readIORef sR
  log s Debug $ "leftMouse " ++ show p
  case mouseCallback s of
    Nothing -> return ()
    Just act -> do
      let o = fromPixel s x y
          cx :+ cy = center (location s)
          c = fromRational cx :+ fromRational cy
          r = radius (location s)
          r' = 0.9 * r
          c' = 0.9 * c + 0.1 * o
      act c' r'
iMouse sR (MouseButton MiddleButton) Down _ p@(Position x y) = do
  s <- readIORef sR
  log s Debug $ "middleMouse " ++ show p
  case mouseCallback s of
    Nothing -> return ()
    Just act -> act (fromPixel s x y) (radius (location s))
iMouse sR (MouseButton RightButton ) Down _ p@(Position x y) = do
  s <- readIORef sR
  log s Debug $ "rightMouse " ++ show p
  case mouseCallback s of
    Nothing -> return ()
    Just act -> do
      let o = fromPixel s x y
          cx :+ cy = center (location s)
          c = fromRational cx :+ fromRational cy
          r = radius (location s)
          r' = 1.1 * r
          c' = 1.1 * c - 0.1 * o
      act c' r'
iMouse _ _ _ _ _ = return ()

fromPixel :: GruffImage -> Double -> Double -> Complex R
fromPixel g x y = let r:+i = pixelLocation (window g) (viewport g) (location g) x y in fromRational r :+ fromRational i

quadDistance :: Quad -> Quad -> Double
quadDistance q0 q1 =
  let Quad{ quadLevel = l0, quadWest = r0, quadNorth = i0 } = q0
      Quad{ quadLevel = l, quadWest = r, quadNorth = i} = q1
      dl = sqr (fromIntegral l - fromIntegral l0)
      d x x0
        | l >  l0 = fromIntegral $ sqr (x - x0 * 2 ^ (l - l0))
        | l == l0 = fromIntegral $ sqr (x - x0)
        | l <  l0 = fromIntegral $ sqr (x0 - x * 2 ^ (l0 - l))
      d _ _ = error "score"
  in  dl + d r r0 + d i i0

sqr :: Num a => a -> a
sqr x = x * x

prune :: IORef GruffImage -> IO ()
prune sR = do
  s0 <- readIORef sR
  let cacheSize = M.size (tiles s0)
  when (cacheSize > cacheSizeMax s0) $ do
    log s0 Debug . concat $
      [ "pruning texture cache "
      , show cacheSize, " > ", show (cacheSizeMax s0)
      , " --> ", show (cacheSizeMin s0)
      ]
    bad <- atomicModifyIORef sR $ \s -> 
      let Just q0 = originQuad (location s) (bufferSize (window s))
          score = quadDistance q0
          o = comparing (score . fst)
          (good, bad)
            = splitAt (cacheSizeMin s) . sortBy o . M.toList . tiles $ s
      in  (s{ tiles = M.fromList good }, bad)
    let t (_, (t1, t2, t3)) = [t1, t2, t3]
    deleteObjectNames $ concatMap t bad

update :: IORef GruffImage -> IO ()
update sR = do
  s' <- readIORef sR
  log s' Debug $ "updateCallback "
  todo' <- atomicModifyIORef sR $ \s ->
    let vq@(qs0, qs1) = fromMaybe ([],[]) $ visibleQuads (window s) (viewport s) (location s)
        qs = qs0 ++ qs1
        todo = S.filter (`M.notMember` tiles s) (S.fromList (map snd qs))
    in  (s{ viewQuads = vq }, todo)
  -- cancel in-progress jobs that aren't still needed
  _ <- takeMVar (jobs s')
  p <- atomicModifyIORef sR $ \s -> (s{ progress = M.filter (`S.member` todo') (progress s) }, progress s)
  mapM_ (`poke` 1) . filter (\w -> case M.lookup w p of Nothing -> False ; Just q -> q `S.notMember` todo') . workers $ s'
  -- set new jobs
  s <- readIORef sR
  let Just q0 = originQuad (location s) (bufferSize (window s))
      score = quadDistance q0
  putMVar (jobs s') . sortBy (comparing score) . S.toList $ todo' \\ S.fromList (M.elems p)

putTile :: IORef GruffImage -> Ptr CInt -> Tile -> IO ()
putTile sR p t = do
  qu <- atomicModifyIORef sR $ \s ->
    (s{ progress = M.delete p (progress s) }, queue s)
  ts <- takeMVar qu
  putMVar qu (t:ts)

putJobs :: IORef GruffImage -> [Quad] -> IO ()
putJobs sR qs = do
  s <- readIORef sR
  _ <- takeMVar (jobs s)
  putMVar (jobs s) qs

takeJob :: IORef GruffImage -> Ptr CInt -> IO Quad
takeJob sR p = do
  s <- readIORef sR
  qs <- takeMVar (jobs s)
  case qs of
    [] -> do
      putMVar (jobs s) []
      threadDelay 10000
      takeJob sR p
    (q:qs') -> do
      atomicModifyIORef sR $ \s' ->
        ( s'{ progress = M.insert p q (progress s') }, ())
      putMVar (jobs s) qs'
      return q

worker :: IORef GruffImage -> Ptr CInt -> IO ()
worker sR p = forever $ do
  s <- readIORef sR
  q <- takeJob sR p
  mt <- getTile (log s Debug) (cacheDir s) p q
  case mt of
    Nothing -> return ()
    Just t -> putTile sR p t

timer :: IORef GruffImage -> IO ()
timer sR = do
  s <- readIORef sR
  mtls <- tryTakeMVar (queue s)
  case mtls of
    Just tls -> do
      putMVar (queue s) tls
      unless (null tls) $ postRedisplay (gl s)
    _ -> return ()

upload :: Ptr CFloat -> IO TextureObject
upload p = do
  [tex] <- genObjectNames 1
  texture Texture2D $= Enabled
  textureBinding Texture2D $= Just tex
  glTexImage2D gl_TEXTURE_2D 0 (fromIntegral gl_R32F)
    (fromIntegral tileSize) (fromIntegral tileSize) 0
      gl_LUMINANCE gl_FLOAT p
  textureFilter Texture2D $= ((Nearest, Nothing), Nearest)
  textureWrapMode Texture2D S $= (Repeated, ClampToEdge)
  textureWrapMode Texture2D T $= (Repeated, ClampToEdge)
  textureBinding Texture2D $= Nothing
  texture Texture2D $= Disabled
  return tex

msPerFrame :: Int
msPerFrame = 200

data Browser = Browser
  { browserWindow :: GTK.Window
  , browserRender :: Image -> IO () -> IO () -> IO ()
  , browserAbort  :: IO ()
  , browserSaveImage :: FilePath -> IO ()
  , browserSetExitCallback :: IO () -> IO ()
  , browserSetReshapeCallback :: (Int -> Int -> IO ()) -> IO ()
  , browserSetMouseCallback :: (Complex R -> Double -> IO ()) -> IO ()
  , browserResize :: Int -> Int -> Double -> IO ()
  }

browserRenders :: Browser -> [(Image, FilePath)] -> IO ()
browserRenders _ [] = print "done"
browserRenders b ((i, f):ifs) = browserRender b i
  (browserSaveImage b f >> browserRenders b ifs)
  (print "aborted")

browserNew :: GLUTGtk -> Pixbuf -> Logger -> FilePath -> IO Browser
browserNew gl' icon lg cacheDir' = do
  -- image window
  iw <- windowNew
  let defW = width  defWindow
      defH = height defWindow
  windowSetDefaultSize iw defW defH
  windowSetGeometryHints iw (Nothing `asTypeOf` Just iw)  (Just (160, 120)) (Just (2048, 1536)) Nothing Nothing Nothing
  set iw
    [ containerBorderWidth := 0
    , containerChild := widget gl'
    , windowIcon  := Just icon
    , windowTitle := "gruff browser"
    ]
  queue' <- newMVar []
  jobs' <- newMVar []
  iR <- newIORef GruffImage
      -- image parameters
    { location = defLocation
    , viewport = defViewport
    , window = defWindow
    , colours = defColours
      -- callbacks
    , exitCallback = Nothing
    , Browser.reshapeCallback = Nothing
    , mouseCallback = Nothing
    , doneCallback = Nothing
    , abortCallback = Nothing
      -- job queue
    , tiles = M.empty
    , queue = queue'
    , jobs = jobs'
    , progress = M.empty
    , viewQuads = ([], [])
    , gl = gl'
    , cacheDir = cacheDir'
    , log = Log.log lg
    , workers = []
    , prog = error "prog"
    , fbo = 0
    , combineProg = error "combineProg"
    , tsheet0 = TextureObject 0
    , tsheet1 = TextureObject 0
    , cacheSizeMin = 1000
    , cacheSizeMax = 1500
    , doClear = False
    }
  realizeCallback gl' $= iRealize iR
  GLUTGtk.reshapeCallback gl' $= iReshape iR Nothing
  displayCallback gl' $= iDisplay iR
  let browserAbort' = do
        i <- readIORef iR
        putJobs iR []
        mapM_ (`poke` 1) . workers $ i
        act <- atomicModifyIORef iR $ \s -> (s{ progress = M.empty, doneCallback = Nothing, abortCallback = Nothing }, abortCallback s)
        fromMaybe (return ()) act
      atExit = do
        i <- readIORef iR
        case exitCallback i of
          Nothing -> return ()
          Just act -> act
      browserRender' img done aborted = do
        atomicModifyIORef iR $ \i -> (i
          { doneCallback = Just done
          , abortCallback = Just aborted
          , location = imageLocation img
          , viewport = imageViewport img
          , window = imageWindow img
          , colours = imageColours img
          , doClear = True
          }, ())
        update iR
      browserSaveImage' fname = do
        s <- readIORef iR
        writeSnapshot fname (GL.Position 0 0) (GL.Size (fromIntegral (width (window s))) (fromIntegral (height (window s))))
      browserSetExitCallback' act = atomicModifyIORef iR $ \i ->
        ( i{ exitCallback = Just act }, () )
      browserSetReshapeCallback' act = atomicModifyIORef iR $ \i ->
        ( i{ Browser.reshapeCallback = Just act }, () )
      browserSetMouseCallback' act = atomicModifyIORef iR $ \i ->
        ( i{ mouseCallback = Just act }, () )
      browserResize' w h s = windowResize iw w h >> iReshape iR (Just s) (Size w h)
  keyboardMouseCallback gl' $= iMouse iR
  _ <- timeoutAdd (timer iR >> return True) msPerFrame
  _ <- iw `onDestroy` atExit
  return Browser
    { browserWindow = iw
    , browserRender = browserRender'
    , browserAbort = browserAbort'
    , browserSaveImage = browserSaveImage'
    , browserSetExitCallback = browserSetExitCallback'
    , browserSetReshapeCallback = browserSetReshapeCallback'
    , browserSetMouseCallback = browserSetMouseCallback'
    , browserResize = browserResize'
    }

fromColours :: Colours -> (Color3 GLfloat, Color3 GLfloat, Color3 GLfloat)
fromColours c =
  ( fromColour (colourInterior c)
  , fromColour (colourBoundary c)
  , fromColour (colourExterior c)
  )

fromColour :: Colour -> Color3 GLfloat
fromColour (Colour r g b) = Color3 (realToFrac r) (realToFrac g) (realToFrac b)

iRealize :: IORef GruffImage -> IO ()
iRealize iR = do
  s <- readIORef iR
  log s Debug "realizeCallback"
  drawBuffer $= BackBuffers
  glClampColor gl_CLAMP_VERTEX_COLOR gl_FALSE
  glClampColor gl_CLAMP_READ_COLOR gl_FALSE
  glClampColor gl_CLAMP_FRAGMENT_COLOR gl_FALSE
  f <- getDataFileName "minimal.frag"
  prog' <- shader Nothing (Just f)
  f' <- getDataFileName "merge.frag"
  combineProg' <- shader Nothing (Just f')
  fbo' <- newFBO
  [tsheet0', tsheet1'] <- genObjectNames 2
  ps <- replicateM numCapabilities $ do
    p <- malloc
    _ <- forkIO (worker iR p)
    return p
  atomicModifyIORef iR $ \i -> (i
    { prog = prog'
    , workers = ps
    , fbo = fbo'
    , combineProg = combineProg'
    , tsheet0 = tsheet0'
    , tsheet1 = tsheet1'
    }, ())
  reallocateBuffers iR

reallocateBuffers :: IORef GruffImage -> IO ()
reallocateBuffers iR = do
  s <- readIORef iR
  let Size tw' th' = sheetSize s
      ts = [tsheet0 s, tsheet1 s]
  forM_ (ts `zip` [(tw', th'), (tw' * 2, th' * 2)]) $ \(t, (tw, th)) -> do
    texture Texture2D $= Enabled
    textureBinding Texture2D $= Just t
    glTexImage2D gl_TEXTURE_2D 0 (fromIntegral gl_RGBA)
      (fromIntegral tw) (fromIntegral th) 0 gl_RGBA gl_UNSIGNED_BYTE
      nullPtr
    textureFilter Texture2D $= ((Linear', Just Linear'), Linear')
    textureWrapMode Texture2D S $= (Repeated, ClampToEdge)
    textureWrapMode Texture2D T $= (Repeated, ClampToEdge)
    textureBinding Texture2D $= Nothing
    texture Texture2D $= Disabled

newFBO :: IO GLuint
newFBO = alloca $ \p -> glGenFramebuffers 1 p >> peek p

bindFBO :: GLuint -> TextureObject -> IO ()
bindFBO f (TextureObject t) = do
  glBindFramebuffer gl_FRAMEBUFFER f
  glFramebufferTexture2D gl_FRAMEBUFFER gl_COLOR_ATTACHMENT0 gl_TEXTURE_2D t 0

unbindFBO :: IO ()
unbindFBO = do
  glFramebufferTexture2D gl_FRAMEBUFFER gl_COLOR_ATTACHMENT0 gl_TEXTURE_2D 0 0
  glBindFramebuffer gl_FRAMEBUFFER 0