packages feed

nano-ui-sdl-0.1.0.0: lib/NanoUI/Sdl/Render.hs

module NanoUI.Sdl.Render
  ( RenderBatch
  , newRenderBatch
  , destroyRenderBatch
  , flushRenderBatch
  , renderDrawDataPass
  , snapDamage
  ) where

import NanoUI.Sdl.Image (ImageAtlas, lookupImage)

import Control.Monad (void, when)
import Data.Bits (shiftR, (.&.))
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Primitive.PrimArray (indexPrimArray, sizeofPrimArray)
import Data.Word (Word8)
import Foreign.C.Types (CFloat (..), CInt (..))
import Foreign.ForeignPtr (withForeignPtr)
import Foreign.Marshal.Alloc (free, malloc)
import Foreign.Ptr (Ptr, nullPtr)
import Foreign.Storable (poke)
import NanoUI (Color (..), Rect (..), rectIntersect)
import NanoUI.Testing
  ( Damage (..)
  , DrawCmd (..)
  , DrawData (..)
  , LayerSlice (..)
  , damageIsEmpty
  , glyphAtlasTextureId
  )
import SDL3.Sys.Bindgen.Rect (SDL_Rect (..))
import SDL3.Sys.Bindgen.Render (SDL_Renderer, SDL_Texture)
import SDL3.Sys.Bindgen.Runtime.PtrConst qualified as PtrConst
import SDL3.Sys.Render
  ( renderClearSafe
  , setRenderClipRect
  , setRenderDrawColorSafe
  )

