From 8659041bbc19f22130b11e93b5dc1fede39cb35c Mon Sep 17 00:00:00 2001
From: TheLavaBlock <36247472+TheLavaBlock@users.noreply.github.com>
Date: Wed, 18 Sep 2019 13:36:28 +0200
Subject: [PATCH] v0.4.2

---
 .appveyor.yml                                 |  20 +
 .gitignore                                    |   1 +
 .gitmodules                                   |  52 ++
 .travis.yml                                   |  42 ++
 CMakeLists.txt                                | 254 +++++++++
 LICENSE                                       |  21 +
 README.md                                     | 345 +++++++++++-
 ext/Vulkan-Headers                            |   1 +
 ext/VulkanMemoryAllocator                     |   1 +
 ext/argh                                      |   1 +
 ext/assimp                                    |   1 +
 ext/better-enums                              |   1 +
 ext/bitmap                                    |   1 +
 ext/glfw                                      |   1 +
 ext/gli                                       |   1 +
 ext/glm                                       |   1 +
 ext/json                                      |   1 +
 ext/physfs                                    |   1 +
 ext/selene                                    |   1 +
 ext/spdlog                                    |   1 +
 ext/stb                                       |   1 +
 ext/tinyfiledialogs                           |   1 +
 ext/tinyobjloader                             |   1 +
 ext/volk                                      |   1 +
 liblava/base.hpp                              |  11 +
 liblava/base/base.cpp                         |  71 +++
 liblava/base/base.hpp                         |  76 +++
 liblava/base/device.cpp                       | 212 ++++++++
 liblava/base/device.hpp                       | 129 +++++
 liblava/base/instance.cpp                     | 304 +++++++++++
 liblava/base/instance.hpp                     |  65 +++
 liblava/base/memory.cpp                       | 143 +++++
 liblava/base/memory.hpp                       |  64 +++
 liblava/base/physical_device.cpp              | 109 ++++
 liblava/base/physical_device.hpp              |  49 ++
 liblava/core.hpp                              |  12 +
 liblava/core/data.hpp                         | 166 ++++++
 liblava/core/id.hpp                           | 200 +++++++
 liblava/core/math.hpp                         |  86 +++
 liblava/core/time.hpp                         | 117 +++++
 liblava/core/types.hpp                        | 122 +++++
 liblava/core/version.hpp                      |  42 ++
 liblava/def.hpp                               |  20 +
 liblava/frame.hpp                             |  13 +
 liblava/frame/frame.cpp                       | 307 +++++++++++
 liblava/frame/frame.hpp                       | 119 +++++
 liblava/frame/input.cpp                       |  97 ++++
 liblava/frame/input.hpp                       | 159 ++++++
 liblava/frame/render_target.cpp               |  93 ++++
 liblava/frame/render_target.hpp               |  86 +++
 liblava/frame/render_thread.hpp               |  70 +++
 liblava/frame/renderer.cpp                    | 164 ++++++
 liblava/frame/renderer.hpp                    |  51 ++
 liblava/frame/swapchain.cpp                   | 237 +++++++++
 liblava/frame/swapchain.hpp                   |  71 +++
 liblava/frame/window.cpp                      | 256 +++++++++
 liblava/frame/window.hpp                      | 147 ++++++
 liblava/fwd.hpp                               |  74 +++
 liblava/lava.hpp                              |  11 +
 liblava/resource.hpp                          |  11 +
 liblava/resource/buffer.cpp                   | 127 +++++
 liblava/resource/buffer.hpp                   |  54 ++
 liblava/resource/format.cpp                   | 460 ++++++++++++++++
 liblava/resource/format.hpp                   |  48 ++
 liblava/resource/image.cpp                    | 113 ++++
 liblava/resource/image.hpp                    |  72 +++
 liblava/resource/mesh.cpp                     | 381 ++++++++++++++
 liblava/resource/mesh.hpp                     | 112 ++++
 liblava/resource/texture.cpp                  | 491 ++++++++++++++++++
 liblava/resource/texture.hpp                  | 103 ++++
 liblava/utils.hpp                             |  12 +
 liblava/utils/file.cpp                        | 437 ++++++++++++++++
 liblava/utils/file.hpp                        | 185 +++++++
 liblava/utils/log.hpp                         |  80 +++
 liblava/utils/random.hpp                      |  59 +++
 liblava/utils/telegram.hpp                    | 100 ++++
 liblava/utils/thread.hpp                      |  97 ++++
 liblava/utils/utility.hpp                     |  53 ++
 .../texture}/lava_block_logo_200.png          | Bin
 .../texture}/lava_block_logo_50.png           | Bin
 tests/driver.cpp                              |  67 +++
 tests/driver.hpp                              |  81 +++
 tests/tests.cpp                               | 220 ++++++++
 83 files changed, 8063 insertions(+), 5 deletions(-)
 create mode 100644 .appveyor.yml
 create mode 100644 .gitignore
 create mode 100644 .gitmodules
 create mode 100644 .travis.yml
 create mode 100644 CMakeLists.txt
 create mode 100644 LICENSE
 create mode 160000 ext/Vulkan-Headers
 create mode 160000 ext/VulkanMemoryAllocator
 create mode 160000 ext/argh
 create mode 160000 ext/assimp
 create mode 160000 ext/better-enums
 create mode 160000 ext/bitmap
 create mode 160000 ext/glfw
 create mode 160000 ext/gli
 create mode 160000 ext/glm
 create mode 160000 ext/json
 create mode 160000 ext/physfs
 create mode 160000 ext/selene
 create mode 160000 ext/spdlog
 create mode 160000 ext/stb
 create mode 160000 ext/tinyfiledialogs
 create mode 160000 ext/tinyobjloader
 create mode 160000 ext/volk
 create mode 100644 liblava/base.hpp
 create mode 100644 liblava/base/base.cpp
 create mode 100644 liblava/base/base.hpp
 create mode 100644 liblava/base/device.cpp
 create mode 100644 liblava/base/device.hpp
 create mode 100644 liblava/base/instance.cpp
 create mode 100644 liblava/base/instance.hpp
 create mode 100644 liblava/base/memory.cpp
 create mode 100644 liblava/base/memory.hpp
 create mode 100644 liblava/base/physical_device.cpp
 create mode 100644 liblava/base/physical_device.hpp
 create mode 100644 liblava/core.hpp
 create mode 100644 liblava/core/data.hpp
 create mode 100644 liblava/core/id.hpp
 create mode 100644 liblava/core/math.hpp
 create mode 100644 liblava/core/time.hpp
 create mode 100644 liblava/core/types.hpp
 create mode 100644 liblava/core/version.hpp
 create mode 100644 liblava/def.hpp
 create mode 100644 liblava/frame.hpp
 create mode 100644 liblava/frame/frame.cpp
 create mode 100644 liblava/frame/frame.hpp
 create mode 100644 liblava/frame/input.cpp
 create mode 100644 liblava/frame/input.hpp
 create mode 100644 liblava/frame/render_target.cpp
 create mode 100644 liblava/frame/render_target.hpp
 create mode 100644 liblava/frame/render_thread.hpp
 create mode 100644 liblava/frame/renderer.cpp
 create mode 100644 liblava/frame/renderer.hpp
 create mode 100644 liblava/frame/swapchain.cpp
 create mode 100644 liblava/frame/swapchain.hpp
 create mode 100644 liblava/frame/window.cpp
 create mode 100644 liblava/frame/window.hpp
 create mode 100644 liblava/fwd.hpp
 create mode 100644 liblava/lava.hpp
 create mode 100644 liblava/resource.hpp
 create mode 100644 liblava/resource/buffer.cpp
 create mode 100644 liblava/resource/buffer.hpp
 create mode 100644 liblava/resource/format.cpp
 create mode 100644 liblava/resource/format.hpp
 create mode 100644 liblava/resource/image.cpp
 create mode 100644 liblava/resource/image.hpp
 create mode 100644 liblava/resource/mesh.cpp
 create mode 100644 liblava/resource/mesh.hpp
 create mode 100644 liblava/resource/texture.cpp
 create mode 100644 liblava/resource/texture.hpp
 create mode 100644 liblava/utils.hpp
 create mode 100644 liblava/utils/file.cpp
 create mode 100644 liblava/utils/file.hpp
 create mode 100644 liblava/utils/log.hpp
 create mode 100644 liblava/utils/random.hpp
 create mode 100644 liblava/utils/telegram.hpp
 create mode 100644 liblava/utils/thread.hpp
 create mode 100644 liblava/utils/utility.hpp
 rename {doc/img => res/texture}/lava_block_logo_200.png (100%)
 rename {doc/img => res/texture}/lava_block_logo_50.png (100%)
 create mode 100644 tests/driver.cpp
 create mode 100644 tests/driver.hpp
 create mode 100644 tests/tests.cpp

