packages feed

moonlight-triangulation-0.1.0.0: src-build/Moonlight/Triangulation/Internal/Cdt/Split.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}

-- | The splitting program: corridors that divide every constraint they cross,
-- driven across as few sealed transactions as the vertex reservation allows.
module Moonlight.Triangulation.Internal.Cdt.Split
  ( SplitChunkShape (..)
  , SplitReceipt (..)
  , SplitChunkOutcome (..)
  , SplitRequest (..)
  , SplitCursor (..)
  , SplitStep (..)
  , addConstraintAndSplit
  , addConstraintsAndSplit
  , splitCensus
  , driveConstraintSplits
  , runSplitChunk
  , splitConstraintCorridor
  , mutableEdgeWasSplit
  , mutableConstraintBetween
  , repairRoundedSplit
  , lineIntersection
  ) where

import Control.Monad.ST (ST, runST)
import Data.Bits (xor)
import qualified Data.Vector as V
import qualified Moonlight.Triangulation.Dcel as Dcel
import Moonlight.Triangulation.Handles.HandleDefs
import Moonlight.Triangulation.Internal.Capacity (ensureCapacity)
import Moonlight.Triangulation.Internal.Cdt.Combinators
  ( asConstraintStep
  , bindMutable
  , directedInt
  , mapLeft
  , vertexInt
  )
import Moonlight.Triangulation.Internal.Cdt.Corridor
  ( constraintWorkspaceFor
  , scanMutableConstraint
  )
import Moonlight.Triangulation.Internal.Cdt.Query
  ( findMutableEdge
  , getConflictingEdgesBetweenVertices
  , validateEndpoints
  )
import Moonlight.Triangulation.Internal.Cdt.Recovery
  ( applyMutableConstraint
  , recoverMutableRequest
  )
import Moonlight.Triangulation.Internal.Cdt.Segment (retireConstraintEdge)
import Moonlight.Triangulation.Internal.Cdt.Site (placeConstraintEndpoint)
import Moonlight.Triangulation.Internal.Cdt.Types
import Moonlight.Triangulation.Internal.Growable
  ( GrowableWord32
  , newGrowableWord32
  )
import Moonlight.Triangulation.Internal.Mutable
import Moonlight.Triangulation.Internal.OperationState
  ( OperationState
  , newOperationState
  )
import Moonlight.Triangulation.Internal.Representation
import Moonlight.Triangulation.Internal.Types
import Moonlight.Triangulation.Math