data ClipState
  = ClipNone
  | ClipKey {-# UNPACK #-} !Int {-# UNPACK #-} !Int {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  deriving (Eq)

{-# INLINE snapDamage #-}
snapDamage :: Float -> Damage -> Damage
snapDamage _ DamageFull = DamageFull
snapDamage scale (DamageClip (Rect x y w h)) =
  let px = fromIntegral (floor (x * scale) :: Int) / scale
      py = fromIntegral (floor (y * scale) :: Int) / scale
      pw = fromIntegral (ceiling ((x + w) * scale) :: Int) / scale - px
      ph = fromIntegral (ceiling ((y + h) * scale) :: Int) / scale - py
   in DamageClip (Rect px py pw ph)

{-# INLINE toClipKey #-}
toClipKey :: Rect -> ClipState
toClipKey (Rect x y w h) =
  let px = floor x :: Int
      py = floor y :: Int
      x1 = ceiling (x + w) :: Int
      y1 = ceiling (y + h) :: Int
   in ClipKey px py (max 1 (x1 - px)) (max 1 (y1 - py))

applyClipState :: RenderBatch -> IORef ClipState -> Ptr SDL_Renderer -> ClipState -> IO ()
applyClipState batch ref ren next = do
  prev <- readIORef ref
  when (prev /= next) $ do
    flushRenderBatch batch
    writeIORef ref next
    void $ case next of
      ClipNone -> setRenderClipRect ren (PtrConst.unsafeFromPtr nullPtr)
      ClipKey px py pw ph -> do
        let rect = rbClipRect batch
        poke rect (SDL_Rect (fromIntegral px) (fromIntegral py) (fromIntegral pw) (fromIntegral ph))
        setRenderClipRect ren (PtrConst.unsafeFromPtr rect)

-- | Draw every command in layer-slice order, clipped to its own rect and to
-- the damage. A full repaint with a clear colour clears the target first.
renderDrawDataPass :: RenderBatch -> Ptr SDL_Renderer -> Maybe Color -> DrawData -> ImageAtlas -> Ptr SDL_Texture -> Damage -> IO ()
renderDrawDataPass batch ren mClear drawData images glyphTex damage =
  when (not (damageIsEmpty damage)) $ do
    clipRef <- newIORef ClipNone
    void $ setRenderClipRect ren (PtrConst.unsafeFromPtr nullPtr)
    case (mClear, damage) of
      (Just clearColor, DamageFull) -> do
        let (cr, cg, cb, ca) = unpackColor clearColor
        void $ setRenderDrawColorSafe ren cr cg cb ca
        void $ renderClearSafe ren
      (Just _clearColor, DamageClip r) ->
        -- The draw list starts a clip frame with its own window backdrop,
        -- so the clip needs no clear here.
        applyClipState batch clipRef ren (toClipKey r)
      (Nothing, DamageClip r) -> applyClipState batch clipRef ren (toClipKey r)
      (Nothing, DamageFull) -> pure ()
    let clip = case damage of
          DamageFull -> Nothing
          DamageClip r -> Just r
        vc = drawVertexCount drawData
        cmds = drawCommands drawData
        slices = drawLayerSlices drawData
    withForeignPtr (drawVertices drawData) $ \vp ->
      withForeignPtr (drawIndices drawData) $ \ip ->
        let goLy !li
              | li >= sizeofPrimArray slices = pure ()
              | otherwise = do
                  let LayerSlice off cnt = indexPrimArray slices li
                      goCmd !j
                        | j >= cnt = pure ()
                        | otherwise = do
                            drawCmd batch ren vp vc ip images glyphTex clip clipRef (indexPrimArray cmds (off + j))
                            goCmd (j + 1)
                  goCmd 0
                  goLy (li + 1)
         in goLy 0
    applyClipState batch clipRef ren ClipNone

{-# INLINE drawCmd #-}
drawCmd ::
  RenderBatch ->
  Ptr SDL_Renderer ->
  Ptr Word8 ->
  Int ->
  Ptr Word8 ->
  ImageAtlas ->
  Ptr SDL_Texture ->
  Maybe Rect ->
  IORef ClipState ->
  DrawCmd ->
  IO ()
drawCmd batch ren vp vc ip images glyphTex mDamage clipRef cmd = do
  let !count = fromIntegral (cmdIndexCount cmd)
      !cmdRect = Rect (cmdClipX cmd) (cmdClipY cmd) (cmdClipW cmd) (cmdClipH cmd)
      !cmdOpen = cmdClipW cmd >= 1e8 || cmdClipH cmd >= 1e8
      live = case (mDamage, cmdOpen) of
        (Nothing, _) -> Just cmdRect
        (Just dmg, True) -> Just dmg
        (Just dmg, False) -> rectIntersect dmg cmdRect
  when (count >= 3) $
    case live of
      Nothing -> pure ()
      Just clip -> do
        if cmdOpen && mDamage == Nothing
          then applyClipState batch clipRef ren ClipNone
          else applyClipState batch clipRef ren (toClipKey clip)
        let !start = fromIntegral (cmdIndexOffset cmd)
            !texId = cmdTextureId cmd
        tex <-
          if texId == glyphAtlasTextureId
            then pure glyphTex
            else if texId > 0
              then maybe nullPtr id <$> lookupImage images texId
              else pure nullPtr
        batchDrawRange batch vp vc ip start count tex mDamage

{-# INLINE unpackColor #-}
unpackColor :: Color -> (Word8, Word8, Word8, Word8)
unpackColor (Color w) =
  ( fromIntegral ((w `shiftR` 24) .&. 0xFF)
  , fromIntegral ((w `shiftR` 16) .&. 0xFF)
  , fromIntegral ((w `shiftR` 8) .&. 0xFF)
  , fromIntegral (w .&. 0xFF)
  )

-- | The C batch and a clip rect it passes to SDL, both owned for the session.
data RenderBatch = RenderBatch
  { rbBatch :: !(Ptr ())
  , rbClipRect :: !(Ptr SDL_Rect)
  }

-- | Create a persistent render batch. Reusing one batch across frames avoids
-- a C calloc/free pair per presented frame; flush after each render pass.
newRenderBatch :: Ptr SDL_Renderer -> IO RenderBatch
newRenderBatch ren = do
  p <- batchCreate ren
  if p == nullPtr
    then fail "nano_ui_batch_create failed"
    else RenderBatch p <$> malloc

destroyRenderBatch :: RenderBatch -> IO ()
destroyRenderBatch batch = do
  batchDestroy (rbBatch batch)
  free (rbClipRect batch)

flushRenderBatch :: RenderBatch -> IO ()
flushRenderBatch batch = batchFlush (rbBatch batch)

batchDrawRange ::
  RenderBatch ->
  Ptr Word8 ->
  Int ->
  Ptr Word8 ->
  Int ->
  Int ->
  Ptr SDL_Texture ->
  Maybe Rect ->
  IO ()
batchDrawRange batch verts vc indices start n tex mDmg =
  batchDrawRangeC
    (rbBatch batch)
    verts
    (ci vc)
    indices
    (ci start)
    (ci n)
    tex
    hasDmg
    (cf dx)
    (cf dy)
    (cf dw)
    (cf dh)
  where
    ci = fromIntegral
    (hasDmg, dx, dy, dw, dh) = case mDmg of
      Nothing -> (0, 0, 0, 0, 0)
      Just (Rect x y w h) -> (1, x, y, w, h)

cf :: Float -> CFloat
cf = realToFrac

foreign import ccall unsafe "nano_ui_batch_create"
  batchCreate :: Ptr SDL_Renderer -> IO (Ptr ())

foreign import ccall unsafe "nano_ui_batch_destroy"
  batchDestroy :: Ptr () -> IO ()

foreign import ccall unsafe "nano_ui_batch_flush"
  batchFlush :: Ptr () -> IO ()

foreign import ccall unsafe "nano_ui_batch_draw_range"
  batchDrawRangeC ::
    Ptr () ->
    Ptr Word8 ->
    CInt ->
    Ptr Word8 ->
    CInt ->
    CInt ->
    Ptr SDL_Texture ->
    CInt ->
    CFloat ->
    CFloat ->
    CFloat ->
    CFloat ->
    IO ()