packages feed

moonlight-planar-1.1.0.0: src-build/Moonlight/Planar/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.Planar.Internal.OperationState
  ( Counter (..)
  , DiagnosticMetric (..)
  , LegalizationArena (..)
  , OperationState
  , newOperationState
  , legalizationArena
  , legalizationArenaLength
  , storeLegalizationArena
  , writeScratch
  , readScratch
  , addCounter
  , setCounter
  , maxCounter
  , readCounter
  , finalizeBuildStats
  ) 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 qualified Data.Vector.Unboxed as U
import Data.Word (Word32, Word64)
import Moonlight.Planar.Internal.Growable
  ( GrowableWord32
  , newGrowableWord32
  , readGrowable
  , writeGrowable
  )
import Moonlight.Planar.Internal.PackedIndex (packIndex, unpackIndex)
import Moonlight.Planar.Internal.BuildStats (BuildMetric, BuildStats (..), buildMetricCount)

-- | The typed candidate section owned by one operation. The newtype prevents
-- unrelated scratch vectors from being handed to the normalizer while
-- erasing to the same contiguous Word32 arena in the hot path.
newtype LegalizationArena s = LegalizationArena
  (MUV.MVector s Word32)

-- | Reported metrics use their canonical schema; diagnostics occupy a
-- separate finite suffix. Cardinality is not an inhabitable counter.
data Counter
  = Reported !BuildMetric
  | Diagnostic !DiagnosticMetric
  deriving stock (Eq, Ord, Show)

data DiagnosticMetric
  = LegalizationCandidates
  | HullBucketProbeSteps
  | HullKeyRebuilds
  deriving stock (Eq, Ord, Enum, Bounded, Show)

counterIndex :: Counter -> Int
counterIndex (Reported metric) = fromEnum metric
counterIndex (Diagnostic metric) = buildMetricCount + fromEnum metric
{-# INLINE counterIndex #-}

-- | 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 (LegalizationArena s))
  , 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 <- LegalizationArena <$> MUV.new (min (2 * halfEdgeCapacity + 64) initialLegalizationReservation)
  arena <- newSTRef initialArena
  scratch <- newGrowableWord32 (min 64 (halfEdgeCapacity + 8))
  counters <- MUV.replicate (buildMetricCount + fromEnum (maxBound :: DiagnosticMetric) + 1) 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 (LegalizationArena s)
legalizationArena = readSTRef . osLegalizationArena
{-# INLINE legalizationArena #-}

legalizationArenaLength :: LegalizationArena s -> Int
legalizationArenaLength (LegalizationArena values) = MUV.length values
{-# INLINE legalizationArenaLength #-}

storeLegalizationArena :: OperationState s -> LegalizationArena s -> 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 = unpackIndex <$> readGrowable osScratchArena index
{-# INLINE readScratch #-}

addCounter :: OperationState s -> Counter -> Int -> ST s ()
addCounter OperationState{osCounters} counter amount = do
  let !index = counterIndex 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 (counterIndex counter) (fromIntegral value)
{-# INLINE setCounter #-}

maxCounter :: OperationState s -> Counter -> Int -> ST s ()
maxCounter OperationState{osCounters} counter value = do
  let !index = counterIndex 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 (counterIndex counter)
{-# INLINE readCounter #-}

-- | Transfer the reported prefix at the completed operation's publication
-- boundary. Every caller has finished charging counters and never uses the
-- operation again. The immutable result escapes ST; its mutable owner cannot.
finalizeBuildStats :: OperationState s -> ST s BuildStats
finalizeBuildStats OperationState{osCounters} =
  BuildStats <$> U.unsafeFreeze (MUV.slice 0 buildMetricCount osCounters)
{-# INLINE finalizeBuildStats #-}