packages feed

gruff-0.4: src/Browser.hs

{-# LANGUAGE PatternSynonyms #-}
module Browser (Browser(..), browserNew, browserRenders) where

import Prelude hiding (log, lines)
import Control.Concurrent
  ( forkIO, MVar, newEmptyMVar, 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, S, R )
import qualified Graphics.Rendering.OpenGL as GL
import Graphics.GL
  ( glTexImage2D, pattern GL_TEXTURE_2D, pattern GL_R32F, pattern GL_RGBA, pattern GL_RED
  , pattern GL_FLOAT, pattern GL_UNSIGNED_BYTE, pattern GL_FALSE, glClampColor
  , pattern GL_CLAMP_VERTEX_COLOR, pattern GL_CLAMP_READ_COLOR, pattern GL_CLAMP_FRAGMENT_COLOR
  , glGenFramebuffers, glBindFramebuffer, glFramebufferTexture2D
  , pattern GL_FRAMEBUFFER, pattern GL_COLOR_ATTACHMENT0, glGenerateMipmap
  , pattern GL_FRAMEBUFFER_COMPLETE, glCheckFramebufferStatus
  , glGetError
  )
import Graphics.UI.Gtk hiding
  ( get, Window, Viewport, Region, Size, Action, Image, Label, labelText )
import qualified Graphics.UI.Gtk as GTK

import Graphics.Rendering.FTGL as FTGL

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

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(..), Window(..), Viewport(..), Label(..), Line(..)
  , BufferSize(..), bufferSize, tileSize, delta
  , Colours(..), Colour(..)
  , pixelLocation, originQuad, visibleQuads, locationPixel
  , defImage, defWindow
  )
import Interact (MouseCallbacks, Mod(..), But(..))

import Paths_gruff (getDataFileName)

type TextureObject3 = (TextureObject, TextureObject, TextureObject)
type QuadList = [(Complex Int, Quad)]
data GruffImage = GruffImage
  { image :: Image
  -- tile cache
  , 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 ()
  , prog :: Program
  , combineProg :: Program
  , tsheet0 :: TextureObject
  , tsheet1 :: TextureObject
  , sheetCount0 :: Int
  , sheetCount1 :: Int
  , sheetCountTarget :: Int
  , fbo :: GLuint
  , cacheSizeMin :: Int
  , cacheSizeMax :: Int
  -- callbacks
  , exitCallback :: Maybe (IO ())
  , reshapeCallback :: Maybe (Int -> Int -> IO ())
  , mouseCallback :: MouseCallbacks
  , doneCallback :: Maybe (IO ())
  , abortCallback :: Maybe (IO ())
  , doClear :: Bool
  , font :: FTGL.Font
  }

sheetSize :: GruffImage -> BufferSize
sheetSize g = bufferSize (sheetOffset g) (imageWindow (image g))

zoomPhase :: GruffImage -> Double
zoomPhase = delta . imageLocation . image

rotationA :: GruffImage -> Double
rotationA = orient . imageViewport . image

sheetBlend :: GruffImage -> (Int, (Double, Double))
sheetBlend g =
  let z = zoomPhase g
      s = supersamples (imageWindow (image g))
      b = logBase 4 (4 ** z * s)
      h = floor b
      d = fromIntegral h - b
  in  (h, (d, 1 - (1 - 4**d) / (4**(d+1) - 4**d)))

sheetOffset :: GruffImage -> Int
sheetOffset = fst . sheetBlend

{-
sheetDelta :: GruffImage -> Double
sheetDelta = fst . snd . sheetBlend
-}

blendFactor :: GruffImage -> Double
blendFactor = snd . snd . sheetBlend