diff --git a/.appveyor.yml b/.appveyor.yml
new file mode 100644
index 00000000..2c2fa5be
--- /dev/null
+++ b/.appveyor.yml
@@ -0,0 +1,20 @@
+version: '{build}'
+branches:
+  only:
+  - master
+skip_tags: true
+image: Visual Studio 2019
+platform: x64
+configuration: Release
+environment:
+  CFLAGS: /WX
+matrix:
+  fast_finish: true
+install:
+  - git submodule update --init
+before_build:
+  - cmake -G "Visual Studio 16 2019" -A x64 .
+build:
+  project: $(APPVEYOR_BUILD_FOLDER)\$(APPVEYOR_PROJECT_NAME).sln
+  parallel: true
+  verbosity: minimal
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..a4164267
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,52 @@
+[submodule "ext/argh"]
+	path = ext/argh
+	url = https://github.com/adishavit/argh.git
+[submodule "ext/assimp"]
+	path = ext/assimp
+	url = https://github.com/assimp/assimp.git
+[submodule "ext/better-enums"]
+	path = ext/better-enums
+	url = https://github.com/aantron/better-enums.git
+[submodule "ext/bitmap"]
+	path = ext/bitmap
+	url = https://github.com/ArashPartow/bitmap.git
+[submodule "ext/glfw"]
+	path = ext/glfw
+	url = https://github.com/glfw/glfw.git
+[submodule "ext/gli"]
+	path = ext/gli
+	url = https://github.com/g-truc/gli.git
+[submodule "ext/glm"]
+	path = ext/glm
+	url = https://github.com/g-truc/glm.git
+[submodule "ext/json"]
+	path = ext/json
+	url = https://github.com/nlohmann/json.git
+[submodule "ext/physfs"]
+	path = ext/physfs
+	url = https://github.com/criptych/physfs.git
+[submodule "ext/selene"]
+	path = ext/selene
+	url = https://github.com/kmhofmann/selene.git
+	ignore = dirty
+[submodule "ext/spdlog"]
+	path = ext/spdlog
+	url = https://github.com/gabime/spdlog.git
+[submodule "ext/stb"]
+	path = ext/stb
+	url = https://github.com/nothings/stb.git
+[submodule "ext/tinyfiledialogs"]
+	path = ext/tinyfiledialogs
+	url = https://github.com/native-toolkit/tinyfiledialogs.git
+[submodule "ext/tinyobjloader"]
+	path = ext/tinyobjloader
+	url = https://github.com/syoyo/tinyobjloader.git
+[submodule "ext/volk"]
+	path = ext/volk
+	url = https://github.com/zeux/volk.git
+[submodule "ext/Vulkan-Headers"]
+	path = ext/Vulkan-Headers
+	url = https://github.com/KhronosGroup/Vulkan-Headers.git
+[submodule "ext/VulkanMemoryAllocator"]
+	path = ext/VulkanMemoryAllocator
+	url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..79bc7a49
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,42 @@
+language: cpp
+compiler: gcc
+sudo: false
+dist: trusty
+
+os:
+  - linux
+
+git:
+  submodules: false
+
+addons:
+  apt:
+    sources:
+      - ubuntu-toolchain-r-test
+    packages:
+      - g++-8
+
+before_install:
+  - sudo apt-get update
+  - sudo apt-get install -y libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev
+  - git submodule update --init
+
+install:
+  - DEPS_DIR="${TRAVIS_BUILD_DIR}/deps"
+  - mkdir ${DEPS_DIR} && cd ${DEPS_DIR}
+  - travis_retry wget --no-check-certificate https://github.com/Kitware/CMake/releases/download/v3.15.3/cmake-3.15.3-Linux-x86_64.tar.gz
+  - tar -xvf cmake-3.15.3-Linux-x86_64.tar.gz > /dev/null
+  - mv cmake-3.15.3-Linux-x86_64 cmake-install
+  - PATH=${DEPS_DIR}/cmake-install:${DEPS_DIR}/cmake-install/bin:$PATH
+  - cd ${TRAVIS_BUILD_DIR}
+
+script:
+  - echo ${PATH}
+  - cmake --version
+  - export CC=gcc-8
+  - export CXX=g++-8
+  - echo ${CXX}
+  - ${CXX} --version
+  - ${CXX} -v
+  - mkdir -p build && cd build
+  - cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=Release .. && make -j4
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 00000000..3759eabf
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,254 @@
+# file      : CMakeLists.txt
+# copyright : Copyright (c) 2018-present, Lava Block OÜ
+# license   : MIT; see accompanying LICENSE file
+
+cmake_minimum_required(VERSION 3.12)
+
+project(liblava VERSION 0.4.2 LANGUAGES C CXX)
+
+message("")
+message("========================================================================")
+message(" copyright (c) 2018-present, Lava Block OÜ                 MIT licensed ")
+message("========================================================================")
+message("                                                                        ")
+message("  _|  _|  _|            _|                                              ")
+message("  _|      _|_|_|        _|        _|_|_|      _|      _|        _|_|_|  ")
+message("  _|  _|  _|    _|      _|      _|    _|      _|      _|      _|    _|  ")
+message("  _|  _|  _|    _|      _|      _|    _|        _|  _|        _|    _|  ")
+message("  _|  _|  _|_|_|        _|        _|_|_|          _|            _|_|_|  ")
+message("                                                                        ")
+message("========================================================================")
+message(" 2019 preview 2                                                  v0.4.2 ")
+message("========================================================================")
+message(" https://git.io/liblava                                  lava-block.com ")
+message("========================================================================")
+
+if(CMAKE_COMPILER_IS_GNUCXX)
+        set_property(GLOBAL PROPERTY ALLOW_DUPLICATE_CUSTOM_TARGETS ON)
+endif()
+
+if(NOT DEFINED CMAKE_SUPPRESS_DEVELOPER_WARNINGS)
+        set(CMAKE_SUPPRESS_DEVELOPER_WARNINGS 1 CACHE INTERNAL "No dev warnings")
+endif()
+
+set(LIBLAVA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/liblava)
+set(LIBLAVA_EXT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/ext)
+set(LIBLAVA_TESTS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/tests)
+
+# add_subdirectory(ext)
+
+message(">> liblava/core")
+
+if(CMAKE_COMPILER_IS_GNUCXX)
+        find_package (Threads)
+endif()
+
+add_library(lava.core INTERFACE)
+
+target_include_directories(lava.core INTERFACE
+        ${CMAKE_CURRENT_SOURCE_DIR}
+        ${LIBLAVA_EXT_DIR}/better-enums
+        ${LIBLAVA_EXT_DIR}/glm
+        )
+
+target_sources(lava.core INTERFACE
+        ${LIBLAVA_DIR}/core/data.hpp
+        ${LIBLAVA_DIR}/core/id.hpp
+        ${LIBLAVA_DIR}/core/math.hpp
+        ${LIBLAVA_DIR}/core/time.hpp
+        ${LIBLAVA_DIR}/core/types.hpp
+        ${LIBLAVA_DIR}/core/version.hpp
+        )
+
+target_compile_features(lava.core INTERFACE 
+        cxx_std_20
+        )
+
+if(CMAKE_COMPILER_IS_GNUCXX)
+        target_link_libraries(lava.core INTERFACE 
+                stdc++fs 
+                ${CMAKE_THREAD_LIBS_INIT}
+                )
+endif()
+
+message(">> liblava/utils")
+
+add_library(lava.utils STATIC
+        ${LIBLAVA_DIR}/utils/file.cpp
+        ${LIBLAVA_DIR}/utils/file.hpp
+        ${LIBLAVA_DIR}/utils/log.hpp
+        ${LIBLAVA_DIR}/utils/random.hpp
+        ${LIBLAVA_DIR}/utils/telegram.hpp
+        ${LIBLAVA_DIR}/utils/thread.hpp
+        ${LIBLAVA_DIR}/utils/utility.hpp
+        ${LIBLAVA_EXT_DIR}/tinyfiledialogs/tinyfiledialogs.c
+        )
+
+if(WIN32)
+        set_source_files_properties(${LIBLAVA_EXT_DIR}/tinyfiledialogs/tinyfiledialogs.c PROPERTIES COMPILE_FLAGS " /W0 ")
+endif()
+
+target_include_directories(lava.utils PUBLIC
+        ${LIBLAVA_EXT_DIR}/spdlog/include
+        ${LIBLAVA_EXT_DIR}/physfs/src
+        ${LIBLAVA_EXT_DIR}/tinyfiledialogs
+        ${LIBLAVA_EXT_DIR}/json/single_include
+        )
+
+message(">>> ext/physfs")
+
+set(PHYSFS_ARCHIVE_ZIP ON CACHE BOOL "" FORCE)
+set(PHYSFS_ARCHIVE_7Z OFF CACHE BOOL "" FORCE)
+set(PHYSFS_ARCHIVE_GRP OFF CACHE BOOL "" FORCE)
+set(PHYSFS_ARCHIVE_WAD OFF CACHE BOOL "" FORCE)
+set(PHYSFS_ARCHIVE_HOG OFF CACHE BOOL "" FORCE)
+set(PHYSFS_ARCHIVE_MVL OFF CACHE BOOL "" FORCE)
+set(PHYSFS_ARCHIVE_QPAK OFF CACHE BOOL "" FORCE)
+set(PHYSFS_ARCHIVE_SLB OFF CACHE BOOL "" FORCE)
+set(PHYSFS_ARCHIVE_ISO9660 OFF CACHE BOOL "" FORCE)
+set(PHYSFS_ARCHIVE_VDF OFF CACHE BOOL "" FORCE)
+
+set(PHYSFS_BUILD_SHARED OFF CACHE BOOL "" FORCE)
+set(PHYSFS_BUILD_TEST OFF CACHE BOOL "" FORCE)
+add_subdirectory(${LIBLAVA_EXT_DIR}/physfs physfs EXCLUDE_FROM_ALL)
+
+message("<<< ext/physfs")
+
+target_link_libraries(lava.utils
+        lava.core
+        physfs-static
+        )
+
+message(">> liblava/base")
+
+add_library(lava.base STATIC
+        ${LIBLAVA_DIR}/base/base.cpp
+        ${LIBLAVA_DIR}/base/base.hpp
+        ${LIBLAVA_DIR}/base/device.cpp
+        ${LIBLAVA_DIR}/base/device.hpp
+        ${LIBLAVA_DIR}/base/instance.cpp
+        ${LIBLAVA_DIR}/base/instance.hpp
+        ${LIBLAVA_DIR}/base/memory.cpp
+        ${LIBLAVA_DIR}/base/memory.hpp
+        ${LIBLAVA_DIR}/base/physical_device.cpp
+        ${LIBLAVA_DIR}/base/physical_device.hpp
+        ${LIBLAVA_EXT_DIR}/volk/volk.c
+        )
+
+target_include_directories(lava.base PUBLIC
+        ${LIBLAVA_EXT_DIR}/Vulkan-Headers/include
+        ${LIBLAVA_EXT_DIR}/VulkanMemoryAllocator/src
+        ${LIBLAVA_EXT_DIR}/volk
+        )
+
+target_link_libraries(lava.base 
+        lava.utils 
+        ${CMAKE_DL_LIBS}
+        )
+
+message(">> liblava/resource")
+
+option(LIBLAVA_ASSIMP "build assimp library" ON)
+
+add_library(lava.resource STATIC
+        ${LIBLAVA_DIR}/resource/buffer.cpp
+        ${LIBLAVA_DIR}/resource/buffer.hpp
+        ${LIBLAVA_DIR}/resource/format.cpp
+        ${LIBLAVA_DIR}/resource/format.hpp
+        ${LIBLAVA_DIR}/resource/image.cpp
+        ${LIBLAVA_DIR}/resource/image.hpp
+        ${LIBLAVA_DIR}/resource/mesh.cpp
+        ${LIBLAVA_DIR}/resource/mesh.hpp
+        ${LIBLAVA_DIR}/resource/texture.cpp
+        ${LIBLAVA_DIR}/resource/texture.hpp
+        )
+
+if(LIBLAVA_ASSIMP)
+        set(LIBLAVA_ASSIMP_INC ${LIBLAVA_EXT_DIR}/assimp/include)
+        set(LIBLAVA_ASSIMP_LIB assimp)
+        target_compile_definitions(lava.resource PRIVATE LIBLAVA_ASSIMP=1)
+else()
+        set(LIBLAVA_ASSIMP_INC "")
+        set(LIBLAVA_ASSIMP_LIB "")
+endif()
+
+target_include_directories(lava.resource PUBLIC
+        ${LIBLAVA_EXT_DIR}/stb
+        ${LIBLAVA_EXT_DIR}/gli
+        ${LIBLAVA_EXT_DIR}/tinyobjloader
+        ${LIBLAVA_EXT_DIR}/bitmap
+        ${LIBLAVA_EXT_DIR}/selene
+        ${LIBLAVA_ASSIMP_INC}
+        )
+
+message(">>> ext/assimp")
+
+if(LIBLAVA_ASSIMP)
+        set(BUILD_SHARED_LIBS OFF)
+        set(ASSIMP_BUILD_TESTS OFF)
+        set(INJECT_DEBUG_POSTFIX OFF)
+        set(ASSIMP_BUILD_ASSIMP_TOOLS OFF)
+        add_subdirectory(${LIBLAVA_EXT_DIR}/assimp assimp EXCLUDE_FROM_ALL)
+endif()
+
+message("<<< ext/assimp")
+message(">>> ext/selene")
+
+add_subdirectory(${LIBLAVA_EXT_DIR}/selene selene EXCLUDE_FROM_ALL)
+
+message("<<< ext/glfw")
+
+target_link_libraries(lava.resource
+        lava.base
+        ${LIBLAVA_ASSIMP_LIB}
+        )
+
+message(">> liblava/frame")
+
+add_library(lava.frame STATIC
+        ${LIBLAVA_DIR}/frame/frame.cpp
+        ${LIBLAVA_DIR}/frame/frame.hpp
+        ${LIBLAVA_DIR}/frame/input.cpp
+        ${LIBLAVA_DIR}/frame/input.hpp
+        ${LIBLAVA_DIR}/frame/render_target.cpp
+        ${LIBLAVA_DIR}/frame/render_target.hpp
+        ${LIBLAVA_DIR}/frame/render_thread.hpp
+        ${LIBLAVA_DIR}/frame/renderer.cpp
+        ${LIBLAVA_DIR}/frame/renderer.hpp
+        ${LIBLAVA_DIR}/frame/swapchain.cpp
+        ${LIBLAVA_DIR}/frame/swapchain.hpp
+        ${LIBLAVA_DIR}/frame/window.cpp
+        ${LIBLAVA_DIR}/frame/window.hpp
+        )
+
+target_include_directories(lava.frame PUBLIC
+        ${LIBLAVA_EXT_DIR}/glfw/include
+        ${LIBLAVA_EXT_DIR}/argh
+        )
+
+message(">>> ext/glfw")
+
+set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
+set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
+set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
+add_subdirectory(${LIBLAVA_EXT_DIR}/glfw glfw EXCLUDE_FROM_ALL)
+
+message("<<< ext/glfw")
+
+target_link_libraries(lava.frame
+        lava.resource
+        glfw
+        ${GLFW_LIBRARIES}
+        )
+
+message(">> tests/driver")
+
+add_executable(lava 
+        ${LIBLAVA_TESTS_DIR}/driver.cpp
+        ${LIBLAVA_TESTS_DIR}/driver.hpp
+        ${LIBLAVA_TESTS_DIR}/tests.cpp
+        )
+
+target_link_libraries(lava lava.frame)
+
+message("========================================================================")
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..ea0fd86f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018-present, Lava Block OÜ
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 2b96abca..58ceec08 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,340 @@
-<p align="center">
-  <a href="https://lava-block.com">
-    <img src="https://raw.githubusercontent.com/liblava/liblava/master/doc/img/lava_block_logo_200.png">
-  </a>
-</p>
+<img align="left" src="https://raw.githubusercontent.com/liblava/liblava/master/res/texture/lava_block_logo_200.png">
+
+**liblava is a modern and easy-to-use library for the <a href="https://www.khronos.org/vulkan/">Vulkan API</a>**
+
+liblava is a lean framework that provides essentials for low-level graphics and is specially well suited for prototyping, tooling and education.
+
+    + C++20 standard
+    + Modular (5 modules)
+    + Cross Platform (Windows | Linux)
+
+![version](https://img.shields.io/badge/version-0.4.2-blue) [![LoC](https://tokei.rs/b1/github/liblava/liblava?category=code)](https://github.com/liblava/liblava) [![Build Status](https://travis-ci.com/liblava/liblava.svg?branch=master)](https://travis-ci.com/liblava/liblava) [![Build status](https://ci.appveyor.com/api/projects/status/gxvjpo73qf637hy3?svg=true)](https://ci.appveyor.com/project/TheLavaBlock/liblava) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Twitter URL](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&label=Follow)](https://twitter.com/thelavablock)
+
+## features
+
+* written in modern C++
+* latest Vulkan API support
+* run loop abstraction
+* window and input handling
+* render target and renderer
+* load texture and mesh
+* file system and logging
+* test driver
+* and much more...
+
+WIP latest **<a href="https://github.com/liblava/liblava/releases">preview 2 / v0.4.2</a>**  (Sep 18, 2019)
+
+## hello frame
+
+Let's write **Hello World** in Vulkan: *a simple program that just renders a colored window*
+
+Well, just ? - Vulkan is a low-level, verbose graphics API and such a program can take several hundred lines of code.
+
+All we need is a window, a device and a renderer.
+
+The good news is that **liblava** will set up all for you.
+
+```c++
+#include <liblava/lava.h>
+
+using namespace lava;
+```
+
+Here are a few examples to get to know `lava`.
+
+#### 1. frame init
+
+```c++
+int main(int argc, char* argv[]) {
+
+    frame frame( {argc, argv} );
+
+    return frame.ready() ? 0 : error::not_ready;
+}
+```
+
+This is how to initialize `lava::frame` with command line arguments.
+
+#### 2. run loop
+
+```c++
+frame frame(argh);
+if (!frame.ready())
+    return error::not_ready;
+
+auto count = 0;
+
+frame.add_run([&]() {
+
+    sleep(1.0);
+    count++;
+
+    log()->debug("{} - running {} sec", count, frame.get_running_time());
+
+    if (count == 3)
+        frame.shut_down();
+
+    return true;
+});
+
+return frame.run() ? 0 : error::aborted;
+```
+
+The last line performs a loop with the run we added before. If *count* reaches 3 it will exit. 
+
+#### 3. window input
+
+Here is another example that shows how to create a window and handle input. This is straightforward.
+
+```c++
+frame frame(argh);
+if (!frame.ready())
+    return error::not_ready;
+
+window window;
+if (!window.create())
+    return error::create_failed;
+
+input input;
+window.assign(&input);
+
+input.key_listeners.add([&](key_event::ref event) {
+
+    if (event.key == GLFW_KEY_ESCAPE && event.action == GLFW_PRESS)
+        frame.shut_down();
+});
+
+frame.add_run([&]() {
+
+    handle_events(input);
+
+    if (window.has_close_request())
+        frame.shut_down();
+
+    return true;
+});
+
+return frame.run() ? 0 : error::aborted;
+```
+
+With this knowledge in hand let's write **hello frame**...
+
+#### 4. clear color
+
+```c++
+frame frame(argh);
+if (!frame.ready())
+    return error::not_ready;
+
+window window;
+if (!window.create())
+    return error::create_failed;
+
+input input;
+window.assign(&input);
+
+input.key_listeners.add([&](key_event::ref event) {
+
+    if (event.key == GLFW_KEY_ESCAPE && event.action == GLFW_PRESS)
+        frame.shut_down();
+});
+
+auto device = frame.create_device();
+if (!device)
+    return error::create_failed;
+
+auto render_target = create_target(&window, device);
+if (!render_target)
+    return error::create_failed;
+
+renderer simple_renderer;
+if (!simple_renderer.create(render_target->get_swapchain()))
+    return error::create_failed;
+
+auto frame_count = render_target->get_frame_count();
+
+VkCommandPool cmd_pool;
+VkCommandBuffers cmd_bufs(frame_count);
+
+auto build_cmd_bufs = [&]() {
+
+    VkCommandPoolCreateInfo create_info
+    {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
+        .queueFamilyIndex = device->get_graphics_queue().family_index,
+    };
+    if (!check(device->call().vkCreateCommandPool(device->get(), &create_info, memory::alloc(), &cmd_pool)))
+        return false;
+
+    VkCommandBufferAllocateInfo alloc_info
+    {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
+        .commandPool = cmd_pool,
+        .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
+        .commandBufferCount = frame_count,
+    };
+    if (!check(device->call().vkAllocateCommandBuffers(device->get(), &alloc_info, cmd_bufs.data())))
+        return false;
+
+    VkCommandBufferBeginInfo begin_info
+    {
+        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+        .flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT,
+    };
+
+    VkClearColorValue clear_color = { random(0.f, 1.f), random(0.f, 1.f), random(0.f, 1.f), 0.f };
+
+    VkImageSubresourceRange image_range
+    {
+        .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+        .levelCount = 1,
+        .layerCount = 1,
+    };
+
+    for (auto i = 0u; i < frame_count; i++) {
+
+        auto cmd_buf = cmd_bufs[i];
+        auto target_image = render_target->get_backbuffer_image(i);
+
+        if (!check(device->call().vkBeginCommandBuffer(cmd_buf, &begin_info)))
+            return false;
+
+        insert_image_memory_barrier(device, cmd_buf, target_image,
+                                    VK_ACCESS_MEMORY_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
+                                    VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                                    VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 
+                                    image_range);
+
+        device->call().vkCmdClearColorImage(cmd_buf, target_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 
+                                            &clear_color, 1, &image_range);
+
+        insert_image_memory_barrier(device, cmd_buf, target_image,
+                                    VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT,
+                                    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
+                                    VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 
+                                    image_range);
+
+        if (!check(device->call().vkEndCommandBuffer(cmd_buf)))
+            return false;
+    }
+
+    return true;
+};
+
+auto clean_cmd_bufs = [&]() {
+
+    for (auto& cmd_buf : cmd_bufs)
+        device->call().vkFreeCommandBuffers(device->get(), cmd_pool, 1, &cmd_buf);
+
+    device->call().vkDestroyCommandPool(device->get(), cmd_pool, memory::alloc());
+};
+
+build_cmd_bufs();
+
+render_target->on_swapchain_start = build_cmd_bufs;
+render_target->on_swapchain_stop = clean_cmd_bufs;
+
+frame.add_run([&]() {
+
+    handle_events(input);
+
+    if (window.has_close_request())
+        return frame.shut_down();
+
+    if (window.has_resize_request())
+        return window.handle_resize();
+
+    if (window.iconified()) {
+
+        frame.set_wait_for_events(true);
+        return true;
+
+    } else {
+
+        if (frame.waiting_for_events())
+            frame.set_wait_for_events(false);
+    }
+
+    auto frame_index = simple_renderer.begin_frame();
+    if (!frame_index)
+        return true;
+
+    return simple_renderer.end_frame({ cmd_bufs[*frame_index] });
+});
+
+frame.add_run_end([&]() {
+
+    clean_cmd_bufs();
+    simple_renderer.destroy();
+    render_target->destroy();
+});
+
+return frame.run() ? 0 : error::aborted;
+```
+
+Check the [Awesome Vulkan ecosystem](http://www.vinjn.com/awesome-vulkan/) to learn more about Vulkan (Tutorials/Samples/Books).
+
+## tests
+
+Run the driver to test [the above examples](https://github.com/liblava/liblava/blob/master/tests/tests.cpp).
+
+List all tests:
+
+```
+$ lava -t
+```
+
+Run test 2 for example:
+
+```
+$ lava 2
+```
+
+## requirements
+
+* **C++20** compatible compiler
+* CMake **3.12+**
+* [Vulkan SDK](https://vulkan.lunarg.com)
+
+## build
+
+```
+$ git clone https://github.com/liblava/liblava.git
+$ cd liblava
+
+$ git submodule update --init --recursive
+
+$ mkdir build
+$ cd build
+
+$ cmake ..
+$ make
+```
+
+## third-party / license
+
+* [argh](https://github.com/adishavit/argh) / 3-clause BSD
+* [assimp](https://github.com/assimp/assimp) / modified 3-clause BSD
+* [better-enums](https://github.com/aantron/better-enums) / 2-clause BSD
+* [bitmap](https://github.com/ArashPartow/bitmap) / MIT
+* [glfw](https://github.com/glfw/glfw) / zlib
+* [gli](https://github.com/g-truc/gli) / MIT
+* [glm](https://github.com/g-truc/glm) / MIT
+* [json](https://github.com/nlohmann/json) / MIT
+* [physfs](https://github.com/criptych/physfs) / zlib
+* [selene](https://github.com/kmhofmann/selene) / MIT
+* [spdlog](https://github.com/gabime/spdlog) / MIT
+* [stb](https://github.com/nothings/stb) / MIT
+* [tinyfiledialogs](https://github.com/native-toolkit/tinyfiledialogs) / zlib
+* [tinyobjloader](https://github.com/syoyo/tinyobjloader) / MIT
+* [volk](https://github.com/zeux/volk) / MIT
+* [Vulkan-Headers](https://github.com/KhronosGroup/Vulkan-Headers) / Apache 2.0
+* [VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) / MIT
+
+## license
+
+liblava is licensed under [MIT License](LICENSE.md) which allows you to use the software for any purpose you might like, including commercial and for-profit use. However, this library includes several third-party Open-Source libraries, which are licensed under their own respective Open-Source licenses. These licenses allow static linking with closed source software. All copies of liblava must include a copy of the MIT License terms and the copyright notice.
+
+Copyright (c) 2018-present, <a href="https://lava-block.com">Lava Block OÜ</a>
+
+<img src="https://raw.githubusercontent.com/liblava/liblava/master/res/texture/lava_block_logo_50.png">
\ No newline at end of file
diff --git a/ext/Vulkan-Headers b/ext/Vulkan-Headers
new file mode 160000
index 00000000..5b44df19
--- /dev/null
+++ b/ext/Vulkan-Headers
@@ -0,0 +1 @@
+Subproject commit 5b44df19e040fca0048ab30c553a8c2d2cb9623e
diff --git a/ext/VulkanMemoryAllocator b/ext/VulkanMemoryAllocator
new file mode 160000
index 00000000..909f36b7
--- /dev/null
+++ b/ext/VulkanMemoryAllocator
@@ -0,0 +1 @@
+Subproject commit 909f36b714c9239ee0b112a321220213a474ba53
diff --git a/ext/argh b/ext/argh
new file mode 160000
index 00000000..d4f5b4a2
--- /dev/null
+++ b/ext/argh
@@ -0,0 +1 @@
+Subproject commit d4f5b4a2bf6fcf9c6a4f83c512fce9bbcf584b42
diff --git a/ext/assimp b/ext/assimp
new file mode 160000
index 00000000..b6edcb35
--- /dev/null
+++ b/ext/assimp
@@ -0,0 +1 @@
+Subproject commit b6edcb35a8253699a18a5e97f0fc8169fcda175a
diff --git a/ext/better-enums b/ext/better-enums
new file mode 160000
index 00000000..4b76e778
--- /dev/null
+++ b/ext/better-enums
@@ -0,0 +1 @@
+Subproject commit 4b76e7783bacf5220e0f29d19845a72d19ce0d6b
diff --git a/ext/bitmap b/ext/bitmap
new file mode 160000
index 00000000..624fb94c
--- /dev/null
+++ b/ext/bitmap
@@ -0,0 +1 @@
+Subproject commit 624fb94c7e7986d1dda731fa15ee8fce4d7a13c4
diff --git a/ext/glfw b/ext/glfw
new file mode 160000
index 00000000..7105ff2d
--- /dev/null
+++ b/ext/glfw
@@ -0,0 +1 @@
+Subproject commit 7105ff2dfd004a46bd732c1d0c9f461bae6d51b3
diff --git a/ext/gli b/ext/gli
new file mode 160000
index 00000000..559cbe1e
--- /dev/null
+++ b/ext/gli
@@ -0,0 +1 @@
+Subproject commit 559cbe1ec38878e182507d331e0780fbae5baf15
diff --git a/ext/glm b/ext/glm
new file mode 160000
index 00000000..7c07544b
--- /dev/null
+++ b/ext/glm
@@ -0,0 +1 @@
+Subproject commit 7c07544b34c2f8655e4134239137d32aa2ccd5c8
diff --git a/ext/json b/ext/json
new file mode 160000
index 00000000..ea60d40f
--- /dev/null
+++ b/ext/json
@@ -0,0 +1 @@
+Subproject commit ea60d40f4a60a47d3be9560d8f7bc37c163fe47b
diff --git a/ext/physfs b/ext/physfs
new file mode 160000
index 00000000..b574ac6e
--- /dev/null
+++ b/ext/physfs
@@ -0,0 +1 @@
+Subproject commit b574ac6ec7f58aa29cd7bb1c785c962b5d155b5f
diff --git a/ext/selene b/ext/selene
new file mode 160000
index 00000000..27c90148
--- /dev/null
+++ b/ext/selene
@@ -0,0 +1 @@
+Subproject commit 27c90148b48c04f32b788abe724166b14234eda1
diff --git a/ext/spdlog b/ext/spdlog
new file mode 160000
index 00000000..a51b4856
--- /dev/null
+++ b/ext/spdlog
@@ -0,0 +1 @@
+Subproject commit a51b4856377a71f81b6d74b9af459305c4c644f8
diff --git a/ext/stb b/ext/stb
new file mode 160000
index 00000000..052dce11
--- /dev/null
+++ b/ext/stb
@@ -0,0 +1 @@
+Subproject commit 052dce117ed989848a950308bd99eef55525dfb1
diff --git a/ext/tinyfiledialogs b/ext/tinyfiledialogs
new file mode 160000
index 00000000..b5e548ad
--- /dev/null
+++ b/ext/tinyfiledialogs
@@ -0,0 +1 @@
+Subproject commit b5e548ad5eeb1bab4a34a6010057d06d85c26734
diff --git a/ext/tinyobjloader b/ext/tinyobjloader
new file mode 160000
index 00000000..d47e8545
--- /dev/null
+++ b/ext/tinyobjloader
@@ -0,0 +1 @@
+Subproject commit d47e8545eb2b43dc34b542d9a27e6ca5970ebe33
diff --git a/ext/volk b/ext/volk
new file mode 160000
index 00000000..d6c2bde9
--- /dev/null
+++ b/ext/volk
@@ -0,0 +1 @@
+Subproject commit d6c2bde94f70506240eac22763cf3adf930cedf5
diff --git a/liblava/base.hpp b/liblava/base.hpp
new file mode 100644
index 00000000..6c327147
--- /dev/null
+++ b/liblava/base.hpp
@@ -0,0 +1,11 @@
+// file      : liblava/base.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/base/base.hpp>
+#include <liblava/base/device.hpp>
+#include <liblava/base/instance.hpp>
+#include <liblava/base/memory.hpp>
+#include <liblava/base/physical_device.hpp>
diff --git a/liblava/base/base.cpp b/liblava/base/base.cpp
new file mode 100644
index 00000000..57df8644
--- /dev/null
+++ b/liblava/base/base.cpp
@@ -0,0 +1,71 @@
+// file      : liblava/base/base.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/base/base.hpp>
+
+bool lava::check(VkResult result) {
+
+    if (result == VK_SUCCESS)
+        return true;
+
+    if (result > 0) {
+
+        log()->critical("VkResult = {}", to_string(result));
+        return false;
+    }
+
+    log()->error("VkResult = {}", to_string(result));
+    return false;
+}
+
+lava::string lava::to_string(VkResult result) {
+
+#define RETURN_STR(result_code) case result_code: return string(#result_code);
+
+    switch (result) {
+
+        RETURN_STR(VK_SUCCESS)
+        RETURN_STR(VK_NOT_READY)
+        RETURN_STR(VK_TIMEOUT)
+        RETURN_STR(VK_EVENT_SET)
+        RETURN_STR(VK_EVENT_RESET)
+        RETURN_STR(VK_INCOMPLETE)
+        RETURN_STR(VK_ERROR_OUT_OF_HOST_MEMORY)
+        RETURN_STR(VK_ERROR_OUT_OF_DEVICE_MEMORY)
+        RETURN_STR(VK_ERROR_INITIALIZATION_FAILED)
+        RETURN_STR(VK_ERROR_DEVICE_LOST)
+        RETURN_STR(VK_ERROR_MEMORY_MAP_FAILED)
+        RETURN_STR(VK_ERROR_LAYER_NOT_PRESENT)
+        RETURN_STR(VK_ERROR_EXTENSION_NOT_PRESENT)
+        RETURN_STR(VK_ERROR_FEATURE_NOT_PRESENT)
+        RETURN_STR(VK_ERROR_INCOMPATIBLE_DRIVER)
+        RETURN_STR(VK_ERROR_TOO_MANY_OBJECTS)
+        RETURN_STR(VK_ERROR_FORMAT_NOT_SUPPORTED)
+        RETURN_STR(VK_ERROR_FRAGMENTED_POOL)
+        RETURN_STR(VK_ERROR_OUT_OF_POOL_MEMORY)
+        RETURN_STR(VK_ERROR_INVALID_EXTERNAL_HANDLE)
+        RETURN_STR(VK_ERROR_SURFACE_LOST_KHR)
+        RETURN_STR(VK_ERROR_NATIVE_WINDOW_IN_USE_KHR)
+        RETURN_STR(VK_SUBOPTIMAL_KHR)
+        RETURN_STR(VK_ERROR_OUT_OF_DATE_KHR)
+        RETURN_STR(VK_ERROR_INCOMPATIBLE_DISPLAY_KHR)
+        RETURN_STR(VK_ERROR_VALIDATION_FAILED_EXT)
+        RETURN_STR(VK_ERROR_INVALID_SHADER_NV)
+        RETURN_STR(VK_ERROR_INVALID_DRM_FORMAT_MODIFIER_PLANE_LAYOUT_EXT)
+        RETURN_STR(VK_ERROR_FRAGMENTATION_EXT)
+        RETURN_STR(VK_ERROR_NOT_PERMITTED_EXT)
+        RETURN_STR(VK_ERROR_INVALID_DEVICE_ADDRESS_EXT)
+        RETURN_STR(VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT)
+
+    default:
+        return fmt::format("[invalid VkResult {}]", result);
+    }
+
+#undef RETURN_STR
+}
+
+lava::string lava::version_to_string(ui32 version) {
+
+    return fmt::format("{}.{}.{}", VK_VERSION_MAJOR(version), VK_VERSION_MINOR(version), VK_VERSION_PATCH(version));
+}
diff --git a/liblava/base/base.hpp b/liblava/base/base.hpp
new file mode 100644
index 00000000..9743cae9
--- /dev/null
+++ b/liblava/base/base.hpp
@@ -0,0 +1,76 @@
+// file      : liblava/base/base.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/utils.hpp>
+
+#define VK_NO_PROTOTYPES
+#include <vulkan/vulkan.h>
+
+#include <volk.h>
+
+namespace lava {
+
+using VkFormats = std::vector<VkFormat>;
+
+using VkImages = std::vector<VkImage>;
+using VkImagesRef = VkImages const&;
+
+using VkImageViews = std::vector<VkImageView>;
+using VkImageViewsRef = VkImageViews const&;
+
+using VkFramebuffers = std::vector<VkFramebuffer>;
+
+using VkCommandPools = std::vector<VkCommandPool>;
+using VkCommandBuffers = std::vector<VkCommandBuffer>;
+
+using VkFences = std::vector<VkFence>;
+using VkSemaphores = std::vector<VkSemaphore>;
+
+using VkPresentModeKHRs = std::vector<VkPresentModeKHR>;
+
+using VkDescriptorSets = std::vector<VkDescriptorSet>;
+using VkDescriptorSetLayouts = std::vector<VkDescriptorSetLayout>;
+using VkDescriptorSetLayoutBindings = std::vector<VkDescriptorSetLayoutBinding>;
+
+using VkPushConstantRanges = std::vector<VkPushConstantRange>;
+
+using VkAttachmentReferences = std::vector<VkAttachmentReference>;
+
+using VkClearValues = std::vector<VkClearValue>;
+
+using VkPipelineShaderStageCreateInfos = std::vector<VkPipelineShaderStageCreateInfo>;
+
+using VkVertexInputBindingDescriptions = std::vector<VkVertexInputBindingDescription>;
+using VkVertexInputAttributeDescriptions = std::vector<VkVertexInputAttributeDescription>;
+
+using VkPipelineColorBlendAttachmentStates = std::vector<VkPipelineColorBlendAttachmentState>;
+using VkDynamicStates = std::vector<VkDynamicState>;
+
+using VkQueueFamilyPropertiesList = std::vector<VkQueueFamilyProperties>;
+using VkExtensionPropertiesList = std::vector<VkExtensionProperties>;
+
+using VkLayerPropertiesList = std::vector<VkLayerProperties>;
+using VkExtensionPropertiesList = std::vector<VkExtensionProperties>;
+
+using VkPhysicalDevices = std::vector<VkPhysicalDevice>;
+
+bool check(VkResult result);
+inline bool failed(VkResult result) { return !check(result); }
+
+string to_string(VkResult result);
+string version_to_string(ui32 version);
+
+// limits
+
+static constexpr ui32 const Vk_Limit_DescriptorSets		= 4;
+static constexpr ui32 const Vk_Limit_Bindings			= 16;
+static constexpr ui32 const Vk_Limit_Attachments		= 8;
+static constexpr ui32 const Vk_Limit_VertexAttribs		= 16;
+static constexpr ui32 const Vk_Limit_VertexBuffers		= 4;
+static constexpr ui32 const Vk_Limit_PushConstant_Size	= 128;
+static constexpr ui32 const Vk_Limit_UBO_Size			= 16 * 1024;
+
+} // lava
diff --git a/liblava/base/device.cpp b/liblava/base/device.cpp
new file mode 100644
index 00000000..f194382e
--- /dev/null
+++ b/liblava/base/device.cpp
@@ -0,0 +1,212 @@
+// file      : liblava/base/device.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/base/device.hpp>
+#include <liblava/base/instance.hpp>
+#include <liblava/base/physical_device.hpp>
+
+namespace lava {
+
+device::~device() {
+
+    destroy();
+}
+
+bool device::create(create_param const& param) {
+
+    _physical_device = param._physical_device;
+    if (!_physical_device)
+        return false;
+
+    std::vector<VkDeviceQueueCreateInfo> queue_create_info_list(param.queue_info_list.size());
+
+    for (size_t i = 0, e = param.queue_info_list.size(); i != e; ++i) {
+
+        queue_create_info_list[i].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
+
+        ui32 index = 0;
+        if (!_physical_device->get_queue_family(index, param.queue_info_list[i].flags)) {
+
+            log()->error("device::create physical_device::get_queue_family failed");
+            return false;
+        }
+
+        queue_create_info_list[i].queueFamilyIndex = index;
+        queue_create_info_list[i].queueCount = param.queue_info_list[i].count();
+        queue_create_info_list[i].pQueuePriorities = param.queue_info_list[i].priorities.data();
+    }
+
+    VkDeviceCreateInfo create_info
+    {
+        .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
+        .queueCreateInfoCount = to_ui32(queue_create_info_list.size()),
+        .pQueueCreateInfos = queue_create_info_list.data(),
+        .enabledLayerCount = 0,
+        .ppEnabledLayerNames = nullptr,
+        .enabledExtensionCount = to_ui32(param.extensions.size()),
+        .ppEnabledExtensionNames = param.extensions.data(),
+        .pEnabledFeatures = &_physical_device->get_features(),
+    };
+
+    if (failed(vkCreateDevice(_physical_device->get(), &create_info, memory::alloc(), &vk_device))) {
+
+        log()->error("device::create vkCreateDevice failed");
+        return false;
+    }
+
+    volkLoadDeviceTable(&table, vk_device);
+
+    graphics_queues.clear();
+    compute_queues.clear();
+    transfer_queues.clear();
+
+    index_map queue_family_map;
+
+    for (size_t i = 0, ei = queue_create_info_list.size(); i != ei; ++i) {
+
+        if (!queue_family_map.count(queue_create_info_list[i].queueFamilyIndex))
+            queue_family_map.emplace(queue_create_info_list[i].queueFamilyIndex, 0);
+
+        for (size_t j = 0, ej = queue_create_info_list[i].queueCount; j != ej; ++j) {
+
+            auto counter = queue_family_map[queue_create_info_list[i].queueFamilyIndex];
+            queue_family_map[queue_create_info_list[i].queueFamilyIndex]++;
+
+            VkQueue queue = nullptr;
+            call().vkGetDeviceQueue(vk_device, queue_create_info_list[i].queueFamilyIndex, counter, &queue);
+
+            if (param.queue_info_list[i].flags & VK_QUEUE_GRAPHICS_BIT)
+                graphics_queues.push_back({ queue, queue_create_info_list[i].queueFamilyIndex });
+            if (param.queue_info_list[i].flags & VK_QUEUE_COMPUTE_BIT)
+                compute_queues.push_back({ queue, queue_create_info_list[i].queueFamilyIndex });
+            if (param.queue_info_list[i].flags & VK_QUEUE_TRANSFER_BIT)
+                transfer_queues.push_back({ queue, queue_create_info_list[i].queueFamilyIndex });
+        }
+    }
+
+    return create_descriptor_pool();
+}
+
+void device::destroy() {
+
+    if (!vk_device)
+        return;
+
+    graphics_queues.clear();
+    compute_queues.clear();
+    transfer_queues.clear();
+
+    call().vkDestroyDescriptorPool(vk_device, descriptor_pool, memory::alloc());
+    descriptor_pool = nullptr;
+
+    call().vkDestroyDevice(vk_device, memory::alloc());
+    vk_device = nullptr;
+
+    table = {};
+}
+
+bool device::create_descriptor_pool() {
+
+    auto const count = 11u;
+    auto const size = 1000u;
+
+    VkDescriptorPoolSize const pool_size[count] = 
+    {
+        { VK_DESCRIPTOR_TYPE_SAMPLER, size },
+        { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, size },
+        { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, size },
+        { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, size },
+        { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, size },
+        { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, size },
+        { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, size },
+        { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, size },
+        { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, size },
+        { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, size },
+        { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, size },
+    };
+
+    VkDescriptorPoolCreateInfo pool_info
+    {
+        .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
+        .flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,
+        .maxSets = size * count,
+        .poolSizeCount = count,
+        .pPoolSizes = pool_size,
+    };
+
+    return check(call().vkCreateDescriptorPool(vk_device, &pool_info, memory::alloc(), &descriptor_pool));
+}
+
+bool device::is_surface_supported(VkSurfaceKHR surface) const {
+
+    return _physical_device->is_surface_supported(get_graphics_queue().family_index, surface);
+}
+
+VkPhysicalDeviceFeatures const& device::get_features() const { return _physical_device->get_features(); }
+
+VkPhysicalDeviceProperties const& device::get_properties() const { return _physical_device->get_properties(); }
+
+device::ptr device_manager::create() {
+
+    auto& physical_device = instance::get_first_physical_device();
+
+    if (!physical_device.is_swapchain_supported())
+        return nullptr;
+
+    auto device = create(physical_device.create_default_device_param());
+    if (!device)
+        return nullptr;
+
+    auto allocator = allocator::make(physical_device.get(), device->get());
+    if (!allocator)
+        return nullptr;
+
+    device->set_allocator(allocator);
+
+    return device;
+}
+
+device::ptr device_manager::create(device::create_param const& param) {
+
+    auto result = std::make_shared<device>();
+    if (!result->create(param))
+        return nullptr;
+
+    list.push_back(result);
+    return result;
+}
+
+void device_manager::wait_idle() {
+
+    for (auto& device : list)
+        device->wait_for_idle();
+}
+
+void device_manager::clear() {
+
+    for (auto& device : list)
+        device->destroy();
+
+    list.clear();
+}
+
+} // lava
+
+bool lava::load_and_create_shader_module(device* device, data const& data, VkShaderModule& out) {
+
+    VkShaderModuleCreateInfo shader_module_create_info
+    {
+        .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
+        .codeSize = data.size,
+        .pCode = reinterpret_cast<ui32*>(data.ptr),
+    };
+
+    VkShaderModule new_module;
+    auto result = device->call().vkCreateShaderModule(device->get(), &shader_module_create_info, memory::alloc(), &new_module);
+    if (failed(result))
+        return false;
+
+    out = new_module;
+    return true;
+}
diff --git a/liblava/base/device.hpp b/liblava/base/device.hpp
new file mode 100644
index 00000000..3331ca24
--- /dev/null
+++ b/liblava/base/device.hpp
@@ -0,0 +1,129 @@
+// file      : liblava/base/device.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/base/base.hpp>
+#include <liblava/base/memory.hpp>
+
+namespace lava {
+
+// fwd
+struct physical_device;
+
+struct device : no_copy_no_move {
+
+    using ptr = std::shared_ptr<device>;
+    using list = std::vector<device::ptr>;
+
+    struct create_param {
+
+        const physical_device* _physical_device = nullptr;
+
+        struct queue_info {
+
+            using list = std::vector<queue_info>;
+
+            VkQueueFlags flags = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_TRANSFER_BIT;
+
+            using priority_list = std::vector<float>;
+            priority_list priorities;
+
+            ui32 count() const { return to_ui32(priorities.size()); }
+
+            explicit queue_info(ui32 count = 1) {
+
+                for (auto i = 0u; i < count; ++i)
+                    priorities.push_back(1.f);
+            }
+        };
+
+        queue_info::list queue_info_list;
+
+        names extensions;
+
+        void set_default_queues() {
+
+            extensions.push_back("VK_KHR_swapchain");
+            queue_info_list.resize(1);
+        }
+    };
+
+    ~device();
+
+    bool create(create_param const& param);
+    void destroy();
+
+    struct queue {
+
+        using list = std::vector<queue>;
+        using ref = queue const&;
+
+        VkQueue vk_queue = nullptr;
+        ui32 family_index = 0;
+    };
+
+    queue::ref get_graphics_queue(ui32 index = 0) const { return get_graphics_queues().at(index); }
+    queue::ref get_compute_queue(ui32 index = 0) const { return get_compute_queues().at(index); }
+    queue::ref get_transfer_queue(ui32 index = 0) const { return get_transfer_queues().at(index); }
+
+    queue::list const& get_graphics_queues() const { return graphics_queues; }
+    queue::list const& get_compute_queues() const { return compute_queues; }
+    queue::list const& get_transfer_queues() const { return transfer_queues; }
+
+    VkDevice get() const { return vk_device; }
+    VolkDeviceTable const& call() const { return table; }
+
+    bool wait_for_idle() const { return check(call().vkDeviceWaitIdle(vk_device)); }
+
+    VkDescriptorPool get_descriptor_pool() const { return descriptor_pool; }
+
+    physical_device const* get_physical_device() const { return _physical_device; }
+
+    VkPhysicalDeviceFeatures const& get_features() const;
+    VkPhysicalDeviceProperties const& get_properties() const;
+
+    bool is_surface_supported(VkSurfaceKHR surface) const;
+
+    void set_allocator(allocator::ptr value) { _allocator = value; }
+    allocator::ptr get_allocator() { return _allocator; }
+
+    VmaAllocator alloc() const { return _allocator != nullptr ? _allocator->get() : nullptr; }
+
+private:
+    bool create_descriptor_pool();
+
+    VkDevice vk_device = nullptr;
+    physical_device const* _physical_device = nullptr;
+
+    VolkDeviceTable table = {};
+
+    VkDescriptorPool descriptor_pool = nullptr;
+
+    queue::list graphics_queues;
+    queue::list compute_queues;
+    queue::list transfer_queues;
+
+    allocator::ptr _allocator;
+};
+
+struct device_manager {
+
+    device::ptr create();
+
+    device::ptr create(device::create_param const& param);
+
+    device::list const& get_all() const { return list; }
+
+    void wait_idle();
+
+    void clear();
+
+private:
+    device::list list;
+};
+
+bool load_and_create_shader_module(device* device, data const& data, VkShaderModule& out);
+
+} // lava
diff --git a/liblava/base/instance.cpp b/liblava/base/instance.cpp
new file mode 100644
index 00000000..42f08ef7
--- /dev/null
+++ b/liblava/base/instance.cpp
@@ -0,0 +1,304 @@
+// file      : liblava/base/instance.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/base/instance.hpp>
+#include <liblava/base/memory.hpp>
+
+#define VK_LAYER_LUNARG_STANDARD_VALIDATION_NAME "VK_LAYER_LUNARG_standard_validation"
+#define VK_LAYER_LUNARG_ASSISTENT_LAYER_NAME "VK_LAYER_LUNARG_assistant_layer"
+#define VK_LAYER_RENDERDOC_CAPTURE_NAME "VK_LAYER_RENDERDOC_Capture"
+
+namespace lava {
+
+struct instance::impl {
+
+    explicit impl(instance* instance_) : _instance(instance_) {}
+
+    instance* _instance = nullptr;
+    instance::debug debug;
+
+    void print_info() const;
+
+    bool create_validation_report();
+    void destroy_validation_report();
+
+    VkDebugUtilsMessengerEXT debug_messanger = nullptr;
+};
+
+instance::instance() : _impl(std::make_unique<impl>(this)) {}
+
+instance::~instance() {
+
+    if (vk_instance)
+        destroy();
+}
+
+bool instance::create(create_param& param, debug& debug, name appName) {
+
+    _impl->debug = debug;
+
+    if (_impl->debug.validation) {
+
+        if (!exists(param.layer_to_enable, VK_LAYER_LUNARG_STANDARD_VALIDATION_NAME))
+            param.layer_to_enable.push_back(VK_LAYER_LUNARG_STANDARD_VALIDATION_NAME);
+    }
+
+    if (_impl->debug.render_doc) {
+
+        if (!exists(param.layer_to_enable, VK_LAYER_RENDERDOC_CAPTURE_NAME))
+            param.layer_to_enable.push_back(VK_LAYER_RENDERDOC_CAPTURE_NAME);
+    }
+
+    if (_impl->debug.assistent) {
+
+        if (!exists(param.layer_to_enable, VK_LAYER_LUNARG_ASSISTENT_LAYER_NAME))
+            param.layer_to_enable.push_back(VK_LAYER_LUNARG_ASSISTENT_LAYER_NAME);
+    }
+
+    if (_impl->debug.utils) {
+
+        if (!exists(param.extension_to_enable, VK_EXT_DEBUG_UTILS_EXTENSION_NAME))
+            param.extension_to_enable.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
+    }
+
+    if (!param.check()) {
+
+        log()->error("instance create param failed");
+        return false;
+    }
+
+    VkApplicationInfo application_info
+    {
+        .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+        .pApplicationName = appName ? appName : _lava_,
+        .applicationVersion = VK_MAKE_VERSION(0, 1, 0),
+        .pEngineName = _lava_,
+    };
+
+    application_info.engineVersion = VK_MAKE_VERSION(_internal_version.major, _internal_version.minor, _internal_version.patch);
+    application_info.apiVersion = VK_API_VERSION_1_0;
+
+    VkInstanceCreateInfo instanceCreateInfo
+    {
+        .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+        .pApplicationInfo = &application_info,
+        .enabledLayerCount = to_ui32(param.layer_to_enable.size()),
+        .ppEnabledLayerNames = param.layer_to_enable.data(),
+        .enabledExtensionCount = to_ui32(param.extension_to_enable.size()),
+        .ppEnabledExtensionNames = param.extension_to_enable.data(),
+    };
+
+    auto result = vkCreateInstance(&instanceCreateInfo, memory::alloc(), &vk_instance);
+    if (failed(result))
+        return false;
+
+    volkLoadInstance(vk_instance);
+
+    if (!enumerate_physical_devices())
+        return false;
+
+    if (_impl->debug.info)
+        _impl->print_info();
+
+    if (_impl->debug.utils)
+        if (!_impl->create_validation_report())
+            return false;
+
+    return true;
+}
+
+void instance::destroy() {
+
+    if (!vk_instance)
+        return
+
+    physical_devices.clear();
+
+    if (_impl->debug.utils)
+        _impl->destroy_validation_report();
+
+    vkDestroyInstance(vk_instance, memory::alloc());
+    vk_instance = nullptr;
+}
+
+bool instance::create_param::check() const {
+
+    auto layer_properties = enumerate_layer_properties();
+    for (auto const& layer_name : layer_to_enable) {
+
+        auto itr = std::find_if(layer_properties.begin(), layer_properties.end(), 
+                            [&](VkLayerProperties const& extProp) {
+                                return strcmp(layer_name, extProp.layerName) == 0;
+                            });
+
+        if (itr == layer_properties.end())
+            return false;
+    }
+
+    auto extensions_properties = enumerate_extension_properties();
+    for (auto const& ext_name : extension_to_enable) {
+
+        auto itr = std::find_if(extensions_properties.begin(), extensions_properties.end(),
+                            [&](VkExtensionProperties const& extProp) {
+                                return strcmp(ext_name, extProp.extensionName) == 0;
+                            });
+
+        if (itr == extensions_properties.end())
+            return false;
+    }
+
+    return true;
+}
+
+static VKAPI_ATTR VkBool32 VKAPI_CALL validation_callback(VkDebugUtilsMessageSeverityFlagBitsEXT message_severity,
+        VkDebugUtilsMessageTypeFlagsEXT message_type, const VkDebugUtilsMessengerCallbackDataEXT* callback_data, void* user_data) {
+
+    if (message_severity == VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) {
+
+        log()->error("validation report: {}", callback_data->pMessage);
+        assert(!"check validation error");
+
+    } else if (message_severity == VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT)
+        log()->warn("validation report: {}", callback_data->pMessage);
+    else if (message_severity == VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT)
+        log()->info("validation report: {}", callback_data->pMessage);
+    else if (message_severity == VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT)
+        log()->trace("validation report: {}", callback_data->pMessage);
+
+    return VK_FALSE;
+}
+
+bool instance::impl::create_validation_report() {
+
+    VkDebugUtilsMessengerCreateInfoEXT create_info
+    {
+        .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
+        .messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
+        .messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
+        .pfnUserCallback = validation_callback,
+    };
+
+    if (debug.verbose)
+        create_info.messageSeverity |= VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT;
+
+    return check(vkCreateDebugUtilsMessengerEXT(_instance->vk_instance, &create_info, memory::alloc(), &debug_messanger));
+}
+
+void instance::impl::destroy_validation_report() {
+
+    if (!debug_messanger)
+        return;
+
+    vkDestroyDebugUtilsMessengerEXT(_instance->vk_instance, debug_messanger, memory::alloc());
+
+    debug_messanger = nullptr;
+}
+
+void instance::impl::print_info() const {
+
+    auto global_extensions = instance::enumerate_extension_properties();
+    log()->info("found {} global extensions", global_extensions.size());
+    for (auto const& extension : global_extensions) {
+
+        log()->info("- {:38} - {:3}", extension.extensionName, VK_VERSION_PATCH(extension.specVersion));
+    }
+
+    auto layer_properties = instance::enumerate_layer_properties();
+    log()->info("found {} instance layers:", layer_properties.size());
+    for (auto const& layer : layer_properties) {
+
+        log()->info("- {:40} - {:8} - {:2} - {}", layer.layerName, version_to_string(layer.specVersion), layer.implementationVersion, layer.description);
+
+        auto extensions = instance::enumerate_extension_properties(layer.layerName);
+        log()->info("-- Found {} extensions", extensions.size());
+
+        for (auto const& extension : extensions) {
+
+            log()->info("--- {:38} - {:6}", extension.extensionName, version_to_string(extension.specVersion));
+        }
+    }
+
+    log()->info("Found {} physical devices", _instance->physical_devices.size());
+
+    auto device_index = 0u;
+    for (auto& physical_device : _instance->physical_devices) {
+
+        auto& properties = physical_device.get_properties();
+
+        log()->info("- {}. {} - {}", device_index++, properties.deviceName, physical_device.get_device_type_string());
+        log()->info("-- apiVersion: {} - driverVersion: {}", version_to_string(properties.apiVersion), version_to_string(properties.driverVersion));
+        log()->info("-- vendorID: {} - deviceID: {}", properties.vendorID, properties.deviceID);
+    }
+}
+
+VkLayerPropertiesList instance::enumerate_layer_properties() {
+
+    auto layer_count = 0u;
+    auto result = vkEnumerateInstanceLayerProperties(&layer_count, nullptr);
+    if (failed(result))
+        return {};
+
+    VkLayerPropertiesList list(layer_count);
+    result = vkEnumerateInstanceLayerProperties(&layer_count, list.data());
+    if (failed(result))
+        return {};
+
+    return list;
+}
+
+VkExtensionPropertiesList instance::enumerate_extension_properties(name layer_name) {
+
+    auto property_count = 0u;
+    auto result = vkEnumerateInstanceExtensionProperties(layer_name, &property_count, nullptr);
+    if (failed(result))
+        return {};
+
+    VkExtensionPropertiesList list(property_count);
+    result = vkEnumerateInstanceExtensionProperties(layer_name, &property_count, list.data());
+    if (failed(result))
+        return {};
+
+    return list;
+}
+
+bool instance::enumerate_physical_devices() {
+
+    physical_devices.clear();
+
+    auto count = 0u;
+    auto result = vkEnumeratePhysicalDevices(vk_instance, &count, nullptr);
+    if (failed(result))
+        return false;
+
+    VkPhysicalDevices devices(count);
+    result = vkEnumeratePhysicalDevices(vk_instance, &count, devices.data());
+    if (failed(result))
+        return false;
+
+    for (auto& device : devices) {
+
+        physical_device physical_device;
+        physical_device.initialize(device);
+        physical_devices.push_back(std::move(physical_device));
+    }
+
+    return true;
+}
+
+internal_version instance::get_version() {
+
+    ui32 instance_version = VK_API_VERSION_1_0;
+
+    auto enumerate_instance_version = (PFN_vkEnumerateInstanceVersion)vkGetInstanceProcAddr(nullptr, "vkEnumerateInstanceVersion");
+    if (enumerate_instance_version)
+        enumerate_instance_version(&instance_version);
+
+    internal_version version;
+    version.major = VK_VERSION_MAJOR(instance_version);
+    version.minor = VK_VERSION_MINOR(instance_version);
+    version.patch = VK_VERSION_PATCH(VK_HEADER_VERSION);
+    return version;
+}
+
+} // lava
diff --git a/liblava/base/instance.hpp b/liblava/base/instance.hpp
new file mode 100644
index 00000000..d39afe5c
--- /dev/null
+++ b/liblava/base/instance.hpp
@@ -0,0 +1,65 @@
+// file      : liblava/base/instance.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/base/physical_device.hpp>
+
+namespace lava {
+
+struct instance : no_copy_no_move {
+
+    struct create_param {
+
+        names layer_to_enable;
+        names extension_to_enable;
+
+        bool check() const;
+    };
+
+    struct debug {
+
+        bool validation = false;
+        bool assistent = false;
+        bool render_doc = false;
+        bool verbose = false;
+        bool utils = false;
+        bool info = false;
+    };
+
+    static instance& singleton() {
+
+        static instance instance;
+        return instance;
+    }
+
+    bool create(create_param& param, debug& debug, name appName = nullptr);
+    void destroy();
+
+    static VkLayerPropertiesList enumerate_layer_properties();
+    static VkExtensionPropertiesList enumerate_extension_properties(name layer_name = nullptr);
+
+    physical_device::list const& get_physical_devices() const { return physical_devices; }
+
+    static physical_device const& get_first_physical_device() { return singleton().physical_devices.front(); }
+
+    static VkInstance get() { return singleton().vk_instance; }
+
+    static internal_version get_version();
+
+private:
+    explicit instance();
+    ~instance();
+
+    bool enumerate_physical_devices();
+
+    VkInstance vk_instance = nullptr;
+
+    physical_device::list physical_devices;
+
+    struct impl;
+    std::unique_ptr<impl> _impl;
+};
+
+} // lava
diff --git a/liblava/base/memory.cpp b/liblava/base/memory.cpp
new file mode 100644
index 00000000..46ce3c54
--- /dev/null
+++ b/liblava/base/memory.cpp
@@ -0,0 +1,143 @@
+// file      : liblava/base/memory.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/base/memory.hpp>
+
+#ifdef _WIN32
+#pragma warning(push, 4)
+#pragma warning(disable : 4127) // conditional expression is constant
+#pragma warning(disable : 4100) // unreferenced formal parameter
+#pragma warning(disable : 4189) // local variable is initialized but not referenced
+#pragma warning(disable : 4324) // structure was padded due to alignment specifier
+#else
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wtype-limits"
+#endif
+
+#define VMA_STATIC_VULKAN_FUNCTIONS 0
+#define VMA_IMPLEMENTATION
+#include <vk_mem_alloc.h>
+
+#ifdef _WIN32
+#pragma warning(pop)
+#else
+#pragma GCC diagnostic pop
+#endif
+
+#define LAVA_CUSTOM_CPU_ALLOCATION_CALLBACK_USER_DATA (void*)(intptr_t) 20180208
+
+namespace lava {
+
+static void* custom_cpu_allocation(void* user_data, size_t size, size_t alignment, VkSystemAllocationScope allocation_scope) {
+
+    assert(user_data == LAVA_CUSTOM_CPU_ALLOCATION_CALLBACK_USER_DATA);
+    return alloc_data(size, alignment);
+}
+
+static void* custom_cpu_reallocation(void* user_data, void* original, size_t size, size_t alignment, VkSystemAllocationScope allocation_scope) {
+
+    assert(user_data == LAVA_CUSTOM_CPU_ALLOCATION_CALLBACK_USER_DATA);
+    return realloc_data(original, size, alignment);
+}
+
+static void custom_cpu_free(void* user_Data, void* memory) {
+
+    assert(user_Data == LAVA_CUSTOM_CPU_ALLOCATION_CALLBACK_USER_DATA);
+    free_data(memory);
+}
+
+memory::memory() {
+
+    if (!use_custom_cpu_callbacks)
+        return;
+
+    vk_callbacks.pUserData = LAVA_CUSTOM_CPU_ALLOCATION_CALLBACK_USER_DATA;
+    vk_callbacks.pfnAllocation = &custom_cpu_allocation;
+    vk_callbacks.pfnReallocation = &custom_cpu_reallocation;
+    vk_callbacks.pfnFree = &custom_cpu_free;
+}
+
+type memory::find_type_with_properties(VkPhysicalDeviceMemoryProperties properties, ui32 type_bits, VkMemoryPropertyFlags required_properties) {
+
+    auto bits = type_bits;
+    auto len = std::min(properties.memoryTypeCount, 32u);
+
+    for (auto i = 0u; i < len; ++i) {
+
+        if ((bits & 1) == 1)
+            if ((properties.memoryTypes[i].propertyFlags & required_properties) == required_properties)
+                return (int)i;
+
+        bits >>= 1;
+    }
+
+    return no_type;
+}
+
+type memory::find_type(VkPhysicalDevice gpu, VkMemoryPropertyFlags properties, ui32 type_bits) {
+
+    VkPhysicalDeviceMemoryProperties prop{};
+    vkGetPhysicalDeviceMemoryProperties(gpu, &prop);
+
+    for (auto i = 0u; i < prop.memoryTypeCount; ++i)
+        if ((prop.memoryTypes[i].propertyFlags & properties) == properties && type_bits & (1 << i))
+            return i;
+
+    return no_type;
+}
+
+allocator::allocator(VkPhysicalDevice physical_device, VkDevice device) {
+
+    VmaVulkanFunctions vulkan_function
+    {
+        .vkGetPhysicalDeviceProperties = vkGetPhysicalDeviceProperties,
+        .vkGetPhysicalDeviceMemoryProperties = vkGetPhysicalDeviceMemoryProperties,
+        .vkAllocateMemory = vkAllocateMemory,
+        .vkFreeMemory = vkFreeMemory,
+        .vkMapMemory = vkMapMemory,
+        .vkUnmapMemory = vkUnmapMemory,
+        .vkFlushMappedMemoryRanges = vkFlushMappedMemoryRanges,
+        .vkInvalidateMappedMemoryRanges = vkInvalidateMappedMemoryRanges,
+        .vkBindBufferMemory = vkBindBufferMemory,
+        .vkBindImageMemory = vkBindImageMemory,
+        .vkGetBufferMemoryRequirements = vkGetBufferMemoryRequirements,
+        .vkGetImageMemoryRequirements = vkGetImageMemoryRequirements,
+        .vkCreateBuffer = vkCreateBuffer,
+        .vkDestroyBuffer = vkDestroyBuffer,
+        .vkCreateImage = vkCreateImage,
+        .vkDestroyImage = vkDestroyImage,
+        .vkCmdCopyBuffer = vkCmdCopyBuffer,
+#if VMA_DEDICATED_ALLOCATION
+        .vkGetBufferMemoryRequirements2KHR = vkGetBufferMemoryRequirements2KHR,
+        .vkGetImageMemoryRequirements2KHR = vkGetImageMemoryRequirements2KHR,
+#endif
+#if VMA_BIND_MEMORY2
+        .vkBindBufferMemory2KHR = vkBindBufferMemory2KHR,
+        .vkBindImageMemory2KHR = vkBindImageMemory2KHR,
+#endif
+    };
+
+    VmaAllocatorCreateInfo allocator_info
+    {
+        .physicalDevice = physical_device,
+        .device = device,
+        .pAllocationCallbacks = memory::alloc(),
+        .pVulkanFunctions = &vulkan_function,
+    };
+
+    check(vmaCreateAllocator(&allocator_info, &vma_allocator));
+}
+
+allocator::~allocator() {
+
+    if (!vma_allocator)
+        return;
+
+    vmaDestroyAllocator(vma_allocator);
+    vma_allocator = nullptr;
+}
+
+} // lava
diff --git a/liblava/base/memory.hpp b/liblava/base/memory.hpp
new file mode 100644
index 00000000..993b312b
--- /dev/null
+++ b/liblava/base/memory.hpp
@@ -0,0 +1,64 @@
+// file      : liblava/base/memory.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/base/base.hpp>
+
+#include <vk_mem_alloc.h>
+
+namespace lava {
+
+struct allocator {
+
+    explicit allocator(VkPhysicalDevice physical_device, VkDevice device);
+    ~allocator();
+
+    using ptr = std::shared_ptr<allocator>;
+
+    static ptr make(VkPhysicalDevice physical_device, VkDevice device) {
+
+        return std::make_shared<allocator>(physical_device, device);
+    }
+
+    bool valid() const { return vma_allocator != nullptr; }
+
+    VmaAllocator get() const { return vma_allocator; }
+
+private:
+    VmaAllocator vma_allocator = nullptr;
+};
+
+struct memory : no_copy_no_move {
+
+    static memory& get() {
+
+        static memory memory;
+        return memory;
+    }
+
+    static VkAllocationCallbacks* alloc() {
+
+        if (get().use_custom_cpu_callbacks)
+            return &get().vk_callbacks;
+
+        return nullptr;
+    }
+
+    static type find_type_with_properties(VkPhysicalDeviceMemoryProperties properties, ui32 type_bits,
+                                            VkMemoryPropertyFlags required_properties);
+
+    static type find_type(VkPhysicalDevice gpu, VkMemoryPropertyFlags properties, ui32 type_bits);
+
+    void set_callbacks(VkAllocationCallbacks const& callbacks) { vk_callbacks = callbacks; }
+    void set_use_custom_cpu_callbacks(bool value) { use_custom_cpu_callbacks = value; }
+
+private:
+    memory();
+
+    bool use_custom_cpu_callbacks = true;
+    VkAllocationCallbacks vk_callbacks = {};
+};
+
+} // lava
diff --git a/liblava/base/physical_device.cpp b/liblava/base/physical_device.cpp
new file mode 100644
index 00000000..cf07a002
--- /dev/null
+++ b/liblava/base/physical_device.cpp
@@ -0,0 +1,109 @@
+// file      : liblava/base/physical_device.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/base/physical_device.hpp>
+
+namespace lava {
+
+void physical_device::initialize(VkPhysicalDevice vk_device_) {
+
+    vk_device = vk_device_;
+
+    vkGetPhysicalDeviceProperties(vk_device, &properties);
+    vkGetPhysicalDeviceFeatures(vk_device, &features);
+    vkGetPhysicalDeviceMemoryProperties(vk_device, &memory_properties);
+
+    auto queue_family_count = 0u;
+    vkGetPhysicalDeviceQueueFamilyProperties(vk_device, &queue_family_count, nullptr);
+    if (queue_family_count > 0) {
+
+        queue_family_properties.resize(queue_family_count);
+        vkGetPhysicalDeviceQueueFamilyProperties(vk_device, &queue_family_count, queue_family_properties.data());
+    }
+
+    auto extension_count = 0u;
+    vkEnumerateDeviceExtensionProperties(vk_device, nullptr, &extension_count, nullptr);
+    if (extension_count > 0) {
+
+        extension_properties.resize(extension_count);
+        vkEnumerateDeviceExtensionProperties(vk_device, nullptr, &extension_count, extension_properties.data());
+    }
+}
+
+bool physical_device::is_supported(string_ref extension) const {
+
+    for (auto& extension_property : extension_properties) {
+
+        if (string(extension_property.extensionName) == extension)
+            return true;
+    }
+
+    return false;
+}
+
+bool physical_device::get_queue_family(ui32& index, VkQueueFlags flags) const {
+
+    for (size_t i = 0, e = queue_family_properties.size(); i != e; ++i) {
+
+        if (queue_family_properties[i].queueFlags & flags) {
+
+            index = (ui32)i;
+            return true;
+        }
+    }
+
+    return false;
+}
+
+device::create_param physical_device::create_default_device_param() const {
+
+    device::create_param create_param;
+    create_param._physical_device = this;
+    create_param.set_default_queues();
+
+    return create_param;
+}
+
+string physical_device::get_device_type_string() const {
+
+    string result;
+    switch (properties.deviceType) {
+
+        case VK_PHYSICAL_DEVICE_TYPE_OTHER:
+            result = "OTHER";
+            break;
+        case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
+            result = "INTEGRATED_GPU";
+            break;
+        case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU:
+            result = "DISCRETE_GPU";
+            break;
+        case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU:
+            result = "VIRTUAL_GPU";
+            break;
+        case VK_PHYSICAL_DEVICE_TYPE_CPU:
+            result = "CPU";
+            break;
+        default:
+            result = "UNKNOWN";
+            break;
+    }
+    return result;
+}
+
+bool physical_device::is_swapchain_supported() const {
+
+    return is_supported(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+}
+
+bool physical_device::is_surface_supported(ui32 queue_family_index, VkSurfaceKHR surface) const {
+
+    VkBool32 res = VK_FALSE;
+    if (failed(vkGetPhysicalDeviceSurfaceSupportKHR(vk_device, queue_family_index, surface, &res)))
+        return false;
+
+    return res == VK_TRUE;
+}
+
+} // lava
diff --git a/liblava/base/physical_device.hpp b/liblava/base/physical_device.hpp
new file mode 100644
index 00000000..719ef2db
--- /dev/null
+++ b/liblava/base/physical_device.hpp
@@ -0,0 +1,49 @@
+// file      : liblava/base/physical_device.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/base/device.hpp>
+
+namespace lava {
+
+struct physical_device {
+
+    using list = std::vector<physical_device>;
+
+    physical_device() = default;
+
+    void initialize(VkPhysicalDevice vk_device);
+
+    bool is_supported(string_ref extension) const;
+    bool get_queue_family(ui32& index, VkQueueFlags flags) const;
+
+    device::create_param create_default_device_param() const;
+
+    VkPhysicalDeviceProperties const& get_properties() const { return properties; }
+    VkPhysicalDeviceFeatures const& get_features() const { return features; }
+    VkPhysicalDeviceMemoryProperties const& get_memory_properties() const { return memory_properties; }
+
+    VkQueueFamilyPropertiesList const& getQueueFamilyProperties() const { return queue_family_properties; }
+    VkExtensionPropertiesList const& getExtensionProperties() const { return extension_properties; }
+
+    VkPhysicalDevice get() const { return vk_device; }
+
+    string get_device_type_string() const;
+
+    bool is_swapchain_supported() const;
+    bool is_surface_supported(ui32 queue_family_index, VkSurfaceKHR surface) const;
+
+private:
+    VkPhysicalDevice vk_device = nullptr;
+
+    VkPhysicalDeviceProperties properties = {};
+    VkPhysicalDeviceFeatures features = {};
+    VkPhysicalDeviceMemoryProperties memory_properties = {};
+
+    VkQueueFamilyPropertiesList queue_family_properties;
+    VkExtensionPropertiesList extension_properties;
+};
+
+} // lava
diff --git a/liblava/core.hpp b/liblava/core.hpp
new file mode 100644
index 00000000..f1bd43fc
--- /dev/null
+++ b/liblava/core.hpp
@@ -0,0 +1,12 @@
+// file      : liblava/core.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/data.hpp>
+#include <liblava/core/id.hpp>
+#include <liblava/core/math.hpp>
+#include <liblava/core/time.hpp>
+#include <liblava/core/types.hpp>
+#include <liblava/core/version.hpp>
diff --git a/liblava/core/data.hpp b/liblava/core/data.hpp
new file mode 100644
index 00000000..b3beca3f
--- /dev/null
+++ b/liblava/core/data.hpp
@@ -0,0 +1,166 @@
+// file      : liblava/core/data.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/types.hpp>
+
+#include <string.h>
+
+namespace lava {
+
+using data_ptr = char*;
+using data_cptr = char const*;
+
+struct data_provider {
+
+    using alloc_func = std::function<data_ptr(size_t, size_t)>;
+    alloc_func alloc;
+
+    using free_func = std::function<void()>;
+    free_func free;
+
+    using realloc_func = std::function<data_ptr(data_ptr, size_t, size_t)>;
+    realloc_func realloc;
+};
+
+template <typename T>
+inline data_ptr as_ptr(T* value) { return (data_ptr)value; }
+
+struct data {
+
+    data_ptr ptr = nullptr;
+
+    size_t size = 0;
+    size_t alignment = 0;
+};
+
+template <typename T>
+inline T align_up(T val, T align) { return (val + align - 1) / align * align; }
+
+inline size_t align(size_t size, size_t min = 0) {
+
+    if (min == 0)
+        return align_up(size, sizeof(void*));
+
+    return align_up((size + min - 1) & ~(min - 1), sizeof(void*));
+}
+
+template <typename T>
+inline size_t align(size_t min = 0) { return align(sizeof(T), min); }
+
+inline void* alloc_data(size_t size, size_t alignment = 8) {
+#if _WIN32
+    return _aligned_malloc(size, alignment);
+#else
+    return aligned_alloc(alignment, size);
+#endif
+}
+
+inline void free_data(void* data) {
+#if _WIN32
+    _aligned_free(data);
+#else
+    free(data);
+#endif
+}
+
+inline void* realloc_data(void* data, size_t size, size_t alignment) {
+#if _WIN32
+    return _aligned_realloc(data, size, alignment);
+#else
+    return realloc(data, align(size, alignment));
+#endif
+}
+
+inline size_t next_pow_2(size_t x) {
+
+    x--;
+    x |= x >> 1;
+    x |= x >> 2;
+    x |= x >> 4;
+    x |= x >> 8;
+    x |= x >> 16;
+    x++;
+    return x;
+}
+
+struct scope_data : data {
+
+    explicit scope_data(size_t length = 0, bool alloc = true) {
+
+        if (length)
+            set(length, alloc);
+    }
+
+    explicit scope_data(i64 length, bool alloc = true) : scope_data(to_size_t(length), alloc) {}
+    explicit scope_data(data const& data) {
+
+        ptr = data.ptr;
+        size = data.size;
+        alignment = data.alignment;
+    }
+
+    ~scope_data() {
+
+        free();
+    }
+
+    void set(size_t length, bool alloc = true) {
+
+        size = length;
+        alignment = align<data_ptr>();
+
+        if (alloc)
+            allocate();
+    }
+
+    bool allocate() {
+
+        ptr = as_ptr(alloc_data(size, alignment));
+        return ptr != nullptr;
+    }
+
+    void free() {
+
+        if (!ptr)
+            return;
+
+        free_data(ptr);
+        ptr = nullptr;
+    }
+};
+
+#ifndef __GNUC__
+#define strndup(p, n) _strdup(p)
+#endif
+
+inline char* human_readable(size_t const sz) {
+
+    static ui32 const buffer_size = 32;
+
+    char const prefixes[] = "KMGTPEZY";
+    char buf[buffer_size];
+    i32 which = -1;
+
+    auto result = to_r64(sz);
+    while (result > 1024 && which < 7) {
+
+        result /= 1024;
+        ++which;
+    }
+
+    char unit[] = "\0i";
+    if (which >= 0)
+        unit[0] = prefixes[which];
+
+    snprintf(buf, buffer_size, "%.2f %sB", result, unit);
+    return strndup(buf, buffer_size);
+}
+
+#ifndef __GNUC__
+#undef strndup
+#endif
+
+} // lava
diff --git a/liblava/core/id.hpp b/liblava/core/id.hpp
new file mode 100644
index 00000000..e4584b9f
--- /dev/null
+++ b/liblava/core/id.hpp
@@ -0,0 +1,200 @@
+// file      : liblava/core/id.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/types.hpp>
+
+#include <memory>
+#include <atomic>
+#include <deque>
+#include <mutex>
+#include <set>
+
+namespace lava {
+
+struct id {
+
+    using ref = id const&;
+    using set = std::set<id>;
+    using set_ref = set const&;
+    using list = std::vector<id>;
+    using map = std::map<id, id>;
+    using index_map = std::map<id, index>;
+    using string_map = std::map<id, string>;
+
+    type value = undef;
+    ui32 version = 0;
+
+    bool is_valid() const { return value != undef; }
+    string to_string(bool show_version = false) const {
+
+        char result[32];
+        if (show_version)
+            snprintf(result, sizeof(result), "%u.%u", value, version);
+        else
+            snprintf(result, sizeof(result), "%u", value);
+        return string(result);
+    }
+
+    void invalidate() { *this = {}; }
+
+    bool operator==(ref rhs) const { return (value == rhs.value) && (version == rhs.version); }
+    bool operator!=(ref rhs) const { return !(*this == rhs); }
+    bool operator<(ref rhs) const { return std::tie(value, version) < std::tie(rhs.value, rhs.version); }
+
+    bool check(id::map& map) {
+
+        if (!is_valid())
+            return false;
+
+        if (!map.count(*this))
+            return false;
+
+        *this = map.at(*this);
+        return true;
+    }
+};
+
+using string_id_map = std::map<string, id>;
+constexpr id const undef_id = id();
+
+struct ids {
+
+    static ids& global() {
+
+        static ids instance;
+        return instance;
+    }
+
+    static id next() { return ids::global().get_next(); }
+    static void free(id::ref id) { ids::global().reuse(id); }
+
+    id get_next() {
+
+        if (!reuse_ids)
+            return { ++next_id };
+
+        return get_next_locked();
+    }
+
+    void reuse(id::ref id) {
+
+        if (reuse_ids)
+            reuse_locked(id);
+    }
+
+    void set_reuse(bool state) { reuse_ids = state; }
+    bool is_reusing() const { return reuse_ids; }
+
+    type get_max() const { return next_id; }
+    void set_max(type max) {
+
+        if (max > next_id)
+            next_id = max;
+    }
+
+private:
+    id get_next_locked() {
+
+        std::unique_lock<std::mutex> lock(queue_mutex);
+        if (free_ids.empty())
+            return { ++next_id };
+
+        auto next_id = free_ids.front();
+        free_ids.pop_front();
+        return { next_id.value, next_id.version + 1 };
+    }
+
+    void reuse_locked(id::ref id) {
+
+        std::unique_lock<std::mutex> lock(queue_mutex);
+        free_ids.push_back(id);
+    }
+
+    std::atomic<type> next_id = { undef };
+    std::mutex queue_mutex;
+
+    bool reuse_ids = true;
+    std::deque<id> free_ids;
+};
+
+template <typename T>
+inline id add_to_id_map(T const& object, std::map<id, T>& map) {
+
+    auto next = ids::next();
+    map.emplace(next, std::move(object));
+    return next;
+}
+
+template <typename T>
+inline bool remove_from_id_map(id::ref object, std::map<id, T>& map) {
+
+    if (!map.count(object))
+        return false;
+
+    map.erase(object);
+    ids::free(object);
+
+    return true;
+}
+
+template <typename T>
+struct listeners {
+
+    id add(typename T::func const& listener) {
+
+        return add_to_id_map(listener, list);
+    }
+
+    void remove(id& id) {
+
+        if (remove_from_id_map(id, list))
+            id.invalidate();
+    }
+
+    typename T::listeners const& get_list() const { return list; }
+
+private:
+    typename T::listeners list = {};
+};
+
+template <typename T, typename Meta>
+struct registry {
+
+    using ptr = std::shared_ptr<T>;
+    using map = std::map<id, ptr>;
+
+    using meta_map = std::map<id, Meta>;
+
+    id create(Meta info = {}) {
+
+        auto object = std::make_shared<T>();
+        add(object, info);
+
+        return object->get_id();
+    }
+
+    void add(ptr object, Meta info = {}) {
+
+        objects.emplace(object->get_id(), object);
+        meta.emplace(object->get_id(), info);
+    }
+
+    bool has(id::ref object) const { return objects.count(object); }
+
+    ptr get(id::ref object) const { return objects.at(object).get(); }
+    Meta get_meta(id::ref object) const { return meta.at(object).get(); }
+
+    map const& get_all() const { return objects; }
+    meta_map const& get_all_meta() const { return meta; }
+
+    void remove(id::ref object) { objects.erase(object); }
+
+private:
+    map objects;
+    meta_map meta;
+};
+
+} // lava
diff --git a/liblava/core/math.hpp b/liblava/core/math.hpp
new file mode 100644
index 00000000..587c8a1e
--- /dev/null
+++ b/liblava/core/math.hpp
@@ -0,0 +1,86 @@
+// file      : liblava/core/math.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/types.hpp>
+
+#define GLM_FORCE_RADIANS
+#define GLM_FORCE_DEPTH_ZERO_TO_ONE
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+#include <glm/gtc/type_ptr.hpp>
+
+namespace lava {
+
+using v2 = glm::vec2;
+using v3 = glm::vec3;
+using v4 = glm::vec4;
+
+using uv2 = glm::uvec2;
+
+using mat3 = glm::mat3;
+using mat4 = glm::mat4;
+
+using iv2 = glm::ivec2;
+using iv3 = glm::ivec3;
+
+struct rect {
+
+    rect() = default;
+
+    rect(i32 left, i32 top, ui32 width, ui32 height) : left_top({ left, top }) {
+
+        set_size({ width, height });
+    }
+
+    rect(iv2 const& left_top, ui32 width, ui32 height) : left_top(left_top) {
+
+        set_size({ width, height });
+    }
+
+    rect(iv2 const& left_top, uv2 const& size) : left_top(left_top) {
+
+        set_size(size);
+    }
+
+    iv2 const& get_origin() const { return left_top; }
+    iv2 const& get_end_point() const { return right_bottom; }
+
+    uv2 get_size() const {
+
+        assert(left_top.x <= right_bottom.x);
+        assert(left_top.y <= right_bottom.y);
+        return { right_bottom.x - left_top.x, right_bottom.y - left_top.y };
+    }
+
+    void set_size(uv2 const& size) {
+
+        right_bottom.x = left_top.x + size.x;
+        right_bottom.y = left_top.y + size.y;
+    }
+
+    void move(iv2 const& offset) {
+
+        left_top += offset;
+        right_bottom += offset;
+    }
+
+    bool contains(iv2 point) const {
+
+        return (left_top.x < point.x) && (left_top.y < point.y) && 
+                (right_bottom.x > point.x) && (right_bottom.y > point.y);
+    }
+
+private:
+    iv2 left_top = iv2();
+    iv2 right_bottom = iv2();
+};
+
+template <typename T>
+inline T ceil_div(T x, T y) { return (x + y - 1) / y; }
+
+v3 const default_color = v3{ 0.8118f, 0.0627f, 0.1255f }; // #CF1020 : 207, 16, 32
+
+} // lava
diff --git a/liblava/core/time.hpp b/liblava/core/time.hpp
new file mode 100644
index 00000000..eaaef8df
--- /dev/null
+++ b/liblava/core/time.hpp
@@ -0,0 +1,117 @@
+// file      : liblava/core/time.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/types.hpp>
+
+#include <chrono>
+#include <iomanip>
+#include <sstream>
+
+namespace lava {
+
+inline time ms_in_seconds(ui32 ms) { return ms / 1000.f; }
+
+inline ui32 seconds_in_ms(time sec) { return to_ui32(sec * 1000.f); }
+
+struct time_info {
+
+    ui32 hours = 0;
+    ui32 minutes = 0;
+    ui32 seconds = 0;
+    ui32 ms = 0;
+};
+
+inline time to_time(ui32 hours = 0, ui32 minutes = 0, ui32 seconds = 0, ui32 ms = 0) {
+
+    return hours * 3600 + minutes * 60 + seconds + ms / 1000.;
+}
+
+inline time to_time(time_info info) {
+
+    return to_time(info.hours, info.minutes, info.seconds, info.ms);
+}
+
+inline time_info to_time(time time) {
+
+    time_info info;
+    info.hours = to_ui32(time / 3600);
+    auto temp = time - info.hours * 3600;
+    info.minutes = to_ui32(temp / 60);
+    temp = temp - info.minutes * 60;
+    info.seconds = to_ui32(temp);
+    temp = temp - info.seconds;
+    info.ms = to_ui32(temp * 1000);
+    return info;
+}
+
+using time_point = std::chrono::high_resolution_clock::time_point;
+using duration = std::chrono::high_resolution_clock::duration;
+
+inline float to_float_seconds(duration d) {
+
+    return std::chrono::duration_cast<std::chrono::duration<float>>(d).count();
+}
+
+struct timer {
+
+    timer() : time_point(clock::now()) {}
+
+    void reset() { time_point = clock::now(); }
+
+    time elapsed() const {
+
+        return std::chrono::duration_cast<second>(clock::now() - time_point).count();
+    }
+
+private:
+    using clock = std::chrono::high_resolution_clock;
+    using second = std::chrono::duration<time, std::ratio<1>>;
+
+    std::chrono::time_point<clock> time_point;
+};
+
+struct run_time {
+
+    time seconds = 0.;
+    time clock = 0.016;
+
+    time system = 0.;
+    time delta = 0.;
+
+    bool use_fix_delta = false;
+    time fix_delta = 0.02;
+
+    r32 speed = 1.f;
+    bool paused = false;
+
+    time get_speed_delta() const { return delta * speed; }
+};
+
+#pragma warning(push)
+#pragma warning(disable : 4996) //_CRT_SECURE_NO_WARNINGS
+
+template <typename CLOCK_TYPE = std::chrono::system_clock>
+inline string time_stamp(const typename CLOCK_TYPE::time_point& time_point, string_ref format = "%Y-%m-%d %H-%M-%S") {
+
+    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_point.time_since_epoch()) % 1000;
+
+    const std::time_t t = CLOCK_TYPE::to_time_t(time_point);
+    const std::tm tm = *std::localtime(std::addressof(t));
+
+    std::ostringstream stm;
+    stm << std::put_time(std::addressof(tm), format.c_str()) << '.' << std::setfill('0') << std::setw(3) << ms.count();
+    return stm.str();
+}
+
+inline string get_current_time_and_date() {
+
+    auto now = std::chrono::system_clock::now();
+    return time_stamp(now);
+}
+
+#pragma warning(pop)
+
+} // lava
diff --git a/liblava/core/types.hpp b/liblava/core/types.hpp
new file mode 100644
index 00000000..76cd14dd
--- /dev/null
+++ b/liblava/core/types.hpp
@@ -0,0 +1,122 @@
+// file      : liblava/core/types.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/def.hpp>
+
+#include <cassert>
+#include <cstdint>
+#include <string>
+#include <string_view>
+#include <vector>
+#include <map>
+
+#include <functional>
+
+#include <enum.h>
+
+namespace lava {
+
+using int8 = std::int8_t;
+using i8 = int8;
+
+using uint8 = std::uint8_t;
+using ui8 = uint8;
+
+using int16 = std::int16_t;
+using i16 = int16;
+
+using uint16 = std::uint16_t;
+using ui16 = uint16;
+
+using int32 = std::int32_t;
+using i32 = int32;
+
+using uint32 = std::uint32_t;
+using ui32 = uint32;
+
+using int64 = std::int64_t;
+using i64 = int64;
+
+using uint64 = std::uint64_t;
+using ui64 = uint64;
+
+using char8 = std::int_fast8_t;
+using c8 = char8;
+
+using uchar8 = std::uint_fast8_t;
+using uc8 = uchar8;
+
+using char16 = int16;
+using c16 = char16;
+
+using uchar16 = uint16;
+using uc16 = uchar16;
+
+using char32 = int32;
+using c32 = char32;
+
+using uchar32 = uint32;
+using uc32 = uchar32;
+
+using size_t = std::size_t;
+using uchar = unsigned char;
+using r32 = float;
+using r64 = double;
+
+using time = r64;
+using real = r64;
+
+using type = ui32;
+constexpr type const no_type = 0xffffffff;
+constexpr type const undef = 0;
+
+using index = type;
+constexpr index const no_index = no_type;
+using index_list = std::vector<index>;
+using index_map = std::map<index, index>;
+
+using string = std::string;
+using string_ref = string const&;
+using string_list = std::vector<string>;
+using string_view = std::string_view;
+
+using name = char const*;
+using names = std::vector<name>;
+using names_ref = names const&;
+
+constexpr name _lava_ = "lava";
+constexpr name _liblava_ = "liblava";
+
+template <typename T>
+inline r32 to_r32(T value) { return static_cast<r32>(value); }
+
+template <typename T>
+inline r64 to_r64(T value) { return static_cast<r64>(value); }
+
+template <typename T>
+inline i32 to_i32(T value) { return static_cast<i32>(value); }
+
+template <typename T>
+inline i64 to_i64(T value) { return static_cast<i64>(value); }
+
+template <typename T>
+inline ui32 to_ui32(T value) { return static_cast<ui32>(value); }
+
+template <typename T>
+inline ui64 to_ui64(T value) { return static_cast<ui64>(value); }
+
+template <typename T>
+inline size_t to_size_t(T value) { return static_cast<size_t>(value); }
+
+struct no_copy_no_move {
+
+    no_copy_no_move() = default;
+    no_copy_no_move(no_copy_no_move const&) = delete;
+
+    void operator=(no_copy_no_move const&) = delete;
+};
+
+} // lava
diff --git a/liblava/core/version.hpp b/liblava/core/version.hpp
new file mode 100644
index 00000000..626b3d34
--- /dev/null
+++ b/liblava/core/version.hpp
@@ -0,0 +1,42 @@
+// file      : liblava/core/version.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/types.hpp>
+
+namespace lava {
+
+struct internal_version {
+
+    i32 major = LIBLAVA_VERSION_MAJOR;
+    i32 minor = LIBLAVA_VERSION_MINOR;
+    i32 patch = LIBLAVA_VERSION_PATCH;
+};
+
+constexpr internal_version const _internal_version = {};
+
+enum class version_stage {
+
+    preview,
+    alpha,
+    beta,
+    rc,
+    release
+};
+
+struct version {
+
+    i32 year = 2019;
+    i32 release = 0;
+    version_stage stage = version_stage::preview;
+    i32 rev = 2;
+};
+
+constexpr version const _version = {};
+
+constexpr name _build_date = LIBLAVA_BUILD_DATE;
+constexpr name _build_time = LIBLAVA_BUILD_TIME;
+
+} // lava
diff --git a/liblava/def.hpp b/liblava/def.hpp
new file mode 100644
index 00000000..06045de3
--- /dev/null
+++ b/liblava/def.hpp
@@ -0,0 +1,20 @@
+// file      : liblava/def.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#if defined(NDEBUG)
+#define LIBLAVA_DEBUG 0
+#endif
+
+#ifndef LIBLAVA_DEBUG
+#define LIBLAVA_DEBUG 1
+#endif
+
+#define LIBLAVA_BUILD_DATE __DATE__
+#define LIBLAVA_BUILD_TIME __TIME__
+
+#define LIBLAVA_VERSION_MAJOR 0
+#define LIBLAVA_VERSION_MINOR 4
+#define LIBLAVA_VERSION_PATCH 2
diff --git a/liblava/frame.hpp b/liblava/frame.hpp
new file mode 100644
index 00000000..a1828e0c
--- /dev/null
+++ b/liblava/frame.hpp
@@ -0,0 +1,13 @@
+// file      : liblava/frame.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/frame/frame.hpp>
+#include <liblava/frame/input.hpp>
+#include <liblava/frame/render_target.hpp>
+#include <liblava/frame/render_thread.hpp>
+#include <liblava/frame/renderer.hpp>
+#include <liblava/frame/swapchain.hpp>
+#include <liblava/frame/window.hpp>
diff --git a/liblava/frame/frame.cpp b/liblava/frame/frame.cpp
new file mode 100644
index 00000000..2306778d
--- /dev/null
+++ b/liblava/frame/frame.cpp
@@ -0,0 +1,307 @@
+// file      : liblava/frame/frame.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/frame/frame.hpp>
+#include <liblava/base/memory.hpp>
+
+#if !LIBLAVA_DEBUG && _WIN32
+#include <windows.h>
+#include <iostream>
+#endif
+
+void hide_console(lava::name program) {
+
+#if !LIBLAVA_DEBUG && _WIN32
+    std::cout << "starting " << program;
+
+    const auto dot_count = 3;
+    for (auto i = 0u; i < dot_count; ++i) {
+
+        lava::sleep(1.0 / dot_count);
+        std::cout << ".";
+    }
+
+    FreeConsole();
+#endif
+}
+
+void log_command_line(argh::parser& cmd_line) {
+
+    if (!cmd_line.pos_args().empty()) {
+
+        for (auto& pos_arg : cmd_line.pos_args())
+            lava::log()->info("cmd line (pos) {}", pos_arg.c_str());
+    }
+
+    if (!cmd_line.flags().empty()) {
+
+        for (auto& flag : cmd_line.flags())
+            lava::log()->info("cmd line (flag) {}", flag.c_str());
+    }
+
+    if (!cmd_line.params().empty()) {
+
+        for (auto& param : cmd_line.params())
+            lava::log()->info("cmd line (para) {} = {}", param.first.c_str(), param.second.c_str());
+    }
+}
+
+lava::time lava::now() { return glfwGetTime(); }
+
+namespace lava {
+
+static bool _initialized = false;
+
+frame::frame(argh::parser cmd_line) { 
+
+    frame_config config;
+    config.cmd_line = cmd_line;
+
+    setup(config);
+}
+
+frame::frame(frame_config config) {
+
+    setup(config);
+}
+
+frame::~frame() { teardown(); }
+
+bool frame::ready() const { return _initialized; }
+
+bool frame::setup(frame_config config) {
+
+    if (_initialized)
+        return false;
+
+    cmd_line = config.cmd_line;
+
+#if LIBLAVA_DEBUG
+    config.log.debug = true;
+    config.debug.validation = true;
+    config.debug.utils = true;
+#endif
+
+    hide_console(config.app);
+
+    if (cmd_line[{ "-d", "--debug" }])
+        config.debug.validation = true;
+
+    if (cmd_line[{ "-a", "--assist" }])
+        config.debug.assistent = true;
+
+    if (cmd_line[{ "-r", "--renderdoc" }])
+        config.debug.render_doc = true;
+
+    if (cmd_line[{ "-v", "--verbose" }])
+        config.debug.verbose = true;
+
+    if (cmd_line[{ "-u", "--utils" }])
+        config.debug.utils = true;
+
+    if (cmd_line[{ "-i", "--info" }])
+        config.debug.info = true;
+
+    if (auto log_level = -1; cmd_line({ "-l", "--log" }) >> log_level)
+        config.log.level = log_level;
+
+    setup_log(config.log);
+
+    log()->info(">>> {} / {} - {} {}", to_string(_version).c_str(), to_string(_internal_version).c_str(), _build_date, _build_time);
+
+    log_command_line(cmd_line);
+
+    if (config.log.level >= 0)
+        log()->info("log {}", spdlog::level::to_str((spdlog::level::level_enum)config.log.level));
+
+    glfwSetErrorCallback([](i32 error, name description) {
+
+        log()->error("glfw {} - {}", error, description);
+    });
+
+    log()->debug("glfw {}", glfwGetVersionString());
+
+    if (glfwInit() != GLFW_TRUE) {
+
+        log()->error("glfw init failed");
+        return false;
+    }
+
+    if (glfwVulkanSupported() != GLFW_TRUE) {
+
+        log()->error("glfw vulkan not supported");
+        return false;
+    }
+
+    glfwDefaultWindowHints();
+    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+
+    auto result = volkInitialize();
+    if (failed(result)) {
+
+        log()->error("volk init failed");
+        return false;
+    }
+
+    log()->info("vulkan {}", to_string(instance::get_version()).c_str());
+
+    instance::create_param param;
+
+    auto glfw_extensions_count = 0u;
+    auto glfw_extensions = glfwGetRequiredInstanceExtensions(&glfw_extensions_count);
+    for (auto i = 0u; i < glfw_extensions_count; ++i)
+        param.extension_to_enable.push_back(glfw_extensions[i]);
+
+    if (!instance::singleton().create(param, config.debug, config.app)) {
+
+        log()->error("instance create failed");
+        return false;
+    }
+
+    log()->debug("physfs {}", to_string(file_system::get_version()).c_str());
+
+    if (!file_system::get().initialize(cmd_line[0].c_str(), config.org, config.app, config.ext)) {
+
+        log()->error("file system init failed");
+        return false;
+    }
+
+    file_system::get().mount_res();
+
+    if (config.data_folder)
+        file_system::get().create_data_folder();
+
+    _initialized = true;
+
+    log()->info("---");
+
+    return true;
+}
+
+void frame::teardown() {
+
+    if (!_initialized)
+        return;
+
+    manager.clear();
+
+    instance::singleton().destroy();
+
+    glfwTerminate();
+
+    log()->info("<<<");
+
+    log()->flush();
+    spdlog::drop_all();
+
+    file_system::get().terminate();
+
+    _initialized = false;
+}
+
+bool frame::run() {
+
+    if (running)
+        return false;
+
+    running = true;
+    start_time = now();
+
+    while (running) {
+
+        if (!run_step())
+            return false;
+    }
+
+    manager.wait_idle();
+
+    trigger_run_end();
+
+    running = false;
+    start_time = 0.0;
+
+    return true;
+}
+
+bool frame::run_step() {
+
+    handle_events(wait_for_events);
+
+    for (auto& func : run_map)
+        if (!func.second())
+            return false;
+
+    return true;
+}
+
+bool frame::shut_down() {
+
+    if (!running)
+        return false;
+
+    running = false;
+
+    return true;
+}
+
+id frame::add_run(run_func_ref func) {
+
+    auto id = ids::next();
+    run_map.emplace(id, func);
+
+    return id;
+}
+
+bool frame::remove_run(id::ref id) {
+
+    if (!run_map.count(id))
+        return false;
+
+    run_map.erase(id);
+
+    return true;
+}
+
+id frame::add_run_end(run_end_func_ref func) {
+
+    auto id = ids::next();
+    run_end_map.emplace(id, func);
+
+    return id;
+}
+
+bool frame::remove_run_end(id::ref id) {
+
+    if (!run_end_map.count(id))
+        return false;
+
+    run_end_map.erase(id);
+
+    return true;
+}
+
+void frame::trigger_run_end() {
+
+    for (auto& func : run_end_map)
+        func.second();
+}
+
+} // lava
+
+VkSurfaceKHR lava::create_surface(GLFWwindow* window) {
+
+    VkSurfaceKHR surface = nullptr;
+    if (failed(glfwCreateWindowSurface(instance::get(), window, memory::alloc(), &surface)))
+        return nullptr;
+
+    return surface;
+}
+
+void lava::handle_events(bool wait_for_events) {
+
+    if (wait_for_events)
+        glfwWaitEvents();
+    else
+        glfwPollEvents();
+}
diff --git a/liblava/frame/frame.hpp b/liblava/frame/frame.hpp
new file mode 100644
index 00000000..1d82b8e8
--- /dev/null
+++ b/liblava/frame/frame.hpp
@@ -0,0 +1,119 @@
+// file      : liblava/frame/frame.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/base/instance.hpp>
+#include <liblava/base/device.hpp>
+
+#define GLFW_INCLUDE_NONE
+#define GLFW_INCLUDE_VULKAN
+#include <GLFW/glfw3.h>
+
+#include <argh.h>
+
+namespace lava {
+
+struct frame_config {
+
+    argh::parser cmd_line;
+
+    name org = _liblava_;
+    name app = _lava_;
+    name ext = _zip_;
+
+    log_config log;
+    instance::debug debug;
+
+    bool data_folder = false;
+};
+
+enum error {
+
+    not_ready       = -1,
+    create_failed   = -2,
+    aborted         = -3,
+};
+
+time now();
+
+struct frame : no_copy_no_move {
+
+    using ptr = std::shared_ptr<frame>;
+
+    explicit frame(argh::parser cmd_line);
+    explicit frame(frame_config config);
+    ~frame();
+
+    static ptr make(argh::parser cmd_line) { return std::make_shared<frame>(cmd_line); }
+    static ptr make(frame_config config) { return std::make_shared<frame>(config); }
+
+    bool ready() const;
+
+    bool run();
+    bool run_step();
+
+    bool shut_down();
+
+    device* create_device() {
+
+        auto device = manager.create();
+        if (!device)
+            return nullptr;
+
+        return device.get();
+    }
+
+    device_manager manager;
+
+    using run_func = std::function<bool()>;
+    using run_func_ref = run_func const&;
+
+    id add_run(run_func_ref func);
+    bool remove_run(id::ref id);
+
+    time get_running_time() const {
+
+        if (start_time == 0.0)
+            return 0.0;
+
+        return now() - start_time;
+    }
+
+    using run_end_func = std::function<void()>;
+    using run_end_func_ref = run_end_func const&;
+
+    id add_run_end(run_end_func_ref func);
+    bool remove_run_end(id::ref id);
+
+    void trigger_run_end();
+
+    argh::parser& get_cmd_line() { return cmd_line; }
+
+    bool waiting_for_events() const { return wait_for_events; }
+    void set_wait_for_events(bool value = true) { wait_for_events = value; }
+
+private:
+
+    bool setup(frame_config config = {});
+    void teardown();
+
+    argh::parser cmd_line;
+
+    bool running = false;
+    bool wait_for_events = false;
+    time start_time = 0.0;
+
+    using run_func_map = std::map<id, run_func>;
+    run_func_map run_map;
+
+    using run_end_func_map = std::map<id, run_end_func>;
+    run_end_func_map run_end_map;
+};
+
+VkSurfaceKHR create_surface(GLFWwindow* window);
+
+void handle_events(bool wait_for_events = false);
+
+} // lava
diff --git a/liblava/frame/input.cpp b/liblava/frame/input.cpp
new file mode 100644
index 00000000..6ee8c7ee
--- /dev/null
+++ b/liblava/frame/input.cpp
@@ -0,0 +1,97 @@
+// file      : liblava/frame/input.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/frame/input.hpp>
+
+namespace lava {
+
+void input::clear_all_events() {
+
+    clear_key_events();
+    clear_scroll_events();
+    clear_mouse_button_events();
+    clear_mouse_move_events();
+    clear_mouse_active_events();
+}
+
+} // lava
+
+void lava::handle_key_events(input& input) {
+
+    for (auto& event : input.get_key_events()) {
+
+        for (auto& listener : input.key_listeners.get_list())
+            listener.second(event);
+
+        for (auto& callback : input.get_callbacks())
+            callback->on_key_event(event);
+    }
+
+    input.clear_key_events();
+}
+
+void lava::handle_scroll_events(input& input) {
+
+    for (auto& event : input.get_scroll_events()) {
+
+        for (auto& listener : input.scroll_listeners.get_list())
+            listener.second(event);
+
+        for (auto& callback : input.get_callbacks())
+            callback->on_scroll_event(event);
+    }
+
+    input.clear_scroll_events();
+}
+
+void lava::handle_mouse_button_events(input& input) {
+
+    for (auto& event : input.get_mouse_button_events()) {
+
+        for (auto& listener : input.mouse_button_listeners.get_list())
+            listener.second(event);
+
+        for (auto& callback : input.get_callbacks())
+            callback->on_mouse_button_event(event);
+    }
+
+    input.clear_mouse_button_events();
+}
+
+void lava::handle_mouse_move_events(input& input) {
+
+    for (auto& event : input.get_mouse_move_events()) {
+
+        for (auto& listener : input.mouse_move_listeners.get_list())
+            listener.second(event);
+
+        for (auto& callback : input.get_callbacks())
+            callback->on_mouse_move_event(event);
+    }
+
+    input.clear_mouse_move_events();
+}
+
+void lava::handle_mouse_active_events(input& input) {
+
+    for (auto& event : input.get_mouse_active_events()) {
+
+        for (auto& listener : input.mouse_active_listeners.get_list())
+            listener.second(event);
+
+        for (auto& callback : input.get_callbacks())
+            callback->on_mouse_active_event(event);
+    }
+
+    input.clear_mouse_active_events();
+}
+
+void lava::handle_events(input& input) {
+
+    handle_key_events(input);
+    handle_scroll_events(input);
+    handle_mouse_button_events(input);
+    handle_mouse_move_events(input);
+    handle_mouse_active_events(input);
+}
diff --git a/liblava/frame/input.hpp b/liblava/frame/input.hpp
new file mode 100644
index 00000000..35fb5fd8
--- /dev/null
+++ b/liblava/frame/input.hpp
@@ -0,0 +1,159 @@
+// file      : liblava/frame/input.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/utils/utility.hpp>
+#include <liblava/core/id.hpp>
+
+namespace lava {
+
+struct key_event {
+
+    using ref = key_event const&;
+    using func = std::function<void(ref)>;
+    using listeners = std::map<id, func>;
+    using list = std::vector<key_event>;
+
+    id sender;
+
+    i32 key = 0;
+    i32 scancode = 0;
+    i32 action = 0;
+    i32 mods = 0;
+};
+
+struct scroll_offset {
+
+    r64 x = 0.0;
+    r64 y = 0.0;
+};
+
+struct scroll_event {
+
+    using ref = scroll_event const&;
+    using func = std::function<void(ref)>;
+    using listeners = std::map<id, func>;
+    using list = std::vector<scroll_event>;
+
+    id sender;
+
+    scroll_offset offset;
+};
+
+struct mouse_button_event {
+
+    using ref = mouse_button_event const&;
+    using func = std::function<void(ref)>;
+    using listeners = std::map<id, func>;
+    using list = std::vector<mouse_button_event>;
+
+    id sender;
+
+    i32 button = 0;
+    i32 action = 0;
+    i32 modes = 0;
+};
+
+struct mouse_position {
+
+    r64 x = 0.0;
+    r64 y = 0.0;
+};
+
+struct mouse_move_event {
+
+    using ref = mouse_move_event const&;
+    using func = std::function<void(ref)>;
+    using listeners = std::map<id, func>;
+    using list = std::vector<mouse_move_event>;
+
+    id sender;
+
+    mouse_position position;
+};
+
+struct mouse_active_event {
+
+    using ref = mouse_active_event const&;
+    using func = std::function<void(ref)>;
+    using listeners = std::map<id, func>;
+    using list = std::vector<mouse_active_event>;
+
+    id sender;
+
+    bool active = false;
+};
+
+struct input_callback {
+
+    using list = std::vector<input_callback*>;
+
+    key_event::func on_key_event;
+    scroll_event::func on_scroll_event;
+    mouse_button_event::func on_mouse_button_event;
+    mouse_move_event::func on_mouse_move_event;
+    mouse_active_event::func on_mouse_active_event;
+};
+
+struct input {
+
+    void add_key_event(key_event::ref event) { key_events.push_back(event); }
+    void add_scroll_event(scroll_event::ref event) { scroll_events.push_back(event); }
+    void add_mouse_button_event(mouse_button_event::ref event) { mouse_button_events.push_back(event); }
+    void add_mouse_move_event(mouse_move_event::ref event) { mouse_move_events.push_back(event); }
+    void add_mouse_active_event(mouse_active_event::ref event) { mouse_active_events.push_back(event); }
+
+    listeners<key_event> key_listeners;
+    listeners<scroll_event> scroll_listeners;
+    listeners<mouse_button_event> mouse_button_listeners;
+    listeners<mouse_move_event> mouse_move_listeners;
+    listeners<mouse_active_event> mouse_active_listeners;
+
+    void add_callback(input_callback* callback) { callbacks.push_back(callback); }
+    void remove_callback(input_callback* callback) { remove(callbacks, callback); }
+    input_callback::list const& get_callbacks() const { return callbacks; }
+
+    key_event::list const& get_key_events() const { return key_events; }
+    scroll_event::list const& get_scroll_events() const { return scroll_events; }
+    mouse_button_event::list const& get_mouse_button_events() const { return mouse_button_events; }
+    mouse_move_event::list const& get_mouse_move_events() const { return mouse_move_events; }
+    mouse_active_event::list const& get_mouse_active_events() const { return mouse_active_events; }
+
+    void clear_key_events() { key_events.clear(); }
+    void clear_scroll_events() { scroll_events.clear(); }
+    void clear_mouse_button_events() { mouse_button_events.clear(); }
+    void clear_mouse_move_events() { mouse_move_events.clear(); }
+    void clear_mouse_active_events() { mouse_active_events.clear(); }
+
+    void clear_all_events();
+
+    mouse_position get_mouse_position() const { return _mouse_position; }
+    void set_mouse_position(mouse_position const& position) { _mouse_position = position; }
+
+private:
+    mouse_position _mouse_position;
+
+    key_event::list key_events;
+    scroll_event::list scroll_events;
+    mouse_button_event::list mouse_button_events;
+    mouse_move_event::list mouse_move_events;
+    mouse_active_event::list mouse_active_events;
+
+    input_callback::list callbacks;
+};
+
+void handle_key_events(input& input);
+
+void handle_scroll_events(input& input);
+
+void handle_mouse_button_events(input& input);
+
+void handle_mouse_move_events(input& input);
+
+void handle_mouse_active_events(input& input);
+
+void handle_events(input& input);
+
+} // lava
diff --git a/liblava/frame/render_target.cpp b/liblava/frame/render_target.cpp
new file mode 100644
index 00000000..7eb2ee5a
--- /dev/null
+++ b/liblava/frame/render_target.cpp
@@ -0,0 +1,93 @@
+// file      : liblava/frame/render_target.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/frame/render_target.hpp>
+#include <liblava/frame/window.hpp>
+#include <liblava/frame/frame.hpp>
+
+namespace lava {
+
+bool render_target::create(device* device, VkSurfaceKHR surface, uv2 size) {
+
+    if (!_swapchain.create(device, surface, size))
+        return false;
+
+    set_clear_color();
+
+    _swapchain_callback.on_created = [&]() {
+
+        if (on_create_attachments) {
+
+            auto target_attachments = on_create_attachments();
+
+            for (auto& callback : target_callbacks)
+                if (!callback->on_created(target_attachments, get_size()))
+                    return false;
+        }
+
+        if (on_swapchain_start)
+            if (!on_swapchain_start())
+                return false;
+
+        return true;
+    };
+
+    _swapchain_callback.on_destroyed = [&]() {
+
+        if (on_swapchain_stop)
+            on_swapchain_stop();
+
+        for (auto& callback : target_callbacks)
+            callback->on_destroyed();
+
+        if (on_destroy_attachments)
+            on_destroy_attachments();
+    };
+
+    _swapchain.add_callback(&_swapchain_callback);
+
+    return true;
+}
+
+void render_target::destroy() {
+
+    target_callbacks.clear();
+
+    _swapchain.remove_callback(&_swapchain_callback);
+    _swapchain.destroy();
+}
+
+void render_target::set_clear_color(v3 value) {
+
+    clear_color.float32[0] = value.r;
+    clear_color.float32[1] = value.g;
+    clear_color.float32[2] = value.b;
+    clear_color.float32[3] = 1.f;
+}
+
+} // lava
+
+lava::render_target::ptr lava::create_target(window* window, device* device) {
+
+    auto surface = create_surface(window->get());
+    if (!surface)
+        return nullptr;
+
+    if (!device->is_surface_supported(surface))
+        return nullptr;
+
+    auto width = 0u;
+    auto height = 0u;
+    window->get_framebuffer_size(width, height);
+
+    auto target = std::make_shared<render_target>();
+    if (!target->create(device, surface, { width, height }))
+        return nullptr;
+
+    auto target_ptr = target.get();
+
+    window->on_resize = [&, target_ptr](ui32 new_width, ui32 new_height) { return target_ptr->resize({ new_width, new_height }); };
+
+    return target;
+}
diff --git a/liblava/frame/render_target.hpp b/liblava/frame/render_target.hpp
new file mode 100644
index 00000000..c98458ec
--- /dev/null
+++ b/liblava/frame/render_target.hpp
@@ -0,0 +1,86 @@
+// file      : liblava/frame/render_target.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/frame/swapchain.hpp>
+#include <liblava/fwd.hpp>
+
+namespace lava {
+
+struct render_target {
+
+    using ptr = std::shared_ptr<render_target>;
+
+    struct callback {
+
+        using list = std::vector<callback*>;
+
+        using created_func = std::function<bool(VkImageViews const&, uv2)>;
+        created_func on_created;
+
+        using destroyed_func = std::function<void()>;
+        destroyed_func on_destroyed;
+    };
+
+    bool create(device* device, VkSurfaceKHR surface, uv2 size);
+    void destroy();
+
+    void set_clear_color(v3 value = default_color);
+    VkClearColorValue get_clear_color() const { return clear_color; }
+
+    uv2 get_size() const { return _swapchain.get_size(); }
+    bool resize(uv2 new_size) { return _swapchain.resize(new_size); }
+
+    ui32 get_frame_count() const { return _swapchain.get_backbuffer_count(); }
+
+    bool must_reload() const { return _swapchain.must_reload(); }
+    void reload() { _swapchain.resize(_swapchain.get_size()); }
+
+    device* get_device() { return _swapchain.get_device(); }
+    swapchain* get_swapchain() { return &_swapchain; }
+
+    VkFormat get_format() const { return _swapchain.get_format(); }
+
+    image::list const& get_backbuffers() const { return _swapchain.get_backbuffers(); }
+    inline image::ptr get_backbuffer(index index) { 
+
+        auto& backbuffers = get_backbuffers();
+        if (index >= backbuffers.size())
+            return nullptr;
+
+        return backbuffers.at(index);
+    }
+
+    inline VkImage get_backbuffer_image(index index) {
+
+        auto result = get_backbuffer(index);
+        return result ? result->get() : nullptr;
+    }
+
+    void add_target_callback(callback* callback) { target_callbacks.push_back(callback); }
+
+    using swapchain__start_func = std::function<bool()>;
+    swapchain__start_func on_swapchain_start;
+
+    using swapchain_stop_func = std::function<void()>;
+    swapchain_stop_func on_swapchain_stop;
+
+    using create_attachments_func = std::function<VkImageViews()>;
+    create_attachments_func on_create_attachments;
+
+    using destroy_attachments_func = std::function<void()>;
+    destroy_attachments_func on_destroy_attachments;
+
+private:
+    swapchain _swapchain;
+    swapchain::callback _swapchain_callback;
+    VkClearColorValue clear_color = {};
+
+    callback::list target_callbacks;
+};
+
+render_target::ptr create_target(window* window, device* device);
+
+} // lava
diff --git a/liblava/frame/render_thread.hpp b/liblava/frame/render_thread.hpp
new file mode 100644
index 00000000..5fb3fe73
--- /dev/null
+++ b/liblava/frame/render_thread.hpp
@@ -0,0 +1,70 @@
+// file      : liblava/frame/render_thread.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/frame/renderer.hpp>
+
+#include <thread>
+
+namespace lava {
+
+struct render_thread {
+
+    ~render_thread() { destroy(); }
+
+    bool create(swapchain* swapchain) {
+
+        _renderer.on_destroy = [&]() { stop(); };
+
+        return _renderer.create(swapchain);
+    }
+
+    void destroy() { _renderer.destroy(); }
+
+    void start() { thread = std::thread(&render_thread::render, this); }
+
+    void stop() {
+
+        if (!running)
+            return;
+
+        running = false;
+        thread.join();
+    }
+
+    using render_func = std::function<VkCommandBuffers(ui32)>;
+    render_func on_render;
+
+    renderer* get_renderer() { return &_renderer; }
+    bool is_running() const { return running; }
+
+private:
+    void render() {
+
+        running = true;
+
+        while (running) {
+
+            if (!_renderer.active)
+                continue;
+
+            if (!on_render)
+                continue;
+
+            auto frame_index = _renderer.begin_frame();
+            if (!frame_index)
+                continue;
+
+            _renderer.end_frame(on_render(*frame_index));
+        }
+    }
+
+    std::thread thread;
+    bool running = false;
+
+    renderer _renderer;
+};
+
+} // lava
diff --git a/liblava/frame/renderer.cpp b/liblava/frame/renderer.cpp
new file mode 100644
index 00000000..2eb3c72e
--- /dev/null
+++ b/liblava/frame/renderer.cpp
@@ -0,0 +1,164 @@
+// file      : liblava/frame/renderer.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/frame/renderer.hpp>
+
+#include <array>
+
+namespace lava {
+
+bool renderer::create(swapchain* target_) {
+
+    target = target_;
+    dev = target->get_device();
+
+    queue = dev->get_graphics_queue();
+    queued_frames = target->get_backbuffer_count();
+
+    fences.resize(queued_frames);
+    image_acquired_semaphores.resize(queued_frames);
+    render_complete_semaphores.resize(queued_frames);
+
+    for (auto i = 0u; i < queued_frames; ++i) {
+
+        {
+            VkFenceCreateInfo create_info
+            {
+                .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+                .flags = VK_FENCE_CREATE_SIGNALED_BIT,
+            };
+
+            if (failed(dev->call().vkCreateFence(dev->get(), &create_info, memory::alloc(), &fences[i])))
+                return false;
+        }
+
+        {
+            VkSemaphoreCreateInfo create_info
+            {
+                .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
+            };
+
+            if (failed(dev->call().vkCreateSemaphore(dev->get(), &create_info, memory::alloc(), &image_acquired_semaphores[i])))
+                return false;
+
+            if (failed(dev->call().vkCreateSemaphore(dev->get(), &create_info, memory::alloc(), &render_complete_semaphores[i])))
+                return false;
+        }
+    }
+
+    return true;
+}
+
+void renderer::destroy() {
+
+    if (on_destroy)
+        on_destroy();
+
+    for (auto i = 0u; i < queued_frames; ++i) {
+
+        dev->call().vkDestroyFence(dev->get(), fences[i], memory::alloc());
+        dev->call().vkDestroySemaphore(dev->get(), image_acquired_semaphores[i], memory::alloc());
+        dev->call().vkDestroySemaphore(dev->get(), render_complete_semaphores[i], memory::alloc());
+    }
+
+    fences.clear();
+    image_acquired_semaphores.clear();
+    render_complete_semaphores.clear();
+
+    queued_frames = 0;
+}
+
+std::optional<index> renderer::begin_frame() {
+
+    if (!active)
+        return {};
+
+    std::array<VkFence, 1> const wait_fences = { fences[current_sync] };
+
+    for (;;) {
+
+        auto result = dev->call().vkWaitForFences(dev->get(), to_ui32(wait_fences.size()), wait_fences.data(), VK_TRUE, 100);
+        if (result == VK_SUCCESS)
+            break;
+
+        if (result == VK_TIMEOUT)
+            continue;
+
+        if (result == VK_ERROR_OUT_OF_DATE_KHR) {
+
+            target->request_reload();
+            return {};
+        }
+
+        if (failed(result))
+            return {};
+    }
+
+    auto current_semaphore = image_acquired_semaphores[current_sync];
+
+    auto result = dev->call().vkAcquireNextImageKHR(dev->get(), target->get(), UINT64_MAX, current_semaphore, nullptr, &frame_index);
+    if (result == VK_ERROR_OUT_OF_DATE_KHR) {
+
+        target->request_reload();
+        return {};
+    }
+
+    if (failed(result))
+        return {};
+
+    if (!check(dev->call().vkResetFences(dev->get(), to_ui32(wait_fences.size()), wait_fences.data())))
+        return {};
+
+    return get_current_frame();
+}
+
+bool renderer::end_frame(VkCommandBuffers const& cmd_buffers) {
+
+    assert(!cmd_buffers.empty());
+
+    std::array<VkSemaphore, 1> const wait_semaphores = { image_acquired_semaphores[current_sync] };
+    std::array<VkSemaphore, 1> const sync_present_semaphores = { render_complete_semaphores[current_sync] };
+
+    VkPipelineStageFlags const wait_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+
+    VkSubmitInfo submit_info
+    {
+        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
+        .waitSemaphoreCount = to_ui32(wait_semaphores.size()),
+        .pWaitSemaphores = wait_semaphores.data(),
+        .pWaitDstStageMask = &wait_stage,
+        .commandBufferCount = to_ui32(cmd_buffers.size()),
+        .pCommandBuffers = cmd_buffers.data(),
+        .signalSemaphoreCount = to_ui32(sync_present_semaphores.size()),
+        .pSignalSemaphores = sync_present_semaphores.data(),
+    };
+
+    std::array<VkSubmitInfo, 1> const submit_infos = { submit_info };
+    VkFence current_fence = fences[current_sync];
+
+    if (failed(dev->call().vkQueueSubmit(queue.vk_queue, to_ui32(submit_infos.size()), submit_infos.data(), current_fence)))
+        return false;
+
+    std::array<VkSwapchainKHR, 1> const swapchains = { target->get() };
+    std::array<ui32, 1> const indices = { frame_index };
+
+    VkPresentInfoKHR present_info
+    {
+        .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
+        .waitSemaphoreCount = to_ui32(sync_present_semaphores.size()),
+        .pWaitSemaphores = sync_present_semaphores.data(),
+        .swapchainCount = to_ui32(swapchains.size()),
+        .pSwapchains = swapchains.data(),
+        .pImageIndices = indices.data(),
+    };
+
+    if (failed(dev->call().vkQueuePresentKHR(queue.vk_queue, &present_info)))
+    return false;
+
+    current_sync = (current_sync + 1) % queued_frames;
+
+    return true;
+}
+
+} // lava
diff --git a/liblava/frame/renderer.hpp b/liblava/frame/renderer.hpp
new file mode 100644
index 00000000..69d21bbc
--- /dev/null
+++ b/liblava/frame/renderer.hpp
@@ -0,0 +1,51 @@
+// file      : liblava/frame/renderer.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/frame/swapchain.hpp>
+
+#include <optional>
+
+namespace lava {
+
+struct renderer {
+
+    bool create(swapchain* target);
+    void destroy();
+
+    std::optional<index> begin_frame();
+    bool end_frame(VkCommandBuffers const& cmd_buffers);
+
+    bool frame(VkCommandBuffers const& cmd_buffers) {
+
+        if (!begin_frame())
+            return false;
+
+        return end_frame(cmd_buffers);
+    }
+
+    index get_current_frame() const { return frame_index; }
+
+    using destroy_func = std::function<void()>;
+    destroy_func on_destroy;
+
+    bool active = true;
+
+private:
+    device* dev = nullptr;
+    device::queue queue;
+
+    swapchain* target = nullptr;
+
+    index frame_index = 0;
+    ui32 queued_frames = 2;
+
+    ui32 current_sync = 0;
+    VkFences fences = {};
+    VkSemaphores image_acquired_semaphores = {};
+    VkSemaphores render_complete_semaphores = {};
+};
+
+} // lava
diff --git a/liblava/frame/swapchain.cpp b/liblava/frame/swapchain.cpp
new file mode 100644
index 00000000..5916034a
--- /dev/null
+++ b/liblava/frame/swapchain.cpp
@@ -0,0 +1,237 @@
+// file      : liblava/frame/swapchain.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/frame/swapchain.hpp>
+#include <liblava/base/instance.hpp>
+
+namespace lava {
+
+bool swapchain::create(device* device_, VkSurfaceKHR surface_, uv2 size_) {
+
+    dev = device_;
+    surface = surface_;
+    size = size_;
+
+    set_surface_format();
+
+    return create_internal();
+}
+
+void swapchain::destroy() {
+
+    dev->wait_for_idle();
+
+    destroy_backbuffer_views();
+    destroy_internal();
+
+    vkDestroySurfaceKHR(instance::get(), surface, memory::alloc());
+    surface = nullptr;
+}
+
+bool swapchain::resize(uv2 new_size) {
+
+    dev->wait_for_idle();
+
+    if (!backbuffers.empty()) {
+
+        for (auto& callback : callbacks)
+            callback->on_destroyed();
+
+        destroy_backbuffer_views();
+    }
+
+    size = new_size;
+    if (size.x == 0 || size.y == 0)
+        return true;
+
+    auto result = create_internal();
+    assert(result);
+    if (!result)
+        return false;
+
+    for (auto& callback : reverse(callbacks)) {
+
+        result = callback->on_created();
+        assert(result);
+        if (!result)
+            return false;
+    }
+
+    reload_request = false;
+    return true;
+}
+
+void swapchain::set_surface_format() {
+
+    auto count = 0u;
+    check(vkGetPhysicalDeviceSurfaceFormatsKHR(dev->get_physical_device()->get(), surface, &count, nullptr));
+
+    std::vector<VkSurfaceFormatKHR> formats(count);
+    check(vkGetPhysicalDeviceSurfaceFormatsKHR(dev->get_physical_device()->get(), surface, &count, formats.data()));
+
+    if (count == 1) {
+
+        if (formats[0].format == VK_FORMAT_UNDEFINED) {
+
+            format.format = VK_FORMAT_B8G8R8A8_UNORM;
+            format.colorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR;
+
+        } else {
+
+            format = formats[0];
+        }
+
+    } else {
+
+        VkFormat requestSurfaceImageFormat[] = { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_R8G8B8_UNORM };
+
+        VkColorSpaceKHR requestSurfaceColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR;
+
+        auto requestedFound = false;
+        for (auto& i : requestSurfaceImageFormat) {
+
+            if (requestedFound)
+                break;
+
+            for (ui32 j = 0; j < count; j++) {
+
+                if (formats[j].format == i &&
+                    formats[j].colorSpace == requestSurfaceColorSpace) {
+
+                    format = formats[j];
+                    requestedFound = true;
+                }
+            }
+        }
+
+        if (!requestedFound)
+            format = formats[0];
+    }
+}
+
+VkPresentModeKHR swapchain::choose_present_mode(VkPresentModeKHRs const& present_modes) const {
+
+    for (auto const& present_mode : present_modes)
+        if (present_mode == VK_PRESENT_MODE_MAILBOX_KHR)
+            return present_mode;
+
+    return VK_PRESENT_MODE_FIFO_KHR;
+}
+
+bool swapchain::create_internal() {
+
+    auto present_mode_count = 0u;
+    if (vkGetPhysicalDeviceSurfacePresentModesKHR(dev->get_physical_device()->get(), surface, &present_mode_count, nullptr) != VK_SUCCESS || present_mode_count == 0) {
+
+        log()->error("swapchain::create_internal vkGetPhysicalDeviceSurfacePresentModesKHR(1) failed");
+        return false;
+    }
+
+    VkPresentModeKHRs present_modes(present_mode_count);
+    if (vkGetPhysicalDeviceSurfacePresentModesKHR(dev->get_physical_device()->get(), surface, &present_mode_count, present_modes.data()) != VK_SUCCESS) {
+
+        log()->error("swapchain::create_internal vkGetPhysicalDeviceSurfacePresentModesKHR(2) failed");
+        return false;
+    }
+
+    VkPresentModeKHR present_mode = choose_present_mode(present_modes);
+
+    VkSwapchainKHR old_swapchain = vk_swapchain;
+
+    VkSwapchainCreateInfoKHR info
+    {
+        .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
+        .surface = surface,
+        .minImageCount = 0,
+        .imageFormat = format.format,
+        .imageColorSpace = format.colorSpace,
+        .imageExtent = {},
+        .imageArrayLayers = 1,
+        .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
+        .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
+        .queueFamilyIndexCount = 0,
+        .preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
+        .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
+        .presentMode = present_mode,
+        .clipped = VK_TRUE,
+        .oldSwapchain = old_swapchain,
+    };
+
+    VkSurfaceCapabilitiesKHR cap{};
+    check(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(dev->get_physical_device()->get(), surface, &cap));
+
+    if (cap.maxImageCount > 0)
+        info.minImageCount = (cap.minImageCount + 2 < cap.maxImageCount) ? (cap.minImageCount + 2) : cap.maxImageCount;
+    else
+        info.minImageCount = cap.minImageCount + 2;
+
+    if (cap.currentExtent.width == 0xffffffff) {
+
+        info.imageExtent.width = size.x;
+        info.imageExtent.height = size.y;
+
+    } else {
+
+        size.x = cap.currentExtent.width;
+        size.y = cap.currentExtent.height;
+        info.imageExtent.width = size.x;
+        info.imageExtent.height = size.y;
+    }
+
+    info.preTransform = cap.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR ? VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR : cap.currentTransform;
+
+    check(dev->call().vkCreateSwapchainKHR(dev->get(), &info, memory::alloc(), &vk_swapchain));
+
+    auto backbuffer_count = 0u;
+    check(dev->call().vkGetSwapchainImagesKHR(dev->get(), vk_swapchain, &backbuffer_count, nullptr));
+
+    VkImages images(backbuffer_count);
+    check(dev->call().vkGetSwapchainImagesKHR(dev->get(), vk_swapchain, &backbuffer_count, images.data()));
+
+    for (auto& image : images) {
+
+        auto backbuffer = image::make(format.format, image);
+        if (!backbuffer) {
+
+            log()->error("swapchain::create_internal backbuffer make failed");
+            return false;
+        }
+
+        if (!backbuffer->create(dev, size)) {
+
+            log()->error("swapchain::create_internal backBuffer create failed");
+            return false;
+        }
+
+        backbuffers.push_back(backbuffer);
+    }
+
+    if (old_swapchain)
+        dev->call().vkDestroySwapchainKHR(dev->get(), old_swapchain, memory::alloc());
+
+    return true;
+}
+
+void swapchain::destroy_internal() {
+
+    if (!vk_swapchain)
+        return;
+
+    dev->call().vkDestroySwapchainKHR(dev->get(), vk_swapchain, memory::alloc());
+    vk_swapchain = nullptr;
+}
+
+void swapchain::destroy_backbuffer_views() {
+
+    for (auto& backBuffer : backbuffers)
+        backBuffer->destroy_view();
+
+    backbuffers.clear();
+}
+
+void swapchain::add_callback(callback* cb) { callbacks.push_back(cb); }
+
+void swapchain::remove_callback(callback* cb) { remove(callbacks, cb); }
+
+} // lava
diff --git a/liblava/frame/swapchain.hpp b/liblava/frame/swapchain.hpp
new file mode 100644
index 00000000..3e669083
--- /dev/null
+++ b/liblava/frame/swapchain.hpp
@@ -0,0 +1,71 @@
+// file      : liblava/frame/swapchain.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/resource/image.hpp>
+
+namespace lava {
+
+struct swapchain {
+
+    bool create(device* device, VkSurfaceKHR surface, uv2 size);
+
+    void destroy();
+
+    bool resize(uv2 new_size);
+
+    void request_reload() { reload_request = true; }
+    bool must_reload() const { return reload_request; }
+
+    device* get_device() { return dev; }
+
+    uv2 get_size() const { return size; }
+    VkFormat get_format() const { return format.format; }
+
+    VkSwapchainKHR get() const { return vk_swapchain; }
+
+    ui32 get_backbuffer_count() const { return to_ui32(backbuffers.size()); }
+    image::list const& get_backbuffers() const { return backbuffers; }
+
+    struct callback {
+
+        using list = std::vector<callback*>;
+
+        using created_func = std::function<bool()>;
+        created_func on_created;
+
+        using destroyed_func = std::function<void()>;
+        destroyed_func on_destroyed;
+    };
+
+    void add_callback(callback* cb);
+    void remove_callback(callback* cb);
+
+private:
+    void set_surface_format();
+
+    VkPresentModeKHR choose_present_mode(VkPresentModeKHRs const& present_modes) const;
+
+    bool create_internal();
+
+    void destroy_internal();
+    void destroy_backbuffer_views();
+
+    device* dev = nullptr;
+
+    VkSurfaceKHR surface = nullptr;
+    VkSurfaceFormatKHR format = {};
+
+    VkSwapchainKHR vk_swapchain = nullptr;
+
+    image::list backbuffers;
+
+    uv2 size;
+    bool reload_request = false;
+
+    callback::list callbacks;
+};
+
+} // lava
diff --git a/liblava/frame/window.cpp b/liblava/frame/window.cpp
new file mode 100644
index 00000000..97b8a29c
--- /dev/null
+++ b/liblava/frame/window.cpp
@@ -0,0 +1,256 @@
+// file      : liblava/frame/window.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/frame/window.hpp>
+#include <liblava/frame/frame.hpp>
+
+namespace lava {
+
+bool window::create(name save_name_, state* state) {
+
+    save_name = save_name_;
+
+    auto primary = glfwGetPrimaryMonitor();
+    auto mode = glfwGetVideoMode(primary);
+
+    string default_title = title;
+    if (debug_title)
+        default_title = fmt::format("%s [%s]", title.c_str(), save_name.c_str());
+
+    if (state) {
+
+        windowed = !state->fullscreen;
+
+        if (state->fullscreen) {
+
+            handle = glfwCreateWindow(mode->width, mode->height, default_title.c_str(), primary, nullptr);
+            if (!handle) {
+
+                log()->error("Window::create glfwCreateWindow(1) failed");
+                return false;
+            }
+
+        } else {
+
+            handle = glfwCreateWindow(state->width, state->height, default_title.c_str(), nullptr, nullptr);
+            if (!handle) {
+
+                log()->error("window::create glfwCreateWindow(2) failed");
+                return false;
+            }
+
+            glfwSetWindowPos(handle, state->x, state->y);
+        }
+
+        set_floating(state->floating);
+        set_resizable(state->resizable);
+        set_decorated(state->decorated);
+
+        if (state->maximized)
+            maximize();
+
+    } else {
+
+        if (!windowed) {
+
+            handle = glfwCreateWindow(mode->width, mode->height, default_title.c_str(), primary, nullptr);
+            if (!handle) {
+
+                log()->error("window::create glfwCreateWindow(3) failed");
+                return false;
+            }
+
+        } else {
+
+            handle = glfwCreateWindow(mode->width / 2, mode->height / 2, default_title.c_str(), nullptr, nullptr);
+            if (!handle) {
+
+                log()->error("window::create glfwCreateWindow(4) failed");
+                return false;
+            }
+
+            glfwSetWindowPos(handle, mode->width / 4, mode->height / 4);
+        }
+    }
+
+    switch_mode_request = false;
+    handle_message();
+
+    return true;
+}
+
+void window::destroy() {
+
+    _input = nullptr;
+
+    glfwDestroyWindow(handle);
+    handle = nullptr;
+}
+
+window::state window::get_state() const {
+
+    window::state state;
+
+    get_position(state.x, state.y);
+    get_size(state.width, state.height);
+
+    state.fullscreen = fullscreen();
+    state.floating = floating();
+    state.resizable = resizable();
+    state.decorated = decorated();
+    state.maximized = maximized();
+
+    return state;
+}
+
+void window::set_title(name text) {
+
+    title = text;
+
+    if (!handle)
+        return;
+
+    if (debug_title)
+        glfwSetWindowTitle(handle, fmt::format("%s [%s]", title.c_str(), save_name.c_str()).c_str());
+    else
+        glfwSetWindowTitle(handle, title.c_str());
+}
+
+bool window::switch_mode() {
+
+    destroy();
+
+    return create(save_name.c_str());
+}
+
+void window::handle_message() {
+
+    glfwSetWindowUserPointer(handle, this);
+
+    glfwSetFramebufferSizeCallback(handle, [](GLFWwindow* handle, i32 width, i32 height) {
+
+        auto window = get_window(handle);
+        if (!window)
+            return;
+
+        window->width = to_ui32(width);
+        window->height = to_ui32(height);
+        window->resize_request = true;
+    });
+
+    glfwSetKeyCallback(handle, [](GLFWwindow* handle, i32 key, i32 scancode, i32 action, i32 mods) {
+
+        auto window = get_window(handle);
+        if (!window)
+            return;
+
+        window->_input->add_key_event({ window->get_id(), key, scancode, action, mods });
+    });
+
+    glfwSetScrollCallback(handle, [](GLFWwindow* handle, r64 x_offset, r64 y_offset) {
+
+        auto window = get_window(handle);
+        if (!window)
+            return;
+
+        if (window->_input)
+            window->_input->add_scroll_event({ window->get_id(), x_offset, y_offset });
+    });
+
+    glfwSetMouseButtonCallback(handle, [](GLFWwindow* handle, i32 button, i32 action, i32 mods) {
+
+        auto window = get_window(handle);
+        if (!window)
+            return;
+
+        if (window->_input)
+            window->_input->add_mouse_button_event({ window->get_id(), button, action, mods });
+    });
+
+    glfwSetCursorPosCallback(handle, [](GLFWwindow* handle, r64 x_position, r64 y_position) {
+
+        auto window = get_window(handle);
+        if (!window)
+            return;
+
+        if (window->_input)
+            window->_input->add_mouse_move_event({ window->get_id(), { x_position, y_position } });
+    });
+
+    glfwSetCursorEnterCallback(handle, [](GLFWwindow* handle, i32 entered) {
+
+        auto window = get_window(handle);
+        if (!window)
+            return;
+
+        if (window->_input)
+            window->_input->add_mouse_active_event({ window->get_id(), entered > 0 });
+    });
+}
+
+template <int attr>
+static int is_attribute_set(GLFWwindow* handle) { return glfwGetWindowAttrib(handle, attr); }
+
+template <int attr>
+static bool is_bool_attribute_set(GLFWwindow* handle) { return is_attribute_set<attr>(handle) == 1; }
+
+void window::set_position(i32 x, i32 y) { glfwSetWindowPos(handle, x, y); }
+
+void window::get_position(i32& x, i32& y) const { glfwGetWindowPos(handle, &x, &y); }
+
+void window::set_size(ui32 w, ui32 h) { glfwSetWindowSize(handle, w, h); }
+
+void window::get_size(ui32& w, ui32& h) const { glfwGetWindowSize(handle, (i32*)&w, (i32*)&h); }
+
+void window::get_framebuffer_size(ui32& w, ui32& h) const { glfwGetFramebufferSize(handle, (i32*)&w, (i32*)&h); }
+
+void window::set_mouse_position(r64 x, r64 y) { glfwSetCursorPos(handle, x, y); }
+
+void window::get_mouse_position(r64& x, r64& y) const { glfwGetCursorPos(handle, &x, &y); }
+
+void window::hide_mouse_cursor() { glfwSetInputMode(handle, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); }
+
+void window::show_mouse_cursor() { glfwSetInputMode(handle, GLFW_CURSOR, GLFW_CURSOR_NORMAL); }
+
+float window::get_aspect_ratio() const { return height != 0 ? to_r32(width) / to_r32(height) : 0.f; }
+
+void window::show() { glfwShowWindow(handle); }
+
+void window::hide() { glfwHideWindow(handle); }
+
+bool window::visible() const { return is_bool_attribute_set<GLFW_VISIBLE>(handle); }
+
+void window::iconify() { glfwIconifyWindow(handle); }
+
+bool window::iconified() const { return is_bool_attribute_set<GLFW_ICONIFIED>(handle); }
+
+void window::restore() { glfwRestoreWindow(handle); }
+
+void window::maximize() { glfwMaximizeWindow(handle); }
+
+bool window::maximized() const { return is_bool_attribute_set<GLFW_MAXIMIZED>(handle); }
+
+void window::focus() { glfwFocusWindow(handle); }
+
+bool window::focused() const { return is_bool_attribute_set<GLFW_FOCUSED>(handle); }
+
+bool window::hovered() const { return is_bool_attribute_set<GLFW_HOVERED>(handle); }
+
+bool window::resizable() const { return is_bool_attribute_set<GLFW_RESIZABLE>(handle); }
+
+void window::set_resizable(bool value) { glfwSetWindowAttrib(handle, GLFW_RESIZABLE, value); }
+
+bool window::decorated() const { return is_bool_attribute_set<GLFW_DECORATED>(handle); }
+
+void window::set_decorated(bool value) { glfwSetWindowAttrib(handle, GLFW_DECORATED, value); }
+
+bool window::floating() const { return is_bool_attribute_set<GLFW_FLOATING>(handle); }
+
+void window::set_floating(bool value) { glfwSetWindowAttrib(handle, GLFW_FLOATING, value); }
+
+window* window::get_window(GLFWwindow* handle) { return static_cast<window*>(glfwGetWindowUserPointer(handle)); }
+
+bool window::has_close_request() const { return glfwWindowShouldClose(handle) == 1; }
+
+} // lava
diff --git a/liblava/frame/window.hpp b/liblava/frame/window.hpp
new file mode 100644
index 00000000..c245ef97
--- /dev/null
+++ b/liblava/frame/window.hpp
@@ -0,0 +1,147 @@
+// file      : liblava/frame/window.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/frame/input.hpp>
+
+// fwd
+struct GLFWwindow;
+
+namespace lava {
+
+struct window {
+
+    struct state {
+
+        i32 x = 0;
+        i32 y = 0;
+        ui32 width = 0;
+        ui32 height = 0;
+        bool fullscreen = false;
+        bool floating = false;
+        bool resizable = true;
+        bool decorated = true;
+        bool maximized = false;
+    };
+
+    using ptr = std::shared_ptr<window>;
+    using event = std::function<void(ptr)>;
+    using map = std::map<id, ptr>;
+
+    window() = default;
+    explicit window(id::ref id) : _id(id) {}
+    explicit window(name title) : title(title) {}
+
+    bool create(name save_name = "0", state* state = nullptr);
+    void destroy();
+
+    id::ref get_id() const { return _id; }
+    state get_state() const;
+
+    void set_title(name text);
+    name get_title() const { return title.c_str(); }
+
+    name get_save_name() const { return save_name.c_str(); }
+
+    void set_position(i32 x, i32 y);
+    void get_position(i32& x, i32& y) const;
+    void set_size(ui32 width, ui32 height);
+    void get_size(ui32& width, ui32& height) const;
+
+    void get_framebuffer_size(ui32& width, ui32& height) const;
+
+    void set_mouse_position(r64 x, r64 y);
+    void get_mouse_position(r64& x, r64& y) const;
+
+    void hide_mouse_cursor();
+    void show_mouse_cursor();
+
+    float get_aspect_ratio() const;
+
+    void show();
+    void hide();
+    bool visible() const;
+
+    void iconify();
+    bool iconified() const;
+    void restore();
+
+    void maximize();
+    bool maximized() const;
+
+    void focus();
+    bool focused() const;
+
+    void set_fullscreen(bool active) {
+
+        if (windowed == active)
+            switch_mode_request = true;
+
+        windowed = !active;
+    }
+    bool fullscreen() const { return !windowed; }
+
+    bool hovered() const;
+
+    bool resizable() const;
+    void set_resizable(bool value);
+
+    bool decorated() const;
+    void set_decorated(bool value);
+
+    bool floating() const;
+    void set_floating(bool value);
+
+    static window* get_window(GLFWwindow* handle);
+
+    bool has_close_request() const;
+    bool has_switch_mode_request() const { return switch_mode_request; }
+
+    bool switch_mode();
+
+    GLFWwindow* get() const { return handle; }
+
+    bool has_resize_request() const { return resize_request; }
+    bool handle_resize() {
+
+        if (on_resize)
+            if (!on_resize(width, height))
+                return false;
+
+        resize_request = false;
+        return true;
+    }
+
+    using resize_func = std::function<bool(ui32, ui32)>;
+    resize_func on_resize;
+
+    void assign(input* callback) { _input = callback; }
+
+    void set_debug_title(bool value = true) { debug_title = value; }
+    bool has_debug_title() const { return debug_title; }
+
+    void update_title() { set_title(title.c_str()); }
+
+private:
+    void handle_message();
+
+    id _id;
+
+    GLFWwindow* handle = nullptr;
+    input* _input = nullptr;
+
+    string title = _lava_;
+    string save_name;
+
+    bool windowed = true;
+    bool switch_mode_request = false;
+    bool debug_title = false;
+
+    bool resize_request = false;
+    ui32 width = 0;
+    ui32 height = 0;
+};
+
+} // lava
diff --git a/liblava/fwd.hpp b/liblava/fwd.hpp
new file mode 100644
index 00000000..b8dcc522
--- /dev/null
+++ b/liblava/fwd.hpp
@@ -0,0 +1,74 @@
+// file      : liblava/fwd.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+namespace lava {
+
+    // liblava/core.h
+    struct data_provider;
+    struct data;
+    struct scope_data;
+    struct id;
+    struct ids;
+    struct rect;
+    struct time_info;
+    struct timer;
+    struct run_time;
+    struct internal_version;
+    struct version;
+
+    // liblava/utils.h
+    struct file_guard;
+    struct file_system;
+    struct file;
+    struct file_data;
+    struct file_dialog;
+    struct log_config;
+    struct config_file_callback;
+    struct config_file;
+    struct irandom;
+    struct random_generator;
+    struct random;
+    struct pseudo_random_generator;
+    struct telegram;
+    struct dispatcher;
+    struct thread_pool;
+
+    // liblava/base.h
+    struct device_manager;
+    struct device;
+    struct instance;
+    struct allocator;
+    struct memory;
+    struct physical_device;
+
+    // liblava/resource.h
+    struct buffer;
+    struct image;
+    struct vertex;
+    struct mesh_data;
+    struct mesh;
+    struct texture_file_format;
+    struct texture;
+
+    // liblava/frame.h
+    struct frame_config;
+    struct frame;
+    struct key_event;
+    struct scroll_offset;
+    struct scroll_event;
+    struct mouse_button_event;
+    struct mouse_position;
+    struct mouse_move_event;
+    struct mouse_active_event;
+    struct input_callback;
+    struct input;
+    struct render_target;
+    struct render_thread;
+    struct renderer;
+    struct swapchain;
+    struct window;
+
+} // lava
diff --git a/liblava/lava.hpp b/liblava/lava.hpp
new file mode 100644
index 00000000..29ceb989
--- /dev/null
+++ b/liblava/lava.hpp
@@ -0,0 +1,11 @@
+// file      : liblava/lava.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core.hpp>
+#include <liblava/utils.hpp>
+#include <liblava/base.hpp>
+#include <liblava/resource.hpp>
+#include <liblava/frame.hpp>
diff --git a/liblava/resource.hpp b/liblava/resource.hpp
new file mode 100644
index 00000000..d027cf8a
--- /dev/null
+++ b/liblava/resource.hpp
@@ -0,0 +1,11 @@
+// file      : liblava/resource.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/resource/buffer.hpp>
+#include <liblava/resource/format.hpp>
+#include <liblava/resource/image.hpp>
+#include <liblava/resource/mesh.hpp>
+#include <liblava/resource/texture.hpp>
diff --git a/liblava/resource/buffer.cpp b/liblava/resource/buffer.cpp
new file mode 100644
index 00000000..37e4fb00
--- /dev/null
+++ b/liblava/resource/buffer.cpp
@@ -0,0 +1,127 @@
+// file      : liblava/resource/buffer.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/resource/buffer.hpp>
+
+namespace lava {
+
+VkPipelineStageFlags buffer::usage_to_possible_stages(VkBufferUsageFlags usage) {
+
+    VkPipelineStageFlags flags = 0;
+
+    if (usage & (VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT))
+        flags |= VK_PIPELINE_STAGE_TRANSFER_BIT;
+    if (usage & (VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT))
+        flags |= VK_PIPELINE_STAGE_VERTEX_INPUT_BIT;
+    if (usage & VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT)
+        flags |= VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT;
+    if (usage & (VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT))
+        flags |= VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
+    if (usage & VK_BUFFER_USAGE_STORAGE_BUFFER_BIT)
+        flags |= VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
+
+    return flags;
+}
+
+VkAccessFlags buffer::usage_to_possible_access(VkBufferUsageFlags usage) {
+
+    VkAccessFlags flags = 0;
+
+    if (usage & (VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT))
+        flags |= VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
+    if (usage & VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)
+        flags |= VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;
+    if (usage & VK_BUFFER_USAGE_INDEX_BUFFER_BIT)
+        flags |= VK_ACCESS_INDEX_READ_BIT;
+    if (usage & VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT)
+        flags |= VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
+    if (usage & VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT)
+        flags |= VK_ACCESS_UNIFORM_READ_BIT;
+    if (usage & VK_BUFFER_USAGE_STORAGE_BUFFER_BIT)
+        flags |= VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
+
+    return flags;
+}
+
+buffer::buffer() : _id(ids::next()) {}
+
+buffer::~buffer() {
+
+    ids::free(_id);
+
+    destroy();
+}
+
+bool buffer::create(device* device, void const* data, size_t size, VkBufferUsageFlags usage, bool mapped, VmaMemoryUsage memoryUsage) {
+
+    dev = device;
+
+    VkBufferCreateInfo buffer_info
+    {
+        .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
+        .size = size,
+        .usage = usage,
+        .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+        .queueFamilyIndexCount = 0,
+    };
+
+    VmaAllocationCreateFlags const alloc_flags = mapped ? VMA_ALLOCATION_CREATE_MAPPED_BIT : 0;
+
+    VmaAllocationCreateInfo alloc_info
+    {
+        .flags = alloc_flags,
+        .usage = memoryUsage,
+    };
+
+    if (failed(vmaCreateBuffer(dev->alloc(), &buffer_info, &alloc_info, &vk_buffer, &allocation, &allocation_info))) {
+
+        log()->error("buffer::create vmaCreateBuffer failed");
+        return false;
+    }
+
+    if (!mapped) {
+
+        data_ptr map = nullptr;
+        if (failed(vmaMapMemory(dev->alloc(), allocation, (void**)(&map)))) {
+
+            log()->error("buffer::create vmaMapMemory failed");
+            return false;
+        }
+
+        memcpy(map, data, size);
+
+        vmaUnmapMemory(dev->alloc(), allocation);
+
+    } else if (data) {
+
+        memcpy(allocation_info.pMappedData, data, size);
+
+        flush();
+    }
+
+    descriptor.buffer = vk_buffer;
+    descriptor.offset = 0;
+    descriptor.range = size;
+
+    return true;
+}
+
+void buffer::destroy() {
+
+    if (!vk_buffer)
+        return;
+
+    vmaDestroyBuffer(dev->alloc(), vk_buffer, allocation);
+    vk_buffer = nullptr;
+    allocation = nullptr;
+
+    dev = nullptr;
+}
+
+void buffer::flush(VkDeviceSize offset, VkDeviceSize size) {
+
+    vmaFlushAllocation(dev->alloc(), allocation, offset, size);
+}
+
+} // lava
diff --git a/liblava/resource/buffer.hpp b/liblava/resource/buffer.hpp
new file mode 100644
index 00000000..9a10924d
--- /dev/null
+++ b/liblava/resource/buffer.hpp
@@ -0,0 +1,54 @@
+// file      : liblava/resource/buffer.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/base/device.hpp>
+#include <liblava/base/memory.hpp>
+
+namespace lava {
+
+struct buffer {
+
+    using ptr = std::shared_ptr<buffer>;
+    using list = std::vector<ptr>;
+
+    static VkPipelineStageFlags usage_to_possible_stages(VkBufferUsageFlags usage);
+    static VkAccessFlags usage_to_possible_access(VkBufferUsageFlags usage);
+
+    explicit buffer();
+    ~buffer();
+
+    static ptr make() { return std::make_shared<buffer>(); }
+
+    bool create(device* device, void const* data, size_t size, VkBufferUsageFlags usage, bool mapped = false, 
+                                VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_GPU_ONLY);
+    void destroy();
+
+    id::ref get_id() const { return _id; }
+    device* get_device() { return dev; }
+
+    bool is_valid() const { return vk_buffer != nullptr; }
+
+    VkBuffer get() const { return vk_buffer; }
+    VkDescriptorBufferInfo const& get_descriptor() const { return descriptor; }
+
+    size_t get_size() const { return allocation_info.size; }
+    void* get_mapped_data() const { return allocation_info.pMappedData; }
+    VkDeviceMemory get_device_memory() const { return allocation_info.deviceMemory; }
+
+    void flush(VkDeviceSize offset = 0, VkDeviceSize size = VK_WHOLE_SIZE);
+
+private:
+    id _id;
+    device* dev = nullptr;
+
+    VkBuffer vk_buffer = nullptr;
+    VmaAllocation allocation = nullptr;
+
+    VmaAllocationInfo allocation_info = {};
+    VkDescriptorBufferInfo descriptor = {};
+};
+
+} // lava
diff --git a/liblava/resource/format.cpp b/liblava/resource/format.cpp
new file mode 100644
index 00000000..ba67e45a
--- /dev/null
+++ b/liblava/resource/format.cpp
@@ -0,0 +1,460 @@
+// file      : liblava/resource/format.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/resource/format.hpp>
+#include <liblava/base/memory.hpp>
+
+bool lava::format_is_depth(VkFormat format) {
+
+    switch (format) {
+
+        case VK_FORMAT_D16_UNORM:
+        case VK_FORMAT_D16_UNORM_S8_UINT:
+        case VK_FORMAT_D24_UNORM_S8_UINT:
+        case VK_FORMAT_D32_SFLOAT:
+        case VK_FORMAT_X8_D24_UNORM_PACK32:
+        case VK_FORMAT_D32_SFLOAT_S8_UINT:
+            return true;
+
+        default:
+            return false;
+    }
+}
+
+bool lava::format_is_stencil(VkFormat format) { return format == VK_FORMAT_S8_UINT; }
+
+bool lava::format_is_depth_stencil(VkFormat format) { return format_is_depth(format) || format_is_stencil(format); }
+
+VkImageAspectFlags lava::format_to_aspect_mask(VkFormat format) {
+
+    switch (format) {
+
+        case VK_FORMAT_UNDEFINED:
+            return 0;
+
+        case VK_FORMAT_S8_UINT:
+            return VK_IMAGE_ASPECT_STENCIL_BIT;
+
+        case VK_FORMAT_D16_UNORM_S8_UINT:
+        case VK_FORMAT_D24_UNORM_S8_UINT:
+        case VK_FORMAT_D32_SFLOAT_S8_UINT:
+            return VK_IMAGE_ASPECT_STENCIL_BIT | VK_IMAGE_ASPECT_DEPTH_BIT;
+
+        case VK_FORMAT_D16_UNORM:
+        case VK_FORMAT_D32_SFLOAT:
+        case VK_FORMAT_X8_D24_UNORM_PACK32:
+            return VK_IMAGE_ASPECT_DEPTH_BIT;
+
+        default:
+            return VK_IMAGE_ASPECT_COLOR_BIT;
+    }
+}
+
+void lava::format_block_dim(VkFormat format, ui32& width, ui32& height) {
+
+#define fmt(x, w, h)    \
+    case VK_FORMAT_##x: \
+        width = w;      \
+        height = h;     \
+        break
+
+    switch (format) {
+
+        fmt(ETC2_R8G8B8A8_UNORM_BLOCK, 4, 4);
+        fmt(ETC2_R8G8B8A8_SRGB_BLOCK, 4, 4);
+        fmt(ETC2_R8G8B8A1_UNORM_BLOCK, 4, 4);
+        fmt(ETC2_R8G8B8A1_SRGB_BLOCK, 4, 4);
+        fmt(ETC2_R8G8B8_UNORM_BLOCK, 4, 4);
+        fmt(ETC2_R8G8B8_SRGB_BLOCK, 4, 4);
+        fmt(EAC_R11_UNORM_BLOCK, 4, 4);
+        fmt(EAC_R11_SNORM_BLOCK, 4, 4);
+        fmt(EAC_R11G11_UNORM_BLOCK, 4, 4);
+        fmt(EAC_R11G11_SNORM_BLOCK, 4, 4);
+
+        fmt(BC1_RGB_UNORM_BLOCK, 4, 4);
+        fmt(BC1_RGB_SRGB_BLOCK, 4, 4);
+        fmt(BC1_RGBA_UNORM_BLOCK, 4, 4);
+        fmt(BC1_RGBA_SRGB_BLOCK, 4, 4);
+        fmt(BC2_UNORM_BLOCK, 4, 4);
+        fmt(BC2_SRGB_BLOCK, 4, 4);
+        fmt(BC3_UNORM_BLOCK, 4, 4);
+        fmt(BC3_SRGB_BLOCK, 4, 4);
+
+        fmt(ASTC_4x4_SRGB_BLOCK, 4, 4);
+        fmt(ASTC_5x4_SRGB_BLOCK, 5, 4);
+        fmt(ASTC_5x5_SRGB_BLOCK, 5, 5);
+        fmt(ASTC_6x5_SRGB_BLOCK, 6, 5);
+        fmt(ASTC_6x6_SRGB_BLOCK, 6, 6);
+        fmt(ASTC_8x5_SRGB_BLOCK, 8, 5);
+        fmt(ASTC_8x6_SRGB_BLOCK, 8, 6);
+        fmt(ASTC_8x8_SRGB_BLOCK, 8, 8);
+        fmt(ASTC_10x5_SRGB_BLOCK, 10, 5);
+        fmt(ASTC_10x6_SRGB_BLOCK, 10, 6);
+        fmt(ASTC_10x8_SRGB_BLOCK, 10, 8);
+        fmt(ASTC_10x10_SRGB_BLOCK, 10, 10);
+        fmt(ASTC_12x10_SRGB_BLOCK, 12, 10);
+        fmt(ASTC_12x12_SRGB_BLOCK, 12, 12);
+        fmt(ASTC_4x4_UNORM_BLOCK, 4, 4);
+        fmt(ASTC_5x4_UNORM_BLOCK, 5, 4);
+        fmt(ASTC_5x5_UNORM_BLOCK, 5, 5);
+        fmt(ASTC_6x5_UNORM_BLOCK, 6, 5);
+        fmt(ASTC_6x6_UNORM_BLOCK, 6, 6);
+        fmt(ASTC_8x5_UNORM_BLOCK, 8, 5);
+        fmt(ASTC_8x6_UNORM_BLOCK, 8, 6);
+        fmt(ASTC_8x8_UNORM_BLOCK, 8, 8);
+        fmt(ASTC_10x5_UNORM_BLOCK, 10, 5);
+        fmt(ASTC_10x6_UNORM_BLOCK, 10, 6);
+        fmt(ASTC_10x8_UNORM_BLOCK, 10, 8);
+        fmt(ASTC_10x10_UNORM_BLOCK, 10, 10);
+        fmt(ASTC_12x10_UNORM_BLOCK, 12, 10);
+        fmt(ASTC_12x12_UNORM_BLOCK, 12, 12);
+
+        default:
+            width = 1;
+            height = 1;
+            break;
+    }
+
+#undef fmt
+}
+
+void lava::format_align_dim(VkFormat format, ui32& width, ui32& height) {
+
+    ui32 align_width, align_height;
+    format_block_dim(format, align_width, align_height);
+    width = ((width + align_width - 1) / align_width) * align_width;
+    height = ((height + align_height - 1) / align_height) * align_height;
+}
+
+void lava::format_num_blocks(VkFormat format, ui32& width, ui32& height) {
+
+    ui32 align_width, align_height;
+    format_block_dim(format, align_width, align_height);
+    width = (width + align_width - 1) / align_width;
+    height = (height + align_height - 1) / align_height;
+}
+
+lava::ui32 lava::format_block_size(VkFormat format) {
+
+#define fmt(x, bpp)     \
+    case VK_FORMAT_##x: \
+        return bpp
+
+    switch (format) {
+
+        fmt(R4G4_UNORM_PACK8, 1);
+        fmt(R4G4B4A4_UNORM_PACK16, 2);
+        fmt(B4G4R4A4_UNORM_PACK16, 2);
+        fmt(R5G6B5_UNORM_PACK16, 2);
+        fmt(B5G6R5_UNORM_PACK16, 2);
+        fmt(R5G5B5A1_UNORM_PACK16, 2);
+        fmt(B5G5R5A1_UNORM_PACK16, 2);
+        fmt(A1R5G5B5_UNORM_PACK16, 2);
+        fmt(R8_UNORM, 1);
+        fmt(R8_SNORM, 1);
+        fmt(R8_USCALED, 1);
+        fmt(R8_SSCALED, 1);
+        fmt(R8_UINT, 1);
+        fmt(R8_SINT, 1);
+        fmt(R8_SRGB, 1);
+        fmt(R8G8_UNORM, 2);
+        fmt(R8G8_SNORM, 2);
+        fmt(R8G8_USCALED, 2);
+        fmt(R8G8_SSCALED, 2);
+        fmt(R8G8_UINT, 2);
+        fmt(R8G8_SINT, 2);
+        fmt(R8G8_SRGB, 2);
+        fmt(R8G8B8_UNORM, 3);
+        fmt(R8G8B8_SNORM, 3);
+        fmt(R8G8B8_USCALED, 3);
+        fmt(R8G8B8_SSCALED, 3);
+        fmt(R8G8B8_UINT, 3);
+        fmt(R8G8B8_SINT, 3);
+        fmt(R8G8B8_SRGB, 3);
+        fmt(R8G8B8A8_UNORM, 4);
+        fmt(R8G8B8A8_SNORM, 4);
+        fmt(R8G8B8A8_USCALED, 4);
+        fmt(R8G8B8A8_SSCALED, 4);
+        fmt(R8G8B8A8_UINT, 4);
+        fmt(R8G8B8A8_SINT, 4);
+        fmt(R8G8B8A8_SRGB, 4);
+        fmt(B8G8R8A8_UNORM, 4);
+        fmt(B8G8R8A8_SNORM, 4);
+        fmt(B8G8R8A8_USCALED, 4);
+        fmt(B8G8R8A8_SSCALED, 4);
+        fmt(B8G8R8A8_UINT, 4);
+        fmt(B8G8R8A8_SINT, 4);
+        fmt(B8G8R8A8_SRGB, 4);
+        fmt(A8B8G8R8_UNORM_PACK32, 4);
+        fmt(A8B8G8R8_SNORM_PACK32, 4);
+        fmt(A8B8G8R8_USCALED_PACK32, 4);
+        fmt(A8B8G8R8_SSCALED_PACK32, 4);
+        fmt(A8B8G8R8_UINT_PACK32, 4);
+        fmt(A8B8G8R8_SINT_PACK32, 4);
+        fmt(A8B8G8R8_SRGB_PACK32, 4);
+        fmt(A2B10G10R10_UNORM_PACK32, 4);
+        fmt(A2B10G10R10_SNORM_PACK32, 4);
+        fmt(A2B10G10R10_USCALED_PACK32, 4);
+        fmt(A2B10G10R10_SSCALED_PACK32, 4);
+        fmt(A2B10G10R10_UINT_PACK32, 4);
+        fmt(A2B10G10R10_SINT_PACK32, 4);
+        fmt(A2R10G10B10_UNORM_PACK32, 4);
+        fmt(A2R10G10B10_SNORM_PACK32, 4);
+        fmt(A2R10G10B10_USCALED_PACK32, 4);
+        fmt(A2R10G10B10_SSCALED_PACK32, 4);
+        fmt(A2R10G10B10_UINT_PACK32, 4);
+        fmt(A2R10G10B10_SINT_PACK32, 4);
+        fmt(R16_UNORM, 2);
+        fmt(R16_SNORM, 2);
+        fmt(R16_USCALED, 2);
+        fmt(R16_SSCALED, 2);
+        fmt(R16_UINT, 2);
+        fmt(R16_SINT, 2);
+        fmt(R16_SFLOAT, 2);
+        fmt(R16G16_UNORM, 4);
+        fmt(R16G16_SNORM, 4);
+        fmt(R16G16_USCALED, 4);
+        fmt(R16G16_SSCALED, 4);
+        fmt(R16G16_UINT, 4);
+        fmt(R16G16_SINT, 4);
+        fmt(R16G16_SFLOAT, 4);
+        fmt(R16G16B16_UNORM, 6);
+        fmt(R16G16B16_SNORM, 6);
+        fmt(R16G16B16_USCALED, 6);
+        fmt(R16G16B16_SSCALED, 6);
+        fmt(R16G16B16_UINT, 6);
+        fmt(R16G16B16_SINT, 6);
+        fmt(R16G16B16_SFLOAT, 6);
+        fmt(R16G16B16A16_UNORM, 8);
+        fmt(R16G16B16A16_SNORM, 8);
+        fmt(R16G16B16A16_USCALED, 8);
+        fmt(R16G16B16A16_SSCALED, 8);
+        fmt(R16G16B16A16_UINT, 8);
+        fmt(R16G16B16A16_SINT, 8);
+        fmt(R16G16B16A16_SFLOAT, 8);
+        fmt(R32_UINT, 4);
+        fmt(R32_SINT, 4);
+        fmt(R32_SFLOAT, 4);
+        fmt(R32G32_UINT, 8);
+        fmt(R32G32_SINT, 8);
+        fmt(R32G32_SFLOAT, 8);
+        fmt(R32G32B32_UINT, 12);
+        fmt(R32G32B32_SINT, 12);
+        fmt(R32G32B32_SFLOAT, 12);
+        fmt(R32G32B32A32_UINT, 16);
+        fmt(R32G32B32A32_SINT, 16);
+        fmt(R32G32B32A32_SFLOAT, 16);
+        fmt(R64_UINT, 8);
+        fmt(R64_SINT, 8);
+        fmt(R64_SFLOAT, 8);
+        fmt(R64G64_UINT, 16);
+        fmt(R64G64_SINT, 16);
+        fmt(R64G64_SFLOAT, 16);
+        fmt(R64G64B64_UINT, 24);
+        fmt(R64G64B64_SINT, 24);
+        fmt(R64G64B64_SFLOAT, 24);
+        fmt(R64G64B64A64_UINT, 32);
+        fmt(R64G64B64A64_SINT, 32);
+        fmt(R64G64B64A64_SFLOAT, 32);
+        fmt(B10G11R11_UFLOAT_PACK32, 4);
+        fmt(E5B9G9R9_UFLOAT_PACK32, 4);
+        fmt(D16_UNORM, 2);
+        fmt(X8_D24_UNORM_PACK32, 4);
+        fmt(D32_SFLOAT, 4);
+        fmt(S8_UINT, 1);
+        fmt(D16_UNORM_S8_UINT, 3); // doesn't make sense.
+        fmt(D24_UNORM_S8_UINT, 4);
+        fmt(D32_SFLOAT_S8_UINT, 5); // doesn't make sense.
+
+        // ETC2
+        fmt(ETC2_R8G8B8A8_UNORM_BLOCK, 16);
+        fmt(ETC2_R8G8B8A8_SRGB_BLOCK, 16);
+        fmt(ETC2_R8G8B8A1_UNORM_BLOCK, 8);
+        fmt(ETC2_R8G8B8A1_SRGB_BLOCK, 8);
+        fmt(ETC2_R8G8B8_UNORM_BLOCK, 8);
+        fmt(ETC2_R8G8B8_SRGB_BLOCK, 8);
+        fmt(EAC_R11_UNORM_BLOCK, 8);
+        fmt(EAC_R11_SNORM_BLOCK, 8);
+        fmt(EAC_R11G11_UNORM_BLOCK, 16);
+        fmt(EAC_R11G11_SNORM_BLOCK, 16);
+
+        // BC
+        fmt(BC1_RGB_UNORM_BLOCK, 8);
+        fmt(BC1_RGB_SRGB_BLOCK, 8);
+        fmt(BC1_RGBA_UNORM_BLOCK, 8);
+        fmt(BC1_RGBA_SRGB_BLOCK, 8);
+        fmt(BC2_UNORM_BLOCK, 16);
+        fmt(BC2_SRGB_BLOCK, 16);
+        fmt(BC3_UNORM_BLOCK, 16);
+        fmt(BC3_SRGB_BLOCK, 16);
+
+        // ASTC
+        fmt(ASTC_4x4_SRGB_BLOCK, 16);
+        fmt(ASTC_5x4_SRGB_BLOCK, 16);
+        fmt(ASTC_5x5_SRGB_BLOCK, 16);
+        fmt(ASTC_6x5_SRGB_BLOCK, 16);
+        fmt(ASTC_6x6_SRGB_BLOCK, 16);
+        fmt(ASTC_8x5_SRGB_BLOCK, 16);
+        fmt(ASTC_8x6_SRGB_BLOCK, 16);
+        fmt(ASTC_8x8_SRGB_BLOCK, 16);
+        fmt(ASTC_10x5_SRGB_BLOCK, 16);
+        fmt(ASTC_10x6_SRGB_BLOCK, 16);
+        fmt(ASTC_10x8_SRGB_BLOCK, 16);
+        fmt(ASTC_10x10_SRGB_BLOCK, 16);
+        fmt(ASTC_12x10_SRGB_BLOCK, 16);
+        fmt(ASTC_12x12_SRGB_BLOCK, 16);
+        fmt(ASTC_4x4_UNORM_BLOCK, 16);
+        fmt(ASTC_5x4_UNORM_BLOCK, 16);
+        fmt(ASTC_5x5_UNORM_BLOCK, 16);
+        fmt(ASTC_6x5_UNORM_BLOCK, 16);
+        fmt(ASTC_6x6_UNORM_BLOCK, 16);
+        fmt(ASTC_8x5_UNORM_BLOCK, 16);
+        fmt(ASTC_8x6_UNORM_BLOCK, 16);
+        fmt(ASTC_8x8_UNORM_BLOCK, 16);
+        fmt(ASTC_10x5_UNORM_BLOCK, 16);
+        fmt(ASTC_10x6_UNORM_BLOCK, 16);
+        fmt(ASTC_10x8_UNORM_BLOCK, 16);
+        fmt(ASTC_10x10_UNORM_BLOCK, 16);
+        fmt(ASTC_12x10_UNORM_BLOCK, 16);
+        fmt(ASTC_12x12_UNORM_BLOCK, 16);
+
+        default:
+            assert(0 && "Unknown format.");
+            return 0;
+    }
+#undef fmt
+}
+
+bool lava::get_supported_depth_format(VkPhysicalDevice physical_device, VkFormat* depth_format) {
+
+    VkFormats depth_formats = { VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D32_SFLOAT, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D16_UNORM_S8_UINT, VK_FORMAT_D16_UNORM };
+
+    for (auto& format : depth_formats) {
+
+        VkFormatProperties format_props;
+        vkGetPhysicalDeviceFormatProperties(physical_device, format, &format_props);
+
+        if (format_props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
+
+            *depth_format = format;
+            return true;
+        }
+    }
+
+    return false;
+}
+
+VkImageMemoryBarrier lava::new_image_memory_barrier(VkImage image, VkImageLayout old_layout, VkImageLayout new_layout) {
+
+    return {
+        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+        .pNext = nullptr,
+        .srcAccessMask = 0,
+        .dstAccessMask = 0,
+        .oldLayout = old_layout,
+        .newLayout = new_layout,
+        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+        .image = image,
+        .subresourceRange = {},
+    };
+}
+
+void lava::set_image_layout(VkCommandBuffer cmd_buffer, VkImage image, VkImageLayout old_image_layout, VkImageLayout new_image_layout,
+                            VkImageSubresourceRange subresource_range, VkPipelineStageFlags src_stage_mask, VkPipelineStageFlags dst_stage_mask) {
+
+    auto image_memory_barrier = new_image_memory_barrier(image, old_image_layout, new_image_layout);
+    image_memory_barrier.subresourceRange = subresource_range;
+
+    switch (old_image_layout) {
+
+        case VK_IMAGE_LAYOUT_UNDEFINED:
+            image_memory_barrier.srcAccessMask = 0;
+            break;
+
+        case VK_IMAGE_LAYOUT_PREINITIALIZED:
+            image_memory_barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
+            break;
+
+        case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
+            image_memory_barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+            break;
+
+        case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
+            image_memory_barrier.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+            break;
+
+        case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
+            image_memory_barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
+            break;
+
+        case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
+            image_memory_barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
+            break;
+
+        case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
+            image_memory_barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
+            break;
+        default:
+            break;
+    }
+
+    switch (new_image_layout) {
+
+        case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
+            image_memory_barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
+            break;
+
+        case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
+            image_memory_barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
+            break;
+
+        case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
+            image_memory_barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+            break;
+
+        case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
+            image_memory_barrier.dstAccessMask = image_memory_barrier.dstAccessMask | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+            break;
+
+        case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
+            if (image_memory_barrier.srcAccessMask == 0)
+                image_memory_barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
+
+            image_memory_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
+            break;
+        default:
+            break;
+    }
+
+    vkCmdPipelineBarrier(cmd_buffer, src_stage_mask, dst_stage_mask, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier);
+}
+
+void lava::set_image_layout(VkCommandBuffer cmd_buffer, VkImage image, VkImageAspectFlags aspect_mask, VkImageLayout old_image_layout,
+                            VkImageLayout new_image_layout, VkPipelineStageFlags src_stage_mask, VkPipelineStageFlags dst_stage_mask) {
+
+    VkImageSubresourceRange subresource_range
+    {
+        .aspectMask = aspect_mask,
+        .baseMipLevel = 0,
+        .levelCount = 1,
+        .baseArrayLayer = 0,
+        .layerCount = 1,
+    };
+
+    set_image_layout(cmd_buffer, image, old_image_layout, new_image_layout, subresource_range, src_stage_mask, dst_stage_mask);
+}
+
+void lava::insert_image_memory_barrier(lava::device* device, VkCommandBuffer cmd_buffer, VkImage image, VkAccessFlags src_access_mask, VkAccessFlags dst_access_mask,
+                                        VkImageLayout old_image_layout, VkImageLayout new_image_layout, 
+                                        VkPipelineStageFlags src_stage_mask, VkPipelineStageFlags dst_stage_mask, VkImageSubresourceRange subresource_range) {
+
+    auto image_memory_barrier = new_image_memory_barrier(image, old_image_layout, new_image_layout);
+
+    image_memory_barrier.srcAccessMask = src_access_mask;
+    image_memory_barrier.dstAccessMask = dst_access_mask;
+    image_memory_barrier.subresourceRange = subresource_range;
+
+    device->call().vkCmdPipelineBarrier(cmd_buffer, src_stage_mask, dst_stage_mask, 0, 0, nullptr, 0, nullptr, 1, &image_memory_barrier);
+}
diff --git a/liblava/resource/format.hpp b/liblava/resource/format.hpp
new file mode 100644
index 00000000..63b4a449
--- /dev/null
+++ b/liblava/resource/format.hpp
@@ -0,0 +1,48 @@
+// file      : liblava/resource/format.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/base/device.hpp>
+#include <liblava/core/data.hpp>
+
+namespace lava {
+
+bool format_is_depth(VkFormat format);
+
+bool format_is_stencil(VkFormat format);
+
+bool format_is_depth_stencil(VkFormat format);
+
+VkImageAspectFlags format_to_aspect_mask(VkFormat format);
+
+void format_block_dim(VkFormat format, ui32& width, ui32& height);
+
+void format_align_dim(VkFormat format, ui32& width, ui32& height);
+
+void format_num_blocks(VkFormat format, ui32& width, ui32& height);
+
+ui32 format_block_size(VkFormat format);
+
+bool get_supported_depth_format(VkPhysicalDevice physical_device, VkFormat* depth_format);
+
+VkImageMemoryBarrier new_image_memory_barrier(VkImage image, VkImageLayout old_layout, VkImageLayout new_layout);
+
+void set_image_layout(VkCommandBuffer cmd_buffer, VkImage image, VkImageLayout old_image_layout,
+                        VkImageLayout new_image_layout, VkImageSubresourceRange subresource_range,
+                        VkPipelineStageFlags src_stage_mask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+                        VkPipelineStageFlags dst_stage_mask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
+
+void set_image_layout(VkCommandBuffer cmd_buffer, VkImage image, VkImageAspectFlags aspect_mask,
+                        VkImageLayout old_image_layout, VkImageLayout new_image_layout,
+                        VkPipelineStageFlags src_stage_mask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+                        VkPipelineStageFlags dst_stage_mask = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
+
+void insert_image_memory_barrier(lava::device* device, VkCommandBuffer cmd_buffer, VkImage image, VkAccessFlags src_access_mask,
+                                    VkAccessFlags dst_access_mask, VkImageLayout old_image_layout,
+                                    VkImageLayout new_image_layout, VkPipelineStageFlags src_stage_mask,
+                                    VkPipelineStageFlags dst_stage_mask,
+                                    VkImageSubresourceRange subresource_range);
+
+} // lava
diff --git a/liblava/resource/image.cpp b/liblava/resource/image.cpp
new file mode 100644
index 00000000..1ce8d5ba
--- /dev/null
+++ b/liblava/resource/image.cpp
@@ -0,0 +1,113 @@
+// file      : liblava/resource/image.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/resource/image.hpp>
+
+namespace lava {
+
+image::image(VkFormat format, VkImage vk_image) : _id(ids::next()), vk_image(vk_image) {
+
+    info =
+    {
+        .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+        .pNext = nullptr,
+        .flags = 0,
+        .imageType = VK_IMAGE_TYPE_2D,
+        .format = format,
+        .extent = { 0, 0, 1 },
+        .mipLevels = 1,
+        .arrayLayers = 1,
+        .samples = VK_SAMPLE_COUNT_1_BIT,
+        .tiling = VK_IMAGE_TILING_OPTIMAL,
+        .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
+        .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+        .queueFamilyIndexCount = 0,
+        .pQueueFamilyIndices = nullptr,
+        .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+    };
+
+    subresource_range =
+    {
+        .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+        .baseMipLevel = 0,
+        .levelCount = 1,
+        .baseArrayLayer = 0,
+        .layerCount = 1,
+    };
+
+    view_info =
+    {
+        .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+        .pNext = nullptr,
+        .flags = 0,
+        .image = vk_image,
+        .viewType = VK_IMAGE_VIEW_TYPE_2D,
+        .format = format,
+        .components = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A },
+        .subresourceRange = subresource_range,
+    };
+}
+
+image::~image() { ids::free(_id); }
+
+image::ptr image::make(VkFormat format, VkImage vk_image) { return std::make_shared<image>(format, vk_image);  }
+
+image::ptr image::make(VkFormat format, device* device, uv2 size, VkImage vk_image) {
+
+    auto result = make(format, vk_image);
+
+    if (!result->create(device, size))
+        return nullptr;
+
+    return result;
+}
+
+bool image::create(device* device, uv2 size, VmaMemoryUsage memory_usage, bool mip_levels_generation) {
+
+    dev = device;
+
+    info.extent = { size.x, size.y, 1 };
+
+    if (!vk_image) {
+
+        VmaAllocationCreateInfo allocInfo
+        {
+            .usage = memory_usage,
+        };
+
+        if (failed(vmaCreateImage(dev->alloc(), &info, &allocInfo, &vk_image, &allocation, nullptr))) {
+
+            log()->error("image::create vmaCreateImage failed");
+            return false;
+        }
+    }
+
+    view_info.image = vk_image;
+    view_info.subresourceRange = subresource_range;
+
+    return check(device->call().vkCreateImageView(device->get(), &view_info, memory::alloc(), &view));
+}
+
+void image::destroy(bool only_view) {
+
+    if (view) {
+
+        dev->call().vkDestroyImageView(dev->get(), view, memory::alloc());
+        view = nullptr;
+    }
+
+    if (only_view)
+        return;
+
+    if (vk_image) {
+
+        vmaDestroyImage(dev->alloc(), vk_image, allocation);
+        vk_image = nullptr;
+        allocation = nullptr;
+    }
+
+    dev = nullptr;
+}
+
+} // lava
diff --git a/liblava/resource/image.hpp b/liblava/resource/image.hpp
new file mode 100644
index 00000000..e98125d4
--- /dev/null
+++ b/liblava/resource/image.hpp
@@ -0,0 +1,72 @@
+// file      : liblava/resource/image.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/base/device.hpp>
+#include <liblava/base/memory.hpp>
+#include <liblava/core/id.hpp>
+#include <liblava/core/math.hpp>
+
+namespace lava {
+
+struct image {
+
+    using ptr = std::shared_ptr<image>;
+    using map = std::map<id, ptr>;
+    using list = std::vector<ptr>;
+
+    static ptr make(VkFormat format, VkImage vk_image = nullptr);
+    static ptr make(VkFormat format, device* device, uv2 size, VkImage vk_image = nullptr);
+
+    explicit image(VkFormat format, VkImage vk_image = nullptr);
+    ~image();
+
+    bool create(device* device, uv2 size, VmaMemoryUsage memory_usage = VMA_MEMORY_USAGE_GPU_ONLY, bool mip_levels_generation = false);
+    void destroy(bool only_view = false);
+    void destroy_view() { destroy(true); }
+
+    id::ref get_id() const { return _id; }
+    device* get_device() { return dev; }
+
+    VkFormat get_format() const { return info.format; }
+    uv2 get_size() const { return { info.extent.width, info.extent.height }; }
+    ui32 get_depth() const { return info.extent.depth; }
+
+    VkImage get() const { return vk_image; }
+    VkImageView get_view() const { return view; }
+
+    VkImageCreateInfo const& get_info() const { return info; }
+    VkImageViewCreateInfo const& get_view_info() const { return view_info; }
+    VkImageSubresourceRange const& get_subresource_range() const { return subresource_range; }
+
+    void set_flags(VkImageCreateFlagBits flags) { info.flags = flags; }
+    void set_tiling(VkImageTiling tiling) { info.tiling = tiling; }
+    void set_usage(VkImageUsageFlags usage) { info.usage = usage; }
+    void set_layout(VkImageLayout initial) { info.initialLayout = initial; }
+
+    void set_aspectMask(VkImageAspectFlags aspectMask) { subresource_range.aspectMask = aspectMask; }
+
+    void set_level_count(ui32 levels) { subresource_range.levelCount = levels; info.mipLevels = levels; }
+    void set_layer_count(ui32 layers) { subresource_range.layerCount = layers; info.arrayLayers = layers; }
+
+    void set_component(VkComponentMapping mapping = {}) { view_info.components = mapping; }
+    void set_view_type(VkImageViewType type) { view_info.viewType = type; }
+
+private:
+    id _id;
+    device* dev = nullptr;
+
+    VkImage vk_image = nullptr;
+    VkImageCreateInfo info;
+
+    VmaAllocation allocation = nullptr;
+
+    VkImageView view = nullptr;
+
+    VkImageViewCreateInfo view_info;
+    VkImageSubresourceRange subresource_range;
+};
+
+} // lava
diff --git a/liblava/resource/mesh.cpp b/liblava/resource/mesh.cpp
new file mode 100644
index 00000000..32cb0155
--- /dev/null
+++ b/liblava/resource/mesh.cpp
@@ -0,0 +1,381 @@
+// file      : liblava/resource/mesh.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/resource/mesh.hpp>
+
+#ifndef LIBLAVA_ASSIMP
+#define LIBLAVA_ASSIMP 0
+#endif
+
+#if LIBLAVA_ASSIMP
+#include <assimp/Importer.hpp>
+#include <assimp/cimport.h>
+#include <assimp/postprocess.h>
+#include <assimp/scene.h>
+#endif
+
+#ifndef LIBLAVA_TINYOBJLOADER
+#define LIBLAVA_TINYOBJLOADER 1
+#endif
+
+#if LIBLAVA_TINYOBJLOADER
+
+#ifdef _WIN32
+#pragma warning(push, 4)
+#else
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
+#endif
+
+#define TINYOBJLOADER_IMPLEMENTATION
+#include <tiny_obj_loader.h>
+
+#ifdef _WIN32
+#pragma warning(pop)
+#else
+#pragma GCC diagnostic pop
+#endif
+
+#endif
+
+namespace lava {
+
+mesh::mesh() : _id(ids::next()) {}
+
+mesh::~mesh() {
+
+    ids::free(_id);
+
+    destroy();
+}
+
+void mesh::add_data(mesh_data const& value) {
+
+    auto index_base = to_ui32(data.vertices.size());
+
+    data.vertices.insert(data.vertices.end(), value.vertices.begin(),value.vertices.end());
+
+    for (auto& index : value.indices)
+        data.indices.push_back(index_base + index);
+}
+
+bool mesh::create(device* device, bool mapped_, VmaMemoryUsage memory_usage_) {
+
+    dev = device;
+    mapped = mapped_;
+    memory_usage = memory_usage_;
+
+    if (!data.vertices.empty()) {
+
+        vertex_buffer = buffer::make();
+
+        if (!vertex_buffer->create(device, data.vertices.data(), sizeof(vertex) * data.vertices.size(), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, mapped, memory_usage)) {
+
+            log()->error("mesh::create vertexBuffer create failed");
+            return false;
+        }
+    }
+
+    if (!data.indices.empty()) {
+
+        index_buffer = buffer::make();
+
+        if (!index_buffer->create(device, data.indices.data(), sizeof(ui32) * data.indices.size(), VK_BUFFER_USAGE_INDEX_BUFFER_BIT, mapped, memory_usage)) {
+
+            log()->error("mesh::create indexBuffer create failed");
+            return false;
+        }
+    }
+
+	return true;
+}
+
+void mesh::destroy() {
+
+    vertex_buffer = nullptr;
+    index_buffer = nullptr;
+
+    dev = nullptr;
+}
+
+bool mesh::reload() {
+
+    auto device = dev;
+    destroy();
+
+    return create(device, mapped, memory_usage);
+}
+
+void mesh::bind(VkCommandBuffer cmd_buffer) const {
+
+    if (vertex_buffer && vertex_buffer->is_valid()) {
+
+        std::array<VkDeviceSize, 1> const buffer_offsets = { 0 };
+        std::array<VkBuffer, 1> const buffers = { vertex_buffer->get() };
+
+        vkCmdBindVertexBuffers(cmd_buffer, 0, to_ui32(buffers.size()), buffers.data(), buffer_offsets.data());
+    }
+
+    if (index_buffer && index_buffer->is_valid())
+        vkCmdBindIndexBuffer(cmd_buffer, index_buffer->get(), 0, VK_INDEX_TYPE_UINT32);
+}
+
+void mesh::draw(VkCommandBuffer cmd_buffer) const {
+
+    if (!data.indices.empty())
+        vkCmdDrawIndexed(cmd_buffer, to_ui32(data.indices.size()), 1, 0, 0, 0);
+    else
+        vkCmdDraw(cmd_buffer, to_ui32(data.vertices.size()), 1, 0, 0);
+}
+
+} // lava
+
+lava::mesh::ptr lava::load_mesh(device* device, name filename) {
+
+#if LIBLAVA_TINYOBJLOADER
+    if (has_extension(filename, "OBJ")) {
+
+        tinyobj::attrib_t attrib;
+        std::vector<tinyobj::shape_t> shapes;
+        std::vector<tinyobj::material_t> materials;
+        std::string err;
+        std::string warn;
+
+        string target_file = filename;
+
+        file_guard temp_file_remover;
+        {
+            file file(filename);
+            if (file.is_open() && file.get_type()._value == file_type::fs) {
+
+                string temp_file;
+                temp_file = file_system::get_pref_dir();
+                temp_file += get_filename_from(target_file, true);
+
+                scope_data temp_data(file.get_size());
+                if (!temp_data.ptr)
+                    return nullptr;
+
+                if (is_file_error(file.read(temp_data.ptr)))
+                    return nullptr;
+
+                if (!write_file(temp_file.c_str(), temp_data.ptr, temp_data.size))
+                    return nullptr;
+
+                target_file = temp_file;
+
+                temp_file_remover.filename = target_file;
+            }
+        }
+
+        if (tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, target_file.c_str())) {
+
+            auto mesh = mesh::make();
+
+            for (auto const& shape : shapes) {
+
+                for (auto const& index : shape.mesh.indices) {
+
+                    vertex vertex;
+
+                    vertex.position = v3(attrib.vertices[3 * index.vertex_index],
+                                        attrib.vertices[3 * index.vertex_index + 1],
+                                        attrib.vertices[3 * index.vertex_index + 2]);
+
+                    vertex.color = v3(1.f, 1.f, 1.f);
+
+                    if (!attrib.texcoords.empty())
+                        vertex.uv = v2(attrib.texcoords[2 * index.texcoord_index], 1.f - attrib.texcoords[2 * index.texcoord_index + 1]);
+
+                    vertex.normal = attrib.normals.empty() ? v3(0.f) : v3(attrib.normals[3 * index.normal_index], 
+                                                                            attrib.normals[3 * index.normal_index + 1], 
+                                                                            attrib.normals[3 * index.normal_index + 2]);
+
+                    mesh->get_vertices().push_back(vertex);
+                    mesh->get_indices().push_back(mesh->get_indices_count());
+                }
+            }
+
+            if (mesh->empty())
+                return nullptr;
+
+            if (!mesh->create(device))
+                return nullptr;
+
+            return mesh;
+        }
+    }
+#endif
+
+#if LIBLAVA_ASSIMP
+    Assimp::Importer importer;
+
+    file file(filename);
+    scope_data temp_data(file.get_size(), false);
+
+    if (file.is_open()) {
+
+        if (!temp_data.allocate())
+            return nullptr;
+
+        if (is_file_error(file.read(temp_data.ptr)))
+            return nullptr;
+    }
+
+    static ui32 const assimp_flags = aiProcess_FlipWindingOrder | aiProcess_Triangulate | aiProcess_PreTransformVertices | aiProcess_GenNormals;
+
+    aiScene const* scene = nullptr;
+    if (file.is_open())
+        scene = importer.ReadFileFromMemory(temp_data.ptr, temp_data.size, assimp_flags);
+    else
+        scene = importer.ReadFile(filename, assimp_flags);
+
+    if (!scene)
+        return nullptr;
+
+    auto mesh = mesh::make();
+
+    for (auto m = 0u; m < scene->mNumMeshes; ++m) {
+
+        for (auto v = 0u; v < scene->mMeshes[m]->mNumVertices; ++v) {
+
+            vertex vertex;
+
+            vertex.position = glm::make_vec3(&scene->mMeshes[m]->mVertices[v].x);
+            vertex.color = (scene->mMeshes[m]->HasVertexColors(0)) ? glm::make_vec3(&scene->mMeshes[m]->mColors[0][v].r) : v3(1.f);
+            vertex.uv = (scene->mMeshes[m]->HasTextureCoords(0)) ? glm::make_vec2(&scene->mMeshes[m]->mTextureCoords[0][v].x) : v2(0.f);
+            vertex.normal = scene->mMeshes[m]->mNormals ? glm::make_vec3(&scene->mMeshes[m]->mNormals[v].x) : v3(0.f);
+
+            vertex.position.y *= -1.0f;
+
+            mesh->get_vertices().push_back(vertex);
+        }
+
+        auto index_base = mesh->get_indices_count();
+        for (auto f = 0u; f < scene->mMeshes[m]->mNumFaces; ++f) {
+
+            for (auto i = 0u; i < 3; ++i) {
+
+                mesh->get_indices().push_back(scene->mMeshes[m]->mFaces[f].mIndices[i] + index_base);
+            }
+        }
+    }
+
+    if (mesh->empty())
+        return nullptr;
+
+    if (!mesh->create(device))
+        return nullptr;
+
+    return mesh;
+#else
+    return nullptr;
+#endif
+}
+
+lava::mesh::ptr lava::load_mesh(device* device, mesh_type type) {
+
+    switch (type._value) {
+
+    case mesh_type::cube: {
+
+        auto cube = mesh::make();
+        cube->get_vertices() = {
+
+            // front
+            { { 1.f, 1.f, 1.f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f }, { 0.f, 0.f, 1.f } },
+            { { -1.f, 1.f, 1.f }, { 1.f, 1.f, 1.f }, { 0.f, 1.f }, { 0.f, 0.f, 1.f } },
+            { { -1.f, -1.f, 1.f }, { 1.f, 1.f, 1.f }, { 0.f, 0.f }, { 0.f, 0.f, 1.f } },
+            { { 1.f, -1.f, 1.f }, { 1.f, 1.f, 1.f }, { 1.f, 0.f }, { 0.f, 0.f, 1.f } },
+
+            // back
+            { { 1.f, 1.f, -1.f }, { 1.f, 1.f, 1.f }, { 0.f, 1.f }, { 0.f, 0.f, -1.f } },
+            { { -1.f, 1.f, -1.f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f }, { 0.f, 0.f, -1.f } },
+            { { -1.f, -1.f, -1.f }, { 1.f, 1.f, 1.f }, { 1.f, 0.f }, { 0.f, 0.f, -1.f } },
+            { { 1.f, -1.f, -1.f }, { 1.f, 1.f, 1.f }, { 0.f, 0.f }, { 0.f, 0.f, -1.f } },
+
+            // left
+            { { -1.f, 1.f, 1.f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f }, { -1.f, 0.f, 0.f } },
+            { { -1.f, 1.f, -1.f }, { 1.f, 1.f, 1.f }, { 0.f, 1.f }, { -1.f, 0.f, 0.f } },
+            { { -1.f, -1.f, -1.f }, { 1.f, 1.f, 1.f }, { 0.f, 0.f }, { -1.f, 0.f, 0.f } },
+            { { -1.f, -1.f, 1.f }, { 1.f, 1.f, 1.f }, { 1.f, 0.f }, { -1.f, 0.f, 0.f } },
+
+            // right
+            { { 1.f, 1.f, 1.f }, { 1.f, 1.f, 1.f }, { 0.f, 1.f }, { 1.f, 0.f, 0.f } },
+            { { 1.f, -1.f, 1.f }, { 1.f, 1.f, 1.f }, { 0.f, 0.f }, { 1.f, 0.f, 0.f } },
+            { { 1.f, -1.f, -1.f }, { 1.f, 1.f, 1.f }, { 1.f, 0.f }, { 1.f, 0.f, 0.f } },
+            { { 1.f, 1.f, -1.f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f }, { 1.f, 0.f, 0.f } },
+
+            // bottom
+            { { 1.f, 1.f, 1.f }, { 1.f, 1.f, 1.f }, { 1.f, 0.f }, { 0.f, 1.f, 0.f } },
+            { { -1.f, 1.f, 1.f }, { 1.f, 1.f, 1.f }, { 0.f, 0.f }, { 0.f, 1.f, 0.f } },
+            { { -1.f, 1.f, -1.f }, { 1.f, 1.f, 1.f }, { 0.f, 1.f }, { 0.f, 1.f, 0.f } },
+            { { 1.f, 1.f, -1.f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f }, { 0.f, 1.f, 0.f } },
+
+            // top
+            { { 1.f, -1.f, 1.f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f }, { 0.f, -1.f, 0.f } },
+            { { -1.f, -1.f, 1.f }, { 1.f, 1.f, 1.f }, { 0.f, 1.f }, { 0.f, -1.f, 0.f } },
+            { { -1.f, -1.f, -1.f }, { 1.f, 1.f, 1.f }, { 0.f, 0.f }, { 0.f, -1.f, 0.f } },
+            { { 1.f, -1.f, -1.f }, { 1.f, 1.f, 1.f }, { 1.f, 0.f }, { 0.f, -1.f, 0.f } },
+        };
+
+        cube->get_indices() = { 0, 1, 2,
+                                2, 3, 0,
+                                4, 7, 6,
+                                6, 5, 4,
+                                8, 9, 10,
+                                10, 11, 8,
+                                12, 13, 14,
+                                14, 15, 12,
+                                16, 19, 18,
+                                18, 17, 16,
+                                20, 21, 22,
+                                22, 23, 20,
+        };
+
+        if (!cube->create(device))
+            return nullptr;
+
+        return cube;
+    }
+
+    case mesh_type::triangle: {
+
+        auto triangle = mesh::make();
+
+        triangle->get_vertices().push_back({ { 1.f, 1.f, 0.f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f }, { 0.f, 0.f, 1.f } });
+        triangle->get_vertices().push_back({ { -1.f, 1.f, 0.f }, { 1.f, 1.f, 1.f }, { 0.f, 1.f }, { 0.f, 0.f, 1.f } });
+        triangle->get_vertices().push_back({ { 0.f, -1.f, 0.f }, { 1.f, 1.f, 1.f }, { 0.5f, 0.f }, { 0.f, 0.f, 1.f } });
+
+        if (!triangle->create(device))
+            return nullptr;
+
+        return triangle;
+    }
+
+    case mesh_type::quad: {
+
+        auto quad = mesh::make();
+
+        quad->get_vertices() = 
+        {
+            { { 1.f, 1.f, 0.f }, { 1.f, 1.f, 1.f }, { 1.f, 1.f }, { 0.f, 0.f, 1.f } },
+            { { -1.f, 1.f, 0.f }, { 1.f, 1.f, 1.f }, { 0.f, 1.f }, { 0.f, 0.f, 1.f } },
+            { { -1.f, -1.f, 0.f }, { 1.f, 1.f, 1.f }, { 0.f, 0.f }, { 0.f, 0.f, 1.f } },
+            { { 1.f, -1.f, 0.f }, { 1.f, 1.f, 1.f }, { 1.f, 0.f }, { 0.f, 0.f, 1.f } },
+        };
+
+        quad->get_indices() = { 0, 1, 2, 2, 3, 0 };
+
+        if (!quad->create(device))
+            return nullptr;
+
+        return quad;
+    }
+
+    case mesh_type::none:
+    default:
+        return nullptr;
+    }
+}
diff --git a/liblava/resource/mesh.hpp b/liblava/resource/mesh.hpp
new file mode 100644
index 00000000..876ea452
--- /dev/null
+++ b/liblava/resource/mesh.hpp
@@ -0,0 +1,112 @@
+// file      : liblava/resource/mesh.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/resource/buffer.hpp>
+#include <liblava/core/math.hpp>
+#include <liblava/core/data.hpp>
+
+namespace lava {
+
+struct vertex {
+
+    using list = std::vector<vertex>;
+
+    v3 position;
+    v3 color;
+    v2 uv;
+    v3 normal;
+
+    bool operator==(vertex const& other) const {
+
+        return position == other.position && color == other.color && uv == other.uv && normal == other.normal;
+    }
+};
+
+struct mesh_data {
+
+    vertex::list vertices;
+    index_list indices;
+
+    void move(v3 position) {
+
+        for (auto& vertex : vertices)
+            vertex.position += position;
+    }
+
+    void scale(r32 factor) {
+
+        for (auto& vertex : vertices)
+            vertex.position *= factor;
+    }
+};
+
+struct mesh {
+
+    using ptr = std::shared_ptr<mesh>;
+    using map = std::map<id, ptr>;
+    using list = std::vector<ptr>;
+
+    static ptr make() { return std::make_shared<mesh>(); }
+
+    explicit mesh();
+    ~mesh();
+
+    bool create(device* device, bool mapped = false, VmaMemoryUsage memory_usage = VMA_MEMORY_USAGE_CPU_TO_GPU);
+    void destroy();
+
+    void bind(VkCommandBuffer cmd_buffer) const;
+    void draw(VkCommandBuffer cmd_buffer) const;
+
+    id::ref get_id() const { return _id; }
+    device* get_device() { return dev; }
+
+    bool empty() const { return data.vertices.empty(); }
+
+    void set_data(mesh_data const& value) { data = value; }
+    mesh_data& get_data() { return data; }
+    void add_data(mesh_data const& value);
+
+    vertex::list& get_vertices() { return data.vertices; }
+    vertex::list const& get_vertices() const { return data.vertices; }
+    ui32 get_vertices_count() const { return to_ui32(data.vertices.size()); }
+
+    index_list& get_indices() { return data.indices; }
+    index_list const& get_indices() const { return data.indices; }
+    ui32 get_indices_count() const { return to_ui32(data.indices.size()); }
+
+    bool reload();
+
+    buffer::ptr get_vertex_buffer() { return vertex_buffer; }
+    buffer::ptr get_index_buffer() { return index_buffer; }
+
+private:
+    id _id;
+    device* dev = nullptr;
+
+    mesh_data data;
+
+    buffer::ptr vertex_buffer;
+    buffer::ptr index_buffer;
+
+    bool mapped = false;
+    VmaMemoryUsage memory_usage = VMA_MEMORY_USAGE_CPU_TO_GPU;
+};
+
+mesh::ptr load_mesh(device* device, name filename);
+
+BETTER_ENUM(mesh_type, type, none = 0, cube, triangle, quad)
+
+mesh::ptr load_mesh(device* device, mesh_type type);
+
+struct mesh_meta {
+
+    string filename; // empty -> type
+    mesh_type type = mesh_type::none;
+};
+
+using mesh_registry = registry<mesh, mesh_meta>;
+
+} // lava
diff --git a/liblava/resource/texture.cpp b/liblava/resource/texture.cpp
new file mode 100644
index 00000000..e8db225f
--- /dev/null
+++ b/liblava/resource/texture.cpp
@@ -0,0 +1,491 @@
+// file      : liblava/resource/texture.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/resource/texture.hpp>
+#include <liblava/resource/format.hpp>
+
+#include <bitmap_image.hpp>
+#include <selene/img/pixel/PixelTypeAliases.hpp>
+#include <selene/img/typed/ImageView.hpp>
+#include <selene/img_ops/ImageConversions.hpp>
+
+#ifdef _WIN32
+#pragma warning(push, 4)
+#pragma warning(disable : 4458)
+#pragma warning(disable : 4100)
+#pragma warning(disable : 5054)
+#else
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wignored-qualifiers"
+#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wtype-limits"
+#pragma GCC diagnostic ignored "-Wempty-body"
+#pragma GCC diagnostic ignored "-Wunused-result"
+#endif
+
+#include <gli/gli.hpp>
+
+#ifdef _WIN32
+#pragma warning(pop)
+#else
+#pragma GCC diagnostic pop
+#endif
+
+#define STB_IMAGE_IMPLEMENTATION
+#include <stb_image.h>
+
+namespace lava {
+
+texture::texture() : _id(ids::next()) {}
+
+texture::~texture() {
+
+    ids::free(_id);
+
+    destroy();
+}
+
+bool texture::create(device* device, uv2 size, VkFormat format, layer::list const& layers_, texture_type type_) {
+
+    layers = layers_;
+    type = type_;
+
+    if (layers.empty()) {
+
+        layer layer;
+
+        mip_level level;
+        level.extent = size;
+
+        layer.levels.push_back(level);
+        layers.push_back(layer);
+    }
+
+    VkSamplerAddressMode sampler_address_mode = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+    if (type._value == texture_type::array || type._value == texture_type::cube_map)
+        sampler_address_mode = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+
+    VkSamplerCreateInfo sampler_info
+    {
+        .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
+        .pNext = nullptr,
+        .flags = 0,
+        .magFilter = VK_FILTER_LINEAR,
+        .minFilter = VK_FILTER_LINEAR,
+        .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR,
+        .addressModeU = sampler_address_mode,
+        .addressModeV = sampler_address_mode,
+        .addressModeW = sampler_address_mode,
+        .mipLodBias = 0.f,
+        .anisotropyEnable = device->get_features().samplerAnisotropy,
+        .maxAnisotropy = device->get_properties().limits.maxSamplerAnisotropy,
+        .compareEnable = VK_FALSE,
+        .compareOp = VK_COMPARE_OP_NEVER,
+        .minLod = 0.f,
+        .maxLod = to_r32(layers.front().levels.size()),
+        .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
+        .unnormalizedCoordinates = VK_FALSE,
+    };
+
+
+    if (failed(device->call().vkCreateSampler(device->get(), &sampler_info, memory::alloc(), &sampler))) {
+
+        log()->error("texture create vkCreateSampler failed");
+        return false;
+    }
+
+    _image = image::make(format);
+
+    if (type._value == texture_type::cube_map)
+        _image->set_flags(VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT);
+
+    auto view_type = VK_IMAGE_VIEW_TYPE_2D;
+    if (type._value == texture_type::array) {
+
+        view_type = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
+
+    } else if (type._value == texture_type::cube_map) {
+
+        view_type = VK_IMAGE_VIEW_TYPE_CUBE;
+    }
+
+    _image->set_tiling(VK_IMAGE_TILING_LINEAR);
+    _image->set_level_count(to_ui32(layers.front().levels.size()));
+    _image->set_layer_count(to_ui32(layers.size()));
+    _image->set_view_type(view_type);
+
+    if (!_image->create(device, size, VMA_MEMORY_USAGE_GPU_ONLY)) {
+
+        log()->error("texture create failed");
+        return false;
+    }
+
+    descriptor.sampler = sampler;
+    descriptor.imageView = _image->get_view();
+    descriptor.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+
+    return true;
+}
+
+void texture::destroy() {
+
+    destroy_upload_buffer();
+
+    device* device = nullptr;
+    if (_image)
+        device = _image->get_device();
+
+    if (sampler) {
+
+        if (device)
+            device->call().vkDestroySampler(device->get(), sampler, memory::alloc());
+
+        sampler = nullptr;
+    }
+
+    if (_image) {
+
+        _image->destroy();
+        _image = nullptr;
+    }
+}
+
+void texture::destroy_upload_buffer() {
+
+    upload_buffer = nullptr;
+}
+
+bool texture::upload(void const* data, size_t data_size) {
+
+    auto device = _image->get_device();
+    upload_buffer = buffer::make();
+    return upload_buffer->create(device, data, data_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, false, VMA_MEMORY_USAGE_CPU_TO_GPU);
+}
+
+bool texture::stage(VkCommandBuffer cmd_buffer) {
+
+    if (!upload_buffer || !upload_buffer->is_valid()) {
+
+        log()->error("texture stage failed");
+        return false;
+    }
+
+    VkImageSubresourceRange subresource_range
+    {
+        .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+        .baseMipLevel = 0,
+        .levelCount = to_ui32(layers.front().levels.size()),
+        .baseArrayLayer = 0,
+        .layerCount = to_ui32(layers.size()),
+    };
+
+    auto device = _image->get_device();
+
+    set_image_layout(cmd_buffer, _image->get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, subresource_range,
+                                                VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+
+    std::vector<VkBufferImageCopy> regions;
+
+    if (to_ui32(layers.front().levels.size()) > 1) {
+
+        auto offset = 0u;
+
+        for (auto layer = 0u; layer < layers.size(); ++layer) {
+
+            for (auto level = 0u; level < to_ui32(layers.front().levels.size()); ++level) {
+
+                VkImageSubresourceLayers image_subresource
+                {
+                    .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                    .mipLevel = level,
+                    .baseArrayLayer = layer,
+                    .layerCount = 1,
+                };
+
+                VkExtent3D image_extent
+                {
+                    .width = layers[layer].levels[level].extent.x,
+                    .height = layers[layer].levels[level].extent.y,
+                    .depth = 1,
+                };
+
+                VkBufferImageCopy buffer_copy_region
+                {
+                    .bufferOffset = offset,
+                    .imageSubresource = image_subresource,
+                    .imageExtent = image_extent,
+                };
+
+                regions.push_back(buffer_copy_region);
+
+                offset += layers[layer].levels[level].size;
+            }
+        }
+
+    } else {
+
+        VkImageSubresourceLayers subresource_layers
+        {
+            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+            .mipLevel = 0,
+            .baseArrayLayer = 0,
+            .layerCount = to_ui32(layers.size()),
+        };
+
+        auto size = _image->get_size();
+
+        VkBufferImageCopy region
+        {
+            .bufferOffset = 0,
+            .bufferRowLength = size.x,
+            .bufferImageHeight = size.y,
+            .imageSubresource = subresource_layers,
+            .imageOffset = {},
+            .imageExtent = { size.x, size.y, 1 },
+        };
+
+        regions.push_back(region);
+    }
+
+    device->call().vkCmdCopyBufferToImage(cmd_buffer, upload_buffer->get(), _image->get(),
+                                            VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, to_ui32(regions.size()), regions.data());
+
+    set_image_layout(cmd_buffer, _image->get(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                                            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, subresource_range,
+                                            VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+
+    return true;
+}
+
+bool staging::stage(VkCommandBuffer cmdBuffer, index frame) {
+
+    if (!staged.empty() && staged.count(frame) && !staged.at(frame).empty()) {
+
+        for (auto& texture : staged.at(frame))
+            texture->destroy_upload_buffer();
+
+        staged.erase(frame);
+    }
+
+    if (todo.empty())
+        return false;
+
+    texture::list stage_done;
+
+    for (auto& texture : todo) {
+
+        if (!texture->stage(cmdBuffer))
+            continue;
+
+        stage_done.push_back(texture);
+    }
+
+    if (!staged.count(frame))
+        staged.emplace(frame, texture::list());
+
+    for (auto& texture : stage_done) {
+
+        if (!contains(staged.at(frame), texture))
+            staged.at(frame).push_back(texture);
+
+        remove(todo, texture);
+    }
+
+    return true;
+}
+
+} // lava
+
+lava::texture::ptr lava::load_texture(device* device, file_format filename, texture_type type) {
+
+    auto supported =    (filename.format == VK_FORMAT_R8G8B8A8_UNORM) ||
+                        (device->get_features().textureCompressionBC && (filename.format == VK_FORMAT_BC3_UNORM_BLOCK)) ||
+                        (device->get_features().textureCompressionASTC_LDR && (filename.format == VK_FORMAT_ASTC_8x8_UNORM_BLOCK)) ||
+                        (device->get_features().textureCompressionETC2 && (filename.format == VK_FORMAT_ETC2_R8G8B8A8_UNORM_BLOCK));
+    if (!supported)
+        return nullptr;
+
+    auto use_gli = has_extension(filename.path.c_str(), { "DDS", "KTX", "KMG" });
+    auto use_stbi = false;
+
+    if (!use_gli)
+        use_stbi = has_extension(filename.path.c_str(), { "JPG", "PNG", "TGA", "BMP", "PSD", "GIF", "HDR", "PIC" });
+
+    if (!use_gli && !use_stbi)
+        return nullptr;
+
+    file file(filename.path.c_str());
+    scope_data temp_data(file.get_size(), false);
+
+    if (file.is_open()) {
+
+        if (!temp_data.allocate())
+            return nullptr;
+
+        if (is_file_error(file.read(temp_data.ptr)))
+            return nullptr;
+    }
+
+    auto texture = texture::make();
+
+    if (use_gli) {
+
+        texture::layer::list layers;
+
+        switch (type) {
+
+            case texture_type::tex_2d: {
+
+                gli::texture2d tex(file.is_open() ? gli::load(temp_data.ptr, temp_data.size) : gli::load(filename.path));
+                assert(!tex.empty());
+                if (tex.empty())
+                    return nullptr;
+
+                auto mip_levels = to_ui32(tex.levels());
+
+                texture::layer layer;
+
+                for (auto m = 0u; m < mip_levels; ++m) {
+
+                    texture::mip_level level;
+                    level.extent = { tex[m].extent().x, tex[m].extent().y };
+                    level.size = to_ui32(tex[m].size());
+                    layer.levels.push_back(level);
+                }
+
+                layers.push_back(layer);
+
+                uv2 size = { tex[0].extent().x, tex[0].extent().y };
+                if (!texture->create(device, size, filename.format, layers, type))
+                    return nullptr;
+
+                if (!texture->upload(tex.data(), tex.size()))
+                    return nullptr;
+
+                break;
+            }
+
+            case texture_type::array: {
+
+                gli::texture2d_array tex(file.is_open() ? gli::load(temp_data.ptr, temp_data.size) : gli::load(filename.path));
+                assert(!tex.empty());
+                if (tex.empty())
+                    return nullptr;
+
+                auto layer_count = to_ui32(tex.layers());
+                auto mip_levels = to_ui32(tex.levels());
+
+                for (auto i = 0u; i < layer_count; ++i) {
+
+                    texture::layer layer;
+
+                    for (auto m = 0u; m < mip_levels; ++m) {
+
+                        texture::mip_level level;
+                        level.extent = { tex[i][m].extent().x, tex[i][m].extent().y };
+                        level.size = to_ui32(tex[i][m].size());
+                        layer.levels.push_back(level);
+                    }
+
+                    layers.push_back(layer);
+                }
+
+                uv2 size = { tex[0].extent().x, tex[0].extent().y };
+                if (!texture->create(device, size, filename.format, layers, type))
+                    return nullptr;
+
+                if (!texture->upload(tex.data(), tex.size()))
+                    return nullptr;
+
+                break;
+            }
+
+            case texture_type::cube_map: {
+
+                gli::texture_cube tex(file.is_open() ? gli::load(temp_data.ptr, temp_data.size) : gli::load(filename.path));
+                assert(!tex.empty());
+                if (tex.empty())
+                    return nullptr;
+
+                auto layer_count = to_ui32(tex.faces());
+                auto mip_levels = to_ui32(tex.levels());
+
+                for (auto i = 0u; i < layer_count; ++i) {
+
+                    texture::layer layer;
+
+                    for (auto m = 0u; m < mip_levels; ++m) {
+
+                        texture::mip_level level;
+                        level.extent = { tex[i][m].extent().x, tex[i][m].extent().y };
+                        level.size = to_ui32(tex[i][m].size());
+                        layer.levels.push_back(level);
+                    }
+
+                    layers.push_back(layer);
+                }
+
+                uv2 size = { tex[0].extent().x, tex[0].extent().y };
+                if (!texture->create(device, size, filename.format, layers, type))
+                    return nullptr;
+
+                if (!texture->upload(tex.data(), tex.size()))
+                    return nullptr;
+
+                break;
+            }
+        }
+    }
+    else { // use_stbi
+
+        i32 tex_width, tex_height, tex_channels = 0;
+        stbi_uc* data = nullptr;
+
+        if (file.is_open())
+            data = stbi_load_from_memory((stbi_uc const*)temp_data.ptr, to_i32(temp_data.size), &tex_width, &tex_height, &tex_channels, STBI_rgb_alpha);
+        else
+            data = stbi_load(filename.path.c_str(), &tex_width, &tex_height, &tex_channels, STBI_rgb_alpha);
+
+        if (!data)
+            return nullptr;
+
+        uv2 size = { tex_width, tex_height };
+        if (!texture->create(device, size, VK_FORMAT_R8G8B8A8_UNORM))
+            return nullptr;
+
+        auto uploadSize = tex_width * tex_height * tex_channels * sizeof(char);
+        auto result = texture->upload(data, uploadSize);
+
+        stbi_image_free(data);
+
+        if (!result)
+            return nullptr;
+    }
+
+    return texture;
+}
+
+lava::texture::ptr lava::create_default_texture(device* device, uv2 size) {
+
+    auto result = texture::make();
+
+    if (!result->create(device, size, VK_FORMAT_R8G8B8A8_UNORM))
+        return nullptr;
+
+    bitmap_image image(size.x, size.y);
+    checkered_pattern(64, 64, 255, 255, 255, image);
+
+    image.bgr_to_rgb();
+
+    sln::TypedLayout typed_layout((sln::PixelLength)size.x, (sln::PixelLength)size.y, (sln::Stride)(image.width() * image.bytes_per_pixel()));
+
+    sln::ImageView<sln::PixelRGB_8u, sln::ImageModifiability::Mutable> img_rgb((uint8_t*)image.data(), typed_layout);
+
+    sln::Image<sln::PixelRGBA_8u> const img_rgba = sln::convert_image<sln::PixelFormat::RGBA>(img_rgb, std::uint8_t{ 192 });
+
+    if (!result->upload(img_rgba.data(), img_rgba.total_bytes()))
+        return nullptr;
+
+    return result;
+}
diff --git a/liblava/resource/texture.hpp b/liblava/resource/texture.hpp
new file mode 100644
index 00000000..45d5fba3
--- /dev/null
+++ b/liblava/resource/texture.hpp
@@ -0,0 +1,103 @@
+// file      : liblava/resource/texture.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/resource/buffer.hpp>
+#include <liblava/resource/image.hpp>
+
+namespace lava {
+
+BETTER_ENUM(texture_type, type, none = 0, tex_2d, array, cube_map)
+
+struct file_format {
+
+    using list = std::vector<file_format>;
+
+    string path;
+    VkFormat format = VK_FORMAT_R8G8B8A8_UNORM;
+};
+
+struct texture {
+
+    using ptr = std::shared_ptr<texture>;
+    using map = std::map<id, ptr>;
+    using list = std::vector<ptr>;
+
+    struct mip_level {
+
+        using list = std::vector<mip_level>;
+
+        uv2 extent;
+        ui32 size = 0;
+    };
+
+    struct layer {
+
+        using list = std::vector<layer>;
+
+        mip_level::list levels;
+    };
+
+    explicit texture();
+    ~texture();
+
+    static ptr make() { return std::make_shared<texture>(); }
+
+    bool create(device* device, uv2 size, VkFormat format, layer::list const& layers = {}, texture_type type = texture_type::tex_2d);
+    void destroy();
+
+    bool upload(void const* data, size_t data_size);
+    bool stage(VkCommandBuffer cmd_buffer);
+    void destroy_upload_buffer();
+
+    VkDescriptorImageInfo get_descriptor() const { return descriptor; }
+    image::ptr get_image() { return _image; }
+
+    uv2 get_size() const { return _image ? _image->get_size() : uv2(); }
+    texture_type get_type() const { return type; }
+
+    id::ref get_id() const { return _id; }
+
+    VkFormat get_format() const { return _image ? _image->get_format() : VK_FORMAT_UNDEFINED; }
+
+private:
+    id _id;
+
+    image::ptr _image;
+
+    texture_type type = texture_type::none;
+    layer::list layers;
+
+    VkSampler sampler = nullptr;
+    VkDescriptorImageInfo descriptor = {};
+
+    buffer::ptr upload_buffer;
+};
+
+texture::ptr load_texture(device* device, file_format filename, texture_type type = texture_type::tex_2d);
+
+inline texture::ptr load_texture(device* device, string filename, VkFormat format = VK_FORMAT_R8G8B8A8_UNORM, texture_type type = texture_type::tex_2d) {
+
+    return load_texture(device, { filename, format }, type);
+}
+
+texture::ptr create_default_texture(device* device, uv2 size = { 512, 512});
+
+struct staging {
+
+    void add(texture::ptr texture) { todo.push_back(texture); }
+
+    bool stage(VkCommandBuffer cmdBuffer, index frame);
+
+private:
+    texture::list todo;
+
+    using frame_stage_map = std::map<index, texture::list>;
+    frame_stage_map staged;
+};
+
+using texture_registry = registry<texture, file_format>;
+
+} // lava
diff --git a/liblava/utils.hpp b/liblava/utils.hpp
new file mode 100644
index 00000000..8c7cf6a8
--- /dev/null
+++ b/liblava/utils.hpp
@@ -0,0 +1,12 @@
+// file      : liblava/utils.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/utils/file.hpp>
+#include <liblava/utils/log.hpp>
+#include <liblava/utils/random.hpp>
+#include <liblava/utils/telegram.hpp>
+#include <liblava/utils/thread.hpp>
+#include <liblava/utils/utility.hpp>
diff --git a/liblava/utils/file.cpp b/liblava/utils/file.cpp
new file mode 100644
index 00000000..cca1b198
--- /dev/null
+++ b/liblava/utils/file.cpp
@@ -0,0 +1,437 @@
+// file      : liblava/utils/file.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/utils/file.hpp>
+#include <liblava/utils/utility.hpp>
+
+#include <physfs.h>
+#include <tinyfiledialogs.h>
+
+bool lava::read_file(std::vector<char>& out, name filename) {
+
+    std::ifstream file(filename, std::ios::ate | std::ios::binary);
+    assert(file.is_open());
+
+    if (!file.is_open())
+        return false;
+
+    out.clear();
+
+    auto file_size = to_size_t(file.tellg());
+    if (file_size > 0) {
+
+        out.resize(file_size);
+        file.seekg(0, std::ios::beg);
+        file.read(out.data(), file_size);
+    }
+
+    file.close();
+    return true;
+}
+
+bool lava::write_file(name filename, char const* data, size_t data_size) {
+
+    std::ofstream file(filename, std::ofstream::binary);
+    assert(file.is_open());
+
+    if (!file.is_open())
+        return false;
+
+    file.write(data, data_size);
+    file.close();
+    return true;
+}
+
+bool lava::has_extension(name file_name, name extension) {
+
+    string fn = file_name;
+    string ext = extension;
+
+    string to_check = fn.substr(fn.find_last_of('.') + 1);
+
+    std::transform(to_check.begin(), to_check.end(), to_check.begin(), ::tolower);
+    std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
+
+    return to_check == ext;
+}
+
+bool lava::has_extension(name filename, names extensions) {
+
+    for (auto& extension : extensions)
+        if (has_extension(filename, extension))
+            return true;
+
+    return false;
+}
+
+lava::string lava::get_filename_from(string_ref path, bool with_extension) {
+
+    fs::path target(path);
+    return with_extension ? target.filename().string() : target.stem().string();
+}
+
+bool lava::remove_existing_path(string& target, string_ref path) {
+
+    auto pos = target.find(path);
+    if (pos != std::string::npos) {
+
+        target.erase(pos, path.length());
+
+#ifdef _WIN32
+        std::replace(target.begin(), target.end(), '\\', '/');
+#endif
+
+        return true;
+    }
+
+    return false;
+}
+
+namespace lava {
+
+internal_version file_system::get_version() {
+
+    PHYSFS_Version result;
+    PHYSFS_getLinkedVersion(&result);
+
+    return { result.major, result.minor, result.patch };
+}
+
+name file_system::get_base_dir() { return PHYSFS_getBaseDir(); }
+
+string file_system::get_base_dir_str() { return string(get_base_dir()); }
+
+name file_system::get_pref_dir() { return PHYSFS_getPrefDir(get().org, get().app); }
+
+string file_system::get_res_dir() {
+
+    string res_dir = get_base_dir();
+    res_dir += get().res_path;
+    string_ref const_res_dir = res_dir;
+
+    return fs::path(const_res_dir).lexically_normal().string();
+}
+
+bool file_system::mount(string_ref path) { return PHYSFS_mount(path.c_str(), nullptr, 1) != 0; }
+
+bool file_system::mount(name base_dir_path) { return mount(get_base_dir_str() + base_dir_path); }
+
+bool file_system::exists(name file) { return PHYSFS_exists(file) != 0; }
+
+name file_system::get_real_dir(name file) { return PHYSFS_getRealDir(file); }
+
+bool file_system::initialize(name argv_0, name org_, name app_, name ext_) {
+
+    assert(!initialized); // only once
+    if (initialized)
+        return initialized;
+
+    if (!initialized) {
+
+        PHYSFS_init(argv_0);
+
+        PHYSFS_setSaneConfig(org_, app_, ext_, 0, 0);
+        initialized = true;
+    }
+
+    return initialized;
+}
+
+void file_system::terminate() { 
+
+    if (!initialized)
+        return;
+
+    PHYSFS_deinit(); 
+}
+
+void file_system::mount_res() {
+
+#if LIBLAVA_DEBUG
+#if _WIN32
+    res_path = "../../res/";
+#else
+    res_path = "../res/";
+#endif
+#else
+    res_path = "res/";
+#endif
+
+    if (fs::exists(get_res_dir().c_str()))
+        if (file_system::mount(res_path.c_str()))
+            log()->debug("mounted {}", get_res_dir().c_str());
+
+    string archive_file = "res.zip";
+    if (fs::exists({ archive_file }))
+        if (file_system::mount(archive_file.c_str()))
+            log()->debug("mounted {}", archive_file.c_str());
+}
+
+bool file_system::create_data_folder() {
+
+    fs::path data_path = fs::current_path();
+    data_path += fs::path::preferred_separator;
+    data_path += "data";
+
+    if (!fs::exists(data_path))
+        fs::create_directories(data_path);
+
+    return fs::exists(data_path);
+}
+
+file::file(name path_, bool write) { open(path_, write); }
+
+file::~file() { close(); }
+
+bool file::open(name path_, bool write) {
+
+    if (!path_)
+        return false;
+
+    path = path_;
+    write_mode = write;
+
+    if (write_mode)
+        fs_file = PHYSFS_openWrite(path);
+    else
+        fs_file = PHYSFS_openRead(path);
+
+    if (fs_file) {
+
+        type = file_type::fs;
+
+    } else {
+
+        if (write) {
+
+            o_stream = std::ofstream(path, std::ofstream::binary);
+            if (o_stream.is_open())
+                type = file_type::f_stream;
+
+        } else {
+
+            i_stream = std::ifstream(path, std::ios::binary | std::ios::ate);
+            if (i_stream.is_open())
+                type = file_type::f_stream;
+        }
+    }
+
+    return is_open();
+}
+
+void file::close() {
+
+    if (type._value == file_type::fs) {
+
+        PHYSFS_close(fs_file);
+
+    } else if (type._value == file_type::f_stream) {
+
+        if (write_mode)
+            o_stream.close();
+        else
+            i_stream.close();
+    }
+}
+
+bool file::is_open() const {
+
+    if (type._value == file_type::fs) {
+
+        return fs_file != nullptr;
+
+    } else if (type._value == file_type::f_stream) {
+
+        if (write_mode)
+            return o_stream.is_open();
+        else
+            return i_stream.is_open();
+    }
+
+    return false;
+}
+
+i64 file::get_size() const {
+
+    if (type._value == file_type::fs) {
+
+        return PHYSFS_fileLength(fs_file);
+
+    } else if (type._value == file_type::f_stream) {
+
+        if (write_mode)
+            return to_i64(o_stream.tellp());
+        else
+            return to_i64(i_stream.tellg());
+    }
+
+    return file_error;
+}
+
+i64 file::read(data_ptr data, ui64 size) {
+
+    if (write_mode)
+        return file_error;
+
+    if (type._value == file_type::fs) {
+
+        return PHYSFS_readBytes(fs_file, data, size);
+
+    } else if (type._value == file_type::f_stream) {
+
+        i_stream.seekg(0, std::ios::beg);
+        i_stream.read(data, size);
+        return to_i64(size);
+    }
+
+    return file_error;
+}
+
+i64 file::write(data_cptr data, ui64 size) {
+
+    if (!write_mode)
+        return file_error;
+
+    if (type._value == file_type::fs) {
+
+        return PHYSFS_writeBytes(fs_file, data, size);
+
+    } else if (type._value == file_type::f_stream) {
+
+        o_stream.write(data, size);
+        return to_i64(size);
+    }
+
+    return file_error;
+}
+
+config_file::config_file(name path) : path(path) {}
+
+void config_file::add_callback(config_file_callback* callback) { callbacks.push_back(callback); }
+
+void config_file::remove_callback(config_file_callback* callback) { remove(callbacks, callback); }
+
+bool config_file::load() {
+
+    scope_data data;
+    if (!load_file_data(path, data)) {
+
+        log()->error("couldn't load config file {}", path.c_str());
+        return false;
+    }
+
+    auto j = json::parse({ data.ptr, data.size });
+
+    for (auto callback : callbacks)
+        callback->on_load(j);
+
+    return true;
+}
+
+bool config_file::save() {
+
+    file file(path.c_str(), true);
+    if (!file.is_open()) {
+
+        log()->error("couldn't save config file {}", path.c_str());
+        return false;
+    }
+
+    json j;
+
+    for (auto callback : callbacks)
+        callback->on_save(j);
+
+    auto jString = j.dump(4);
+
+    file.write(jString.data(), jString.size());
+
+    return true;
+}
+
+} // lava
+
+bool lava::load_file_data(string_ref filename, scope_data& data) {
+
+    file file(filename.c_str());
+    if (!file.is_open()) {
+
+        log()->error("couldn't open file {} for reading", filename);
+        return false;
+    }
+
+    data.set(to_size_t(file.get_size()));
+    if (!data.ptr)
+        return false;
+
+    if (is_file_error(file.read(data.ptr))) {
+
+        log()->error("couldn't read file {}", filename);
+        return false;
+    }
+
+    return true;
+}
+
+lava::string_list lava::open_file_dialog(file_dialog const& dialog, bool multiFile) {
+
+    auto dialog_result = tinyfd_openFileDialog(dialog.title, dialog.default_path_and_file,
+                                                to_i32(dialog.filter_pattern.size()), dialog.filter_pattern.data(),
+                                                dialog.single_filter_description, multiFile ? 1 : 0);
+
+    if (!dialog_result)
+        return {};
+
+    string_list result;
+
+    if (multiFile) {
+
+        string s = dialog_result;
+        string delimiter = "|";
+
+        size_t pos = 0;
+        string token;
+
+        auto count = std::count(s.begin(), s.end(), '|');
+        result.resize(to_ui32(count) + 1);
+
+        if (count > 0) {
+
+            auto index = 0u;
+            while ((pos = s.find(delimiter)) != string::npos) {
+
+                token = s.substr(0, pos);
+                result.at(index) = token;
+                s.erase(0, pos + delimiter.length());
+                ++index;
+            }
+
+            result.at(index) = s;
+
+        } else {
+
+            result.at(0) = dialog_result;
+        }
+
+    } else {
+
+        result.resize(1);
+        result.at(0) = dialog_result;
+    }
+
+    return result;
+}
+
+lava::string lava::save_file_dialog(file_dialog const& dialog) {
+
+    return tinyfd_saveFileDialog(dialog.title, dialog.default_path_and_file,
+                                    to_i32(dialog.filter_pattern.size()),
+                                    dialog.filter_pattern.data(),
+                                    dialog.single_filter_description);
+}
+
+lava::string lava::select_folder_dialog(name title, name default_path) {
+
+    return tinyfd_selectFolderDialog(title, default_path);
+}
diff --git a/liblava/utils/file.hpp b/liblava/utils/file.hpp
new file mode 100644
index 00000000..4d5c75fb
--- /dev/null
+++ b/liblava/utils/file.hpp
@@ -0,0 +1,185 @@
+// file      : liblava/file.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/data.hpp>
+#include <liblava/utils/log.hpp>
+
+#include <fstream>
+#include <filesystem>
+
+#include <nlohmann/json.hpp>
+
+// fwd
+struct PHYSFS_File;
+
+namespace lava {
+
+using json = nlohmann::json;
+
+namespace fs = std::filesystem;
+
+bool read_file(std::vector<char>& out, name filename);
+
+bool write_file(name filename, char const* data, size_t data_size);
+
+bool has_extension(name file_name, name extension);
+
+bool has_extension(name filename, names extensions);
+
+string get_filename_from(string_ref path, bool with_extension = false);
+
+bool remove_existing_path(string& target, string_ref path);
+
+struct file_guard : no_copy_no_move {
+
+    explicit file_guard(name filename = nullptr) : filename(filename) {}
+    explicit file_guard(string filename) : filename(filename) {}
+
+    ~file_guard() {
+
+        if (remove)
+            fs::remove(filename);
+    }
+
+    string filename = nullptr;
+    bool remove = true;
+};
+
+constexpr name _zip_ = "zip";
+
+struct file_system : no_copy_no_move {
+
+    static file_system& get() {
+
+        static file_system fs;
+        return fs;
+    }
+
+    static internal_version get_version();
+
+    static name get_base_dir();
+    static string get_base_dir_str();
+    static name get_pref_dir();
+    static string get_res_dir();
+
+    static bool mount(string_ref path);
+    static bool mount(name base_dir_path);
+    static bool exists(name file);
+    static name get_real_dir(name file);
+
+    bool initialize(name argv_0, name org, name app, name ext);
+    void terminate();
+
+    void mount_res();
+    bool create_data_folder();
+
+    name get_org() const { return org; }
+    name get_app() const { return app; }
+    name get_ext() const { return ext; }
+
+    bool is_initialized() const { return initialized; }
+
+private:
+    file_system() = default;
+
+    bool initialized = false;
+
+    name org = nullptr;
+    name app = nullptr;
+    name ext = nullptr;
+
+    string res_path;
+};
+
+BETTER_ENUM(file_type, type, none = 0, fs, f_stream)
+
+constexpr i64 const file_error = -1;
+
+inline bool is_file_error(i64 result) { return result == file_error; }
+
+struct file : no_copy_no_move {
+
+    explicit file(name path = nullptr, bool write = false);
+    ~file();
+
+    bool open(name path, bool write = false);
+    void close();
+
+    bool is_open() const;
+    i64 get_size() const;
+
+    i64 read(data_ptr data) { return read(data, to_ui64(get_size())); }
+    i64 read(data_ptr data, ui64 size);
+
+    i64 write(data_cptr data, ui64 size);
+
+    bool is_write_mode() const { return write_mode; }
+    file_type get_type() const { return type; }
+
+    name get_path() const { return path; }
+
+private:
+    file_type type = file_type::none;
+    bool write_mode = false;
+
+    name path = nullptr;
+
+    PHYSFS_File* fs_file = nullptr;
+    mutable std::ifstream i_stream;
+    mutable std::ofstream o_stream;
+};
+
+bool load_file_data(string_ref filename, scope_data& data);
+
+struct file_data {
+
+    explicit file_data(string_ref filename) { load_file_data(filename, _data); }
+
+    data const& get() const { return _data; }
+
+private:
+    scope_data _data;
+};
+
+struct file_dialog {
+
+    name title = "";
+    name default_path_and_file = "";
+    names filter_pattern = {};
+    name single_filter_description = nullptr;
+};
+
+string_list open_file_dialog(file_dialog const& dialog = {}, bool multi_file = false);
+
+string save_file_dialog(file_dialog const& dialog = {});
+
+string select_folder_dialog(name title = "", name default_path = "");
+
+struct config_file_callback {
+
+    using list = std::vector<config_file_callback*>;
+
+    using func = std::function<void(json&)>;
+    func on_load;
+    func on_save;
+};
+
+struct config_file {
+
+    explicit config_file(name path);
+
+    void add_callback(config_file_callback* callback);
+    void remove_callback(config_file_callback* callback);
+
+    bool load();
+    bool save();
+
+private:
+    string path;
+    config_file_callback::list callbacks;
+};
+
+} // lava
diff --git a/liblava/utils/log.hpp b/liblava/utils/log.hpp
new file mode 100644
index 00000000..530a0375
--- /dev/null
+++ b/liblava/utils/log.hpp
@@ -0,0 +1,80 @@
+// file      : liblava/log.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/types.hpp>
+#include <liblava/core/version.hpp>
+
+#include <spdlog/spdlog.h>
+#include <spdlog/sinks/stdout_sinks.h>
+#include <spdlog/sinks/file_sinks.h>
+
+namespace lava {
+
+using logger = std::shared_ptr<spdlog::logger>;
+
+inline logger log(name name = _lava_) { return spdlog::get(name); }
+
+inline string to_string(string_ref id, string_ref name) {
+
+    return fmt::format("{} | {}", id.c_str(), name.c_str());
+}
+
+inline string to_string(internal_version const& version) {
+
+    return fmt::format("{}.{}.{}", version.major, version.minor, version.patch);
+}
+
+inline name to_string(version_stage stage) {
+
+    switch (stage) {
+
+        case version_stage::preview: return "preview";
+        case version_stage::alpha: return "alpha";
+        case version_stage::beta: return "beta";
+        case version_stage::rc: return "rc";
+        default:
+            return "";
+    }
+}
+
+inline string to_string(version const& version) {
+
+    string stage_str = to_string(version.stage);
+    if ((version.rev > 1) && (version.stage != version_stage::release))
+        stage_str += fmt::format(" {}", version.rev);
+
+    if (version.release == 0)
+        return fmt::format("{} {}", version.year, stage_str.c_str());
+    else
+        return fmt::format("{}.{} {}", version.year, version.release, stage_str.c_str());
+}
+
+constexpr name _lava_log_file_ = "lava.log";
+
+struct log_config {
+
+    name logger = _lava_;
+    name file = _lava_log_file_;
+
+    i32 level = -1;
+    bool debug = false;
+};
+
+inline void setup_log(log_config config = {}) {
+
+    if (config.debug) {
+
+        spdlog::set_level((config.level < 0) ? spdlog::level::debug : (spdlog::level::level_enum)config.level);
+        spdlog::stdout_color_mt(config.logger);
+
+    } else {
+
+        spdlog::set_level((config.level < 0) ? spdlog::level::warn : (spdlog::level::level_enum)config.level);
+        spdlog::basic_logger_mt(config.logger, config.file);
+    }
+}
+
+} // lava
diff --git a/liblava/utils/random.hpp b/liblava/utils/random.hpp
new file mode 100644
index 00000000..756cbd37
--- /dev/null
+++ b/liblava/utils/random.hpp
@@ -0,0 +1,59 @@
+// file      : liblava/random.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/types.hpp>
+
+#include <random>
+
+namespace lava {
+
+struct random_generator {
+
+    static random_generator& instance() {
+
+        static random_generator generator;
+        return generator;
+    }
+
+    i32 get(i32 low, i32 high) {
+
+        std::uniform_int_distribution<i32> dist(low, high);
+        return dist(mt);
+    }
+
+    template <typename T = real>
+    T get(T low, T high) {
+
+        std::uniform_real_distribution<T> dist(low, high);
+        return dist(mt);
+    }
+
+private:
+    random_generator() {
+
+        std::random_device rd;
+        mt = std::mt19937(rd());
+    }
+
+    std::mt19937 mt;
+};
+
+template <typename T>
+inline T random(T low, T high) { return random_generator::instance().get(low, high); }
+
+struct pseudo_random_generator {
+
+    explicit pseudo_random_generator(ui32 seed) : seed(seed) {}
+
+    void set_seed(ui32 value) { seed = value; }
+    ui32 get() { return generate_fast() ^ (generate_fast() >> 7); }
+
+private:
+    ui32 seed = 0;
+    ui32 generate_fast() { return seed = (seed * 196314165 + 907633515); }
+};
+
+} // lava
diff --git a/liblava/utils/telegram.hpp b/liblava/utils/telegram.hpp
new file mode 100644
index 00000000..805ab1f7
--- /dev/null
+++ b/liblava/utils/telegram.hpp
@@ -0,0 +1,100 @@
+// file      : liblava/telegram.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/utils/thread.hpp>
+
+#include <any>
+#include <cmath>
+#include <set>
+
+namespace lava {
+
+constexpr time const telegram_min_delay = 0.25;
+
+struct telegram {
+
+    using ref = telegram const&;
+
+    explicit telegram(id::ref sender, id::ref receiver, type msg, time dispatch_time = 0.0, std::any info = {}) 
+                        : sender(sender), receiver(receiver), msg(msg), dispatch_time(dispatch_time), info(std::move(info)) {}
+
+    bool operator==(ref rhs) const {
+
+        return (fabs(dispatch_time - rhs.dispatch_time) < telegram_min_delay) &&
+                    (sender == rhs.sender) && (receiver == rhs.receiver) && (msg == rhs.msg);
+    }
+
+    bool operator<(ref rhs) const {
+
+        if (*this == rhs)
+            return false;
+
+        return (dispatch_time < rhs.dispatch_time);
+    }
+
+    id sender;
+    id receiver;
+
+    type msg = no_type;
+
+    time dispatch_time = 0.0;
+    std::any info;
+};
+
+struct dispatcher {
+
+    void setup(ui32 threadcount) { pool.setup(threadcount); }
+
+    void teardown() { pool.teardown(); }
+
+    void update(time current) {
+
+        current_time = current;
+        dispatch_delayed_messages(current_time);
+    }
+
+    void add_message(id::ref receiver, id::ref sender, type message, time delay = 0.0, std::any const& info = {}) {
+
+        telegram msg(sender, receiver, message, current_time, info);
+
+        if (delay <= 0.0) {
+
+            discharge(msg); // now
+            return;
+        }
+
+        msg.dispatch_time += delay;
+        messages.insert(msg);
+    }
+
+    using message_func = std::function<void(telegram::ref, id::ref)>;
+    message_func on_message;
+
+private:
+    void discharge(telegram::ref message) {
+
+        pool.enqueue([&, message](id::ref thread) {
+            if (on_message)
+                on_message(message, thread);
+        });
+    }
+
+    void dispatch_delayed_messages(time time) {
+
+        while (!messages.empty() && (messages.begin()->dispatch_time < time) && (messages.begin()->dispatch_time > 0.0)) {
+
+            discharge(*messages.begin());
+            messages.erase(messages.begin());
+        }
+    }
+
+    time current_time = 0.0;
+
+    thread_pool pool;
+    std::set<telegram> messages;
+};
+
+} // lava
diff --git a/liblava/utils/thread.hpp b/liblava/utils/thread.hpp
new file mode 100644
index 00000000..225f14f4
--- /dev/null
+++ b/liblava/utils/thread.hpp
@@ -0,0 +1,97 @@
+// file      : liblava/thread.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/id.hpp>
+#include <liblava/core/time.hpp>
+
+#include <condition_variable>
+#include <deque>
+#include <mutex>
+#include <thread>
+
+namespace lava {
+
+inline void sleep(time seconds) {
+
+    std::this_thread::sleep_for(std::chrono::milliseconds(seconds_in_ms(seconds)));
+}
+
+struct thread_pool {
+
+    using task = std::function<void(id::ref)>; // thread id
+
+    void setup(ui32 count = 2) {
+
+        for (auto i = 0u; i < count; ++i)
+            workers.emplace_back(worker(*this));
+    }
+
+    void teardown() {
+
+        stop = true;
+        condition.notify_all();
+
+        for (auto& worker : workers)
+            worker.join();
+
+        workers.clear();
+    }
+
+    template <typename F>
+    void enqueue(F f) {
+
+        {
+            std::unique_lock<std::mutex> lock(queueMutex);
+            tasks.push_back(task(f));
+        }
+        condition.notify_one();
+    }
+
+private:
+    struct worker {
+
+        explicit worker(thread_pool& pool) : pool(pool) {}
+
+        void operator()() {
+
+            auto thread_id = ids::next();
+
+            task task;
+            while (true) {
+
+                {
+                    std::unique_lock<std::mutex> lock(pool.queueMutex);
+
+                    while (!pool.stop && pool.tasks.empty())
+                        pool.condition.wait(lock);
+
+                    if (pool.stop)
+                        break;
+
+                    task = pool.tasks.front();
+                    pool.tasks.pop_front();
+                }
+
+                task(thread_id);
+            }
+
+            ids::free(thread_id);
+        }
+
+    private:
+        thread_pool& pool;
+    };
+
+    std::vector<std::thread> workers;
+    std::deque<task> tasks;
+
+    std::mutex queueMutex;
+    std::condition_variable condition;
+
+    bool stop = false;
+};
+
+} // lava
diff --git a/liblava/utils/utility.hpp b/liblava/utils/utility.hpp
new file mode 100644
index 00000000..fdd74709
--- /dev/null
+++ b/liblava/utils/utility.hpp
@@ -0,0 +1,53 @@
+// file      : liblava/utility.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#pragma once
+
+#include <liblava/core/types.hpp>
+
+#include <algorithm>
+#include <cstring>
+#include <utility>
+
+namespace lava {
+
+inline bool exists(names_ref list, name item) {
+
+    auto itr = std::find_if(list.begin(), list.end(), [&](name entry) { return strcmp(entry, item) == 0; });
+    return itr != list.end();
+}
+
+template <typename T>
+inline void remove(std::vector<T>& list, T item) {
+
+    list.erase(std::remove(list.begin(), list.end(), item), list.end());
+}
+
+template <typename T>
+inline bool contains(std::vector<T>& list, T item) {
+
+    return std::find(list.begin(), list.end(), item) != list.end();
+}
+
+template <typename T>
+inline void append(std::vector<T>& list, std::vector<T>& items) {
+
+    list.insert(list.end(), items.begin(), items.end());
+}
+
+// reversed iterable
+
+template <typename T>
+struct reversion_wrapper { T& iterable; };
+
+template <typename T>
+inline auto begin(reversion_wrapper<T> w) { return std::rbegin(w.iterable); }
+
+template <typename T>
+inline auto end(reversion_wrapper<T> w) { return std::rend(w.iterable); }
+
+template <typename T>
+inline reversion_wrapper<T> reverse(T&& iterable) { return { iterable }; }
+
+} // lava
diff --git a/doc/img/lava_block_logo_200.png b/res/texture/lava_block_logo_200.png
similarity index 100%
rename from doc/img/lava_block_logo_200.png
rename to res/texture/lava_block_logo_200.png
diff --git a/doc/img/lava_block_logo_50.png b/res/texture/lava_block_logo_50.png
similarity index 100%
rename from doc/img/lava_block_logo_50.png
rename to res/texture/lava_block_logo_50.png
diff --git a/tests/driver.cpp b/tests/driver.cpp
new file mode 100644
index 00000000..90eb862a
--- /dev/null
+++ b/tests/driver.cpp
@@ -0,0 +1,67 @@
+// file      : tests/driver.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <tests/driver.hpp>
+
+#include <iostream>
+
+using namespace lava;
+
+int run(int argc, char** argv) {
+
+    auto& tests = driver::instance().get();
+
+    argh::parser cmd_line(argc, argv);
+
+    if (cmd_line[{ "-t", "--tests" }]) {
+
+        for (auto& t : tests)
+            std::cout << t.first << " - " << t.second->descr << std::endl;
+
+        return to_i32(tests.size());
+    }
+
+    if (cmd_line.pos_args().size() > 1) {
+
+        char* end_ptr = nullptr;
+        auto selected = std::strtol(cmd_line.pos_args().at(1).c_str(), &end_ptr, 10);
+        if (*end_ptr != '\0') {
+
+            std::cerr << "wrong arguments" << std::endl;
+            return test_result::wrong_arguments;
+        }
+
+        if (!tests.count(selected)) {
+
+            std::cerr << "test " << selected << " not found" << std::endl;
+            return test_result::not_found;
+        }
+
+        if (tests.count(selected))
+            return tests.at(selected)->on_func(cmd_line);
+    }
+
+    for (auto& t : reverse(tests)) {
+
+        std::cout << "test " << t.first << " - " << t.second->descr << std::endl;
+        return t.second->on_func(cmd_line);
+    }
+
+    std::cerr << "no tests" << std::endl;
+    return test_result::no_tests;
+}
+
+int main(int argc, char* argv[]) {
+
+    return run(argc, argv);
+}
+
+namespace lava {
+
+test::test(ui32 id, name descr, func func) : id(id), descr(descr), on_func(func) {
+
+    driver::instance().add_test(this);
+}
+
+} // lava
diff --git a/tests/driver.hpp b/tests/driver.hpp
new file mode 100644
index 00000000..f21709c8
--- /dev/null
+++ b/tests/driver.hpp
@@ -0,0 +1,81 @@
+// file      : tests/driver.hpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <liblava/lava.hpp>
+
+/* ---
+
+LAVA_TEST(ID, DESCRIPTION)
+{
+    // command line arguments
+    // argh::parser argh;
+
+    ...
+
+    return 0; // return int
+}
+
+copy and paste:
+
+LAVA_TEST(1, "first test")
+{
+    return argh.size();
+}
+
+*/
+
+namespace lava {
+
+enum test_result {
+
+    no_tests        = -100,
+    not_found       = -101,
+    wrong_arguments = -102
+};
+
+struct test {
+
+    using map = std::map<index, test*>;
+    using func = std::function<i32(argh::parser)>;
+
+    explicit test(ui32 id, name descr, func func);
+
+    index id = 0;
+    string descr;
+    func on_func;
+};
+
+struct driver {
+
+    static driver& instance() {
+
+        static driver singleton;
+        return singleton;
+    }
+
+    void add_test(test* test) {
+
+        tests.emplace(test->id, test);
+    }
+
+    test::map const& get() const { return tests; }
+
+private:
+    driver() = default;
+
+    test::map tests;
+};
+
+} // lava
+
+#define OBJ test_
+#define FUNC _test
+
+#define STR_(n,m) n##m
+#define STR(n,m) STR_(n,m)
+
+#define LAVA_TEST(ID, NAME) \
+i32 STR(FUNC,ID)(argh::parser argh); \
+lava::test STR(OBJ,ID)(ID, NAME, ::STR(FUNC,ID)); \
+i32 STR(FUNC,ID)(argh::parser argh)
diff --git a/tests/tests.cpp b/tests/tests.cpp
new file mode 100644
index 00000000..8a5c41ea
--- /dev/null
+++ b/tests/tests.cpp
@@ -0,0 +1,220 @@
+// file      : tests/tests.cpp
+// copyright : Copyright (c) 2018-present, Lava Block OÜ
+// license   : MIT; see accompanying LICENSE file
+
+#include <tests/driver.hpp>
+
+using namespace lava;
+
+LAVA_TEST(1, "frame init")
+{
+    frame frame(argh);
+
+    return frame.ready() ? 0 : error::not_ready;
+}
+
+LAVA_TEST(2, "run loop")
+{
+    frame frame(argh);
+    if (!frame.ready())
+        return error::not_ready;
+
+    auto count = 0;
+
+    frame.add_run([&]() {
+
+        sleep(1.0);
+        count++;
+
+        log()->debug("{} - running {} sec", count, frame.get_running_time());
+
+        if (count == 3)
+            frame.shut_down();
+
+        return true;
+    });
+
+    return frame.run() ? 0 : error::aborted;
+}
+
+LAVA_TEST(3, "window input")
+{
+    frame frame(argh);
+    if (!frame.ready())
+        return error::not_ready;
+
+    window window;
+    if (!window.create())
+        return error::create_failed;
+
+    input input;
+    window.assign(&input);
+
+    input.key_listeners.add([&](key_event::ref event) {
+
+        if (event.key == GLFW_KEY_ESCAPE && event.action == GLFW_PRESS)
+            frame.shut_down();
+    });
+
+    frame.add_run([&]() {
+
+        handle_events(input);
+
+        if (window.has_close_request())
+            frame.shut_down();
+
+        return true;
+    });
+
+    return frame.run() ? 0 : error::aborted;
+}
+
+LAVA_TEST(4, "clear color")
+{
+    frame frame(argh);
+    if (!frame.ready())
+        return error::not_ready;
+
+    window window;
+    if (!window.create())
+        return error::create_failed;
+
+    input input;
+    window.assign(&input);
+
+    input.key_listeners.add([&](key_event::ref event) {
+
+        if (event.key == GLFW_KEY_ESCAPE && event.action == GLFW_PRESS)
+            frame.shut_down();
+    });
+
+    auto device = frame.create_device();
+    if (!device)
+        return error::create_failed;
+
+    auto render_target = create_target(&window, device);
+    if (!render_target)
+        return error::create_failed;
+
+    renderer simple_renderer;
+    if (!simple_renderer.create(render_target->get_swapchain()))
+        return error::create_failed;
+
+    auto frame_count = render_target->get_frame_count();
+
+    VkCommandPool cmd_pool;
+    VkCommandBuffers cmd_bufs(frame_count);
+
+    auto build_cmd_bufs = [&]() {
+
+        VkCommandPoolCreateInfo create_info
+        {
+            .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
+            .queueFamilyIndex = device->get_graphics_queue().family_index,
+        };
+        if (!check(device->call().vkCreateCommandPool(device->get(), &create_info, memory::alloc(), &cmd_pool)))
+            return false;
+
+        VkCommandBufferAllocateInfo alloc_info
+        {
+            .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
+            .commandPool = cmd_pool,
+            .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
+            .commandBufferCount = frame_count,
+        };
+        if (!check(device->call().vkAllocateCommandBuffers(device->get(), &alloc_info, cmd_bufs.data())))
+            return false;
+
+        VkCommandBufferBeginInfo begin_info
+        {
+            .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+            .flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT,
+        };
+
+        VkClearColorValue clear_color = { random(0.f, 1.f), random(0.f, 1.f), random(0.f, 1.f), 0.f };
+
+        VkImageSubresourceRange image_range
+        {
+            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+            .levelCount = 1,
+            .layerCount = 1,
+        };
+
+        for (auto i = 0u; i < frame_count; ++i) {
+
+            auto cmd_buf = cmd_bufs[i];
+            auto target_image = render_target->get_backbuffer_image(i);
+
+            if (!check(device->call().vkBeginCommandBuffer(cmd_buf, &begin_info)))
+                return false;
+
+            insert_image_memory_barrier(device, cmd_buf, target_image,
+                                        VK_ACCESS_MEMORY_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
+                                        VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                                        VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, image_range);
+
+            device->call().vkCmdClearColorImage(cmd_buf, target_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear_color, 1, &image_range);
+
+            insert_image_memory_barrier(device, cmd_buf, target_image,
+                                        VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT,
+                                        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
+                                        VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, image_range);
+
+            if (!check(device->call().vkEndCommandBuffer(cmd_buf)))
+                return false;
+        }
+
+        return true;
+    };
+
+    auto clean_cmd_bufs = [&]() {
+
+        for (auto& cmd_buf : cmd_bufs)
+            device->call().vkFreeCommandBuffers(device->get(), cmd_pool, 1, &cmd_buf);
+
+        device->call().vkDestroyCommandPool(device->get(), cmd_pool, memory::alloc());
+    };
+
+    if (!build_cmd_bufs())
+        return error::create_failed;
+
+    render_target->on_swapchain_start = build_cmd_bufs;
+    render_target->on_swapchain_stop = clean_cmd_bufs;
+
+    frame.add_run([&]() {
+
+        handle_events(input);
+
+        if (window.has_close_request())
+            return frame.shut_down();
+
+        if (window.has_resize_request())
+            return window.handle_resize();
+
+        if (window.iconified()) {
+
+            frame.set_wait_for_events(true);
+            return true;
+
+        } else {
+
+            if (frame.waiting_for_events())
+                frame.set_wait_for_events(false);
+        }
+
+        auto frame_index = simple_renderer.begin_frame();
+        if (!frame_index)
+            return true;
+
+        return simple_renderer.end_frame({ cmd_bufs[*frame_index] });
+    });
+
+    frame.add_run_end([&]() {
+
+        clean_cmd_bufs();
+        simple_renderer.destroy();
+        render_target->destroy();
+    });
+
+    return frame.run() ? 0 : error::aborted;
+}
-- 
GitLab