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 #-}