packages feed

moonlight-triangulation-0.1.0.0: src-serialize/Moonlight/Triangulation/Serialization.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | The versioned binary surface: encode a triangulation to bytes and read it
-- back. Decoding refuses a payload whose format version or coordinate encoding this
-- build does not own, rather than reinterpreting it.
module Moonlight.Triangulation.Serialization
  ( SerializationError (..)
  , serializationVersion
  , encodeTriangulation
  , decodeTriangulation
  ) where

import Control.Monad (replicateM, unless, when)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
import Data.Binary (Binary (..))
import Data.Binary.Get
  ( Get
  , getDoublebe
  , getWord16be
  , getWord32be
  , getWord64be
  , getWord8
  , runGetOrFail
  )
import Data.Binary.Put
  ( Put
  , putDoublebe
  , putWord16be
  , putWord32be
  , putWord64be
  , putWord8
  , runPut
  )
import qualified Data.ByteString.Lazy as BL
import Data.Foldable (traverse_)
import qualified Data.IntSet as IntSet
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Map.Strict as Map
import Data.Proxy (Proxy (..))
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as U
import Data.Word (Word16, Word64, Word8)
import Moonlight.Triangulation.Internal.BoxedPaged (boxedFromVector, boxedToVector)
import Moonlight.Triangulation.Internal.Paged (fromLocalVector, fromVector, toVector)
import Moonlight.Triangulation.Internal.PointIndex (buildPointIndex)
import Moonlight.Triangulation.Handles.HandleDefs
import Moonlight.Triangulation.Internal.Representation
import Moonlight.Triangulation.Internal.Types
import Moonlight.Triangulation.Math (mkQueryPoint)
import Moonlight.Triangulation.Validation (validateTriangulation)

instance Binary (Point) where
  put (Point x y) = putDoublebe x >> putDoublebe y
  get = Point <$> getDoublebe <*> getDoublebe

instance Binary VertexId where
  put (VertexId value) = putWord32be value
  get = VertexId <$> getWord32be

instance Binary FaceId where
  put (FaceId value) = putWord32be value
  get = FaceId <$> getWord32be

instance Binary DirectedEdgeId where
  put (DirectedEdgeId value) = putWord32be value
  get = DirectedEdgeId <$> getWord32be

instance Binary UndirectedEdgeId where
  put (UndirectedEdgeId value) = putWord32be value
  get = UndirectedEdgeId <$> getWord32be

