packages feed

moonlight-triangulation-0.1.0.0: src-core/Moonlight/Triangulation/Internal/Growable.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE NamedFieldPuns #-}

module Moonlight.Triangulation.Internal.Growable
  ( GrowableWord32
  , newGrowableWord32
  , clearGrowable
  , growableLength
  , pushGrowable
  , popGrowableOr
  , readGrowable
  , writeGrowable
  ) where

import Control.Monad (when)
import Control.Monad.ST (ST)
import Data.STRef (STRef, newSTRef, readSTRef, writeSTRef)
import qualified Data.Vector.Unboxed.Mutable as MUV
import Data.Word (Word32)

-- | Transaction-local vector. It starts small and grows only with the local
-- frontier; no operation allocates scratch memory proportional to mesh size.
-- The size lives in an unboxed cell: the stack is popped and pushed inside
-- every legalization drain, and a boxed counter would allocate on each step.
data GrowableWord32 s = GrowableWord32
  { growableVector :: !(STRef s (MUV.MVector s Word32))
  , growableSize :: !(MUV.MVector s Int)
  }

newGrowableWord32 :: Int -> ST s (GrowableWord32 s)
newGrowableWord32 requested = do
  vector <- MUV.new (max 8 requested)
  growableVector <- newSTRef vector
  growableSize <- MUV.replicate 1 0
  pure GrowableWord32{growableVector, growableSize}

clearGrowable :: GrowableWord32 s -> ST s ()
clearGrowable GrowableWord32{growableSize} = MUV.unsafeWrite growableSize 0 0
{-# INLINE clearGrowable #-}

growableLength :: GrowableWord32 s -> ST s Int
growableLength GrowableWord32{growableSize} = MUV.unsafeRead growableSize 0
{-# INLINE growableLength #-}

pushGrowable :: GrowableWord32 s -> Word32 -> ST s ()
pushGrowable growable@GrowableWord32{growableSize} value = do
  index <- MUV.unsafeRead growableSize 0
  vector <- ensureCapacity growable (index + 1)
  MUV.unsafeWrite vector index value
  MUV.unsafeWrite growableSize 0 (index + 1)
{-# INLINE pushGrowable #-}

-- | Pop, answering the caller's sentinel on emptiness instead of allocating a
-- 'Maybe' per step. Sound only against a sentinel no push can store.
popGrowableOr :: Word32 -> GrowableWord32 s -> ST s Word32
popGrowableOr sentinel GrowableWord32{growableVector, growableSize} = do
  size <- MUV.unsafeRead growableSize 0
  if size <= 0
    then pure sentinel
    else do
      let !index = size - 1
      vector <- readSTRef growableVector
      value <- MUV.unsafeRead vector index
      MUV.unsafeWrite growableSize 0 index
      pure value
{-# INLINE popGrowableOr #-}

readGrowable :: GrowableWord32 s -> Int -> ST s Word32
readGrowable GrowableWord32{growableVector} index = do
  vector <- readSTRef growableVector
  MUV.unsafeRead vector index
{-# INLINE readGrowable #-}

writeGrowable :: GrowableWord32 s -> Int -> Word32 -> ST s ()
writeGrowable growable@GrowableWord32{growableSize} index value = do
  vector <- ensureCapacity growable (index + 1)
  MUV.unsafeWrite vector index value
  size <- MUV.unsafeRead growableSize 0
  when (index >= size) (MUV.unsafeWrite growableSize 0 (index + 1))
{-# INLINE writeGrowable #-}

-- Answering with the vector is what keeps a push to one cell read: the caller
-- would otherwise re-read the reference this just proved current.
ensureCapacity :: GrowableWord32 s -> Int -> ST s (MUV.MVector s Word32)
ensureCapacity GrowableWord32{growableVector} required = do
  vector <- readSTRef growableVector
  let !current = MUV.length vector
  if required <= current
    then pure vector
    else do
      let !next = until (>= required) (* 2) current
      grown <- MUV.grow vector (next - current)
      writeSTRef growableVector grown
      pure grown