packages feed

moonlight-triangulation-0.1.0.0: src-dcel/Moonlight/Triangulation/Internal/OperationState.hs

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

-- | The state one operation owns while it works: the legalization arena, the
-- shared scratch arena, and the instrumentation cells. None of it is
-- topology, so none of it lives on @MutableDcel@ — a transaction allocates
-- this record when it thaws, hands it down to the operations it runs, and
-- reads the counters back once when it freezes. The hot loops thread their
-- stack top, maximum depth and flip count as strict loop variables and charge
-- these cells once per drain; cold events (one per insertion, one per walk
-- probe) charge them where they happen.
module Moonlight.Triangulation.Internal.OperationState
  ( Counter (..)
  , OperationState
  , newOperationState
  , legalizationArena
  , storeLegalizationArena
  , writeScratch
  , readScratch
  , addCounter
  , setCounter
  , maxCounter
  , readCounter
  , freezeBuildStats
  ) 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, Word64)
import Moonlight.Triangulation.Internal.Growable
  ( GrowableWord32
  , newGrowableWord32
  , readGrowable
  , writeGrowable
  )
import Moonlight.Triangulation.Internal.PackedIndex (packIndex)
import Moonlight.Triangulation.Internal.Types (BuildStats (..))

-- | Instrumentation cells. The first twenty constructors are exactly the
-- t'BuildStats' fields, in t'BuildStats' order; anything after
-- 'CounterRefinementQueuePops' is a diagnostic with no t'BuildStats' field and
-- is read only by instrumented entries, so 'freezeBuildStats' enumerates the
-- leading block and never sees the rest.
data Counter
  = CounterInputPoints
  | CounterUniquePoints
  | CounterExistingPoints
  | CounterDuplicatePoints
  | CounterSpatialSeedPoints
  | CounterFaceSplits
  | CounterInteriorEdgeSplits
  | CounterBoundaryEdgeSplits
  | CounterHullInsertions
  | CounterLineSplits
  | CounterLineExtensions
  | CounterLineToAreaTransitions
  | CounterEdgeFlips
  | CounterLocationWalkSteps
  | CounterLocationFallbacks
  | CounterLocationMaxWalk
  | CounterLegalizationMaxStack
  | CounterSteinerPoints
  | CounterRefinementFaceChecks
  | CounterRefinementQueuePops
  | CounterSweepFastPoints
  | CounterSweepSkippedPoints
  | CounterDiagLegalizationCandidates
  | CounterDiagHullBucketProbeSteps
  | CounterDiagHullKeyRebuilds
  | CounterCount
  deriving stock (Eq, Ord, Enum, Bounded, Show)

-- | One operation's working state. Scratch is fixed and raw: writes always
-- precede reads within an epoch. The legalization vector is held behind one
-- reference solely so an adversarial generic drain can grow it without
-- reintroducing mesh-global work state. A drain reads that reference once,
-- carries the vector and all stack metrics strictly, and stores it once when
-- finished; there is no per-candidate reference traffic.
data OperationState s = OperationState
  { osLegalizationArena :: !(STRef s (MUV.MVector s Word32))
  , osScratchArena :: !(GrowableWord32 s)
  , osCounters :: !(MUV.MVector s Word64)
  }

-- | Allocate transaction-sized working state. The legalization reservation is a
-- starting size, not a semantic limit: a generic flip pops one candidate and may
-- push four, no linear worst-case depth follows from the input size, and
-- overflow grows the operation-owned vector by doubling. So the reservation is
-- capped. A transaction-sized one charges every singleton verb a fresh block
-- group whose tail no drain reaches, and the doublings that reach a real peak
-- copy less in total than reserving that tail costs.
--
-- Scratch retains four disjoint half-edge sections as its semantic limit: the
-- removal kernel locally glues border, retired-edge, retired-face, and new-fan
-- sections there. Its physical storage still grows only with the star or strip
-- actually observed. Reserving the full mesh bound made every singleton
-- persistent edit allocate an arena whose untouched tail was orders of
-- magnitude larger than the edit.
newOperationState :: Int -> ST s (OperationState s)
newOperationState halfEdgeCapacity = do
  initialArena <- MUV.new (min (2 * halfEdgeCapacity + 64) initialLegalizationReservation)
  arena <- newSTRef initialArena
  scratch <- newGrowableWord32 (min 64 (halfEdgeCapacity + 8))
  counters <- MUV.replicate (fromEnum CounterCount) 0
  pure
    OperationState
      { osLegalizationArena = arena
      , osScratchArena = scratch
      , osCounters = counters
      }