iDisplay :: IORef GruffImage -> IO ()
iDisplay iR = do
  checkError "display begin"
  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
        checkError "upload ds"
        tit <- upload ns
        checkError "upload ns"
        ttt <- upload ts
        checkError "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 (imageLocation (image s))
  c <- atomicModifyIORef iR $ \s' -> (s'{ doClear = False }, doClear s')
  when c $ do
    clearSheet s False
    checkError "clear sheet F"
    clearSheet s True
    checkError "clear sheet T"
    atomicModifyIORef iR $ \s' -> (s'{ sheetCount0 = 0, sheetCount1 = 0 }, ())
  (todo0, upped0) <- renderSheet s False
  checkError "render sheet F"
  (todo1, upped1) <- renderSheet s True
  checkError "render sheet T"
  atomicModifyIORef iR $ \s' ->
    ( s'{ viewQuads = (todo0, todo1)
        , sheetCount0 = sheetCount0 s' + upped0
        , sheetCount1 = sheetCount1 s' + upped1
        }, () )
  let w = width (imageWindow (image s))
      h = height (imageWindow (image s))
  GL.viewport $=
    (GL.Position 0 0, GL.Size (fromIntegral w) (fromIntegral h))
  checkError "clear V"
  loadIdentity
  checkError "clear I"
  ortho2D 0 (fromIntegral w) 0 (fromIntegral h)
  checkError "clear O"
  clearColor $= Color4 0.5 0.5 0.5 1
  checkError "clear CC"
  clear [ColorBuffer]
  checkError "clear C"
  currentProgram $= Just (combineProg s)
  checkError "shader 1"
  lsheet0 <- get $ uniformLocation (combineProg s) "sheet0"
  checkError "shader 2a"
  lsheet1 <- get $ uniformLocation (combineProg s) "sheet1"
  checkError "shader 2b"
  lblend  <- get $ uniformLocation (combineProg s) "blend"
  checkError "shader 2c"
  uniform lsheet0 $= TexCoord1 (0 :: GLint)
  checkError "shader 3a"
  uniform lsheet1 $= TexCoord1 (1 :: GLint)
  checkError "shader 3b"
  uniform lblend $= TexCoord1 (realToFrac (blendFactor s) :: GLfloat)
  checkError "shader 3c"
  activeTexture $= TextureUnit 0
  checkError "shader 4"
  textureBinding Texture2D $= Just (tsheet0 s)
  checkError "shader 5"
  activeTexture $= TextureUnit 1
  checkError "shader 6"
  textureBinding Texture2D $= Just (tsheet1 s)
  checkError "shader 7"
  let t x0 y0 = texCoord $ TexCoord2 (0.5 + x' :: GLdouble) (0.5 + y' :: GLdouble)
        where
          p = realToFrac . sqrt . aspect . imageViewport . image $ 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.125 * 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
  checkError "quad"
  textureBinding Texture2D $= Nothing
  checkError "shader 8"
  activeTexture $= TextureUnit 0
  checkError "shader 9"
  textureBinding Texture2D $= Nothing
  checkError "shader A"
  currentProgram $= Nothing
  checkError "shader B"

  -- save state
  m <- get matrixMode
  matrixMode $= Modelview 0
  colour0 <- get currentColor
  -- pixel mapping
  let i = image s
      locP = locationPixel (imageWindow i) (imageViewport i) (imageLocation i)
      locL (a, b) = [locP a, locP b]
  -- draw lines
  forM_ (imageLines i) $ \l -> do
    let ps = concatMap locL (lineSegments l)
        Colour r g b = lineColour l
    currentColor $= Color4 (realToFrac r) (realToFrac g) (realToFrac b) 1
    renderPrimitive Lines $ forM_ ps $ \(x, y) -> do
      vertex $ Vertex2 (realToFrac x) (fromIntegral h - realToFrac y :: GLdouble)
  -- draw labels
  forM_ (imageLabels i) $ \l -> do
    let (x, y) = locP (labelCoords l)
        Colour r g b = labelColour l
    currentColor $= Color4 (realToFrac r) (realToFrac g) (realToFrac b) 1
    translate (Vector3 (realToFrac x) (fromIntegral h - realToFrac y) (0 :: GLdouble))
    renderFont (font s) (labelText l) All
    translate (Vector3 (-realToFrac x) (realToFrac y - fromIntegral h) (0 :: GLdouble))
  -- restore state
  currentColor $= colour0
  matrixMode $= m

  s'' <- readIORef iR
  when (sheetCount0 s'' + sheetCount1 s'' >= sheetCountTarget s'') $ do
    case doneCallback s of
      Nothing -> return ()
      Just act -> do
        atomicModifyIORef iR $ \s' -> (s'{ doneCallback = Nothing, abortCallback = Nothing }, ())
        act
  prune iR
  checkError "display end"

clearSheet :: GruffImage -> Bool -> IO ()
clearSheet s b = do
  checkError "clearSheet begin"
  let tw' = texels $ sheetSize s
      th' = tw'
      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
  checkError "clearSheet fbo bind"
  GL.viewport $=
    (GL.Position 0 0, GL.Size (fromIntegral tw) (fromIntegral th))
  checkError "clearSheet V"
  loadIdentity
  checkError "clearSheet I"
  clearColor $= Color4 0 0 1 1
  checkError "clearSheet CC"
  clear [ColorBuffer]
  checkError "clearSheet C"
  unbindFBO
  checkError "clearSheet fbo unbind"
  textureBinding Texture2D $= Just tsheet
  checkError "clearSheet tex bind"
  glGenerateMipmap GL_TEXTURE_2D
  checkError "clearSheet tex mipmap"
  textureBinding Texture2D $= Nothing
  checkError "clearSheet tex unbind"
  checkError "clear end"

