packages feed

h-raylib-5.1.1.0: examples/bunnymark/src/Main.hs

{-# OPTIONS -Wall #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE TemplateHaskell #-}

-- Writing performant h-raylib code requires the use of pointers and other
-- un-Haskelly functionality. Unfortunately, this cannot be avoided.

module Main where

import Control.Monad (forM_, unless, void, when)
import Foreign
  ( Ptr,
    Storable (alignment, peek, peekByteOff, poke, pokeByteOff, sizeOf),
    advancePtr,
    callocArray,
    free,
    malloc,
    nullPtr,
    peek,
    plusPtr,
    poke,
  )
import Foreign.C (CFloat, withCString)
import Raylib.Core
  ( beginDrawing,
    c'getMouseX,
    c'getMouseY,
    c'getRandomValue,
    changeDirectory,
    clearBackground,
    endDrawing,
    getApplicationDirectory,
    getFrameTime,
    getScreenHeight,
    getScreenWidth,
    initWindow,
    isMouseButtonDown,
    setTargetFPS,
    windowShouldClose,
  )
import Raylib.Core.Shapes (drawRectangle)
import Raylib.Core.Text (drawFPS, drawText)
import Raylib.Core.Textures (c'drawTexture, c'loadTexture, c'unloadTexture)
import Raylib.Types (Color (Color), MouseButton (MouseButtonLeft), Texture, p'texture'height, p'texture'width)
import Raylib.Util (inGHCi, raylibApplication)
import Raylib.Util.Colors (black, green, maroon, rayWhite)

texPath :: String
texPath = (if not inGHCi then "../../../../../../../../../../" else "./") ++ "examples/bunnymark/assets/wabbit_alpha.png"

maxBunnies :: Int
maxBunnies = 500000 -- 500K bunnies limit

-- Strict fields
data Bunny = Bunny
  { px :: !Float,
    py :: !Float,
    sx :: !Float,
    sy :: !Float,
    color :: !(Ptr Color) -- Store colors as pointers to avoid reallocating memory each time
  }
  deriving (Show, Eq)

cfs :: Int
cfs = sizeOf (0 :: CFloat)

nps :: Int
nps = sizeOf nullPtr

instance Storable Bunny where
  alignment _ = min cfs nps
  sizeOf _ = 4 * cfs + nps

  -- We do not peek/poke entire bunnies in this example, so the code below is never called
  -- (we advance the pointers using the `p'*` functions to directly access the fields)
  peek _p = do
    _px <- realToFrac <$> (peekByteOff _p (0 * cfs) :: IO CFloat)
    _py <- realToFrac <$> (peekByteOff _p (1 * cfs) :: IO CFloat)
    _sx <- realToFrac <$> (peekByteOff _p (2 * cfs) :: IO CFloat)
    _sy <- realToFrac <$> (peekByteOff _p (3 * cfs) :: IO CFloat)
    _color <- peekByteOff _p (4 * cfs) :: IO (Ptr Color)
    return $ Bunny {px = _px, py = _py, sx = _sx, sy = _sy, color = _color}
  poke _p (Bunny _px _py _sx _sy _color) = do
    pokeByteOff _p (0 * cfs) (realToFrac _px :: CFloat)
    pokeByteOff _p (1 * cfs) (realToFrac _py :: CFloat)
    pokeByteOff _p (2 * cfs) (realToFrac _sx :: CFloat)
    pokeByteOff _p (3 * cfs) (realToFrac _sy :: CFloat)
    pokeByteOff _p (4 * cfs) _color
    return ()

p'px :: Ptr Bunny -> Ptr CFloat
p'px = (`plusPtr` (0 * cfs))

p'py :: Ptr Bunny -> Ptr CFloat
p'py = (`plusPtr` (1 * cfs))

p'sx :: Ptr Bunny -> Ptr CFloat
p'sx = (`plusPtr` (2 * cfs))

p'sy :: Ptr Bunny -> Ptr CFloat
p'sy = (`plusPtr` (3 * cfs))

p'color :: Ptr Bunny -> Ptr (Ptr Color)
p'color = (`plusPtr` (4 * cfs))

data AppState = AppState
  { texBunny :: !(Ptr Texture),
    halfTexWidth :: !CFloat,
    halfTexHeight :: !CFloat,
    bunnies :: !(Ptr Bunny), -- Store the bunnies as a pointer because Haskell linked lists are extremely slow
    bunniesCount :: !Int
  }
  deriving (Show, Eq)

startup :: IO AppState
startup = do
  _ <- initWindow 800 450 "raylib [textures] example - bunnymark"
  setTargetFPS 60
  unless inGHCi (void $ changeDirectory =<< getApplicationDirectory)
  texPtr <- withCString texPath c'loadTexture
  -- Use `peek` when you need to access the underlying fields

  -- This could be rewritten as
  --   tex <- peek texPtr
  --   let tWidth = texture'width tex
  --   ...
  -- but the code below is faster as it doesn't have to load the entire structure into Haskell
  tWidth <- peek (p'texture'width texPtr)
  tHeight <- peek (p'texture'height texPtr)
  bunniesPtr <- callocArray maxBunnies
  return
    ( AppState
        { texBunny = texPtr,
          bunnies = bunniesPtr,
          halfTexWidth = fromIntegral tWidth / 2,
          halfTexHeight = fromIntegral tHeight / 2,
          bunniesCount = 0
        }
    )

mainLoop :: AppState -> IO AppState
mainLoop state = do
  screenWidth <- getScreenWidth
  screenHeight <- getScreenHeight

  beginDrawing
  clearBackground rayWhite
  forM_
    [0 .. (bunniesCount state - 1)]
    ( \(!i) ->
        do
          -- Advancing the array pointer to get a pointer to a bunny
          let bunny = advancePtr (bunnies state) i
          -- Advancing the bunny pointer to access the fields
          _px <- peek $ p'px bunny
          _py <- peek $ p'py bunny
          _color <- peek $ p'color bunny
          c'drawTexture (texBunny state) (floor _px) (floor _py) _color
    )
  drawRectangle 0 0 screenWidth 40 black
  drawText ("bunnies: " ++ show (bunniesCount state)) 120 10 20 green
  drawText ("batched draw calls: " ++ show (1 + (bunniesCount state `div` 8192))) 320 10 20 maroon
  drawFPS 10 10
  endDrawing

  forM_
    [0 .. (bunniesCount state - 1)]
    ( \(!i) ->
        do
          -- Same thing as before, but reading (`peek`) _and_ writing (`poke`)
          let bunny = advancePtr (bunnies state) i
          _px <- peek $ p'px bunny
          _py <- peek $ p'py bunny
          _sx <- peek $ p'sx bunny
          _sy <- peek $ p'sy bunny
          _color <- peek $ p'color bunny
          let px' = _px + _sx
              py' = _py + _sy
              adjX = px' + halfTexWidth state
              adjY = py' + halfTexHeight state
          poke (p'px bunny) px'
          poke (p'py bunny) py'
          when (adjX > fromIntegral screenWidth || adjX < 0) $ poke (p'sx bunny) (-_sx)
          when (adjY > fromIntegral screenHeight || adjY < 40) $ poke (p'sy bunny) (-_sy)
    )

  do
    lDown <- isMouseButtonDown MouseButtonLeft
    if lDown
      then do
        frameTime <- getFrameTime
        let newBunnies = min (round (10000 * frameTime)) (maxBunnies - bunniesCount state)
        mx <- realToFrac <$> c'getMouseX
        my <- realToFrac <$> c'getMouseY
        forM_
          [bunniesCount state .. (bunniesCount state + newBunnies - 1)]
          ( \(!i) ->
              do
                -- Creating elements uses `poke`, just like writing
                let bunny = advancePtr (bunnies state) i
                xSpeed <- (/ 60) . fromIntegral <$> c'getRandomValue (-250) 250
                ySpeed <- (/ 60) . fromIntegral <$> c'getRandomValue (-250) 250
                r <- fromIntegral <$> c'getRandomValue 50 240
                g <- fromIntegral <$> c'getRandomValue 80 240
                b <- fromIntegral <$> c'getRandomValue 100 240
                ptr <- malloc
                poke ptr (Color r g b 255)

                poke (p'px bunny) mx
                poke (p'py bunny) my
                poke (p'sx bunny) xSpeed
                poke (p'sy bunny) ySpeed
                poke (p'color bunny) ptr
          )
        return $ state {bunniesCount = bunniesCount state + newBunnies}
      else return state

shouldClose :: AppState -> IO Bool
shouldClose _ = windowShouldClose

teardown :: AppState -> IO ()
teardown state = do
  -- Unload and free functions have to be manually called
  c'unloadTexture (texBunny state)
  free (texBunny state)
  forM_ [0 .. (bunniesCount state - 1)] (\(!i) -> let bunny = advancePtr (bunnies state) i in free =<< peek (p'color bunny))
  free (bunnies state)

$(raylibApplication 'startup 'mainLoop 'shouldClose 'teardown)