gruff-0.1: Browser.hs
module Browser (iInitialize) where
import Prelude hiding (log)
import Control.Concurrent
( forkIO, MVar, newMVar, takeMVar, putMVar, tryTakeMVar, threadDelay )
import Control.Monad (forever, forM_, liftM2, replicateM, 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 (isJust, mapMaybe)
import Data.Ord (comparing)
import qualified Data.Set as S
import Data.Set (Set, (\\))
import Foreign (Ptr, malloc, poke)
import Foreign.C (CFloat, CInt)
import GHC.Conc (numCapabilities)
import Graphics.Rendering.OpenGL hiding (Angle, Point, Position, Size)
import qualified Graphics.Rendering.OpenGL as GL
import Graphics.Rendering.OpenGL.Raw
( glTexImage2D, gl_TEXTURE_2D, gl_R32F, gl_LUMINANCE, gl_FLOAT
, gl_FALSE, glClampColor, gl_CLAMP_VERTEX_COLOR, gl_CLAMP_READ_COLOR
, gl_CLAMP_FRAGMENT_COLOR
)
import Graphics.UI.Gtk hiding (get, Region, Size)
import Numeric.QD (QuadDouble)
import GLUTGtk
import Shader (shader)
import QuadTree
import Tile (Tile(Tile), getTile, freeTile, rootSquare)
import Logger (Logger, LogLevel(Debug))
import qualified Logger as Log
import Paths_gruff (getDataFileName)
type TextureObject3 = (TextureObject, TextureObject, TextureObject)
data GruffImage = GruffImage
{ center :: (Rational, Rational)
, level :: Int
, size :: Size
, prog :: Program
, tiles :: Map Quad TextureObject3
, queue :: MVar [Tile]
, progress :: Set Quad
, jobs :: MVar [Quad]
, viewQuads :: [(Square, TextureObject3)]
, workers :: [Ptr CInt]
, gl :: GLUTGtk
, cacheDir :: FilePath
, log :: LogLevel -> String -> IO ()
, hshift :: Double
, hscale :: Double
}
iDisplay :: IORef GruffImage -> IO ()
iDisplay iR = do
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
-- GL.finish
freeTile tile
atomicModifyIORef iR $ \s' ->
( s'{ tiles = M.insert q (tde, tit, ttt) (tiles s')
, progress = S.delete q (progress s') }
, ())
update iR False
Nothing -> return ()
s <- readIORef iR
let Size w h = size s
log s Debug $ "displayCallback " ++ show (center s, level s)
viewport $=
(GL.Position 0 0, GL.Size (fromIntegral w) (fromIntegral h))
loadIdentity
ortho2D 0 (fromIntegral w) (fromIntegral h) 0
clearColor $= Color4 0.5 0.5 0.5 1
clear [ColorBuffer]
currentProgram $= Just (prog s)
lde <- get $ uniformLocation (prog s) "de"
lit <- get $ uniformLocation (prog s) "it"
ltt <- get $ uniformLocation (prog s) "tt"
lhshift <- get $ uniformLocation (prog s) "hshift"
lhscale <- get $ uniformLocation (prog s) "hscale"
uniform lde $= TexCoord1 (0 :: GLint)
uniform lit $= TexCoord1 (1 :: GLint)
uniform ltt $= TexCoord1 (2 :: GLint)
uniform lhshift $= TexCoord1 (realToFrac (hshift s) :: GLfloat)
uniform lhscale $= TexCoord1 (realToFrac (hscale s) :: GLfloat)
mapM_ (drawQuad s) (viewQuads s)
currentProgram $= Nothing
-- writeSnapshot (show (level s) ++ ".ppm") (Position 0 0) (size s)
drawQuad :: GruffImage -> (Square, TextureObject3) -> IO ()
drawQuad s (sq, (tde, tit, ttt)) = do
let t x y = texCoord $ TexCoord2 (x :: GLdouble) (y :: GLdouble)
v x y = let (x', y') = toPixel s x y
in vertex $ Vertex2
(fromRational x' :: GLdouble)
(fromRational y' :: GLdouble)
x0 = squareWest sq
x1 = squareWest sq + squareSize sq
y0 = squareNorth sq
y1 = squareNorth sq + squareSize sq
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
iReshape :: IORef GruffImage -> Size -> IO ()
iReshape iR size' = do
s' <- readIORef iR
log s' Debug $ "reshapeCallback " ++ show size'
atomicModifyIORef iR $ \s -> (s{ size = size' }, ())
update iR True
iMouse :: IORef GruffImage -> IO () -> Key -> KeyState -> [Modifier] -> Position -> IO ()
iMouse sR updateGUI (MouseButton LeftButton ) Down _ p@(Position x y) = do
s' <- readIORef sR
log s' Debug $ "leftMouse " ++ show p
atomicModifyIORef sR $ \s -> let level' = (level s + 1) `min` maxLevel in
( s{ center = fromPixel s (round x) (round y), level = level' }
, ())
update sR True >> updateGUI
iMouse sR updateGUI (MouseButton MiddleButton) Down _ p@(Position x y) = do
s' <- readIORef sR
log s' Debug $ "middleMouse " ++ show p
atomicModifyIORef sR $ \s ->
( s{ center = fromPixel s (round x) (round y) }
, ())
update sR True >> updateGUI
iMouse sR updateGUI (MouseButton RightButton ) Down _ p@(Position x y) = do
s' <- readIORef sR
log s' Debug $ "rightMouse " ++ show p
atomicModifyIORef sR $ \s -> let level' = (level s - 1) `max` 0 in
( s{ center = fromPixel s (round x) (round y), level = level' }
, ())
update sR True >> updateGUI
iMouse _ _ _ _ _ _ = return ()
fromPixel :: GruffImage -> Int -> Int -> (Rational, Rational)
fromPixel s x y = (x', y')
where
Size w h = size s
a = fromIntegral w / fromIntegral h
(cx, cy) = center s
r = 8 / 2 ^ level s * fromIntegral h / (2 * fromIntegral tileSize)
x' = cx + r * (fromIntegral x / fromIntegral w - 0.5) * a
y' = cy + r * (fromIntegral y / fromIntegral h - 0.5)
toPixel :: GruffImage -> Rational -> Rational -> (Rational, Rational)
toPixel s x y = (x', y')
where
Size w h = size s
a = fromIntegral w / fromIntegral h
(cx, cy) = center s
r = 8 / 2 ^ level s * fromIntegral h / (2 * fromIntegral tileSize)
x' = ((x - cx) / a / r + 0.5) * fromIntegral w
y' = ((y - cy) / r + 0.5) * fromIntegral h
tileSize :: Int
tileSize = 256
cacheSizeMin, cacheSizeMax :: Int
cacheSizeMin = 160
cacheSizeMax = 250
prune :: IORef GruffImage -> IO ()
prune sR = do
s0 <- readIORef sR
let cacheSize = M.size (tiles s0)
when (cacheSize > cacheSizeMax) $ do
log s0 Debug . concat $
[ "pruning texture cache "
, show cacheSize, " > ", show cacheSizeMax
, " --> ", show cacheSizeMin
]
bad <- atomicModifyIORef sR $ \s ->
let (cx, cy) = center s
f = contains (Point cx cy) . square rootSquare
refine = filter f . liftM2 child [minBound .. maxBound]
Quad{ quadLevel = l0, quadWest = r0, quadNorth = i0 } =
head $ iterate refine [root] !! (level s + 1)
score Quad{ quadLevel = l, quadWest = r, quadNorth = i}
= dl + d r r0 + d i i0 :: Double
where
dl = abs (fromIntegral l - fromIntegral l0)
d x x0
| l > l0 = abs (xfi - x0fi / 2 ^ (l - l0))
| l == l0 = abs (xfi - x0fi)
| l < l0 = abs (x0fi - xfi / 2 ^ (l0 - l))
where xfi = fromIntegral x ; x0fi = fromIntegral x0
d _ _ = error "score"
o = comparing (score . fst)
(good, bad)
= splitAt cacheSizeMin . 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 -> Bool -> IO ()
update sR newView = do
prune sR
s' <- readIORef sR
log s' Debug $ "updateCallback " ++ show newView
todo' <- atomicModifyIORef sR $ \s ->
let Size w h = size s
((bw,bn),(be,bs)) =
( fromPixel s 0 0
, fromPixel s (fromIntegral w) (fromIntegral h) )
view = Region
{ regionWest = bw, regionNorth = bn
, regionEast = be, regionSouth = bs }
qs = S.fromList $ quads rootSquare view (level s + 1)
todo = S.filter (`M.notMember` tiles s) qs \\ progress s
getQuad q = (,) (square rootSquare q) `fmap` M.lookup q (tiles s)
drp = level s - 4
tke = 6 + (drp `min` 0)
quads' = mapMaybe getQuad . concat . take tke . drop drp $
quadss rootSquare view
in (s{ viewQuads = quads' }, todo)
when newView $ do
-- cancel in-progress jobs
putJobs sR []
mapM_ (`poke` 1) (workers s')
atomicModifyIORef sR $ \s -> (s{ progress = S.empty }, ())
-- set new jobs
putJobs sR (S.toList todo')
postRedisplay (gl s')
putTile :: IORef GruffImage -> Tile -> IO ()
putTile sR t = do
s <- readIORef sR
ts <- takeMVar (queue s)
putMVar (queue s) (t:ts)
putJobs :: IORef GruffImage -> [Quad] -> IO ()
putJobs sR qs = do
s <- readIORef sR
_ <- takeMVar (jobs s)
putMVar (jobs s) qs
takeJob :: IORef GruffImage -> IO Quad
takeJob sR = do
s <- readIORef sR
qs <- takeMVar (jobs s)
case qs of
[] -> do
putMVar (jobs s) []
threadDelay 10000
takeJob sR
(q:qs') -> do
atomicModifyIORef sR $ \s' ->
( s'{ progress = S.insert 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
mt <- getTile (log s Debug) (cacheDir s) p q
case mt of
Nothing -> return ()
Just t -> putTile sR 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
postRedisplay (gl s)
Just [] -> do
putMVar (queue 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
defSize :: Size
defSize = Size 320 200
maxLevel :: Int
maxLevel = 191
msPerFrame :: Int
msPerFrame = 40
iInitialize :: GLUTGtk -> Pixbuf -> Logger -> FilePath
-> IO ( Window
, Maybe QuadDouble -> Maybe QuadDouble -> Maybe QuadDouble -> Maybe Double -> Maybe Double -> IO ()
, (QuadDouble -> QuadDouble -> QuadDouble -> IO ()) -> IO () -> IO ()
)
iInitialize gl' icon lg cacheDir' = do
-- image window
iw <- windowNew
let Size defW defH = defSize
windowSetDefaultSize iw defW defH
set iw
[ containerBorderWidth := 0
, containerChild := widget gl'
, windowIcon := Just icon
, windowTitle := "gruff browser"
]
queue' <- newMVar []
jobs' <- newMVar []
iR <- newIORef GruffImage
{ center = (0, 0)
, level = 0
, size = defSize
, tiles = M.empty
, queue = queue'
, jobs = jobs'
, progress = S.empty
, viewQuads = []
, gl = gl'
, cacheDir = cacheDir'
, log = Log.log lg
, workers = []
, prog = undefined
, hshift = 0
, hscale = 1
}
realizeCallback gl' $= iRealize iR
reshapeCallback gl' $= iReshape iR
displayCallback gl' $= iDisplay iR
let iUpdate mre mim mz mhshift mhscale = do
case liftM2 (,) mre mim of
Just (r, i) -> atomicModifyIORef iR $ \s ->
( s { center = (toRational r, negate $ toRational i) }, () )
Nothing -> return ()
case mz of
Just z | z > 0 -> atomicModifyIORef iR $ \s ->
( s { level = max 0 . min maxLevel . floor . negate . logBase 2 $ z }, () )
_ -> return ()
case mhshift of
Just x -> atomicModifyIORef iR $ \s ->
( s { hshift = x }, () )
_ -> return ()
case mhscale of
Just x -> atomicModifyIORef iR $ \s ->
( s { hscale = x }, () )
_ -> return ()
update iR (any isJust [mre, mim, mz])
postRedisplay gl'
iInitializeLate aUpdate aExit = do
let updateCoordinates = do
i <- readIORef iR
let re = fromRational . fst . center $ i
im = fromRational . negate . snd . center $ i
z = 2 ^^ negate (level i)
aUpdate re im z
_ <- iw `onDestroy` aExit
_ <- timeoutAdd (timer iR >> return True) msPerFrame
keyboardMouseCallback gl' $= iMouse iR updateCoordinates
return (iw, iUpdate, iInitializeLate)
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 "colourize.frag"
prog' <- shader Nothing (Just f)
ps <- replicateM numCapabilities $ do
p <- malloc
_ <- forkIO (worker iR p)
return p
atomicModifyIORef iR $ \i -> (i{ prog = prog', workers = ps }, ())