packages feed

keid-core-0.1.8.0: src/Resource/Buffer.hs

module Resource.Buffer where

import RIO

import Data.Bits ((.|.))
import Data.Vector.Storable qualified as VectorS
import Foreign (Storable)
import Foreign qualified
import UnliftIO.Resource (MonadResource)
import UnliftIO.Resource qualified as Resource
import Vulkan.Core10 qualified as Vk
import Vulkan.NamedType ((:::))
import Vulkan.Zero (zero)
import VulkanMemoryAllocator qualified as VMA

import Engine.Vulkan.Types (HasVulkan(getAllocator), Queues(qTransfer), MonadVulkan)
import Engine.Worker qualified as Worker
import Resource.CommandBuffer (oneshot_)
import Resource.Vulkan.Named qualified as Named

data Store = Staged | Coherent

data Allocated (s :: Store) a = Allocated
  { aBuffer         :: Vk.Buffer
  , aAllocation     :: VMA.Allocation
  , aAllocationInfo :: VMA.AllocationInfo
  , aCapacity       :: Int
  , aUsed           :: Word32
  , aUsage          :: Vk.BufferUsageFlagBits
  , aLabel          :: Maybe Text
  } deriving (Show)

instance Vk.HasObjectType (Allocated s a) where
  objectTypeAndHandle =
    Vk.objectTypeAndHandle . aBuffer