-- | Ordinary cavities are tiny; exceptional stars and recovered strips grow
-- geometrically behind the sealed arena rather than taxing every singleton
-- edit for a pathological frontier it never visits.
initialLegalizationReservation :: Int
initialLegalizationReservation = 64

legalizationArena :: OperationState s -> ST s (MUV.MVector s Word32)
legalizationArena = readSTRef . osLegalizationArena
{-# INLINE legalizationArena #-}

storeLegalizationArena :: OperationState s -> MUV.MVector s Word32 -> ST s ()
storeLegalizationArena = writeSTRef . osLegalizationArena
{-# INLINE storeLegalizationArena #-}

-- | Write a scratch cell. Collection walks carry their topology-derived
-- termination budgets; the growable arena is physical storage, not a second
-- semantic bound capable of disagreeing with those typed obstructions.
writeScratch :: OperationState s -> Int -> Int -> ST s ()
writeScratch OperationState{osScratchArena} index value =
  writeGrowable osScratchArena index (packIndex value)
{-# INLINE writeScratch #-}

readScratch :: OperationState s -> Int -> ST s Int
readScratch OperationState{osScratchArena} index = fromIntegral <$> readGrowable osScratchArena index
{-# INLINE readScratch #-}

addCounter :: OperationState s -> Counter -> Int -> ST s ()
addCounter OperationState{osCounters} counter amount = do
  let !index = fromEnum counter
  current <- MUV.unsafeRead osCounters index
  MUV.unsafeWrite osCounters index (current + fromIntegral amount)
{-# INLINE addCounter #-}

setCounter :: OperationState s -> Counter -> Int -> ST s ()
setCounter OperationState{osCounters} counter value =
  MUV.unsafeWrite osCounters (fromEnum counter) (fromIntegral value)
{-# INLINE setCounter #-}

maxCounter :: OperationState s -> Counter -> Int -> ST s ()
maxCounter OperationState{osCounters} counter value = do
  let !index = fromEnum counter
  current <- MUV.unsafeRead osCounters index
  when (fromIntegral value > current) (MUV.unsafeWrite osCounters index (fromIntegral value))
{-# INLINE maxCounter #-}

readCounter :: OperationState s -> Counter -> ST s Int
readCounter OperationState{osCounters} counter =
  fromIntegral <$> MUV.unsafeRead osCounters (fromEnum counter)
{-# INLINE readCounter #-}

-- | Materialize the public statistics once, at freeze time, from the cells the
-- operation's subsystems charged while it ran. Diagnostic cells past
-- 'CounterRefinementQueuePops' have no t'BuildStats' field and are not read
-- here.
freezeBuildStats :: OperationState s -> ST s BuildStats
freezeBuildStats operation =
  BuildStats
    <$> readCounter operation CounterInputPoints
    <*> readCounter operation CounterUniquePoints
    <*> readCounter operation CounterExistingPoints
    <*> readCounter operation CounterDuplicatePoints
    <*> readCounter operation CounterSpatialSeedPoints
    <*> readCounter operation CounterFaceSplits
    <*> readCounter operation CounterInteriorEdgeSplits
    <*> readCounter operation CounterBoundaryEdgeSplits
    <*> readCounter operation CounterHullInsertions
    <*> readCounter operation CounterLineSplits
    <*> readCounter operation CounterLineExtensions
    <*> readCounter operation CounterLineToAreaTransitions
    <*> readCounter operation CounterEdgeFlips
    <*> readCounter operation CounterLocationWalkSteps
    <*> readCounter operation CounterLocationFallbacks
    <*> readCounter operation CounterLocationMaxWalk
    <*> readCounter operation CounterLegalizationMaxStack
    <*> readCounter operation CounterSteinerPoints
    <*> readCounter operation CounterRefinementFaceChecks
    <*> readCounter operation CounterRefinementQueuePops
    <*> readCounter operation CounterSweepFastPoints
    <*> readCounter operation CounterSweepSkippedPoints
{-# INLINE freezeBuildStats #-}