moonlight-triangulation-0.1.0.0: src-core/Moonlight/Triangulation/Internal/Paged.hs
{-# LANGUAGE BangPatterns #-}
module Moonlight.Triangulation.Internal.Paged
( Paged
, MutablePaged
, emptyPaged
, fromVector
, fromLocalVector
, toVector
, pagedLength
, pagedUnsafeIndex
, pagedFoldl'
, newMutablePaged
, newLocalMutablePaged
, TransactionShape (..)
, thawPaged
, thawPagedDense
, thawPagedShaped
, readPaged
, writePaged
, freezePaged
) where
import Control.DeepSeq (NFData (..))
import Control.Monad.ST (ST, runST)
import Data.Bits (shiftL, shiftR, (.&.))
import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef)
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as MV
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as MUV
-- | One physical store with two lawful local sections. A bulk constructor
-- publishes one contiguous plane for hot traversal. A persistent edit derives
-- fixed-size shared pages from that plane without copying and republishes only
-- pages it wrote. Both constructors denote exactly the same indexed sequence;
-- equality, serialization, and compaction descend through that sequence.
data Paged a
= FlatPaged {-# UNPACK #-} !Int {-# UNPACK #-} !Int !(U.Vector a)
| SharedPaged {-# UNPACK #-} !Int {-# UNPACK #-} !Int !(V.Vector (U.Vector a))
-- | A fresh build owns a contiguous arena. A transaction over a published
-- value owns only its mutable page table and clones a page before its first
-- write. No mutable page or state token can escape this module.
data MutablePaged s a
= MutableFlatPaged {-# UNPACK #-} !Int !(MUV.MVector s a)
| MutableSharedPaged
{-# UNPACK #-} !Int
!(Paged a)
!(V.Vector (U.Vector a))
!(MV.MVector s (MUV.MVector s a))
!(MUV.MVector s Bool)
!(STRef s Int)
localPageBits :: Int
localPageBits = 8
{-# INLINE localPageBits #-}
-- A published plane's page geometry is the clone quantum of every
-- copy-on-write transaction opened on it. Full ladder 2026-08-06 on the
-- star-local excise (persistent-removal 10k/2.5k, min of 4 interleaved
-- rounds): 62.6ms at 12 bits, 57.2ms at 10, 83.2ms at 8 — reproducing the
-- 2026-08-05 finding that at 8 bits the per-transaction table build and
-- freeze scan outweigh the smaller clones (profiled there at 61% of the
-- lane), while at 12 the scattered-touch clones dominate instead. 10 is the
-- measured saddle between the two taxes.
traversalPageBits :: Int
traversalPageBits = 10
{-# INLINE traversalPageBits #-}
pageSize :: Int -> Int
pageSize bits = 1 `shiftL` bits
{-# INLINE pageSize #-}
pageOf :: Int -> Int -> Int
pageOf bits index = index `shiftR` bits
{-# INLINE pageOf #-}
offsetOf :: Int -> Int -> Int
offsetOf bits index = index .&. (pageSize bits - 1)
{-# INLINE offsetOf #-}
pagesFor :: Int -> Int -> Int
pagesFor bits count
| count <= 0 = 0
| otherwise = pageOf bits (count + pageSize bits - 1)
{-# INLINE pagesFor #-}
pagedLength :: Paged a -> Int
pagedLength paged =
case paged of
FlatPaged count _ _ -> count
SharedPaged count _ _ -> count
{-# INLINE pagedLength #-}
pagedBits :: Paged a -> Int
pagedBits paged = case paged of
FlatPaged _ bits _ -> bits
SharedPaged _ bits _ -> bits
{-# INLINE pagedBits #-}
pageVector :: U.Unbox a => Paged a -> V.Vector (U.Vector a)
pageVector source = case source of
FlatPaged count bits values ->
V.generate
(pagesFor bits count)
(\index ->
let !offset = index `shiftL` bits
!width = min (pageSize bits) (count - offset)
in U.unsafeSlice offset width values
)
SharedPaged _ _ pages -> pages
{-# INLINE pageVector #-}
instance (U.Unbox a, Eq a) => Eq (Paged a) where
left == right =
pagedLength left == pagedLength right
&& case (left, right) of
(FlatPaged _ _ leftValues, FlatPaged _ _ rightValues) ->
leftValues == rightValues
_ -> pagedChunksEqual left right
-- Structural equality is a join shortcut and must not materialize either
-- operand. Chunks are compared over the overlap of the two page geometries,
-- so operands with different page widths still compare slice against slice.
pagedChunksEqual :: (U.Unbox a, Eq a) => Paged a -> Paged a -> Bool
pagedChunksEqual left right = walk 0
where
!count = pagedLength left
!leftBits = pagedBits left
!rightBits = pagedBits right
leftPages = pageVector left
rightPages = pageVector right
walk !offset
| offset >= count = True
| otherwise =
let !leftPage = V.unsafeIndex leftPages (pageOf leftBits offset)
!leftOffset = offsetOf leftBits offset
!rightPage = V.unsafeIndex rightPages (pageOf rightBits offset)
!rightOffset = offsetOf rightBits offset
!width =
min
(count - offset)
( min
(U.length leftPage - leftOffset)
(U.length rightPage - rightOffset)
)
in U.unsafeSlice leftOffset width leftPage
== U.unsafeSlice rightOffset width rightPage
&& walk (offset + width)
instance (U.Unbox a, Show a) => Show (Paged a) where
showsPrec precedence = showsPrec precedence . U.toList . toVector
instance NFData (Paged a) where
rnf paged =
case paged of
FlatPaged count bits values -> count `seq` bits `seq` values `seq` ()
SharedPaged count bits pages ->
count `seq` bits `seq` V.foldl' (\() page -> page `seq` ()) () pages
emptyPaged :: Paged a
emptyPaged = SharedPaged 0 traversalPageBits V.empty
-- The padding belongs to page publication rather than dense ingress. A dense
-- plane stores exactly its live sequence and therefore needs no tail value.
fromVector :: U.Unbox a => a -> U.Vector a -> Paged a
fromVector _padding values = FlatPaged (U.length values) traversalPageBits values
-- | Dense ingress whose future edits are expected to be local. The value is
-- still one flat plane until a transaction publishes a changed leaf.
fromLocalVector :: U.Unbox a => a -> U.Vector a -> Paged a
fromLocalVector _padding values = FlatPaged (U.length values) localPageBits values
toVector :: U.Unbox a => Paged a -> U.Vector a
toVector paged =
case paged of
FlatPaged _ _ values -> values
SharedPaged count bits pages
| count <= 0 -> U.empty
| otherwise -> runST $ do
output <- MUV.new count
let copyPage !pageIndex !offset
| offset >= count = pure ()
| otherwise = do
let !width = min (pageSize bits) (count - offset)
U.copy
(MUV.unsafeSlice offset width output)
(U.unsafeSlice 0 width (V.unsafeIndex pages pageIndex))
copyPage (pageIndex + 1) (offset + width)
copyPage 0 0
U.unsafeFreeze output
pagedUnsafeIndex :: U.Unbox a => Paged a -> Int -> a
pagedUnsafeIndex paged index =
case paged of
FlatPaged _ _ values -> U.unsafeIndex values index
SharedPaged _ bits pages ->
U.unsafeIndex (V.unsafeIndex pages (pageOf bits index)) (offsetOf bits index)
{-# INLINE pagedUnsafeIndex #-}
pagedFoldl' :: U.Unbox a => (b -> a -> b) -> b -> Paged a -> b
pagedFoldl' step initial paged =
case paged of
FlatPaged _ _ values -> U.foldl' step initial values
SharedPaged count bits pages -> foldPages 0 0 initial
where
foldPages !pageIndex !offset !accumulated
| offset >= count = accumulated
| otherwise =
let !width = min (pageSize bits) (count - offset)
!next =
U.foldl'
step
accumulated
(U.unsafeSlice 0 width (V.unsafeIndex pages pageIndex))
in foldPages (pageIndex + 1) (offset + width) next
{-# INLINE pagedFoldl' #-}
-- | Allocate the one contiguous mutable arena owned by a fresh constructor.
newMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)
newMutablePaged capacity = MutableFlatPaged traversalPageBits <$> MUV.new (max 0 capacity)
newLocalMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)
newLocalMutablePaged capacity = MutableFlatPaged localPageBits <$> MUV.new (max 0 capacity)
-- | Derive a copy-on-write page table from a published plane. Flat bases
-- become zero-copy slices of their one ByteArray; already shared bases reuse
-- their page vectors. Capacity-only tail pages all point to one empty sentinel
-- and acquire storage only when an append first writes them. Reads stay one
-- direct page-table access — 2026-08-05, measured: routing reads through a
-- dirty-map overlay instead taxed every walk and regressed the local verbs by
-- 2x, so the table is built eagerly and only publication consults ownership.
thawPaged :: U.Unbox a => Int -> Paged a -> ST s (MutablePaged s a)
thawPaged requestedCapacity paged = do
let !count = pagedLength paged
!capacity = max requestedCapacity count
!bits = pagedBits paged
!basePages = pageVector paged
!basePageCount = V.length basePages
!pageCount = pagesFor bits capacity
pages <- MV.new pageCount
owned <- MUV.replicate pageCount False
ownedCount <- newSTRef 0
sentinel <- MUV.new 0
V.imapM_
(\index immutablePage ->
U.unsafeThaw immutablePage >>= MV.unsafeWrite pages index
)
basePages
MV.set
(MV.unsafeSlice basePageCount (pageCount - basePageCount) pages)
sentinel
pure (MutableSharedPaged bits paged basePages pages owned ownedCount)
-- | The two lawful physical sections of one transaction boundary. Both
-- publish the same canonical sequence; they differ in what the transaction
-- pays for. The choice belongs to the operation that knows its own edit
-- volume, never to a public caller.
data TransactionShape = DenseTransaction | LocalTransaction
thawPagedShaped :: U.Unbox a => TransactionShape -> Int -> Paged a -> ST s (MutablePaged s a)
thawPagedShaped shape = case shape of
DenseTransaction -> thawPagedDense
LocalTransaction -> thawPaged
{-# INLINE thawPagedShaped #-}
-- | Materialize one dense mutable arena from a published value. A batch
-- transaction amortizes this single copy over many edits and then reads and
-- writes flat storage with no page bookkeeping; the copy-on-write 'thawPaged'
-- remains the local-edit section whose publication is proportional to dirty
-- pages. Both freeze to the same canonical sequence.
thawPagedDense :: U.Unbox a => Int -> Paged a -> ST s (MutablePaged s a)
thawPagedDense requestedCapacity paged = do
let !count = pagedLength paged
!capacity = max requestedCapacity count
values <- MUV.new capacity
case paged of
FlatPaged _ _ source ->
U.copy (MUV.unsafeSlice 0 count values) source
SharedPaged _ bits pages ->
let copyPage !pageIndex !offset
| offset >= count = pure ()
| otherwise = do
let !width = min (pageSize bits) (count - offset)
U.copy
(MUV.unsafeSlice offset width values)
(U.unsafeSlice 0 width (V.unsafeIndex pages pageIndex))
copyPage (pageIndex + 1) (offset + width)
in copyPage 0 0
pure (MutableFlatPaged (pagedBits paged) values)
readPaged :: U.Unbox a => MutablePaged s a -> Int -> ST s a
readPaged mutable index =
case mutable of
MutableFlatPaged _ values -> MUV.unsafeRead values index
MutableSharedPaged bits _ _ pages _ _ -> do
page <- MV.unsafeRead pages (pageOf bits index)
MUV.unsafeRead page (offsetOf bits index)
{-# INLINE readPaged #-}
writePaged :: U.Unbox a => MutablePaged s a -> Int -> a -> ST s ()
writePaged mutable index value =
case mutable of
MutableFlatPaged _ values -> MUV.unsafeWrite values index value
MutableSharedPaged bits _ _ pages owned ownedCount -> do
let !slot = pageOf bits index
mine <- MUV.unsafeRead owned slot
page <-
if mine
then MV.unsafeRead pages slot
else acquireSharedWritablePage (pageSize bits) pages owned ownedCount slot
MUV.unsafeWrite page (offsetOf bits index) value
{-# INLINE writePaged #-}
-- Keep the once-per-page copy-on-write transition behind one compiled
-- boundary. Both steady-state write paths remain inline; dependent modules do
-- not repeatedly simplify page cloning into every element write.
acquireSharedWritablePage
:: U.Unbox a
=> Int
-> MV.MVector s (MUV.MVector s a)
-> MUV.MVector s Bool
-> STRef s Int
-> Int
-> ST s (MUV.MVector s a)
acquireSharedWritablePage width pages owned ownedCount slot = do
shared <- MV.unsafeRead pages slot
copy <-
if MUV.length shared == width
then MUV.clone shared
else do
copy <- MUV.new width
MUV.copy (MUV.unsafeSlice 0 (MUV.length shared) copy) shared
pure copy
MV.unsafeWrite pages slot copy
MUV.unsafeWrite owned slot True
modifySTRef' ownedCount (+ 1)
pure copy
{-# INLINE[0] acquireSharedWritablePage #-}
freezePaged :: U.Unbox a => Int -> MutablePaged s a -> ST s (Paged a)
freezePaged count mutable
| count <= 0 = pure emptyPaged
| otherwise =
case mutable of
-- 'U.take' is an O(1) slice, so freezing that way republishes the whole
-- growth reservation and holds it for the lifetime of the value. The
-- arena is deliberately loose during construction; carrying that slack
-- past publication is not the same decision. Above an eighth the copy
-- is paid once and the dead tail is released, which is also what the
-- shared branch below already does by generating exactly its pages.
MutableFlatPaged bits values
| MUV.length values <= count + (count `quot` 8) ->
FlatPaged count bits . U.take count <$> U.unsafeFreeze values
| otherwise ->
FlatPaged count bits <$> U.freeze (MUV.unsafeSlice 0 count values)
MutableSharedPaged bits base basePages pages owned ownedCount -> do
dirtyPages <- readSTRef ownedCount
if dirtyPages == 0 && count == pagedLength base
-- An untouched plane republishes as the value it opened on.
then pure base
else
SharedPaged count bits
<$> V.generateM
(pagesFor bits count)
(\index -> do
mine <- MUV.unsafeRead owned index
if mine
then MV.unsafeRead pages index >>= U.unsafeFreeze
else
if index >= V.length basePages
-- A clean slot past the base geometry still holds
-- the zero-length sentinel; publish allocated
-- storage instead, matching the flat arena's
-- answer to an unwritten tail.
then MUV.new (pageSize bits) >>= U.unsafeFreeze
else pure (V.unsafeIndex basePages index)
)