-- | Every way serialization refuses, each naming its witness.
data SerializationError
  = BinaryDecodeFailure !Int64 !String
  | TrailingBytes !Int64
  | InvalidFormatMagic !Word64
  | UnsupportedFormatVersion !Word16
  | ConstraintModeTagMismatch !Word8 !Word8
  | CoordinateEncodingTagMismatch !Word8 !Word8
  | EncodedCountExceedsInt !Word64
  | SerializedCoordinateLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | SerializedVertexPayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | SerializedVertexOutgoingLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | SerializedDirectedEdgeCountOdd {-# UNPACK #-} !Int
  | SerializedNextLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | SerializedPreviousLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | SerializedFaceReferenceLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | SerializedDirectedPayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | SerializedUndirectedPayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | SerializedConstraintLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | SerializedMissingOuterFace
  | SerializedFacePayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | InvalidSerializedPoint {-# UNPACK #-} !Int !PointValidationError
  | NonCanonicalSerializedConstraintFlag !UndirectedEdgeId !Word8
  | SerializedConstraintCountMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | UnconstrainedSerializedConstraints {-# UNPACK #-} !Int
  | DuplicateSerializedCoordinates {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  | DecodedInvariantViolations !(NonEmpty InvariantViolation)
  deriving stock (Eq, Show)

type Decoder = ExceptT SerializationError Get

-- | The envelope version this module writes.
serializationVersion :: Word16
serializationVersion = 4

formatMagic :: Word64
formatMagic = 0x5350414445485307 -- "SPADEHS" + canonical geometry-owned format family

binary64EncodingTag :: Word8
binary64EncodingTag = 2

-- | Write the versioned binary envelope.
encodeTriangulation
  :: forall mode vertex directed undirected face. (KnownConstraintMode mode, Binary vertex, Binary directed, Binary undirected, Binary face)
  => Triangulation mode vertex directed undirected face
  -> BL.ByteString
encodeTriangulation triangulation = runPut $ do
  putWord64be formatMagic
  putWord16be serializationVersion
  putWord8 (modeTag (constraintModeValue (modeProxy triangulation)))
  putWord8 binary64EncodingTag
  let ElementDefaults directedDefault undirectedDefault faceDefault = triElementDefaults triangulation
  put directedDefault
  put undirectedDefault
  put faceDefault
  -- Geometry and payloads are independent components. Persist the authoritative
  -- coordinate pages rather than attempting to recover them from annotations.
  putUVector putDoublebe (toVector (triPointX triangulation))
  putUVector putDoublebe (toVector (triPointY triangulation))
  putBoxedVector put (boxedToVector (triVertexData triangulation))
  putUVector putWord32be (toVector (triVertexOut triangulation))
  -- The wire format stores the four topology planes separately; the interleaved
  -- arena is a resident layout, not a serialization concern.
  let topology = toVector (triHalfTopology triangulation)
      plane field = U.generate (U.length topology `quot` 4) (\edge -> topology U.! (4 * edge + field))
  putUVector putWord32be (plane 0)
  putUVector putWord32be (plane 1)
  putUVector putWord32be (plane 2)
  putUVector putWord32be (plane 3)
  putBoxedVector put (boxedToVector (triDirectedData triangulation))
  putBoxedVector put (boxedToVector (triUndirectedData triangulation))
  putUVector putWord32be (toVector (triFaceEdge triangulation))
  putBoxedVector put (boxedToVector (triFaceData triangulation))
  putUVector putWord8 (toVector (triConstraint triangulation))
  putCount (fromIntegral (triConstraintCount triangulation))

-- | Decode one exact, versioned finite DCEL. Coordinate uniqueness and the
-- complete topology, geometry, and Delaunay/CDT invariants are checked before
-- the opaque value is returned.
decodeTriangulation
  :: forall mode vertex directed undirected face.
     ( KnownConstraintMode mode
     , Binary vertex
     , Binary directed
     , Binary undirected
     , Binary face
     )
  => BL.ByteString
  -> Either SerializationError (Triangulation mode vertex directed undirected face)
decodeTriangulation bytes =
  case runGetOrFail (runExceptT getTriangulation) bytes of
    Left (_, offset, message) -> Left (BinaryDecodeFailure offset message)
    Right (_, _, Left failure) -> Left failure
    Right (remaining, _, Right triangulation)
      | not (BL.null remaining) -> Left (TrailingBytes (BL.length remaining))
      | otherwise ->
          case validateTriangulation triangulation of
            [] -> Right triangulation
            firstViolation : remainingViolations ->
              Left (DecodedInvariantViolations (firstViolation :| remainingViolations))
 where
  getTriangulation :: Decoder (Triangulation mode vertex directed undirected face)
  getTriangulation = do
    magic <- lift getWord64be
    unless (magic == formatMagic) (throwE (InvalidFormatMagic magic))
    version <- lift getWord16be
    unless (version == serializationVersion) (throwE (UnsupportedFormatVersion version))
    encodedMode <- lift getWord8
    let expectedMode = modeTag (constraintModeValue (Proxy :: Proxy mode))
    unless (encodedMode == expectedMode) (throwE (ConstraintModeTagMismatch expectedMode encodedMode))
    encodedScalar <- lift getWord8
    let expectedScalar = binary64EncodingTag
    unless (encodedScalar == expectedScalar) (throwE (CoordinateEncodingTagMismatch expectedScalar encodedScalar))
    defaults <- ElementDefaults <$> lift get <*> lift get <*> lift get
    pointXs <- getUVector (lift getDoublebe)
    pointYs <- getUVector (lift getDoublebe)
    vertexDataVector <- getBoxedVector (lift get)
    vertexOut <- getUVector (lift getWord32be)
    halfOrigin <- getUVector (lift getWord32be)
    halfNext <- getUVector (lift getWord32be)
    halfPrev <- getUVector (lift getWord32be)
    halfFace <- getUVector (lift getWord32be)
    directedDataVector <- getBoxedVector (lift get)
    undirectedDataVector <- getBoxedVector (lift get)
    faceEdge <- getUVector (lift getWord32be)
    faceDataVector <- getBoxedVector (lift get)
    constraints <- getUVector (lift getWord8)
    cachedConstraintCount <- getCount

    let vertexCount = U.length pointXs
        pointYCount = U.length pointYs
        vertexPayloadCount = V.length vertexDataVector
        vertexOutgoingCount = U.length vertexOut
        halfCount = U.length halfOrigin
        nextCount = U.length halfNext
        previousCount = U.length halfPrev
        faceReferenceCount = U.length halfFace
        edgeCount = halfCount `quot` 2
        faceCount = U.length faceEdge
        directedPayloadCount = V.length directedDataVector
        undirectedPayloadCount = V.length undirectedDataVector
        constraintCount = U.length constraints
        facePayloadCount = V.length faceDataVector
        points = zipWith Point (U.toList pointXs) (U.toList pointYs)
    unless (pointYCount == vertexCount) (throwE (SerializedCoordinateLengthMismatch vertexCount pointYCount))
    unless (vertexPayloadCount == vertexCount) (throwE (SerializedVertexPayloadLengthMismatch vertexPayloadCount vertexCount))
    unless (vertexOutgoingCount == vertexCount) (throwE (SerializedVertexOutgoingLengthMismatch vertexOutgoingCount vertexCount))
    unless (even halfCount) (throwE (SerializedDirectedEdgeCountOdd halfCount))
    unless (nextCount == halfCount) (throwE (SerializedNextLengthMismatch nextCount halfCount))
    unless (previousCount == halfCount) (throwE (SerializedPreviousLengthMismatch previousCount halfCount))
    unless (faceReferenceCount == halfCount) (throwE (SerializedFaceReferenceLengthMismatch faceReferenceCount halfCount))
    unless (directedPayloadCount == halfCount) (throwE (SerializedDirectedPayloadLengthMismatch directedPayloadCount halfCount))
    unless (undirectedPayloadCount == edgeCount) (throwE (SerializedUndirectedPayloadLengthMismatch undirectedPayloadCount edgeCount))
    unless (constraintCount == edgeCount) (throwE (SerializedConstraintLengthMismatch constraintCount edgeCount))
    unless (faceCount >= 1) (throwE SerializedMissingOuterFace)
    unless (facePayloadCount == faceCount) (throwE (SerializedFacePayloadLengthMismatch facePayloadCount faceCount))
    traverse_ (uncurry validateStoredPoint) (zip [0 ..] points)

    case U.ifoldr (\index flag found -> if flag /= 0 && flag /= 1 then Just (index, flag) else found) Nothing constraints of
      Nothing -> pure ()
      Just (index, flag) ->
        throwE
          ( NonCanonicalSerializedConstraintFlag
              (UndirectedEdgeId (fromIntegral index))
              flag
          )
    let actualConstraintCount = U.foldl' (\count flag -> if flag == 1 then count + 1 else count) 0 constraints
    unless (cachedConstraintCount == actualConstraintCount) (throwE (SerializedConstraintCountMismatch cachedConstraintCount actualConstraintCount))
    when (expectedMode == 0 && actualConstraintCount /= 0) (throwE (UnconstrainedSerializedConstraints actualConstraintCount))

    let distinctPoints = Map.fromList (map (\point -> (point, ())) points)
        distinctPointCount = Map.size distinctPoints
    unless (distinctPointCount == vertexCount) (throwE (DuplicateSerializedCoordinates vertexCount distinctPointCount))

    let pointXStore = fromLocalVector 0 pointXs
        pointYStore = fromLocalVector 0 pointYs
        topologyStore =
          fromVector maxBound $
            U.generate (4 * halfCount) $ \slot ->
              let (edge, field) = slot `quotRem` 4
               in case field of
                    0 -> halfOrigin U.! edge
                    1 -> halfNext U.! edge
                    2 -> halfPrev U.! edge
                    _ -> halfFace U.! edge
        constraintEdgeIndex =
          U.ifoldl'
            (\edges index flag ->
               if flag == 1 then IntSet.insert index edges else edges
            )
            IntSet.empty
            constraints
    pure
      Triangulation
        { triPointX = pointXStore
        , triPointY = pointYStore
        , triPointIndex = buildPointIndex pointXStore pointYStore
        , triVertexOut = fromLocalVector maxBound vertexOut
        , triVertexData = boxedFromVector Nothing vertexDataVector
        , triHalfTopology = topologyStore
        , triDirectedData = boxedFromVector (Just (defaultDirectedEdgeData defaults)) directedDataVector
        , triUndirectedData = boxedFromVector (Just (defaultUndirectedEdgeData defaults)) undirectedDataVector
        , triFaceEdge = fromLocalVector maxBound faceEdge
        , triFaceData = boxedFromVector (Just (defaultFaceData defaults)) faceDataVector
        , triConstraint = fromVector 0 constraints
        , triConstraintCount = cachedConstraintCount
        , triConstraintEdges = constraintEdgeIndex
        , triElementDefaults = defaults
        }

validateStoredPoint :: Int -> Point -> Decoder ()
validateStoredPoint index point =
  case mkQueryPoint point of
    Left failure -> throwE (InvalidSerializedPoint index failure)
    Right _ -> pure ()

modeProxy :: Triangulation mode vertex directed undirected face -> Proxy mode
modeProxy _ = Proxy

modeTag :: ConstraintMode -> Word8
modeTag Unconstrained = 0
modeTag Constrained = 1

putCount :: Word64 -> Put
putCount = putWord64be

getCount :: Decoder Int
getCount = do
  raw <- lift getWord64be
  when (raw > fromIntegral (maxBound :: Int)) (throwE (EncodedCountExceedsInt raw))
  pure (fromIntegral raw)

putUVector :: U.Unbox a => (a -> Put) -> U.Vector a -> Put
putUVector putElement values = do
  putCount (fromIntegral (U.length values))
  U.mapM_ putElement values

getUVector :: U.Unbox a => Decoder a -> Decoder (U.Vector a)
getUVector getElement = do
  count <- getCount
  U.fromList <$> replicateM count getElement

putBoxedVector :: (a -> Put) -> V.Vector a -> Put
putBoxedVector putElement values = do
  putCount (fromIntegral (V.length values))
  V.mapM_ putElement values

getBoxedVector :: Decoder a -> Decoder (V.Vector a)
getBoxedVector getElement = do
  count <- getCount
  V.fromList <$> replicateM count getElement