packages feed

gbnet-hs-0.2.2.0: src/GBNet/Replication/Interpolation.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

-- |
-- Module      : GBNet.Replication.Interpolation
-- Description : Snapshot interpolation for smooth client-side rendering
--
-- 'SnapshotBuffer' stores timestamped snapshots and interpolates between
-- them at a configurable playback delay, enabling smooth rendering despite
-- network jitter.
--
-- Example usage:
--
-- @
-- -- On receiving server snapshot
-- let updated = pushSnapshot serverTimestamp playerState buffer
--
-- -- On render tick
-- case sampleSnapshot renderTime updated of
--   Nothing -> renderLastKnown
--   Just interpolated -> render interpolated
-- @
module GBNet.Replication.Interpolation
  ( -- * Constants
    defaultBufferDepth,
    defaultPlaybackDelayMs,

    -- * Interpolatable typeclass
    Interpolatable (..),

    -- * Snapshot buffer
    TimestampedSnapshot (..),
    SnapshotBuffer,
    newSnapshotBuffer,
    newSnapshotBufferWithConfig,

    -- * Operations
    pushSnapshot,
    sampleSnapshot,
    snapshotReset,

    -- * Queries
    snapshotCount,
    snapshotIsEmpty,
    snapshotReady,
    snapshotPlaybackDelayMs,

    -- * Configuration
    setPlaybackDelayMs,
  )
where

import Data.Sequence (Seq, ViewL (..), ViewR (..), (|>))
import qualified Data.Sequence as Seq
import Optics ((&), (.~))
import Optics.TH (makeFieldLabelsNoPrefix)

-- | Default number of snapshots to buffer before interpolation begins.
defaultBufferDepth :: Int
defaultBufferDepth = 3

-- | Default playback delay in milliseconds behind the latest received snapshot.
defaultPlaybackDelayMs :: Double
defaultPlaybackDelayMs = 100.0

-- | Typeclass for types that support linear interpolation between two states.
--
-- Implement this for any game state that needs smooth interpolation:
--
-- @
-- instance Interpolatable Vec3 where
--   lerp (Vec3 x1 y1 z1) (Vec3 x2 y2 z2) t =
--     Vec3 (x1 + (x2 - x1) * t)
--          (y1 + (y2 - y1) * t)
--          (z1 + (z2 - z1) * t)
-- @
class Interpolatable a where
  -- | Linearly interpolate between @self@ and @other@ by factor @t@ in [0, 1].
  lerp :: a -> a -> Float -> a

-- | A timestamped snapshot for interpolation (internal).
data TimestampedSnapshot a = TimestampedSnapshot
  { tsTimestamp :: !Double,
    tsState :: !a
  }
  deriving (Show)

makeFieldLabelsNoPrefix ''TimestampedSnapshot

-- | Ring buffer of timestamped snapshots with interpolation sampling.
data SnapshotBuffer a = SnapshotBuffer
  { sbSnapshots :: !(Seq (TimestampedSnapshot a)),
    sbBufferDepth :: !Int,
    sbPlaybackDelayMs :: !Double
  }
  deriving (Show)

makeFieldLabelsNoPrefix ''SnapshotBuffer

-- | Create a snapshot buffer with default settings.
newSnapshotBuffer :: SnapshotBuffer a
newSnapshotBuffer =
  SnapshotBuffer
    { sbSnapshots = Seq.empty,
      sbBufferDepth = defaultBufferDepth,
      sbPlaybackDelayMs = defaultPlaybackDelayMs
    }

-- | Create a snapshot buffer with custom settings.
newSnapshotBufferWithConfig ::
  -- | Buffer depth (number of snapshots before ready)
  Int ->
  -- | Playback delay in milliseconds
  Double ->
  SnapshotBuffer a
newSnapshotBufferWithConfig depth delay =
  SnapshotBuffer
    { sbSnapshots = Seq.empty,
      sbBufferDepth = depth,
      sbPlaybackDelayMs = delay
    }