renderSheet :: GruffImage -> Bool -> IO ([(Complex Int, Quad)], Int)
renderSheet s b = do
  checkError "renderSheet begin"
  let tw' = texels $ sheetSize s
      th' = tw'
      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 ([], 0) else do
    bindFBO (fbo s) tsheet
    checkError "renderSheet fbo bind"
    GL.viewport $=
      (GL.Position 0 0, GL.Size (fromIntegral tw) (fromIntegral th))
    checkError "renderSheet V"
    loadIdentity
    checkError "renderSheet I"
    ortho2D 0 (fromIntegral tw) 0 (fromIntegral th)
    checkError "renderSheet O"
    currentProgram $= Just (prog s)
    checkError "renderSheet shader 1"
    lde <- get $ uniformLocation (prog s) "de"
    checkError "renderSheet shader u1"
    lit <- get $ uniformLocation (prog s) "it"
    checkError "renderSheet shader u2"
    ltt <- get $ uniformLocation (prog s) "tt"
    checkError "renderSheet shader u3"
    lint <- get $ uniformLocation (prog s) "interior"
    checkError "renderSheet shader u4"
    lbrd <- get $ uniformLocation (prog s) "border"
    checkError "renderSheet shader u5"
    lext <- get $ uniformLocation (prog s) "exterior"
    checkError "renderSheet shader u6"
    uniform lde $= TexCoord1 (0 :: GLint)
    checkError "renderSheet shader t1"
    uniform lit $= TexCoord1 (1 :: GLint)
    checkError "renderSheet shader t2"
    uniform ltt $= TexCoord1 (2 :: GLint)
    checkError "renderSheet shader t3"
    let (ci, cb, ce) = fromColours . imageColours . image $ s
    uniform lint $= ci
    checkError "renderSheet shader s1"
    uniform lbrd $= cb
    checkError "renderSheet shader s2"
    uniform lext $= ce
    checkError "renderSheet shader s3"
    todo <- foldM (\a t -> do a' <- drawQuad (tiles s) t ; return (a' ++ a)) [] vquads
    checkError "renderSheet drawQuads"
    currentProgram $= Nothing
    checkError "renderSheet shader 2"
    unbindFBO
    checkError "renderSheet fbo unbind"
    let upped = length vquads - length todo
    when (upped > 0) $ do
      textureBinding Texture2D $= Just tsheet
      checkError "renderSheet tex bind"
      glGenerateMipmap GL_TEXTURE_2D
      checkError "renderSheet tex mipmap"
      textureBinding Texture2D $= Nothing
      checkError "renderSheet tex unbind"
    checkError "renderSheet end"
    return (todo, upped)

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
    checkError "drawQuad begin"
    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
    checkError "drawQuad tex 0"
    textureBinding Texture2D $= Just tde
    checkError "drawQuad tex 0b"
    activeTexture $= TextureUnit 1
    checkError "drawQuad tex 1"
    textureBinding Texture2D $= Just tit
    checkError "drawQuad tex 1b"
    activeTexture $= TextureUnit 2
    checkError "drawQuad tex 2"
    textureBinding Texture2D $= Just ttt
    checkError "drawQuad tex 2b"
    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
    checkError "drawQuad render"
    textureBinding Texture2D $= Nothing
    checkError "drawQuad tex 3b"
    activeTexture $= TextureUnit 1
    checkError "drawQuad tex 3"
    textureBinding Texture2D $= Nothing
    checkError "drawQuad tex 4b"
    activeTexture $= TextureUnit 0
    checkError "drawQuad tex 4"
    textureBinding Texture2D $= Nothing
    checkError "drawQuad tex 5b"
    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 (imageWindow (image s')) ; Just r -> r
  atomicModifyIORef iR $ \s -> (s{ image = (image s)
    { imageWindow = (imageWindow (image s)){ width = w, height = h, supersamples = ss }
    , imageViewport = (imageViewport (image 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 but) Down mods p@(Position x y) | but `elem` buts = do
  let shift = Shift   `elem` mods
      ctrl  = Control `elem` mods
      m | shift && ctrl = SC
        | ctrl          = C
        | shift         = S
        | otherwise     = U
      b = case but of
        LeftButton   -> L
        MiddleButton -> M
        RightButton  -> R
        _ -> error "weasels!!!!111one"
  s <- readIORef sR
  log s Debug $ "mouse " ++ show (b, m) ++ " " ++ show p
  case (b, m) `M.lookup` mouseCallback s of
    Nothing -> return ()
    Just cb -> do
      let i = image s
          c = pixelLocation (imageWindow i) (imageViewport i) (imageLocation i) x y
      cb c i
  where buts = [LeftButton, MiddleButton, RightButton]
iMouse _ _ _ _ _ = return ()

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
    checkError "prune begin"
    log s0 Debug . concat $
      [ "pruning texture cache "
      , show cacheSize, " > ", show (cacheSizeMax s0)
      , " --> ", show (cacheSizeMin s0)
      ]
    bad <- atomicModifyIORef sR $ \s -> 
      let Just q0 = originQuad (imageLocation (image s)) (sheetSize 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
    checkError "prune end"

update :: IORef GruffImage -> IO ()
update sR = do
  s' <- readIORef sR
  log s' Debug $ "updateCallback "
  todo' <- atomicModifyIORef sR $ \s ->
    let i = image s
        vq@(qs0, qs1) = fromMaybe ([],[]) $
          visibleQuads (imageWindow i) (imageViewport i) (imageLocation i) (sheetOffset s)
        qs = qs0 ++ qs1
        todo = S.filter (`M.notMember` tiles s) (S.fromList (map snd qs))
    in  (s{ viewQuads = vq, sheetCountTarget = length qs }, 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 (imageLocation (image s)) (sheetSize 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
  checkError "upload begin"
  [tex] <- genObjectNames 1
  checkError "upload 1"
  texture Texture2D $= Enabled
  checkError "upload 2"
  textureBinding Texture2D $= Just tex
  checkError "upload 3"
  glTexImage2D GL_TEXTURE_2D 0 (fromIntegral GL_R32F)
    (fromIntegral tileSize) (fromIntegral tileSize) 0
      GL_RED GL_FLOAT p
  checkError "upload 4"
  textureFilter Texture2D $= ((Nearest, Nothing), Nearest)
  checkError "upload 5"
  textureWrapMode Texture2D GL.S $= (Repeated, ClampToEdge)
  checkError "upload 6"
  textureWrapMode Texture2D GL.T $= (Repeated, ClampToEdge)
  checkError "upload 7"
  textureBinding Texture2D $= Nothing
  checkError "upload 7"
  texture Texture2D $= Disabled
  checkError "upload end"
  return tex

msPerFrame :: Int
msPerFrame = 200

data Browser = Browser
  { browserWindow :: GTK.Window
  , browserGL :: GLUTGtk
  , browserRender :: Image -> IO () -> IO () -> IO ()
  , browserAbort  :: IO ()
  , browserSaveImage :: FilePath -> IO ()
  , browserSetExitCallback :: IO () -> IO ()
  , browserSetReshapeCallback :: (Int -> Int -> IO ()) -> IO ()
  , browserSetMouseCallback :: MouseCallbacks -> IO ()
  }

browserRenders :: Browser -> [(Image, FilePath)] -> IO ()
browserRenders _ [] = print "done"
browserRenders b ((i, f):ifs) = do
  result <- newEmptyMVar
  postGUISync $ do
          browserRender b i
            (browserSaveImage b f >> putMVar result True)
            (putMVar result False)
          postRedisplay (browserGL b)
  r <- takeMVar result
  if r then browserRenders b ifs else 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 (4096, 4096)) 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
    { image = defImage
      -- callbacks
    , exitCallback = Nothing
    , Browser.reshapeCallback = Nothing
    , mouseCallback = M.empty
    , 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
    , sheetCount0 = 0
    , sheetCount1 = 0
    , sheetCountTarget = maxBound
    , cacheSizeMin = 1000
    , cacheSizeMax = 1500
    , doClear = False
    , font = error "font"
    }
  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
        s <- readIORef iR
        let wr = width  (imageWindow img)
            hr = height (imageWindow img)
        unless (width  (imageWindow (image s)) == wr &&
                height (imageWindow (image s)) == hr) $ do
          windowResize iw wr hr
        atomicModifyIORef iR $ \s' -> (s'{ image = img }, ())
        s' <- readIORef iR
        unless (sheetSize s == sheetSize s') (reallocateBuffers iR)
        atomicModifyIORef iR $ \i -> (i
          { doneCallback = Just done
          , abortCallback = Just aborted
          , doClear = True
          }, ())
        update iR
      browserSaveImage' fname = do
        s <- readIORef iR
        writeSnapshot fname (GL.Position 0 0)
          (GL.Size (fromIntegral (width (imageWindow (image s))))
                   (fromIntegral (height (imageWindow (image 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 = act }, () )
  keyboardMouseCallback gl' $= iMouse iR
  _ <- timeoutAdd (timer iR >> return True) msPerFrame
  _ <- iw `onDestroy` atExit
  return Browser
    { browserWindow = iw
    , browserGL = gl'
    , browserRender = browserRender'
    , browserAbort = browserAbort'
    , browserSaveImage = browserSaveImage'
    , browserSetExitCallback = browserSetExitCallback'
    , browserSetReshapeCallback = browserSetReshapeCallback'
    , browserSetMouseCallback = browserSetMouseCallback'
    }

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
  checkError "realize begin"
  s <- readIORef iR
  log s Debug "realizeCallback"
  drawBuffer $= BackBuffers
  checkError "realize db"
  glClampColor GL_CLAMP_VERTEX_COLOR $ fromIntegral GL_FALSE
  checkError "realize clv"
  glClampColor GL_CLAMP_READ_COLOR $ fromIntegral GL_FALSE
  checkError "realize clr"
  glClampColor GL_CLAMP_FRAGMENT_COLOR $ fromIntegral GL_FALSE
  checkError "realize clf"
  f <- getDataFileName "minimal.frag"
  prog' <- shader Nothing (Just f)
  checkError "realize s1"
  f' <- getDataFileName "merge.frag"
  combineProg' <- shader Nothing (Just f')
  checkError "realize s2"
  fbo' <- newFBO
  checkError "realize fbo"
  [tsheet0', tsheet1'] <- genObjectNames 2
  checkError "realize sheets"
  -- FIXME hardcoded font path
  font' <- createTextureFont "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf"
  -- FIXME destroyFont (font s) at exit
  _ <- setFontFaceSize font' 16 72
  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'
    , sheetCount0 = 0
    , sheetCount1 = 0
    , sheetCountTarget = maxBound
    , font = font'
    }, ())
  reallocateBuffers iR
  checkError "realize end"

reallocateBuffers :: IORef GruffImage -> IO ()
reallocateBuffers iR = do
  checkError "reallocate begin"
  s <- readIORef iR
  let tw' = texels $ sheetSize s
      th' = tw'
      ts = [tsheet0 s, tsheet1 s]
  forM_ (ts `zip` [(tw', th'), (tw' * 2, th' * 2)]) $ \(t, (tw, th)) -> do
    texture Texture2D $= Enabled
    checkError "reallocate 1"
    textureBinding Texture2D $= Just t
    checkError "reallocate 2"
    glTexImage2D GL_TEXTURE_2D 0 (fromIntegral GL_RGBA)
      (fromIntegral tw) (fromIntegral th) 0 GL_RGBA GL_UNSIGNED_BYTE
      nullPtr
    checkError "reallocate 3"
    textureFilter Texture2D $= ((Linear', Just Linear'), Linear')
    checkError "reallocate 4"
    textureWrapMode Texture2D GL.S $= (Repeated, ClampToEdge)
    checkError "reallocate 5"
    textureWrapMode Texture2D GL.T $= (Repeated, ClampToEdge)
    checkError "reallocate 6"
    textureBinding Texture2D $= Nothing
    checkError "reallocate 7"
    texture Texture2D $= Disabled
    checkError "reallocate 8"
  atomicModifyIORef iR $ \s' ->
    (s'{ sheetCount0 = 0, sheetCount1 = 0, sheetCountTarget = maxBound }, ())
  checkError "reallocate end"

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

bindFBO :: GLuint -> TextureObject -> IO ()
bindFBO f (TextureObject t) = do
  checkError "bindFBO begin"
  glBindFramebuffer GL_FRAMEBUFFER f
  checkError "bindFBO 1"
  glFramebufferTexture2D GL_FRAMEBUFFER GL_COLOR_ATTACHMENT0 GL_TEXTURE_2D t 0
  checkError "bindFBO 2"
  err <- glCheckFramebufferStatus GL_FRAMEBUFFER
  when (err /= GL_FRAMEBUFFER_COMPLETE) $
    putStrLn ("OpenGL framebuffer error: " ++ show err)
  checkError "bindFBO end"

unbindFBO :: IO ()
unbindFBO = do
  checkError "unbindFBO begin"
  glFramebufferTexture2D GL_FRAMEBUFFER GL_COLOR_ATTACHMENT0 GL_TEXTURE_2D 0 0
  checkError "unbindFBO 1"
  glBindFramebuffer GL_FRAMEBUFFER 0
  checkError "unbindFBO end"

checkError :: String -> IO ()
checkError s = do
  err <- glGetError
  when (err /= 0) $ putStrLn ("OpenGL error (" ++ s ++ "): " ++ show err)