keid-core-0.1.0.0: src/Resource/Texture.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
module Resource.Texture
( Texture(..)
, destroy
, TextureError(..)
-- * Texture types
, Flat
, CubeMap
, ArrayOf
, TextureLayers(..)
-- * Utilities
, allocateCollectionWith
, allocateTextureWith
, debugNameCollection
, TextureLoader
, createImageView
, imageCI
, imageAllocationCI
, stageBufferCI
, stageAllocationCI
) where
import RIO
import Data.Bits ((.|.))
import Data.List qualified as List
import GHC.Stack (withFrozenCallStack)
import GHC.TypeLits (Nat, KnownNat, natVal)
import RIO.FilePath (takeBaseName)
import UnliftIO.Resource qualified as Resource
import Vulkan.Core10 qualified as Vk
import Vulkan.NamedType ((:::))
import Vulkan.Utils.Debug qualified as Debug
import Vulkan.Zero (zero)
import VulkanMemoryAllocator qualified as VMA
import Engine.Vulkan.Types (HasVulkan(getDevice), MonadVulkan, Queues)
import Resource.Collection qualified as Collection
import Resource.Image (AllocatedImage(..), destroyAllocatedImage, subresource)
import Resource.Image qualified as Image
data TextureError
= LoadError Int64 Text
| LayerError Word32 Word32
| MipLevelsError Word32 Int
| ArrayError Word32 Word32
deriving (Eq, Ord, Show)
instance Exception TextureError
data Texture a = Texture
{ tFormat :: Vk.Format
, tMipLevels :: Word32
, tLayers :: Word32 -- ^ Actual number of layers, up to @ArrayOf a@
, tAllocatedImage :: AllocatedImage
}
deriving (Show)
data CubeMap
data Flat
data ArrayOf (layers :: Nat)
-- | Number of expected texture layers to load from resource.
class TextureLayers a where
textureLayers :: Word32
instance TextureLayers CubeMap where
textureLayers = 6
instance TextureLayers Flat where
textureLayers = 1
instance KnownNat n => TextureLayers (ArrayOf n) where
textureLayers = fromInteger $ natVal (Proxy @n)
type TextureLoader m layers = Vk.Format -> Queues Vk.CommandPool -> FilePath -> m (Texture layers)
type TextureLoaderAction m layers = FilePath -> m (Texture layers)
-- * Allocation wrappers
allocateCollectionWith
:: (Resource.MonadResource m, MonadVulkan env m, Traversable t)
=> TextureLoaderAction m layers
-> t FilePath
-> m (Resource.ReleaseKey, t (Texture layers))
allocateCollectionWith action collection = do
res <- traverse (allocateTextureWith action) collection
key <- Resource.register $
traverse_ (Resource.release . fst) res
pure (key, fmap snd res)
allocateTextureWith
:: (Resource.MonadResource m, MonadVulkan env m)
=> TextureLoaderAction m layers
-> FilePath
-> m (Resource.ReleaseKey, Texture layers)
allocateTextureWith action path = do
context <- ask
createTexture <- toIO $ action path
Resource.allocate createTexture (destroy context)
debugNameCollection
:: ( Traversable t
, MonadVulkan env m
, HasLogFunc env
, HasCallStack
)
=> t (Texture layers)
-> t FilePath
-> m ()
debugNameCollection textures paths = do
device <- asks getDevice
for_ names \((ix, path), Texture{tAllocatedImage}) -> do
withFrozenCallStack . logDebug $ displayShow (ix, path)
Debug.nameObject device (Image.aiImage tAllocatedImage) $
fromString $ show @Natural ix <> ":" <> takeBaseName path
where
names = List.zip
(toList $ Collection.enumerate paths)
(toList textures)
-- * Implementation
destroy :: (MonadIO io, HasVulkan context) => context -> Texture a -> io ()
destroy context Texture{tAllocatedImage} =
destroyAllocatedImage context tAllocatedImage
createImageView
:: (MonadIO io, HasVulkan context)
=> context
-> Vk.Image
-> Vk.Format
-> "mip levels" ::: Word32
-> "array layers" ::: Word32
-> io Vk.ImageView
createImageView context image format mipLevels arrayLayers =
Vk.createImageView (getDevice context) imageViewCI Nothing
where
imageViewCI = zero
{ Vk.image = image
, Vk.viewType = viewType
, Vk.format = format
, Vk.components = zero
, Vk.subresourceRange = colorRange
}
viewType =
if arrayLayers == 6 then
Vk.IMAGE_VIEW_TYPE_CUBE
else
Vk.IMAGE_VIEW_TYPE_2D
colorRange =
subresource Vk.IMAGE_ASPECT_COLOR_BIT mipLevels arrayLayers
imageCI :: Vk.Format -> Vk.Extent3D -> Word32 -> Word32 -> Vk.ImageCreateInfo '[]
imageCI format extent mipLevels arrayLayers = zero
{ Vk.flags = flags
, Vk.imageType = Vk.IMAGE_TYPE_2D
, Vk.format = format
, Vk.extent = extent
, Vk.mipLevels = mipLevels
, Vk.arrayLayers = if isCube then 6 else arrayLayers
, Vk.tiling = Vk.IMAGE_TILING_OPTIMAL
, Vk.initialLayout = Vk.IMAGE_LAYOUT_UNDEFINED
, Vk.usage = usage
, Vk.sharingMode = Vk.SHARING_MODE_EXCLUSIVE
, Vk.samples = Vk.SAMPLE_COUNT_1_BIT -- XXX: no multisampling here
}
where
isCube =
arrayLayers == 6
usage =
Vk.IMAGE_USAGE_SAMPLED_BIT .|. -- Sampler
Vk.IMAGE_USAGE_TRANSFER_DST_BIT -- Staging
flags =
if isCube then
Vk.IMAGE_CREATE_CUBE_COMPATIBLE_BIT
else
zero
imageAllocationCI :: VMA.AllocationCreateInfo
imageAllocationCI = zero
{ VMA.usage = VMA.MEMORY_USAGE_GPU_ONLY
, VMA.requiredFlags = Vk.MEMORY_PROPERTY_DEVICE_LOCAL_BIT
}
stageBufferCI :: Integral a => a -> Vk.BufferCreateInfo '[]
stageBufferCI pixelBytes = zero
{ Vk.size = fromIntegral pixelBytes
, Vk.usage = Vk.BUFFER_USAGE_TRANSFER_SRC_BIT
, Vk.sharingMode = Vk.SHARING_MODE_EXCLUSIVE
}
stageAllocationCI :: VMA.AllocationCreateInfo
stageAllocationCI = zero
{ VMA.flags = VMA.ALLOCATION_CREATE_MAPPED_BIT
, VMA.usage = VMA.MEMORY_USAGE_CPU_TO_GPU
, VMA.requiredFlags = Vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT
}