-- | Push a new snapshot with its server timestamp (in milliseconds).
--
-- Snapshots must arrive in order; out-of-order snapshots are dropped.
pushSnapshot :: Double -> a -> SnapshotBuffer a -> SnapshotBuffer a
pushSnapshot timestamp state buffer =
  let snapshots = sbSnapshots buffer
   in -- Check if newer than latest
      case Seq.viewr snapshots of
        EmptyR ->
          -- First snapshot
          buffer & #sbSnapshots .~ Seq.singleton (TimestampedSnapshot timestamp state)
        _ :> newest ->
          if timestamp <= tsTimestamp newest
            then buffer -- Drop out-of-order
            else
              let appended = snapshots |> TimestampedSnapshot timestamp state
                  -- Keep buffer bounded
                  maxEntries = sbBufferDepth buffer * 2
                  trimmed =
                    if Seq.length appended > maxEntries
                      then Seq.drop (Seq.length appended - maxEntries) appended
                      else appended
               in buffer & #sbSnapshots .~ trimmed

-- | Sample an interpolated state at @renderTime@ (in milliseconds).
--
-- Returns 'Nothing' if insufficient snapshots are buffered.
sampleSnapshot :: (Interpolatable a) => Double -> SnapshotBuffer a -> Maybe a
sampleSnapshot renderTime buffer =
  let targetTime = renderTime - sbPlaybackDelayMs buffer
      snapshots = sbSnapshots buffer
   in if Seq.length snapshots < 2
        then Nothing
        else findAndInterpolate targetTime snapshots

-- | Find bracketing snapshots and interpolate.
findAndInterpolate :: (Interpolatable a) => Double -> Seq (TimestampedSnapshot a) -> Maybe a
findAndInterpolate targetTime snapshots =
  case findBracket targetTime (Seq.viewl snapshots) of
    Just (a, b) ->
      let duration = tsTimestamp b - tsTimestamp a
       in if duration <= 0.0
            then Just (tsState a)
            else
              let t = realToFrac ((targetTime - tsTimestamp a) / duration)
                  tClamped = max 0.0 (min 1.0 t)
               in Just (lerp (tsState a) (tsState b) tClamped)
    Nothing ->
      -- Target outside range - return boundary
      case (Seq.viewl snapshots, Seq.viewr snapshots) of
        (first :< _, _ :> lastSnap)
          | targetTime > tsTimestamp lastSnap -> Just (tsState lastSnap)
          | targetTime < tsTimestamp first -> Just (tsState first)
        _ -> Nothing

-- | Find two snapshots bracketing the target time.
findBracket ::
  Double ->
  ViewL (TimestampedSnapshot a) ->
  Maybe (TimestampedSnapshot a, TimestampedSnapshot a)
findBracket _ EmptyL = Nothing
findBracket _ (_ :< rest) | Seq.null rest = Nothing
findBracket targetTime (a :< rest) =
  case Seq.viewl rest of
    b :< _ ->
      if targetTime >= tsTimestamp a && targetTime <= tsTimestamp b
        then Just (a, b)
        else findBracket targetTime (Seq.viewl rest)
    EmptyL -> Nothing

-- | Number of buffered snapshots.
snapshotCount :: SnapshotBuffer a -> Int
snapshotCount = Seq.length . sbSnapshots

-- | Whether the buffer is empty.
snapshotIsEmpty :: SnapshotBuffer a -> Bool
snapshotIsEmpty = Seq.null . sbSnapshots

-- | Whether enough snapshots are buffered to begin interpolation.
snapshotReady :: SnapshotBuffer a -> Bool
snapshotReady buffer = Seq.length (sbSnapshots buffer) >= sbBufferDepth buffer

-- | Clear all buffered snapshots.
snapshotReset :: SnapshotBuffer a -> SnapshotBuffer a
snapshotReset buffer = buffer & #sbSnapshots .~ Seq.empty

-- | Get the playback delay in milliseconds.
snapshotPlaybackDelayMs :: SnapshotBuffer a -> Double
snapshotPlaybackDelayMs = sbPlaybackDelayMs

-- | Set the playback delay in milliseconds.
setPlaybackDelayMs :: Double -> SnapshotBuffer a -> SnapshotBuffer a
setPlaybackDelayMs delay buffer = buffer & #sbPlaybackDelayMs .~ delay

-- Common instances

instance Interpolatable Float where
  lerp a b t = a + (b - a) * t

instance Interpolatable Double where
  lerp a b t = a + (b - a) * realToFrac t

instance (Interpolatable a, Interpolatable b) => Interpolatable (a, b) where
  lerp (a1, b1) (a2, b2) t = (lerp a1 a2 t, lerp b1 b2 t)

instance (Interpolatable a, Interpolatable b, Interpolatable c) => Interpolatable (a, b, c) where
  lerp (a1, b1, c1) (a2, b2, c2) t = (lerp a1 a2 t, lerp b1 b2 t, lerp c1 c2 t)