allocateCoherent
  :: ( MonadVulkan env m
     , Resource.MonadResource m
     , Storable a
     )
  => Maybe Text
  -> Vk.BufferUsageFlagBits
  -> "initial size" ::: Int
  -> VectorS.Vector a
  -> m (Resource.ReleaseKey, Allocated 'Coherent a)
allocateCoherent label usage initialSize xs = do
  res <- createCoherent label usage initialSize xs
  destroyIO <- toIO do
    context <- ask
    destroy context res
  key <- Resource.register destroyIO
  pure (key, res)

createCoherent
  :: forall a env m
  .  ( MonadVulkan env m
     , Storable a
     )
  => Maybe Text
  -> Vk.BufferUsageFlagBits
  -> "initial size" ::: Int
  -> VectorS.Vector a
  -> m (Allocated 'Coherent a)
createCoherent aLabel usage initialSize xs = do
  allocator <- asks getAllocator
  (aBuffer, aAllocation, aAllocationInfo) <- VMA.createBuffer allocator bci aci
  traverse_ (Named.object aBuffer) aLabel

  when (len > 0) $
    if VMA.mappedData aAllocationInfo == Foreign.nullPtr then
      error "TODO: recover from unmapped data and flush manually"
    else
      liftIO $ VectorS.unsafeWith xs \src ->
        Foreign.copyBytes (Foreign.castPtr $ VMA.mappedData aAllocationInfo) src lenBytes

  pure Allocated
    { aCapacity = max initialSize len
    , aUsed     = fromIntegral len
    , aUsage    = usage
    , ..
    }
  where
    len = VectorS.length xs
    lenBytes = Foreign.sizeOf (undefined :: a) * len

    sizeBytes = Foreign.sizeOf (undefined :: a) * max initialSize len

    bci = zero
      { Vk.size        = fromIntegral sizeBytes
      , Vk.usage       = usage
      , Vk.sharingMode = Vk.SHARING_MODE_EXCLUSIVE
      }

    flags =
      Vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT .|.
      Vk.MEMORY_PROPERTY_HOST_COHERENT_BIT

    aci = zero
      { VMA.flags          = VMA.ALLOCATION_CREATE_MAPPED_BIT
      , VMA.usage          = VMA.MEMORY_USAGE_GPU_ONLY
      , VMA.requiredFlags  = flags
      , VMA.preferredFlags = Vk.MEMORY_PROPERTY_DEVICE_LOCAL_BIT
      }

createStaged
  :: forall a env m
  .  ( Storable a
     , MonadVulkan env m
     )
  => Maybe Text
  -> Queues Vk.CommandPool
  -> Vk.BufferUsageFlagBits
  -> Int
  -> VectorS.Vector a
  -> m (Allocated 'Staged a)
createStaged aLabel commandQueues usage initialSize xs = do
  allocator <- asks getAllocator
  (aBuffer, aAllocation, aAllocationInfo) <- VMA.createBuffer allocator bci aci
  traverse_ (Named.object aBuffer) aLabel

  when (len > 0) $
    VMA.withBuffer allocator stageCI stageAI bracket \(staging, _stage, stageInfo) ->
      if VMA.mappedData stageInfo == Foreign.nullPtr then
        error "TODO: recover from unmapped data and flush manually"
      else do
        liftIO $ VectorS.unsafeWith xs \src ->
          Foreign.copyBytes (Foreign.castPtr $ VMA.mappedData stageInfo) src lenBytes
        context <- ask
        copyBuffer_ context commandQueues aBuffer staging (fromIntegral sizeBytes)

  pure Allocated
    { aCapacity = max initialSize len
    , aUsed     = fromIntegral len
    , aUsage    = usage
    , aLabel    = Nothing
    , ..
    }
  where
    len = VectorS.length xs
    lenBytes = Foreign.sizeOf (undefined :: a) * len

    sizeBytes = Foreign.sizeOf (undefined :: a) * max initialSize len

    bci = zero
      { Vk.size        = fromIntegral sizeBytes
      , Vk.usage       = Vk.BUFFER_USAGE_TRANSFER_DST_BIT .|. usage
      , Vk.sharingMode = Vk.SHARING_MODE_EXCLUSIVE
      }

    aci = zero
      { VMA.usage         = VMA.MEMORY_USAGE_GPU_ONLY
      , VMA.requiredFlags = Vk.MEMORY_PROPERTY_DEVICE_LOCAL_BIT
      }

    stageCI = zero
      { Vk.size        = fromIntegral sizeBytes
      , Vk.usage       = Vk.BUFFER_USAGE_TRANSFER_SRC_BIT
      , Vk.sharingMode = Vk.SHARING_MODE_EXCLUSIVE
      }

    stageAI = zero
      { VMA.flags         = VMA.ALLOCATION_CREATE_MAPPED_BIT
      , VMA.usage         = VMA.MEMORY_USAGE_CPU_TO_GPU
      , VMA.requiredFlags = Vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT
      }

register
  :: ( MonadVulkan env m
     , MonadResource m
     )
  => Allocated stage a
  -> m Resource.ReleaseKey
register Allocated{aBuffer, aAllocation} = do
  allocator <- asks getAllocator
  Resource.register $ VMA.destroyBuffer allocator aBuffer aAllocation

destroy
  :: (MonadUnliftIO io, HasVulkan context)
  => context
  -> Allocated s a
  -> io ()
destroy context a =
  VMA.destroyBuffer (getAllocator context) (aBuffer a) (aAllocation a)

peekCoherent
  :: ( MonadIO m
     , Storable a
     )
  => Word32
  -> Allocated 'Coherent a
  -> m (Maybe a)
peekCoherent ix Allocated{..} =
  case VMA.mappedData aAllocationInfo of
    _ | ix + 1 > aUsed ->
      pure Nothing
    ptr | ptr == Foreign.nullPtr ->
      pure Nothing
    ptr ->
      liftIO . fmap Just $
        Foreign.peekElemOff (Foreign.castPtr ptr) (fromIntegral ix)

{-# INLINE pokeCoherent #-}
pokeCoherent
  :: ( MonadVulkan env m
     , Storable a
     )
  => Allocated 'Coherent a
  -> Word32
  -> a
  -> m ()
pokeCoherent Allocated{..} ix new =
  case VMA.mappedData aAllocationInfo of
  _ | ix + 1 > aUsed ->
    pure ()
  ptr | ptr == Foreign.nullPtr ->
    pure ()
  ptr ->
    liftIO  $
      Foreign.pokeElemOff
        (Foreign.castPtr ptr)
        (fromIntegral ix)
        new

updateCoherent
  :: ( MonadUnliftIO io
     , Foreign.Storable a
     )
  => VectorS.Vector a
  -> Allocated 'Coherent a
  -> io (Allocated 'Coherent a)
updateCoherent xs old = do
  liftIO $ VectorS.unsafeWith xs \src ->
    Foreign.copyBytes dst src lenBytes
  pure old
    { aUsed = fromIntegral len
    }
  where
    dst = Foreign.castPtr $ VMA.mappedData (aAllocationInfo old)
    len = VectorS.length xs
    lenBytes = len * Foreign.sizeOf (VectorS.head xs)

{-# INLINE updateCoherentResize_ #-}
updateCoherentResize_
  :: ( MonadVulkan env m
     , Storable a
     )
  => Allocated 'Coherent a
  -> VectorS.Vector a
  -> m (Allocated 'Coherent a)
updateCoherentResize_ old xs =
  if newSize <= oldSize then
    updateCoherent xs old
  else do
    ctx <- ask
    destroy ctx old
    createCoherent (aLabel old) (aUsage old) newSize xs
  where
    oldSize = aCapacity old
    newSize = VectorS.length xs

-- TODO: add a staged buffer to check out the transfer queue
copyBuffer_
  :: ( MonadUnliftIO io
     , HasVulkan context
     )
  => context
  -> Queues Vk.CommandPool
  -> ("dstBuffer" ::: Vk.Buffer)
  -> ("srcBuffer" ::: Vk.Buffer)
  -> Vk.DeviceSize
  -> io ()
copyBuffer_ context commandQueues dst src sizeBytes =
  oneshot_ context commandQueues qTransfer \cmd ->
    Vk.cmdCopyBuffer cmd src dst
      (pure $ Vk.BufferCopy 0 0 sizeBytes)

type ObserverCoherent a = Worker.ObserverIO (Allocated 'Coherent a)

newObserverCoherent
  :: ( MonadVulkan env m
     , Storable a
     )
  => "label" ::: Text
  -> Vk.BufferUsageFlagBits
  -> Int
  -> VectorS.Vector a
  -> Resource.ResourceT m (ObserverCoherent a)
newObserverCoherent label usage initialCapacity initialData = do
  initialBuffer <- createCoherent (Just label) usage initialCapacity initialData
  observer <- Worker.newObserverIO initialBuffer

  context <- ask
  void $! Resource.register do
    currentBuffer <- Worker.readObservedIO observer
    destroy context currentBuffer

  pure observer

{-# INLINE observeCoherentResize_ #-}
observeCoherentResize_
  :: ( MonadVulkan env m
     , Worker.HasOutput source
     , Worker.GetOutput source ~ VectorS.Vector output
     , Storable output
     )
  => source
  -> ObserverCoherent output
  -> m ()
observeCoherentResize_ source observer = do
  Worker.observeIO_ source observer updateCoherentResize_

{-# INLINE observeCoherentSingle #-}
observeCoherentSingle
  :: ( MonadVulkan env m
     , Worker.HasOutput source
     , Worker.GetOutput source ~ output
     , Storable output
     )
  => source
  -> ObserverCoherent output
  -> m ()
observeCoherentSingle source observer =
  Worker.observeIO_ source observer \a b ->
    pokeCoherent a 0 b $> a