mandulia-0.8.0.1: src/Mandulia.hs
{-
Mandulia -- Mandelbrot/Julia explorer
Copyright (C) 2010,2022 Claude Heiland-Allen <claude@mathr.co.uk>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module Main (main) where
import Control.Concurrent (ThreadId(), forkIO, killThread)
import Control.Monad (replicateM, when)
import Data.Either (partitionEithers)
import Data.IORef
import Data.Maybe (isNothing, catMaybes)
import Data.Time (UTCTime, getCurrentTime, diffUTCTime)
import Data.Version (showVersion)
import System.Environment (getArgs)
import System.Exit (exitFailure, exitSuccess)
import System.IO (hPutStr, stderr, stdout)
import Graphics.UI.GLUT hiding (scale, translate, fullScreen)
import qualified Graphics.UI.GLUT as G
import Unsafe.Coerce (unsafeCoerce)
import Paths_mandulia (version)
import AmmannA3
import Bounds
import Image
import Interface (Interface(..), interface, closeInterface)
import qualified Interface as I
import JobQueue
import Julia
import ResourcePool
import Snapshot
import Sort
import StatsLogger
import TextureCache
import Utils
import Vector
data Mandulia =
Mandulia
{ tiling :: Maybe AmmannA3
, viewMax :: Bounds
, view :: Bounds
, width :: Int
, height :: Int
, fullScreen :: Bool
, oldWidth :: Int
, oldHeight :: Int
, iface :: IORef Interface
, workers :: [ThreadId]
, jobs :: JobQueue JuliaJob
, textures :: TextureCache
, images :: ResourcePool Image
, logStats :: String -> Double -> IO ()
, getStats :: IO [(String, (Double, Double, Double))]
, frameTime :: Maybe UTCTime
}
main :: IO ()
main = do
args <- getArgs
let (opts, args') = span (\o -> "-" == take 1 o) args
when ("--version" `elem` opts || "-V" `elem` opts) $ do
putStr $ unlines
[ "mandulia " ++ showVersion version
, "Copyright (C) 2010,2022 Claude Heiland-Allen <claude@mathr.co.uk>"
, "This is free software; see the source for copying conditions. There is NO"
, "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
]
exitSuccess
when ("--help" `elem` opts || "-h" `elem` opts || "-?" `elem` opts) $ do
putStr $ unlines
[ "Usage: mandulia [OPTION]... CONFIGURATION [ARGUMENT]..."
, ""
, "CONFIGURATION is considered (in this order):"
, " as a file to load directly;"
, " as a module to load from the current directory;"
, " as a module to load from the user settings directory;"
, " as a module to load from the global settings directory."
, ""
, "Configurations available in this package may include:"
, " main"
, ""
, "The ARGUMENT list is supplied to the selected configuration."
, ""
, "Options:"
, " +RTS [OPTION].. -RTS options for the run time system"
, " (Try `mandulia +RTS -? -RTS' for help)"
, " -?, -h, --help print this help text"
, " -V, --version print program version"
, ""
, "Report bugs to <claude@mathr.co.uk>."
]
exitSuccess
when (null args') $ do
hPutStr stderr $ unlines
[ "No configuration specified."
, "Try `mandulia --help' for more information."
]
exitFailure
hPutStr stderr $ unlines
[ "mandulia (GPLv3+) 2010,2022 Claude Heiland-Allen <claude@mathr.co.uk>"
]
mif <- interface (head args') (tail args')
when (isNothing mif) $ do
hPutStr stderr $ unlines
[ "Configuration error."
]
exitFailure
let Just iR = mif
i <- readIORef iR
let jsize = clamp 1 1024 $ iJuliaSize i -- FIXME check power of two
imagen = 1 `max` iImages i
texn = 1 `max` iTextures i
workn = 1 `max` iWorkers i
winW = 1 `max` I.iWidth i
winH = 1 `max` I.iHeight i
full = I.iFullScreen i
mspf = 1 `max` (ceiling $ 1000 / (iFPS i `max` 0.01))
view0 = mkView winW winH 0 0 0
(logStats', getStats') <- statsLogger
jobq <- jobQueue
imgpool <- resourcePool (image jsize jsize 4) imagen
let texcache = textureCache texn
wtids <- replicateM workn
(forkIO $ juliaWorker logStats' jsize jsize imgpool jobq)
manduliaR <- newIORef Mandulia { tiling = Nothing
, viewMax = view0
, view = view0
, width = winW
, height = winH
, fullScreen = full
, oldWidth = winW
, oldHeight = winH
, iface = iR
, workers = wtids
, jobs = jobq
, textures = texcache
, images = imgpool
, logStats = logStats'
, getStats = getStats'
, frameTime = Nothing
}
initialWindowSize $= Size (fromIntegral winW) (fromIntegral winH)
initialDisplayMode $= [RGBAMode, DoubleBuffered]
_ <- getArgsAndInitialize
_ <- createWindow "Mandulia"
displayCallback $= display manduliaR
reshapeCallback $= Just (reshape manduliaR)
keyboardMouseCallback $= Just (kmouse manduliaR)
addTimerCallback mspf $ timer manduliaR
mainLoop
mkView :: Int -> Int -> Double -> Double -> Double -> Bounds
mkView winW winH x y z =
let w = fromIntegral winW
h = fromIntegral winH
ax = if winW > winH then 1 else w / h
ay = if winW < winH then 1 else h / w
r = 16 * phi' ** z
x0 = x - r * ax
y0 = y - r * ay
x1 = x + r * ax
y1 = y + r * ay
in bounds [ V x0 y0 1, V x1 y1 1 ]
quit :: IORef Mandulia -> IO ()
quit mR = do
m <- readIORef mR
mapM_ killThread (workers m)
I.atexit (iface m)
closeInterface (iface m)
exitSuccess
update :: IORef Mandulia -> IO (Julia -> Double)
update mR = do
m0 <- readIORef mR
s <- getStats m0
modifyIORef' (iface m0) (\i -> i{ iStatistics = s })
I.render (iface m0)
i <- readIORef (iface m0)
when (iQuit i) (quit mR)
fullscreen mR (iFullScreen i)
m <- readIORef mR
let (x, y, z) = iView i
v = mkView (width m) (height m) x y z
if v `insideOrEqual` viewMax m
then do
case zoomTo v (ammannA3 $ viewMax m) of
t@(Just _) -> do
writeIORef mR m{ tiling = t, view = v }
return $ score z x y
Nothing -> return $ score z x y -- FIXME should never happen?
else return $ score z x y -- FIXME what to do when out of range?
data Quad =
Quad
{ quadX :: !R
, quadY :: !R
, quadR :: !R
, quadT :: !TextureObject
}
radius :: R -> R -> Int -> R
radius d z i = let x = d + z - fromIntegral i
in clamp 0 1 $ x * 4 / d -- FIXME configure the 4
quads :: IORef Mandulia -> IO ([Julia], [Quad])
quads mR = do
m <- readIORef mR
i <- readIORef (iface m)
let w = fromIntegral (width m)
h = fromIntegral (height m)
s = iDisplaySize i
window = bounds [ V (-s) (-s) 1, V (w+s) (h+s) 1]
viewT = view m `into` window
ctiles = case tiling m of
Just t -> filter ((==) C . tTile) . tiles (ceiling d) $ t
Nothing -> []
d = iDetail i
(_, _, z) = iView i
rads = map ((s *) . radius d z) [0 ..]
return $ partitionEithers
[ case mt of
Nothing -> Left j
Just tex -> Right Quad{ quadX = x
, quadY = y
, quadR = r
, quadT = tex
}
| t <- ctiles
, let V cx cy _ = tCenter t
, let V x y _ = viewT ^^*^ V cx cy 1
, let ii = tId t
, let l = tDepth t
, let r = rads !! l
, let j = Julia{ jId = ii, jLevel = l, jCX = cx, jCY = cy }
, let mt = lookupTexture (textures m) j
]
r2gl :: R -> GLdouble
r2gl = unsafeCoerce -- FIXME there must be a better way...
drawQuad :: Quad -> IO ()
drawQuad Quad{ quadX = x0, quadY = y0, quadR = s, quadT = tex } = do
let t x y = texCoord $ TexCoord2 (x :: GLdouble) (y :: GLdouble)
v x y = vertex $ Vertex2 (r2gl x) (r2gl y)
textureBinding Texture2D $= Just tex
renderPrimitive Quads $ do
color $ Color3 1 1 (1::GLdouble)
t 0 1 >> v (x0 - s) (y0 + s)
t 0 0 >> v (x0 - s) (y0 - s)
t 1 0 >> v (x0 + s) (y0 - s)
t 1 1 >> v (x0 + s) (y0 + s)
display0 :: IORef Mandulia -> IO ()
display0 mR = do
swapBuffers
t1 <- getCurrentTime
mt0 <- frameTime `fmap` readIORef mR
modifyIORef' mR $ \m -> m{ frameTime = Just t1 }
case mt0 of
Nothing -> return ()
Just t0 -> do
let dt = realToFrac (diffUTCTime t1 t0)
readIORef mR >>= \m -> logStats m "frame" dt
curScore <- update mR
qs <- computeJobs mR curScore
drawQuads qs
completeJobs mR curScore
record mR
reportErrors
display :: IORef Mandulia -> IO ()
display mR = do
m <- readIORef mR
(dt, ()) <- time $ display0 mR
logStats m "display" dt
return ()
computeJobs :: IORef Mandulia -> (Julia -> Double) -> IO [Quad]
computeJobs mR curScore = do
m <- readIORef mR
i <- readIORef (iface m)
(js, qs) <- quads mR
cs <- sortIO curScore . filter (notCached (textures m)) $ js
let job j = JuliaJob{ jCoords = j, jDoneAction = Nothing }
js' = map job . take (iJobs i) $ cs
visible q = not ( quadX q + 2 * quadR q < 0
|| fromIntegral (width m) < quadX q - 2 * quadR q
|| quadY q + 2 * quadR q < 0
|| fromIntegral (height m) < quadY q - 2 * quadR q
) && quadR q > 0
reprioritise (jobs m) (const js')
return $ filter visible qs
black :: Color4 GLclampf
black = Color4 0 0 0 1
drawQuads :: [Quad] -> IO ()
drawQuads qs = do
clearColor $= black
clear [ColorBuffer]
blend $= Enabled
blendFunc $= (SrcAlpha, OneMinusSrcAlpha)
texture Texture2D $= Enabled
mapM_ drawQuad qs
textureBinding Texture2D $= Nothing
texture Texture2D $= Disabled
blend $= Disabled
completeJobs :: IORef Mandulia -> (Julia -> Double) -> IO ()
completeJobs mR curScore = do
m <- readIORef mR
tc <- cacheTextures curScore (textures m) . catMaybes . map jDoneAction =<< completed (jobs m)
writeIORef mR m{ textures = tc }
record :: IORef Mandulia -> IO ()
record mR = do
m <- readIORef mR
i <- readIORef (iface m)
when (iRecord i) $ do
hSnapshot stdout (Position 0 0) (Size (fromIntegral (width m)) (fromIntegral (height m)))
fullscreen :: IORef Mandulia -> Bool -> IO ()
fullscreen mR fs = do
m <- readIORef mR
when (fullScreen m /= fs) $ do
if fs
then do
writeIORef mR m{ oldWidth = width m, oldHeight = height m, fullScreen = fs }
G.fullScreen
else do
writeIORef mR m{ fullScreen = fs }
windowSize $= Size (fromIntegral $ oldWidth m) (fromIntegral $ oldHeight m)
reshape :: IORef Mandulia -> Size -> IO ()
reshape mR (Size w h) = do
modifyIORef mR $ \m' -> m'{ width = fromIntegral w, height = fromIntegral h }
m <- readIORef mR
I.reshape (iface m) (fromIntegral w) (fromIntegral h)
i <- readIORef (iface m)
let (x, y, z) = iView i
s = ceiling $ 2 * iDisplaySize i
modifyIORef mR $ \m' -> m'{ view = mkView (width m') (height m') x y z }
viewport $= (Position (-s) (-s), (Size (w + 2 * fromIntegral s) (h + 2 * fromIntegral s)))
matrixMode $= Projection
loadIdentity
ortho (-fromIntegral s) (fromIntegral w + fromIntegral s) (-fromIntegral s) (fromIntegral h + fromIntegral s) (-1) 1
matrixMode $= Modelview 0
loadIdentity
postRedisplay Nothing
timer :: IORef Mandulia -> IO ()
timer mR = do
m <- readIORef mR
i <- readIORef (iface m)
let mspf = ceiling $ 1000 / (iFPS i `max` 0.01)
addTimerCallback mspf $ timer mR
postRedisplay Nothing
kmouse :: IORef Mandulia -> Key -> KeyState -> Modifiers -> Position -> IO ()
kmouse mR (Char '\27') Down _m _p = key mR "Escape"
kmouse mR (Char k) Down _m _p = key mR [ k ]
kmouse mR (SpecialKey sk) Down _m _p = case show sk of
'K':'e':'y':key' -> key mR key'
key' -> key mR key'
kmouse _r _k _s _m _p = return () -- FIXME handle everything
key :: IORef Mandulia -> String -> IO ()
key mR k = do
m <- readIORef mR
I.keyboard (iface m) k