diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2018, Artem M. Chirkin
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of the copyright holder nor the
+      names of its contributors may be used to endorse or promote products
+      derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/easytensor-vulkan.cabal b/easytensor-vulkan.cabal
new file mode 100644
--- /dev/null
+++ b/easytensor-vulkan.cabal
@@ -0,0 +1,37 @@
+name: easytensor-vulkan
+version: 1.0.0.0
+cabal-version: >=1.22
+build-type: Simple
+license: BSD3
+license-file: LICENSE
+copyright: (c) Artem Chirkin
+maintainer: chirkin@arch.ethz.ch
+homepage: https://github.com/achirkin/easytensor#readme
+synopsis: Use easytensor with vulkan-api
+description:
+    Use easytensor DataFrame types with vulkan-api.
+    This package provides an orphan instance of PrimBytes for Vulkan structures,
+    so they can be used as elements of DataFrames.
+    In addition, it provides a few helper functions for working with array-like
+    structures.
+category: geometry, vulkan, bsd3, graphics, library, opengl
+author: Artem Chirkin
+
+source-repository head
+    type: git
+    location: https://github.com/achirkin/easytensor.git
+    subdir: easytensor
+
+
+library
+
+    exposed-modules:
+        Graphics.Vulkan.Marshal.Create.DataFrame
+    build-depends:
+        base >=4.10 && <5,
+        dimensions >=1,
+        easytensor >=1,
+        vulkan-api >=1.1.2.1
+    default-language: Haskell2010
+    hs-source-dirs: src
+    ghc-options: -Wall
diff --git a/src/Graphics/Vulkan/Marshal/Create/DataFrame.hs b/src/Graphics/Vulkan/Marshal/Create/DataFrame.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Vulkan/Marshal/Create/DataFrame.hs
@@ -0,0 +1,236 @@
+{-# LANGUAGE AllowAmbiguousTypes   #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MagicHash             #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE PolyKinds             #-}
+{-# LANGUAGE ScopedTypeVariables   #-}
+{-# LANGUAGE TypeApplications      #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE UnboxedTuples         #-}
+{-# LANGUAGE UndecidableInstances  #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+-- | This module provides an orphan instance of `PrimBytes` for `VulkanMarshalPrim`
+--   structures.
+--   This enables them to be stored in @DataFrames@ from @easytensor@ package.
+--   Thanks to internal structure of Vulkan structures, they can be manipulated
+--   inside DataFrames in a very efficient way (just by copying byte arrays).
+--   However, original @DataFrames@ are based on unpinned arrays;
+--   functions here check this and copy data to new pinned arrays if needed.
+--
+--   In addition to the orphan instance, this module provides a few
+--   handy helper functions.
+module Graphics.Vulkan.Marshal.Create.DataFrame
+  ( setVec, getVec
+  , fillDataFrame, withDFPtr, setDFRef
+    -- * Helpers
+  , VulkanDataFrame (..), VkDataFrame (..)
+  ) where
+
+
+import           Foreign.Storable
+import           GHC.Base
+import           GHC.Ptr                                 (Ptr (..))
+import           Graphics.Vulkan
+import           Graphics.Vulkan.Marshal.Create
+import           Graphics.Vulkan.Marshal.Internal
+import           Numeric.DataFrame
+import           Numeric.DataFrame.Internal.Array.Family (ArraySing (..),
+                                                          ArraySingleton (..))
+import           Numeric.DataFrame.IO
+import           Numeric.Dimensions
+import           Numeric.PrimBytes
+
+
+-- | Write an array of values in one go.
+setVec :: forall fname x t
+        . ( FieldType fname x ~ t
+          , PrimBytes (Vector t (FieldArrayLength fname x))
+          , CanWriteFieldArray fname 0 x
+          )
+       => Vector t (FieldArrayLength fname x) -> CreateVkStruct x '[fname] ()
+setVec v = unsafeIOCreate $ \p -> pokeByteOff p (fieldOffset @fname @x) v
+
+-- | Get an array of values, possibly without copying
+--   (if vector implementation allows).
+getVec :: forall fname x t
+        . ( FieldType fname x ~ t
+          , PrimBytes (Vector t (FieldArrayLength fname x))
+          , CanReadFieldArray fname 0 x
+          , VulkanMarshalPrim x
+          )
+       => x -> Vector t (FieldArrayLength fname x)
+getVec x
+  | ba <- unsafeByteArray x
+  , xaddr <- unsafeAddr x
+  , baddr <- byteArrayContents# ba
+  , I# off <- fieldOffset @fname @x
+  = fromBytes (minusAddr# xaddr baddr +# off) ba
+
+
+instance {-# OVERLAPPABLE#-}
+         (VulkanMarshalPrim a, Storable a) => PrimBytes a where
+    byteSize   a = case sizeOf a of (I# s) -> s
+    {-# INLINE byteSize #-}
+    byteAlign  a = case alignment a of (I# n) -> n
+    {-# INLINE byteAlign #-}
+    byteOffset a = minusAddr# (unsafeAddr a)
+                              (byteArrayContents# (unsafeByteArray a))
+    {-# INLINE byteOffset #-}
+    getBytes = unsafeByteArray
+    {-# INLINE getBytes #-}
+    fromBytes = unsafeFromByteArrayOffset
+    {-# INLINE fromBytes #-}
+    readBytes mba off = unsafeCoerce# (newVkData# f)
+      where
+        f :: Ptr a -> IO ()
+        f (Ptr addr) = IO $ \s ->
+          (# copyMutableByteArrayToAddr# (unsafeCoerce# mba)
+                 off addr (byteSize @a undefined) s
+           , () #)
+    writeBytes mba off a
+      = copyAddrToByteArray# (unsafeAddr a) mba off (byteSize @a undefined)
+    readAddr addr = unsafeCoerce# (peekVkData# (Ptr addr) :: IO a)
+    writeAddr a addr s
+      = case unsafeCoerce# (pokeVkData# (Ptr addr) a :: IO ()) s of
+         (# s', () #) -> s'
+
+
+
+
+-- | Run some operation with a pointer to the first item in the frame.
+--   All items of the frame are kept in a contiguous memory area accessed by
+--   that pointer.
+--
+--   The function attempts to get an underlying `ByteArray#` without data copy;
+--   otherwise, it creates a new pinned `ByteArray#` and passes a pointer to it.
+--   Therefore:
+--
+--     * Sometimes, @Ptr a@ points to the original DF; sometimes, to a copied one.
+--     * If the original DF is based on unpinned `ByteArray#`, using this
+--       performs a copy anyway.
+--
+withDFPtr :: VulkanDataFrame a ds
+          => DataFrame a ds -> (Ptr a -> IO b) -> IO b
+withDFPtr x k
+  | d <- frameToVkData x = do
+  b <- k (Ptr (unsafeAddr d))
+  touchVkData# d
+  return b
+
+-- | A variant of `setVkRef` that writes a pointer to a contiguous array of
+--   structures.
+--
+--   Write a pointer to a vulkan structure - member of current structure
+--    and make sure the member exists as long as this structure exists.
+--
+--   Prefer this function to using @unsafePtr a@, because the latter
+--    does not keep the dependency information in GC, which results in
+--    member structure being garbage-collected and the reference being invalid.
+setDFRef :: forall fname x a ds
+          . ( CanWriteField fname x
+            , FieldType fname x ~ Ptr a
+            , VulkanDataFrame a ds
+            )
+         => DataFrame a ds -> CreateVkStruct x '[fname] ()
+setDFRef v = unsafeCoerce# f -- workaround for the hidden CreateVkStruct constr.
+  where
+    d = frameToVkData v
+    f :: Ptr x -> IO ( ([Ptr ()],[IO ()]) , ())
+    f p = (,) ([],[touchVkData# d])
+       <$> writeField @fname @x p (Ptr (unsafeAddr d))
+
+
+
+-- | Given the number of elements, create a new pinned DataFrame and initialize
+--   it using the provided function.
+--
+--   The argument function is called one time with a `Ptr` pointing to the
+--   beginning of a contiguous array.
+--   This array is converted into a dataframe, possibly without copying.
+--
+--   It is safe to pass result of this function to `withDFPtr`.
+fillDataFrame :: forall a
+               . PrimBytes a
+              => Word -> (Ptr a -> IO ()) -> IO (Vector a (XN 0))
+fillDataFrame n k
+  | Dx (_ :: Dim n) <- someDimVal n
+  , E <- inferASing' @a @'[n]
+  , E <- inferPrim' @a @'[n]
+  = do
+     mdf <- newPinnedDataFrame
+     withDataFramePtr mdf k
+     XFrame <$> unsafeFreezeDataFrame @a @'[n] mdf
+fillDataFrame _ _ = error "fillDataFrame: impossible combination of arguments."
+
+
+
+
+-- | Special data type used to provide @VulkanMarshal@ instance for DataFrames.
+--   It is guaranteed to be pinned.
+data VkDataFrame (t :: l) (ds :: [k]) = VkDataFrame# Addr# ByteArray#
+
+instance PrimBytes (DataFrame t ds) => Storable (VkDataFrame t ds) where
+    sizeOf _ = I# (byteSize @(DataFrame t ds) undefined)
+    {-# INLINE sizeOf #-}
+    alignment _ = I# (byteAlign @(DataFrame t ds) undefined)
+    {-# INLINE alignment #-}
+    peek = peekVkData#
+    {-# INLINE peek #-}
+    poke = pokeVkData#
+    {-# INLINE poke #-}
+
+instance VulkanMarshalPrim (VkDataFrame t ds) where
+    unsafeAddr (VkDataFrame# a _) = a
+    {-# INLINE unsafeAddr #-}
+    unsafeByteArray (VkDataFrame# _ b) = b
+    {-# INLINE unsafeByteArray #-}
+    unsafeFromByteArrayOffset off b
+      = VkDataFrame# (plusAddr# (byteArrayContents# b) off) b
+    {-# INLINE unsafeFromByteArrayOffset #-}
+
+instance PrimBytes (DataFrame t ds) => VulkanMarshal (VkDataFrame t ds) where
+    type StructFields (VkDataFrame t ds) = '[]
+    type CUnionType (VkDataFrame t ds) = 'False
+    type ReturnedOnly (VkDataFrame t ds) = 'False
+    type StructExtends (VkDataFrame t ds) = '[]
+
+
+class VulkanDataFrame a (ds :: [k]) where
+    -- | Construct a new @VkDataFrame@ possibly without copying.
+    --   It performs no copy if the @DataFrame@ implementation is a pinned @ByteArray#@.
+    frameToVkData :: DataFrame a ds -> VkDataFrame a ds
+    -- | Construct a new (pinned if implementation allows) DataFrame from VK data,
+    --   possibly without copying.
+    --
+    --   Note, this is a user responsibility to check if the real size of @VkDataFrame@
+    --   and the dimensionality @ds@ agree.
+    vkDataToFrame :: Dims ds -> VkDataFrame a ds -> DataFrame a ds
+
+
+instance (PrimBytes a, ArraySingleton a ds, Dimensions ds)
+      => VulkanDataFrame a (ds :: [Nat]) where
+    frameToVkData x = case aSing @a @ds of
+      -- Generic ByteArray implementation
+      ABase
+        | ba <- getBytes x
+        , isTrue# (isByteArrayPinned# ba) -- check if it is pinned
+        -> unsafeFromByteArrayOffset (byteOffset x) ba
+      -- For other implementations we have no other options than just to copy the data
+      _ | E <- inferPrim x -> case runRW#
+        ( \s0 -> case newAlignedPinnedByteArray#
+                        (byteSize @(DataFrame a ds) undefined)
+                        (byteAlign @(DataFrame a ds) undefined) s0 of
+          (# s1, mba #) -> unsafeFreezeByteArray# mba (writeBytes mba 0# x s1)
+        ) of (# _, ba #) -> VkDataFrame# (byteArrayContents# ba) ba
+
+    vkDataToFrame _ (VkDataFrame# addr ba)
+      | E <- inferPrim' @a @ds
+      = fromBytes (addr `minusAddr#` byteArrayContents# ba) ba
+
+instance (PrimBytes a, All KnownXNatType ds)
+      => VulkanDataFrame a (ds :: [XNat]) where
+    frameToVkData (XFrame x) = unsafeCoerce# (frameToVkData x)
+    vkDataToFrame (XDims (ds :: Dims ns)) d
+      | E <- inferASing' @a @ns = XFrame (vkDataToFrame ds (unsafeCoerce# d))
