packages feed

keid-core-0.1.9.0: src/Resource/Texture/Ktx2.hs

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE NoForeignFunctionInterface #-}

module Resource.Texture.Ktx2
  ( load
  , loadBytes
  , loadKtx2
  ) where

import RIO

import Codec.Compression.Zstd.FFI qualified as Zstd
import Codec.Ktx.KeyValue qualified as KeyValue
import Codec.Ktx2.Header qualified as Header
import Codec.Ktx2.Level qualified as Level
import Codec.Ktx2.Read qualified as Read
import Data.Kind (Type)
import Data.Vector qualified as Vector
import Foreign qualified
import GHC.Stack (withFrozenCallStack)
import UnliftIO.Resource (MonadResource)
import Vulkan.Core10 qualified as Vk
import VulkanMemoryAllocator qualified as VMA

import Engine.Vulkan.Types (HasVulkan(..), MonadVulkan, Queues)
import Resource.Image qualified as Image
import Resource.Source (Source(..))
import Resource.Source qualified as Source
import Resource.Texture (Texture(..), TextureLayers(..))
import Resource.Texture qualified as Texture

load
  :: forall (a :: Type) env m
  .  ( TextureLayers a
     , MonadVulkan env m
     , MonadResource m
     , MonadThrow m
     , HasLogFunc env
     , Typeable a
     , HasCallStack
     )
  => Queues Vk.CommandPool
  -> Source
  -> m (Texture a)
load pool source =
  withFrozenCallStack $
    case source of
      Source.File label path ->
        -- XXX: the codec has a more efficient loader for files
        loadFile label pool path
      _bytes ->
        Source.load (loadBytes source.label pool) source

loadFile
  :: ( TextureLayers a
     , MonadVulkan env m
     , MonadResource m
     , MonadThrow m
     , HasLogFunc env
     )
  => Maybe Text
  -> Queues Vk.CommandPool
  -> FilePath
  -> m (Texture a)
loadFile label pool path =
  bracket (Read.open path) Read.close $
    loadKtx2 (label <|> Just (fromString path)) pool

loadBytes
  :: ( TextureLayers a
     , MonadVulkan env m
     , MonadResource m
     , MonadThrow m
     , HasLogFunc env
     )
  => Maybe Text
  -> Queues Vk.CommandPool
  -> ByteString
  -> m (Texture a)
loadBytes label pool bytes =
  Read.bytes bytes >>= loadKtx2 label pool

loadKtx2
  :: forall a m env src
  .  ( TextureLayers a
     , MonadVulkan env m
     , MonadResource m
     , MonadThrow m
     , HasLogFunc env
     , Read.ReadChunk src
     , Read.ReadLevel src
     )
  => Maybe Text
  -> Queues Vk.CommandPool
  -> Read.Context src
  -> m (Texture a)
loadKtx2 label pool ktx2@(Read.Context _src header) = do
  logDebug $ displayShow (label, header)

  kvd <- Read.keyValueData ktx2
  logDebug $ displayShow (label, format, extent, numLayers, KeyValue.textual kvd)

  levels <- Read.levels ktx2

  unless (Vector.length levels > 0) $
    throwString "Ktx2 contains no image levels"

  unless (Vector.length levels == fromIntegral header.levelCount) $
    throwString $ "Ktx2 level count mismatch " <> show (Vector.length levels, header.levelCount)

  unless (numLayers == textureLayers @a) $
    throwM $ Texture.ArrayError (textureLayers @a) numLayers

  let
    levelSizes = fmap (fromIntegral . (.uncompressedByteLength)) levels
    totalSize = Vector.sum levelSizes
    offsets = Vector.init $ Vector.scanl' (+) 0 levelSizes

  dst <- Image.allocateDst
    pool
    label
    extent
    (fromIntegral $ Vector.length levels)
    numLayers
    format

  vma <- asks getAllocator
  VMA.withBuffer vma (Texture.stageBufferCI totalSize) Texture.stageAllocationCI bracket \(staging, stage, stageInfo) -> do
    liftIO case header.supercompressionScheme of
      Header.SC_NONE ->
        Vector.forM_ (Vector.zip offsets levels) \(offset, level) ->
          liftIO . Read.levelToPtr ktx2 level $
            Foreign.plusPtr (VMA.mappedData stageInfo) offset

      Header.SC_ZSTANDARD -> do
        let maxSize = Vector.maximum levelSizes
        Foreign.allocaBytesAligned maxSize 16 \src ->
          Vector.forM_ (Vector.zip offsets levels) \(offset, level) -> do
            let expected = fromIntegral level.uncompressedByteLength
            Read.levelToPtr ktx2 level src

            res <-
              Zstd.checkError $
                Zstd.decompress
                  (Foreign.plusPtr (VMA.mappedData stageInfo) offset)
                  expected
                  src
                  (fromIntegral level.byteLength)
            case res of
              Right size | size == expected ->
                pure ()
              Right unexpected ->
                throwString $
                  "Zstd decompressed unexpected amount of bytes: " <> show (unexpected, expected)
              Left err ->
                throwString err

      huh ->
        error $ "Unexpected supercompression scheme: " ++ show huh

    VMA.flushAllocation vma stage 0 Vk.WHOLE_SIZE

    final <- Image.copyBufferToDst
      pool
      staging
      dst
      offsets

    pure Texture
      { tFormat         = format
      , tMipLevels      = header.levelCount
      , tLayers         = numLayers
      , tAllocatedImage = final
      }
  where
    format = Vk.Format (fromIntegral header.vkFormat)

    extent = Vk.Extent3D
      { width = header.pixelWidth
      , height = header.pixelHeight
      , depth = max 1 header.pixelDepth
      }

    -- XXX: can be flat array or a cubemap
    numLayers = max header.faceCount header.layerCount