{- $Id: Animate.hs,v 1.3 2004/11/18 13:02:26 henrik Exp $
******************************************************************************
* I N V A D E R S *
* *
* Module: Animate *
* Purpose: Animation of graphical signal functions. *
* Author: Henrik Nilsson *
* *
* Copyright (c) Yale University, 2003 *
* *
******************************************************************************
-}
-- Approach: The signal function is sampled as frequently as possible. It's
-- the OS's task to allocate resources, so we can just as well use up all the
-- CPU cycles we get. But since drawing is very time consuming, we draw at a
-- fixed, user-defineable, presumably lower rate, thus allowing the user to
-- control the ratio between the cycles spent on drawing and the cycles spent
-- on editing/simulation. This approach may result in rather uneven sampling
-- intervals, but embedSynch can be used to provide a stable time base when
-- that is important, e.g. for simulation, as long as there are enough cycles
-- on average to keep up.
--
-- For some reason, context switching does not work as it should unless
-- the window tick mechanism is enabled. For that reason, we use a high
-- frequency tick (1kHz). (Alternatively, passing the -C runtime flag (e.g.
-- +RTS -C1) forces regular context switches. Moreover, getting events
-- without delay seems to require yielding to ensure that the thread
-- receiving them gets a chance to run. This can be done using yield prior to
-- galling HGL.maybeGetWindowEvent. Alternatively, we can get the window tick,
-- and since the tick frequency is high, no major waiting should ensue. This
-- is the current method, although it seems as if this method means that
-- window close events often will be missed.
module Animate (WinInput, animate) where
import Control.Monad (when)
-- import Data.Maybe (isJust, fromJust)
-- import Posix (SysVar(..), ProcessTimes, ClockTick,
-- getSysVar, getProcessTimes, elapsedTime)
-- import Concurrent (yield)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import qualified Graphics.HGL as HGL
import FRP.Yampa
import FRP.Yampa.Event
-- import FRP.Yampa.Internals -- Breaking the Event abstraction barrier here!
import FRP.Yampa.Task (repeatUntil, forAll)
import FRP.Yampa.Forceable
import Diagnostics (intErr)
import PhysicalDimensions
type WinInput = Event HGL.Event
------------------------------------------------------------------------------
-- Animation
------------------------------------------------------------------------------
-- Animate a signal function
-- fr ......... Frame rate.
-- title ...... Window title.
-- width ...... Window width in pixels.
-- height ..... Window height in pixels.
-- render ..... Renderer; invoked at the frame rate.
-- tco ........ Text Console Output; invoked at every step.
-- sf ......... Signal function to animate.
-- !!! Note: it would be easy to add an argument (a -> IO b) as well, allowing
-- !!! arbitrary I/O. One could even replace the text output by such a
-- !!! function. Could one possibly somehow get data back into the signal
-- !!! function by means of continuations? Or maybe the IOTask monad is the
-- !!! way to go, with a special "reactimateIOTask".
animate :: (Forceable a)
=> Frequency -> String -> Int -> Int
-> (a -> HGL.Graphic)
-> (a -> [String])
-> (SF WinInput a)
-> IO ()
animate fr title width height render tco sf = HGL.runGraphics $ do
win <- HGL.openWindowEx title
Nothing -- Initial position.
(width, height) -- Size.
HGL.DoubleBuffered -- Painfully SLOW!!!
-- HGL.Unbuffered -- Flickers!
(Just 1) -- For scheduling!?!
(init, getTimeInput, isClosed) <- mkInitAndGetTimeInput win
{-
reactimate init
getTimeInput
(\_ ea@(e,a) -> do
updateWin render win ea
forAll (tco a) putStrLn
isClosed)
(repeatedly (1/fr) () &&& sf)
-}
reactimate init
getTimeInput
(\_ (ea@(e,a), (e', c)) -> do updateWin render win ea
forAll (tco a) putStrLn -- If this is all maybe, why not use Control.Monad.forM_ ?
when (isEvent e') (putStrLn ("Cycle#: " ++ show c))
isClosed)
((repeatedly (1/fr) () &&& sf)
&&& (repeatedly 1 ()
&&& loop (arr ((+1) . snd)
>>> iPre (0 :: Int)
>>> arr dup)))
HGL.closeWindow win
------------------------------------------------------------------------------
-- Support for reading time and input
------------------------------------------------------------------------------
mkInitAndGetTimeInput
:: HGL.Window
-> IO (IO WinInput, Bool -> IO (DTime,Maybe WinInput), IO Bool)
mkInitAndGetTimeInput win = do
-- clkRes <- fmap fromIntegral (getSysVar ClockTick)
let clkRes = 1000
tpRef <- newIORef errInitNotCalled
wepRef <- newIORef errInitNotCalled
weBufRef <- newIORef Nothing
closedRef <- newIORef False
-- Initialization and initial input
let init = do
-- Initial time
t0 <- getElapsedTime
writeIORef tpRef t0
-- Initial input
mwe <- getWinInput win weBufRef
writeIORef wepRef mwe
-- Initial signal
return (maybeToEvent mwe)
-- Next delta time and input
let getTimeInput _ = do
-- Get time
tp <- readIORef tpRef
t <- getElapsedTime `repeatUntil` (/= tp) -- Wrap around possible!
let dt = if t > tp then fromIntegral (t-tp)/clkRes else 1/clkRes
writeIORef tpRef t
-- Get input
mwe <- getWinInput win weBufRef
mwep <- readIORef wepRef
writeIORef wepRef mwe
-- putStrLn ("dt = " ++ show dt)
-- when (isJust mwe) (putStrLn ("Event = " ++ show (fromJust mwe)))
-- Simplistic "delta encoding": detects only repeated NoEvent.
-- Return time and input, possibly asking to close the program
case (mwep, mwe) of
(Nothing, Nothing) -> return (dt, Nothing)
(_, Just HGL.Closed) -> do writeIORef closedRef True
return (dt, Just (maybeToEvent mwe))
_ -> return (dt, Just (maybeToEvent mwe))
return (init, getTimeInput, readIORef closedRef)
where
errInitNotCalled = intErr "RSAnimate"
"mkInitAndGetTimeInput"
"Init procedure not called."
-- Accurate enough? Resolution seems to be 0.01 s, which could lead
-- to substantial busy waiting above.
-- getElapsedTime :: IO ClockTick
-- getElapsedTime = fmap elapsedTime getProcessTimes
-- Use this for now. Have seen delta times down to 0.001 s. But as
-- the complexity of the simulator signal function gets larger, the
-- processing time for one iteration will presumably be > 0.01 s,
-- and a clock resoltion of 0.01 s vs. 0.001 s becomes a non issue.
getElapsedTime :: IO HGL.Time
getElapsedTime = HGL.getTime
-- Get window input, with "redundant" mouse moves removed.
getWinInput :: HGL.Window -> IORef (Maybe HGL.Event) -> IO (Maybe HGL.Event)
getWinInput win weBufRef = do
mwe <- readIORef weBufRef
case mwe of
Just _ -> do
writeIORef weBufRef Nothing
return mwe
Nothing -> do
mwe' <- gwi win
case mwe' of
Just (HGL.MouseMove {}) -> mmFilter mwe'
_ -> return mwe'
where
mmFilter jmme = do
mwe' <- gwi win
case mwe' of
Nothing -> return jmme
Just (HGL.MouseMove {}) -> mmFilter mwe'
Just _ -> writeIORef weBufRef mwe'
>> return jmme
-- Seems as if we either have to yield or wait for a tick in order
-- to ensure that the thread receiving events gets a chance to
-- work. For some reason, yielding seems to result in window close
-- events getting through, wheras waiting often means they don't.
-- Maybe the process typically dies before the waiting time is up in
-- the latter case?
gwi win = do
-- yield
HGL.getWindowTick win
mwe <- HGL.maybeGetWindowEvent win
return mwe
------------------------------------------------------------------------------
-- Support for output
------------------------------------------------------------------------------
-- Need to force non-displayed elements to avoid space leaks.
-- We also explicitly force displayed elements in case the renderer does not
-- force everything.
updateWin ::
Forceable a => (a -> HGL.Graphic) -> HGL.Window -> (Event (), a) -> IO ()
updateWin render win (e, a) = when (force a `seq` isEvent e)
(HGL.setGraphic win (render a))