twirl-0.4.0.3: src/Twirl.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Twirl (
runApp
, TwirlConfig (..)
, TwirlMonad
, module Twirl.Inputs
, module Keys
) where
import Control.Concurrent (threadDelay)
import Control.Exception (bracket_)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.List (sortBy)
import Data.Ord (comparing)
import Foreign.C.Types (CInt)
import SDL hiding (get)
import qualified SDL.Font as Font
import qualified SDL.Input.Keyboard.Codes as Keys
import qualified SDL.Mixer as Mixer
import Twirl.Graphics
import Twirl.Inputs
data TwirlConfig a = TwirlConfig
{ initialState :: TwirlMonad a
, updateFunction :: a -> InputState -> Double -> TwirlMonad a
, drawFunction :: a -> TwirlMonad ()
, fps :: Int
}
runApp :: TwirlConfig a -> IO ()
runApp config@TwirlConfig{..} = do
initializeAll
withFont $ do
Mixer.withAudio Mixer.defaultAudio audioChunkSize $ do
displays <- getDisplays
let primaryWindowSize = getPrimaryDisplaySize displays
windowSettings = defaultWindow{windowInitialSize = primaryWindowSize, windowBorder = False}
window <- createWindow "" windowSettings
defaultFont <- loadDefaultFont
rendererType <- chooseRendererType
let rendererSettings =
RendererConfig
{ rendererType = rendererType
, rendererTargetTexture = True
}
renderer <- createRenderer window (-1) rendererSettings
rendererDrawBlendMode renderer $= BlendAlphaBlend
_ <- flip runTwirlMonad (TwirlContext renderer window defaultFont) $ do
state <- initialState
fpsState <-
liftIO $
initFPSState
60
( case rendererType of
AcceleratedVSyncRenderer -> True
_ -> False
)
appLoop config state emptyInputState renderer 0 fpsState window
pure ()
withFont :: IO () -> IO ()
withFont = bracket_ Font.initialize Font.quit
getPrimaryDisplaySize :: [Display] -> V2 CInt
getPrimaryDisplaySize modes =
let primaryModes = filter ((==) (P (V2 0 0)) . displayBoundsPosition) modes
in displayBoundsSize $ head primaryModes
chooseRendererType :: IO RendererType
chooseRendererType = do
rendererInfos <- getRenderDriverInfo
pure $ case sortBy (comparing rendererPriority) rendererInfos of
p : _ -> getRendererType p
[] -> SoftwareRenderer
where
getRendererType RendererInfo{rendererInfoFlags = RendererConfig{rendererType}} = rendererType
rendererPriority rendererInfo =
case getRendererType rendererInfo of
AcceleratedRenderer -> 1 :: Int
AcceleratedVSyncRenderer -> 2
UnacceleratedRenderer -> 3
SoftwareRenderer -> 4
audioChunkSize :: Int
audioChunkSize = 128
appLoop :: TwirlConfig a -> a -> InputState -> Renderer -> Double -> FPSState -> Window -> TwirlMonad ()
appLoop config@TwirlConfig{..} state previousInputState renderer carriedOverAccumulator fpsState window = do
(newInputState, quitApp) <- updateInputs previousInputState
(newFPSState, frameTime) <- sleepRemainingTime fpsState
(newState, remainingAccumulator) <- updateState state newInputState (carriedOverAccumulator + frameTime)
rendererDrawColor renderer $= V4 255 255 255 255
clear renderer
drawFunction newState
present renderer
if quitApp
then closeControllers newInputState
else appLoop config newState newInputState renderer remainingAccumulator newFPSState window
where
updateState previousState inputState accumulator = do
let timeStep = 1 / (fromIntegral fps)
if accumulator > timeStep
then do
newState <- updateFunction previousState inputState timeStep
updateState newState inputState (accumulator - timeStep)
else do
pure (previousState, accumulator)
data FPSState = FPSState
{ frameCount :: !Int
, resetTime :: !Double
, secondsPerFrame :: !Double
, lastTime :: !Double
, vsyncActive :: !Bool
}
sleepRemainingTime :: MonadIO m => FPSState -> m (FPSState, Double)
sleepRemainingTime fpsState@FPSState{frameCount, resetTime, secondsPerFrame, lastTime, vsyncActive} = do
currentTime <- time
let targetTime = resetTime + (fromIntegral frameCount) * secondsPerFrame
frameTime = currentTime - lastTime
if vsyncActive
then pure (fpsState{frameCount = frameCount + 1, lastTime = currentTime}, frameTime)
else
if currentTime <= targetTime
then do
let requestedDelay = round $ (targetTime - currentTime) * 1000000
liftIO $ threadDelay requestedDelay
pure (fpsState{frameCount = frameCount + 1, lastTime = currentTime}, frameTime)
else do
pure (fpsState{frameCount = 1, resetTime = currentTime, secondsPerFrame = secondsPerFrame, lastTime = currentTime}, frameTime)
initFPSState :: Int -> Bool -> IO FPSState
initFPSState fps vsyncActive = do
startTime <- time
pure $ FPSState 0 startTime (1 / (fromIntegral fps)) startTime vsyncActive