-- | The part of a splitting request that does not move: the payload maker, the
-- endpoints the failure report must name, the iteration budget, and the
-- transaction's vertex reservation, past which the corridor suspends rather
-- than writes.
data SplitRequest vertex = SplitRequest
  { splitRequestMakeVertex :: !(Point -> vertex)
  , splitRequestFrom :: !VertexId
  , splitRequestTo :: !VertexId
  , splitRequestBudget :: {-# UNPACK #-} !Int
  , splitRequestVertexBound :: {-# UNPACK #-} !Int
  }

-- | The part that does: how much budget is left, which vertex the unrecovered
-- remainder starts at, and the path and edge count accumulated behind it. The
-- path runs newest-first and is reversed once, at publication.
data SplitCursor = SplitCursor
  { splitCursorRemaining :: {-# UNPACK #-} !Int
  , splitCursorAt :: !VertexId
  , splitCursorPath :: ![DirectedEdgeId]
  , splitCursorAdded :: {-# UNPACK #-} !Int
  }

-- | How a corridor run ends inside its transaction: settled at the target, or
-- suspended mid-corridor because the next division would outgrow the vertex
-- reservation. A suspension is not a refusal; the driver publishes,
-- re-reserves against the published mesh, and resumes at the cursor.
data SplitStep
  = SplitSettled !SplitCursor
  | SplitSuspended !SplitCursor

-- | The singleton opens corridor-local copy-on-write pages; the batch pays one
-- dense copy up front and writes flat.
data SplitChunkShape = SparseSplitChunk | DenseSplitChunk

-- | What one settled request leaves behind.
data SplitReceipt = SplitReceipt
  { splitReceiptPath :: !(V.Vector DirectedEdgeId)
  , splitReceiptAdded :: {-# UNPACK #-} !Int
  }

data SplitChunkOutcome vertex directed undirected face = SplitChunkOutcome
  { splitChunkTriangulation :: !(Triangulation 'Constrained vertex directed undirected face)
  , splitChunkReceipts :: ![SplitReceipt]
  , splitChunkSuspended :: !(Maybe SplitCursor)
  }

-- | Glue the requested segment, dividing every constraint it crosses at the
-- crossing point rather than refusing it. One transaction carries the whole
-- corridor: a singleton is a batch of one under the shared driver.
addConstraintAndSplit
  :: (Point -> vertex)
  -> Triangulation 'Constrained vertex directed undirected face
  -> VertexId
  -> VertexId
  -> Either (CdtError) (ConstraintResult vertex directed undirected face)
addConstraintAndSplit makeVertex triangulation from to = do
  validateEndpoints triangulation from to
  if from == to
    then pure (ConstraintResult triangulation V.empty 0)
    else do
      (published, receipts) <-
        driveConstraintSplits SparseSplitChunk makeVertex triangulation (V.singleton (from, to))
      pure
        ( case receipts V.!? 0 of
            Just receipt ->
              ConstraintResult
                { constraintTriangulation = published
                , constraintPath = splitReceiptPath receipt
                , constraintAddedEdges = splitReceiptAdded receipt
                }
            Nothing -> ConstraintResult published V.empty 0
        )

-- | Divide every requested segment inside as few transactions as the vertex
-- reservation allows: usually one, which is the referent semantics for a
-- splitting batch. Nothing is republished between corridors unless a corridor
-- crosses constraints created by an earlier request in the same batch --
-- growth no census against the base can see. In that one case the corridor
-- suspends, the chunk publishes, and the driver re-reserves against the
-- published mesh and resumes at the suspended cursor.
addConstraintsAndSplit
  :: (Point -> vertex)
  -> Triangulation 'Constrained vertex directed undirected face
  -> V.Vector (VertexId, VertexId)
  -> Either (CdtError) (ConstraintSplitBatchResult vertex directed undirected face)
addConstraintsAndSplit makeVertex triangulation requests = do
  (published, receipts) <- driveConstraintSplits DenseSplitChunk makeVertex triangulation requests
  pure
    ConstraintSplitBatchResult
      { splitBatchTriangulation = published
      , splitBatchPaths = V.map splitReceiptPath receipts
      , splitBatchAddedEdges = V.sum (V.map splitReceiptAdded receipts)
      }

-- | The obstruction census for one request against a published mesh: how many
-- constrained crossings its corridor holds, and the first of them. The first
-- crossing is a valid local section of the next transaction before any rewrite
-- occurs, so a chunk's first request carries it across the thaw boundary
-- instead of immediately rediscovering it; every later request rescans,
-- because earlier corridors may have rewritten the topology the witness names.
splitCensus
  :: Triangulation 'Constrained vertex directed undirected face
  -> (VertexId, VertexId)
  -> (Int, Maybe DirectedEdgeId)
splitCensus triangulation (from, to)
  | from == to = (0, Nothing)
  | otherwise =
      foldl'
        countCrossing
        (0, Nothing)
        (getConflictingEdgesBetweenVertices triangulation from to)
 where
  countCrossing :: (Int, Maybe DirectedEdgeId) -> DirectedEdgeId -> (Int, Maybe DirectedEdgeId)
  countCrossing (!count, firstCrossing) crossing =
    ( count + 1
    , case firstCrossing of
        Just first -> Just first
        Nothing -> Just crossing
    )

-- | Interpret splitting requests in order across as few sealed transactions as
-- possible. Each chunk reserves against the exact obstruction census of every
-- remaining request, so a chunk always settles at least its first request:
-- that census ran against the very mesh the chunk thawed and is exact before
-- any in-session rewrite. Termination follows.
driveConstraintSplits
  :: SplitChunkShape
  -> (Point -> vertex)
  -> Triangulation 'Constrained vertex directed undirected face
  -> V.Vector (VertexId, VertexId)
  -> Either
      (CdtError)
      ( Triangulation 'Constrained vertex directed undirected face
      , V.Vector SplitReceipt
      )
driveConstraintSplits shape makeVertex base requests = do
  V.mapM_ (uncurry (validateEndpoints base)) requests
  advance base 0 [] Nothing
 where
  advance triangulation start settled resumed
    | start >= V.length requests =
        Right (triangulation, V.fromList (reverse settled))
    | otherwise = do
        let remaining = V.drop start requests
            firstRound = start == 0 && case resumed of
              Nothing -> True
              Just _ -> False
            -- The opening chunk reserves optimistically -- one crossing per
            -- request -- rather than walking every corridor for an exact
            -- census before any work begins. A request that outgrows the
            -- reservation suspends mid-corridor with its partial work kept,
            -- and the next round censuses exactly against the published mesh,
            -- so heavy batches pay at most one speculative chunk.
            censuses
              | firstRound = V.replicate (V.length remaining) (1, Nothing)
              | otherwise = V.map (splitCensus triangulation) remaining
            !reservedSites = V.sum (V.map fst censuses)
            !capacity = Dcel.numVertices triangulation + 2 * reservedSites + 8
            -- The walk budget bounds divisions per request; constraints born
            -- inside the chunk are covered by the reservation term.
            !budget =
              2 * (Dcel.numConstraints triangulation + 2 * reservedSites + V.length remaining)
                + Dcel.numUndirectedEdges triangulation
                + 8
        mapLeft CdtBuildError (ensureCapacity capacity)
        outcome <- runST $ do
          mutable <- case shape of
            SparseSplitChunk -> thawTriangulation capacity triangulation
            DenseSplitChunk -> thawTriangulationDense capacity triangulation
          operation <- newOperationState (halfEdgeCapacity mutable)
          programWords <- newGrowableWord32 32
          runSplitChunk makeVertex mutable operation programWords remaining censuses capacity budget resumed
        case outcome of
          SplitChunkOutcome published receipts suspended ->
            advance published (start + length receipts) (receipts ++ settled) suspended

-- | One sealed transaction over a prefix of the remaining requests. Receipts
-- run newest-first; a suspension carries no receipt, so the settled count is
-- exactly the receipt count.
runSplitChunk
  :: (Point -> vertex)
  -> MutableDcel s vertex directed undirected face
  -> OperationState s
  -> GrowableWord32 s
  -> V.Vector (VertexId, VertexId)
  -> V.Vector (Int, Maybe DirectedEdgeId)
  -> Int
  -> Int
  -> Maybe SplitCursor
  -> ST s (Either (CdtError) (SplitChunkOutcome vertex directed undirected face))
runSplitChunk makeVertex mutable operation programWords requests censuses capacity budget resumed =
  go 0 [] resumed
 where
  go offset receipts pending
    | offset >= V.length requests = seal receipts Nothing
    | otherwise =
        let (from, to) = requests V.! offset
        in case pending of
             Nothing
               | from == to ->
                   go (offset + 1) (SplitReceipt V.empty 0 : receipts) Nothing
             _ ->
               let witness = if offset == 0 then snd (censuses V.! 0) else Nothing
                   cursor = case pending of
                     Just resumedCursor -> resumedCursor
                     Nothing -> SplitCursor budget from [] 0
                   request =
                     SplitRequest
                       { splitRequestMakeVertex = makeVertex
                       , splitRequestFrom = from
                       , splitRequestTo = to
                       , splitRequestBudget = budget
                       , splitRequestVertexBound = capacity
                       }
               in splitConstraintCorridor request programWords mutable operation witness cursor
                    `bindMutable` \step ->
                      case step of
                        SplitSettled settledCursor ->
                          go (offset + 1) (receiptOf settledCursor : receipts) Nothing
                        SplitSuspended suspendedCursor ->
                          seal receipts (Just suspendedCursor)

  receiptOf cursor =
    SplitReceipt
      { splitReceiptPath = V.fromList (reverse (splitCursorPath cursor))
      , splitReceiptAdded = splitCursorAdded cursor
      }

  seal receipts suspended = do
    frozenOutcome <- freezeTriangulation mutable
    pure $ case frozenOutcome of
      Left obstruction -> Left (CdtBuildError obstruction)
      Right frozen -> Right (SplitChunkOutcome frozen receipts suspended)

splitConstraintCorridor
  :: SplitRequest vertex
  -> GrowableWord32 s
  -> MutableDcel s vertex directed undirected face
  -> OperationState s
  -> Maybe DirectedEdgeId
  -> SplitCursor
  -> ST s (Either (CdtError) SplitStep)
splitConstraintCorridor request programWords mutable operation initialCrossing initialCursor =
  case initialCrossing of
    Just crossing -> divide initialCursor crossing
    Nothing -> descend initialCursor
 where
  !target = splitRequestTo request

  descend cursor
    | splitCursorRemaining cursor <= 0 =
        pure
          ( Left
              ( ConstraintSplitBudgetExhausted
                  (splitRequestFrom request)
                  target
                  (splitRequestBudget request)
              )
          )
    | otherwise = do
        workspace <- constraintWorkspaceFor programWords mutable
        scanMutableConstraint workspace mutable (splitCursorAt cursor) target
          `bindMutable` \scanned ->
            case scanned of
              MutableConstraintScanAdmitted program ->
                recoverMutableRequest workspace mutable operation program
                  `bindMutable` \final ->
                    pure
                      ( Right
                          ( SplitSettled
                              (advanceCursor cursor (splitCursorRemaining cursor) target final)
                          )
                      )
              MutableConstraintScanBlocked crossing -> divide cursor crossing

  divide cursor crossing = do
    occupied <- pointCount mutable
    if occupied + 2 > splitRequestVertexBound request
      then pure (Right (SplitSuspended cursor))
      else divideWithin cursor crossing

  divideWithin cursor crossing = do
    let !crossingEdge = directedInt crossing
        !oldConstraint = asUndirected crossing
    segmentFrom <- pointAt mutable (vertexInt (splitCursorAt cursor))
    segmentTo <- pointAt mutable (vertexInt target)
    oldFrom <- readOrigin mutable crossingEdge
    oldTo <- readOrigin mutable (crossingEdge `xor` 1)
    edgeFrom <- pointAt mutable oldFrom
    edgeTo <- pointAt mutable oldTo
    -- The split point lies on the crossing edge, so both its incident faces
    -- already contain it and the exact walk settles on its first probe. Without
    -- this the locate is unhinted, and an unhinted locate descends vertex by
    -- vertex from the arena's first face -- a distance that grows with the mesh
    -- while the corridor it is splitting stays a fixed few faces wide. A hull
    -- edge carries the outer face on one side, which the locator would refuse.
    incidentFace <- readFace mutable crossingEdge
    twinFace <- readFace mutable (crossingEdge `xor` 1)
    let !splitHint = Just (if incidentFace > 0 then incidentFace else twinFace)
    let !oldEndpoints =
          ( VertexId (fromIntegral oldFrom)
          , VertexId (fromIntegral oldTo)
          )
    case lineIntersection crossing segmentFrom segmentTo edgeFrom edgeTo of
      Left indeterminate -> pure (Left indeterminate)
      Right splitPoint ->
        -- The split lands where the intersection says, not where a round trip
        -- through 'makeVertex' happens to put it.
        asConstraintStep
          ( placeConstraintEndpoint
              mutable
              operation
              splitHint
              splitPoint
              (splitRequestMakeVertex request splitPoint)
          )
          `bindMutable` \splitVertex ->
            repairIfRounded oldConstraint oldEndpoints splitVertex `bindMutable` \() ->
              applyMutableConstraint programWords mutable operation (splitCursorAt cursor) splitVertex
                `bindMutable` \prefix ->
                  case prefix of
                    MutableConstraintRejected blocking ->
                      pure (Left (ConstraintIntersection blocking))
                    MutableConstraintAccepted segment ->
                      descend
                        (advanceCursor cursor (splitCursorRemaining cursor - 1) splitVertex segment)

  repairIfRounded oldConstraint oldEndpoints splitVertex = do
    divided <- mutableEdgeWasSplit mutable oldConstraint oldEndpoints splitVertex
    if divided
      then pure (Right ())
      else repairRoundedSplit programWords mutable operation oldConstraint oldEndpoints splitVertex

  advanceCursor cursor remaining reached segment =
    cursor
      { splitCursorRemaining = remaining
      , splitCursorAt = reached
      , splitCursorPath = accumulatedRequestPath segment ++ splitCursorPath cursor
      , splitCursorAdded = splitCursorAdded cursor + accumulatedRequestAddedEdges segment
      }

-- | Whether the inserted vertex actually divided the constraint it was placed
-- on. It did if that constraint is gone — the split retired the identity — or
-- if both halves now carry one. Rounding can place the vertex somewhere that
-- leaves the old constraint standing and neither half glued.
mutableEdgeWasSplit
  :: MutableDcel s vertex directed undirected face
  -> UndirectedEdgeId
  -> (VertexId, VertexId)
  -> VertexId
  -> ST s Bool
mutableEdgeWasSplit mutable (UndirectedEdgeId raw) (from, to) splitVertex = do
  halfEdges <- directedEdgeCount mutable
  let !directed = fromIntegral raw * 2
  stillConstrained <-
    if directed >= halfEdges
      then pure False
      else readConstraint mutable directed
  if not stillConstrained
    then pure True
    else do
      leading <- mutableConstraintBetween mutable from splitVertex
      if leading
        then mutableConstraintBetween mutable splitVertex to
        else pure False

mutableConstraintBetween
  :: MutableDcel s vertex directed undirected face
  -> VertexId
  -> VertexId
  -> ST s Bool
mutableConstraintBetween mutable from to = do
  found <- findMutableEdge mutable (vertexInt from) (vertexInt to)
  case found of
    Nothing -> pure False
    Just edge -> readConstraint mutable edge

-- | The split point rounded onto neither half of the constraint it was meant to
-- divide. Retire the stale identity and glue both halves against the vertex
-- that was actually sited. What this recovers is repair rather than the
-- requested segment, so it contributes nothing to the request's path.
repairRoundedSplit
  :: GrowableWord32 s
  -> MutableDcel s vertex directed undirected face
  -> OperationState s
  -> UndirectedEdgeId
  -> (VertexId, VertexId)
  -> VertexId
  -> ST s (Either (CdtError) ())
repairRoundedSplit programWords mutable operation oldEdge (from, to) splitVertex =
  retireConstraintEdge mutable operation oldEdge `bindMutable` \() ->
    glue from splitVertex `bindMutable` \() -> glue splitVertex to
 where
  glue start end =
    applyMutableConstraint programWords mutable operation start end `bindMutable` \recovered ->
      pure $ case recovered of
        MutableConstraintRejected blocking -> Left (ConstraintIntersection blocking)
        MutableConstraintAccepted _ -> Right ()

lineIntersection
  :: DirectedEdgeId
  -> Point
  -> Point
  -> Point
  -> Point
  -> Either (CdtError) (Point)
lineIntersection crossing (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy)
  | denominator == 0 =
      Left (ConstraintSplitIntersectionIndeterminate crossing)
  | otherwise =
      Right (canonicalPoint (Point (ax + t * rx) (ay + t * ry)))
 where
  rx = bx - ax
  ry = by - ay
  sx = dx - cx
  sy = dy - cy
  denominator = rx * sy - ry * sx
  t = ((cx - ax) * sy - (cy - ay) * sx) / denominator