diff --git a/.gitignore b/.gitignore
index 7ee4ff1903e902c4715c6e2b0c3e784ed5755aaf..76a0fa8e507371af6821b220a402666e79c340a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,8 @@ cmake-build-release/
 
 # GUI configuration files
 imgui.ini
+
+# Generated source and header files for shaders
+*.hxx
+*.cxx
+
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 33b70018e368ecc3ad019ea33e57485814eb233a..63fda17212cf97f2857ac1891dfad9dd052cbe6a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,8 +1,8 @@
 variables:
   RUN:
     value: "all"
-    description: "The tests that should run. Possible values: ubuntu, win, all."
-  GIT_DEPTH: 1
+    description: "The tests that should run. Possible values: ubuntu, win-msvc, win-mingw, mac, all."
+  GIT_DEPTH: 15
 
 stages:
   - build
@@ -17,13 +17,13 @@ build_ubuntu_gcc:
     - ubuntu-gcc-cached
   variables:
     GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 10m
+  timeout: 15m
   retry: 1
   script:
     - mkdir debug
     - cd debug
     - cmake -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build .
+    - cmake --build . -j 4
   artifacts:
     name: "Documentation - $CI_PIPELINE_ID"
     paths:
@@ -34,13 +34,13 @@ build_ubuntu_gcc:
 build_win10_msvc:
   only:
     variables:
-      - $RUN =~ /\bwin.*/i || $RUN =~ /\ball.*/i
+      - $RUN =~ /\bwin-msvc.*/i || $RUN =~ /\ball.*/i
   stage: build
   tags: 
     - win10-msvc-cached
   variables:
     GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 10m
+  timeout: 15m
   retry: 0
   script:
     - cd 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\'
@@ -49,7 +49,43 @@ build_win10_msvc:
     - mkdir debug
     - cd debug
     - cmake -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build .
+    - cmake --build . -j 4
+
+build_win10_mingw:
+  only:
+    variables:
+      - $RUN =~ /\bwin-mingw.*/i || $RUN =~ /\ball.*/i
+  stage: build
+  tags: 
+    - win10-mingw-cached
+  variables:
+    GIT_SUBMODULE_STRATEGY: recursive
+  timeout: 15m
+  retry: 0
+  script:
+    - mkdir debug
+    - cd debug
+    - cmake --no-warn-unused-cli -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_C_COMPILER:FILEPATH=C:\msys64\mingw64\bin\x86_64-w64-mingw32-gcc.exe -DCMAKE_CXX_COMPILER:FILEPATH=C:\msys64\mingw64\bin\x86_64-w64-mingw32-g++.exe .. -G "Unix Makefiles"
+    - cmake --build . -j 4
+
+build_mac_clang:
+  only:
+    variables:
+      - $RUN =~ /\bmac.*/i || $RUN =~ /\ball.*/i
+  stage: build
+  tags: 
+    - catalina-clang-cached
+  variables:
+    GIT_SUBMODULE_STRATEGY: recursive
+  timeout: 15m
+  retry: 1
+  script:
+    - mkdir debug
+    - cd debug
+    - export LDFLAGS="-L/usr/local/opt/llvm/lib"
+    - export CPPFLAGS="-I/usr/local/opt/llvm/include"
+    - cmake -DCMAKE_C_COMPILER="/usr/local/opt/llvm/bin/clang" -DCMAKE_CXX_COMPILER="/usr/local/opt/llvm/bin/clang++" -DCMAKE_BUILD_TYPE=Debug ..
+    - cmake --build . -j 4
 
 deploy_doc_develop:
   only:
diff --git a/.gitmodules b/.gitmodules
index e0aaf2d17c340f98ae875f7e0f1238bfe04f7e5d..cc3bf1fcd2e1eb8117cbcc7222b04f7041fea520 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -22,3 +22,9 @@
 [submodule "modules/gui/lib/imgui"]
 	path = modules/gui/lib/imgui
 	url = https://github.com/ocornut/imgui.git
+[submodule "lib/VulkanMemoryAllocator-Hpp"]
+	path = lib/VulkanMemoryAllocator-Hpp
+	url = https://github.com/malte-v/VulkanMemoryAllocator-Hpp.git
+[submodule "modules/upscaling/lib/FidelityFX-FSR"]
+	path = modules/upscaling/lib/FidelityFX-FSR
+	url = https://github.com/GPUOpen-Effects/FidelityFX-FSR.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2ae078a428a8e5e640ed8dc7bcc2f4e58e159c6b..dfafe1cd084d4b324c233d502e301c24a5ee95e1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -33,7 +33,8 @@ set(vkcv_flags ${CMAKE_CXX_FLAGS})
 # enabling warnings in the debug build
 if (vkcv_build_debug)
 	if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
-		set(vkcv_flags ${vkcv_flags} " -Weverything")
+		#set(vkcv_flags ${vkcv_flags} " -Weverything")
+		set(vkcv_flags ${vkcv_flags} " -Wextra -Wall")
 	elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
 		set(vkcv_flags ${vkcv_flags} " -Wextra -Wall -pedantic")
 	else()
diff --git a/config/Libraries.cmake b/config/Libraries.cmake
index ec014f84c820abf4988b070d5b733be08c377319..512669ce85a96f8cc94d8181994cfe458fa8b604 100644
--- a/config/Libraries.cmake
+++ b/config/Libraries.cmake
@@ -3,7 +3,13 @@ set(vkcv_config_lib ${vkcv_config}/lib)
 set(vkcv_lib_path ${PROJECT_SOURCE_DIR}/${vkcv_lib})
 
 if(NOT WIN32)
-	set(vkcv_libraries  stdc++fs)
+	if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+		set(vkcv_libraries stdc++fs)
+	endif()
+	
+	if (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
+		list(APPEND vkcv_flags -Xpreprocessor)
+	endif()
 	
 	# optimization for loading times
 	list(APPEND vkcv_flags -pthread)
@@ -19,6 +25,7 @@ set(vkcv_config_msg " - Library: ")
 include(${vkcv_config_lib}/GLFW.cmake)    # glfw-x11 / glfw-wayland					# libglfw3-dev
 include(${vkcv_config_lib}/Vulkan.cmake)  # vulkan-intel / vulkan-radeon / nvidia	# libvulkan-dev
 include(${vkcv_config_lib}/SPIRV_Cross.cmake)  # SPIRV-Cross	                    # libspirv_cross_c_shared
+include(${vkcv_config_lib}/VulkanMemoryAllocator.cmake) # VulkanMemoryAllocator
 
 # cleanup of compiler flags
 if (vkcv_flags)
@@ -33,6 +40,9 @@ endif ()
 # fix dependencies for different Linux distros (looking at you Ubuntu)
 include(${vkcv_config_ext}/CheckLibraries.cmake)
 
+# add custom function to include a file like a shader as string
+include(${vkcv_config_ext}/IncludeShader.cmake)
+
 # cleanup of compiler definitions aka preprocessor variables
 if (vkcv_definitions)
     list(REMOVE_DUPLICATES vkcv_definitions)
diff --git a/config/Sources.cmake b/config/Sources.cmake
index 54bb3485ed975669668d987787975f019aa6358b..41cd0c20f2106dc02700d9b23227f3e6c34a057a 100644
--- a/config/Sources.cmake
+++ b/config/Sources.cmake
@@ -6,6 +6,9 @@ set(vkcv_sources
 
 		${vkcv_include}/vkcv/Core.hpp
 		${vkcv_source}/vkcv/Core.cpp
+		
+		${vkcv_include}/vkcv/File.hpp
+		${vkcv_source}/vkcv/File.cpp
 
 		${vkcv_include}/vkcv/PassConfig.hpp
 		${vkcv_source}/vkcv/PassConfig.cpp
@@ -21,6 +24,8 @@ set(vkcv_sources
 
 		${vkcv_include}/vkcv/Buffer.hpp
 		
+		${vkcv_include}/vkcv/PushConstants.hpp
+		
 		${vkcv_include}/vkcv/BufferManager.hpp
 		${vkcv_source}/vkcv/BufferManager.cpp
 
diff --git a/config/ext/IncludeShader.cmake b/config/ext/IncludeShader.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..e67a8716fb32a953c93a3c6624f0d459a025e950
--- /dev/null
+++ b/config/ext/IncludeShader.cmake
@@ -0,0 +1,75 @@
+
+function(include_shader shader include_dir source_dir)
+	if (NOT EXISTS ${shader})
+		message(WARNING "Shader file does not exist: ${shader}")
+	else()
+		get_filename_component(filename ${shader} NAME)
+		file(SIZE ${shader} filesize)
+		
+		set(include_target_file ${include_dir}/${filename}.hxx)
+		set(source_target_file ${source_dir}/${filename}.cxx)
+		
+		if ((EXISTS ${source_target_file}) AND (EXISTS ${include_target_file}))
+			file(TIMESTAMP ${shader} shader_timestamp "%Y-%m-%dT%H:%M:%S")
+			file(TIMESTAMP ${source_target_file} source_timestamp "%Y-%m-%dT%H:%M:%S")
+			
+			string(COMPARE GREATER ${shader_timestamp} ${source_timestamp} shader_update)
+		else()
+			set(shader_update true)
+		endif()
+		
+		if (shader_update)
+			string(TOUPPER ${filename} varname)
+			string(REPLACE "." "_" varname ${varname})
+			
+			set(shader_header "#pragma once\n")
+			string(APPEND shader_header "// This file is auto-generated via cmake, so don't touch it!\n")
+			string(APPEND shader_header "extern unsigned char ${varname} [${filesize}]\;\n")
+			string(APPEND shader_header "extern unsigned int ${varname}_LEN\;\n")
+			string(APPEND shader_header "const std::string ${varname}_SHADER (reinterpret_cast<const char*>(${varname}), ${varname}_LEN)\;")
+			
+			file(WRITE ${include_target_file} ${shader_header})
+			
+			find_program(xxd_program "xxd")
+			
+			if (EXISTS ${xxd_program})
+				get_filename_component(shader_directory ${shader} DIRECTORY)
+				
+				add_custom_command(
+						OUTPUT ${source_target_file}
+						WORKING_DIRECTORY "${shader_directory}"
+						COMMAND xxd -i -C "${filename}" "${source_target_file}"
+						COMMENT "Processing shader into source files: ${shader}"
+				)
+			else()
+				set(shader_source "// This file is auto-generated via cmake, so don't touch it!\n")
+				string(APPEND shader_source "unsigned char ${varname}[] = {")
+				
+				math(EXPR max_fileoffset "${filesize} - 1" OUTPUT_FORMAT DECIMAL)
+				
+				message(STATUS "Processing shader into source files: ${shader}")
+				
+				foreach(fileoffset RANGE ${max_fileoffset})
+					file(READ ${shader} shader_source_byte OFFSET ${fileoffset} LIMIT 1 HEX)
+					
+					math(EXPR offset_modulo "${fileoffset} % 12" OUTPUT_FORMAT DECIMAL)
+					
+					if (${offset_modulo} EQUAL 0)
+						string(APPEND shader_source "\n  ")
+					endif()
+					
+					if (${fileoffset} LESS ${max_fileoffset})
+						string(APPEND shader_source "0x${shader_source_byte}, ")
+					else()
+						string(APPEND shader_source "0x${shader_source_byte}\n")
+					endif()
+				endforeach()
+				
+				string(APPEND shader_source "}\;\n")
+				string(APPEND shader_source "unsigned int ${varname}_LEN = ${filesize}\;")
+				
+				file(WRITE ${source_target_file} ${shader_source})
+			endif()
+		endif()
+	endif()
+endfunction()
diff --git a/config/lib/GLFW.cmake b/config/lib/GLFW.cmake
index 1b68d8aa97ba59158a7bd805ab2470f554f705aa..9668694de5b6887c163f74b626c71873e3f611f8 100644
--- a/config/lib/GLFW.cmake
+++ b/config/lib/GLFW.cmake
@@ -10,6 +10,7 @@ else()
         add_subdirectory(${vkcv_lib}/glfw)
 
         list(APPEND vkcv_libraries glfw)
+        list(APPEND vkcv_includes ${vkcv_lib_path}/glfw/include)
 
         message(${vkcv_config_msg} " GLFW    -   " ${glfw3_VERSION})
     else()
diff --git a/config/lib/SPIRV_Cross.cmake b/config/lib/SPIRV_Cross.cmake
index 2e705d7d5a006e3851d14d22a57fd667c61c79f5..00ae45527d0f49c5632d71e19509871d437ae691 100644
--- a/config/lib/SPIRV_Cross.cmake
+++ b/config/lib/SPIRV_Cross.cmake
@@ -25,6 +25,7 @@ else()
         add_subdirectory(${vkcv_lib}/SPIRV-Cross)
 
         list(APPEND vkcv_libraries spirv-cross-cpp)
+        list(APPEND vkcv_includes ${vkcv_lib_path}/SPIV-Cross/include)
 
         message(${vkcv_config_msg} " SPIRV Cross    - " ${SPIRV_CROSS_VERSION})
     else()
diff --git a/config/lib/VulkanMemoryAllocator.cmake b/config/lib/VulkanMemoryAllocator.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..5f670ff04633e1747accb8f1598fee028a287168
--- /dev/null
+++ b/config/lib/VulkanMemoryAllocator.cmake
@@ -0,0 +1,22 @@
+
+if (EXISTS "${vkcv_lib_path}/VulkanMemoryAllocator-Hpp")
+	set(VMA_HPP_PATH "${vkcv_lib_path}/VulkanMemoryAllocator-Hpp" CACHE INTERNAL "")
+	
+	set(VMA_RECORDING_ENABLED OFF CACHE INTERNAL "")
+	set(VMA_USE_STL_CONTAINERS OFF CACHE INTERNAL "")
+	set(VMA_STATIC_VULKAN_FUNCTIONS ON CACHE INTERNAL "")
+	set(VMA_DYNAMIC_VULKAN_FUNCTIONS OFF CACHE INTERNAL "")
+	set(VMA_DEBUG_ALWAYS_DEDICATED_MEMORY OFF CACHE INTERNAL "")
+	set(VMA_DEBUG_INITIALIZE_ALLOCATIONS OFF CACHE INTERNAL "")
+	set(VMA_DEBUG_GLOBAL_MUTEX OFF CACHE INTERNAL "")
+	set(VMA_DEBUG_DONT_EXCEED_MAX_MEMORY_ALLOCATION_COUNT OFF CACHE INTERNAL "")
+	
+	add_subdirectory(${vkcv_config_lib}/vma)
+	
+	list(APPEND vkcv_libraries VulkanMemoryAllocator)
+	list(APPEND vkcv_includes ${vkcv_lib_path}/VulkanMemoryAllocator-Hpp)
+	
+	message(${vkcv_config_msg} " VMA     - ")
+else()
+	message(WARNING "VulkanMemoryAllocator is required..! Update the submodules!")
+endif ()
diff --git a/config/lib/vma/CMakeLists.txt b/config/lib/vma/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a2c018f2b4894e5ce8e2851ca10f981e2af36605
--- /dev/null
+++ b/config/lib/vma/CMakeLists.txt
@@ -0,0 +1,59 @@
+cmake_minimum_required(VERSION 3.9)
+
+project(VulkanMemoryAllocator)
+
+find_package(Vulkan REQUIRED)
+
+option(VMA_HPP_PATH "Location of C++ headers" "")
+
+message(STATUS "VMA_BUILD_SAMPLE = ${VMA_BUILD_SAMPLE}")
+message(STATUS "VMA_BUILD_SAMPLE_SHADERS = ${VMA_BUILD_SAMPLE_SHADERS}")
+message(STATUS "VMA_BUILD_REPLAY = ${VMA_BUILD_REPLAY}")
+
+option(VMA_RECORDING_ENABLED "Enable VMA memory recording for debugging" OFF)
+option(VMA_USE_STL_CONTAINERS "Use C++ STL containers instead of VMA's containers" OFF)
+option(VMA_STATIC_VULKAN_FUNCTIONS "Link statically with Vulkan API" OFF)
+option(VMA_DYNAMIC_VULKAN_FUNCTIONS "Fetch pointers to Vulkan functions internally (no static linking)" ON)
+option(VMA_DEBUG_ALWAYS_DEDICATED_MEMORY "Every allocation will have its own memory block" OFF)
+option(VMA_DEBUG_INITIALIZE_ALLOCATIONS "Automatically fill new allocations and destroyed allocations with some bit pattern" OFF)
+option(VMA_DEBUG_GLOBAL_MUTEX "Enable single mutex protecting all entry calls to the library" OFF)
+option(VMA_DEBUG_DONT_EXCEED_MAX_MEMORY_ALLOCATION_COUNT "Never exceed VkPhysicalDeviceLimits::maxMemoryAllocationCount and return error" OFF)
+
+message(STATUS "VMA_RECORDING_ENABLED = ${VMA_RECORDING_ENABLED}")
+message(STATUS "VMA_USE_STL_CONTAINERS = ${VMA_USE_STL_CONTAINERS}")
+message(STATUS "VMA_DYNAMIC_VULKAN_FUNCTIONS = ${VMA_DYNAMIC_VULKAN_FUNCTIONS}")
+message(STATUS "VMA_DEBUG_ALWAYS_DEDICATED_MEMORY = ${VMA_DEBUG_ALWAYS_DEDICATED_MEMORY}")
+message(STATUS "VMA_DEBUG_INITIALIZE_ALLOCATIONS = ${VMA_DEBUG_INITIALIZE_ALLOCATIONS}")
+message(STATUS "VMA_DEBUG_GLOBAL_MUTEX = ${VMA_DEBUG_GLOBAL_MUTEX}")
+message(STATUS "VMA_DEBUG_DONT_EXCEED_MAX_MEMORY_ALLOCATION_COUNT = ${VMA_DEBUG_DONT_EXCEED_MAX_MEMORY_ALLOCATION_COUNT}")
+
+add_library(VulkanMemoryAllocator vma.cpp)
+
+set_target_properties(
+		VulkanMemoryAllocator PROPERTIES
+		
+		CXX_EXTENSIONS OFF
+		# Use C++14
+		CXX_STANDARD 14
+		CXX_STANDARD_REQUIRED ON
+)
+
+target_include_directories(VulkanMemoryAllocator PUBLIC ${VMA_HPP_PATH})
+
+# Only link to Vulkan if static linking is used
+if (NOT ${VMA_DYNAMIC_VULKAN_FUNCTIONS})
+	target_link_libraries(VulkanMemoryAllocator PUBLIC Vulkan::Vulkan)
+endif()
+
+target_compile_definitions(
+		VulkanMemoryAllocator
+		
+		PUBLIC
+		VMA_USE_STL_CONTAINERS=$<BOOL:${VMA_USE_STL_CONTAINERS}>
+		VMA_DYNAMIC_VULKAN_FUNCTIONS=$<BOOL:${VMA_DYNAMIC_VULKAN_FUNCTIONS}>
+		VMA_DEBUG_ALWAYS_DEDICATED_MEMORY=$<BOOL:${VMA_DEBUG_ALWAYS_DEDICATED_MEMORY}>
+		VMA_DEBUG_INITIALIZE_ALLOCATIONS=$<BOOL:${VMA_DEBUG_INITIALIZE_ALLOCATIONS}>
+		VMA_DEBUG_GLOBAL_MUTEX=$<BOOL:${VMA_DEBUG_GLOBAL_MUTEX}>
+		VMA_DEBUG_DONT_EXCEED_MAX_MEMORY_ALLOCATION_COUNT=$<BOOL:${VMA_DEBUG_DONT_EXCEED_MAX_MEMORY_ALLOCATION_COUNT}>
+		VMA_RECORDING_ENABLED=$<BOOL:${VMA_RECORDING_ENABLED}>
+)
diff --git a/config/lib/vma/vma.cpp b/config/lib/vma/vma.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..307c27f096bd1bae2b1deb2ca5994f132adc92cc
--- /dev/null
+++ b/config/lib/vma/vma.cpp
@@ -0,0 +1,58 @@
+
+#ifndef NDEBUG
+#define _DEBUG
+#endif
+
+#ifndef _MSVC_LANG
+#ifdef __MINGW32__
+#include <stdint.h>
+#include <stdlib.h>
+
+class VmaMutex {
+public:
+	VmaMutex() : m_locked(false) {}
+	
+	void Lock() {
+		while (m_locked);
+		m_locked = true;
+	}
+	
+	void Unlock() {
+		m_locked = false;
+	}
+private:
+	bool m_locked;
+};
+
+#define VMA_MUTEX VmaMutex
+
+template <typename T>
+T* custom_overestimate_malloc(size_t size) {
+	return new T[size + (sizeof(T) - 1) / sizeof(T)];
+}
+
+void* custom_aligned_malloc(size_t alignment, size_t size) {
+	if (alignment > 4) {
+		return custom_overestimate_malloc<uint64_t>(size);
+	} else
+	if (alignment > 2) {
+		return custom_overestimate_malloc<uint32_t>(size);
+	} else
+	if (alignment > 1) {
+		return custom_overestimate_malloc<uint16_t>(size);
+	} else {
+		return custom_overestimate_malloc<uint8_t>(size);
+	}
+}
+
+void custom_free(void *ptr) {
+	delete[] reinterpret_cast<uint8_t*>(ptr);
+}
+
+#define VMA_SYSTEM_ALIGNED_MALLOC(size, alignment) (custom_aligned_malloc(alignment, size))
+#define VMA_SYSTEM_FREE(ptr) (custom_free(ptr))
+#endif
+#endif
+
+#define VMA_IMPLEMENTATION
+#include "vk_mem_alloc.hpp"
diff --git a/include/vkcv/Buffer.hpp b/include/vkcv/Buffer.hpp
index ae935ba9501d4d7776cad7e3ba190a2dd02e5e38..f5cd183d21c4ae9a5849ff09fc54af70667c12c6 100644
--- a/include/vkcv/Buffer.hpp
+++ b/include/vkcv/Buffer.hpp
@@ -76,8 +76,8 @@ namespace vkcv {
 		{}
 		
 		[[nodiscard]]
-		static Buffer<T> create(BufferManager* manager, BufferType type, size_t count, BufferMemoryType memoryType) {
-			return Buffer<T>(manager, manager->createBuffer(type, count * sizeof(T), memoryType), type, count, memoryType);
+		static Buffer<T> create(BufferManager* manager, BufferType type, size_t count, BufferMemoryType memoryType, bool supportIndirect) {
+			return Buffer<T>(manager, manager->createBuffer(type, count * sizeof(T), memoryType, supportIndirect), type, count, memoryType);
 		}
 		
 	};
diff --git a/include/vkcv/BufferManager.hpp b/include/vkcv/BufferManager.hpp
index 9eb80d70862a79a01593e6fe4c3aabe98d253ac8..7bec33d8c4fa752be2487a849c16eaeeea0e6237 100644
--- a/include/vkcv/BufferManager.hpp
+++ b/include/vkcv/BufferManager.hpp
@@ -2,6 +2,7 @@
 
 #include <vector>
 #include <vulkan/vulkan.hpp>
+#include <vk_mem_alloc.hpp>
 
 #include "Handles.hpp"
 
@@ -30,9 +31,8 @@ namespace vkcv
 		struct Buffer
 		{
 			vk::Buffer m_handle;
-			vk::DeviceMemory m_memory;
+			vma::Allocation m_allocation;
 			size_t m_size = 0;
-			void* m_mapped = nullptr;
 			bool m_mappable = false;
 		};
 		
@@ -70,7 +70,7 @@ namespace vkcv
 		 * @param memoryType Type of buffers memory
 		 * @return New buffer handle
 		 */
-		BufferHandle createBuffer(BufferType type, size_t size, BufferMemoryType memoryType);
+		BufferHandle createBuffer(BufferType type, size_t size, BufferMemoryType memoryType, bool supportIndirect);
 		
 		/**
 		 * Returns the Vulkan buffer handle of a buffer
diff --git a/include/vkcv/Context.hpp b/include/vkcv/Context.hpp
index 1c01a6134ba1642b3a130a7a4d3d299cc3f7b875..824713fd1e29cbb8b7e60b22768c0019daaa9938 100644
--- a/include/vkcv/Context.hpp
+++ b/include/vkcv/Context.hpp
@@ -1,8 +1,10 @@
 #pragma once
 
 #include <vulkan/vulkan.hpp>
+#include <vk_mem_alloc.hpp>
 
 #include "QueueManager.hpp"
+#include "DrawcallRecording.hpp"
 
 namespace vkcv
 {
@@ -32,12 +34,15 @@ namespace vkcv
         
         [[nodiscard]]
         const QueueManager& getQueueManager() const;
+	
+        [[nodiscard]]
+		const vma::Allocator& getAllocator() const;
         
         static Context create(const char *applicationName,
 							  uint32_t applicationVersion,
-							  std::vector<vk::QueueFlagBits> queueFlags,
-							  std::vector<const char *> instanceExtensions,
-							  std::vector<const char *> deviceExtensions);
+							  const std::vector<vk::QueueFlagBits>& queueFlags,
+							  const std::vector<const char *>& instanceExtensions,
+							  const std::vector<const char *>& deviceExtensions);
 
     private:
         /**
@@ -47,11 +52,14 @@ namespace vkcv
          * @param physicalDevice Vulkan-PhysicalDevice
          * @param device Vulkan-Device
          */
-        Context(vk::Instance instance, vk::PhysicalDevice physicalDevice, vk::Device device, QueueManager&& queueManager) noexcept;
+        Context(vk::Instance instance, vk::PhysicalDevice physicalDevice, vk::Device device,
+				QueueManager&& queueManager, vma::Allocator&& allocator) noexcept;
         
         vk::Instance        m_Instance;
         vk::PhysicalDevice  m_PhysicalDevice;
         vk::Device          m_Device;
 		QueueManager		m_QueueManager;
+		vma::Allocator 		m_Allocator;
+		
     };
 }
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index cbbe1e908cdb74891ab9bfe4416c03e487e76b26..7b5c1d94a6519e626249d55c65da19b1e8f95044 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -138,9 +138,9 @@ namespace vkcv
         static Core create(Window &window,
                            const char *applicationName,
                            uint32_t applicationVersion,
-                           std::vector<vk::QueueFlagBits> queueFlags    = {},
-                           std::vector<const char*> instanceExtensions  = {},
-                           std::vector<const char*> deviceExtensions    = {});
+                           const std::vector<vk::QueueFlagBits>& queueFlags    = {},
+                           const std::vector<const char*>& instanceExtensions  = {},
+                           const std::vector<const char*>& deviceExtensions    = {});
 
         /**
          * Creates a basic vulkan graphics pipeline using @p config from the pipeline config class and returns it using the @p handle.
@@ -163,7 +163,7 @@ namespace vkcv
          */
         [[nodiscard]]
         PipelineHandle createComputePipeline(
-            const ShaderProgram &config, 
+            const ShaderProgram &shaderProgram,
             const std::vector<vk::DescriptorSetLayout> &descriptorSetLayouts);
 
         /**
@@ -185,8 +185,8 @@ namespace vkcv
             * return Buffer-Object
             */
         template<typename T>
-        Buffer<T> createBuffer(vkcv::BufferType type, size_t count, BufferMemoryType memoryType = BufferMemoryType::DEVICE_LOCAL) {
-        	return Buffer<T>::create(m_BufferManager.get(), type, count, memoryType);
+        Buffer<T> createBuffer(vkcv::BufferType type, size_t count, BufferMemoryType memoryType = BufferMemoryType::DEVICE_LOCAL, bool supportIndirect = false) {
+        	return Buffer<T>::create(m_BufferManager.get(), type, count, memoryType, supportIndirect);
         }
         
         /**
@@ -196,11 +196,13 @@ namespace vkcv
          * @param minFilter Minimizing filter
          * @param mipmapMode Mipmapping filter
          * @param addressMode Address mode
+         * @param mipLodBias Mip level of detail bias
          * @return Sampler handle
          */
         [[nodiscard]]
         SamplerHandle createSampler(SamplerFilterType magFilter, SamplerFilterType minFilter,
-									SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode);
+									SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode,
+									float mipLodBias = 0.0f);
 
         /**
          * Creates an #Image with a given format, width, height and depth.
@@ -223,9 +225,13 @@ namespace vkcv
 			Multisampling   multisampling = Multisampling::None);
 
         [[nodiscard]]
-        const uint32_t getImageWidth(ImageHandle imageHandle);
+        uint32_t getImageWidth(const ImageHandle& image);
+        
         [[nodiscard]]
-        const uint32_t getImageHeight(ImageHandle imageHandle);
+        uint32_t getImageHeight(const ImageHandle& image);
+	
+		[[nodiscard]]
+		vk::Format getImageFormat(const ImageHandle& image);
 
         /** TODO:
          *   @param setDescriptions
@@ -243,19 +249,35 @@ namespace vkcv
 		bool beginFrame(uint32_t& width, uint32_t& height);
 
 		void recordDrawcallsToCmdStream(
-            const CommandStreamHandle       cmdStreamHandle,
+			const CommandStreamHandle       cmdStreamHandle,
 			const PassHandle                renderpassHandle, 
 			const PipelineHandle            pipelineHandle,
-			const PushConstantData          &pushConstantData,
+			const PushConstants             &pushConstants,
 			const std::vector<DrawcallInfo> &drawcalls,
 			const std::vector<ImageHandle>  &renderTargets);
 
+		void recordMeshShaderDrawcalls(
+			const CommandStreamHandle               cmdStreamHandle,
+			const PassHandle                        renderpassHandle,
+			const PipelineHandle                    pipelineHandle,
+			const PushConstants&                    pushConstantData,
+            const std::vector<MeshShaderDrawcall>&  drawcalls,
+			const std::vector<ImageHandle>&         renderTargets);
+
 		void recordComputeDispatchToCmdStream(
 			CommandStreamHandle cmdStream,
 			PipelineHandle computePipeline,
 			const uint32_t dispatchCount[3],
 			const std::vector<DescriptorSetUsage> &descriptorSetUsages,
-			const PushConstantData& pushConstantData);
+			const PushConstants& pushConstants);
+
+		void recordComputeIndirectDispatchToCmdStream(
+			const CommandStreamHandle               cmdStream,
+			const PipelineHandle                    computePipeline,
+			const vkcv::BufferHandle                buffer,
+			const size_t                            bufferArgOffset,
+			const std::vector<DescriptorSetUsage>&  descriptorSetUsages,
+			const PushConstants&                    pushConstants);
 
 		/**
 		 * @brief end recording and present image
@@ -283,15 +305,21 @@ namespace vkcv
 			const RecordCommandFunction &record,
 			const FinishCommandFunction &finish);
 
-		void submitCommandStream(const CommandStreamHandle handle);
-		void prepareSwapchainImageForPresent(const CommandStreamHandle handle);
-		void prepareImageForSampling(const CommandStreamHandle cmdStream, const ImageHandle image);
-		void prepareImageForStorage(const CommandStreamHandle cmdStream, const ImageHandle image);
-		void recordImageMemoryBarrier(const CommandStreamHandle cmdStream, const ImageHandle image);
-		void recordBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer);
-		void resolveMSAAImage(CommandStreamHandle cmdStream, ImageHandle src, ImageHandle dst);
+		void submitCommandStream(const CommandStreamHandle& handle);
+		void prepareSwapchainImageForPresent(const CommandStreamHandle& handle);
+		void prepareImageForSampling(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void prepareImageForStorage(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void recordImageMemoryBarrier(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void recordBufferMemoryBarrier(const CommandStreamHandle& cmdStream, const BufferHandle& buffer);
+		void resolveMSAAImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst);
 
+		[[nodiscard]]
 		vk::ImageView getSwapchainImageView() const;
+	
+		void recordMemoryBarrier(const CommandStreamHandle& cmdStream);
+		
+		void recordBlitImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst,
+							 SamplerFilterType filterType);
 		
     };
 }
diff --git a/include/vkcv/DescriptorConfig.hpp b/include/vkcv/DescriptorConfig.hpp
index 7dbbc4ed4b5b2175bec3984e51570fa9f65acb39..f2a089fd624c02c57db4a65c4a101c4acff371b1 100644
--- a/include/vkcv/DescriptorConfig.hpp
+++ b/include/vkcv/DescriptorConfig.hpp
@@ -9,6 +9,7 @@ namespace vkcv
     {
         vk::DescriptorSet       vulkanHandle;
         vk::DescriptorSetLayout layout;
+        size_t                  poolIndex;
     };
 
     /*
@@ -20,7 +21,9 @@ namespace vkcv
         STORAGE_BUFFER,
         SAMPLER,
         IMAGE_SAMPLED,
-		IMAGE_STORAGE
+		IMAGE_STORAGE,
+        UNIFORM_BUFFER_DYNAMIC,
+        STORAGE_BUFFER_DYNAMIC
     };    
     
     /*
diff --git a/include/vkcv/DescriptorWrites.hpp b/include/vkcv/DescriptorWrites.hpp
index f28a6c91e189b13413ffefec0f05e5a0a358ee26..28de2ed7fa6b7e71bfa49b67a337f80f2e05ddcf 100644
--- a/include/vkcv/DescriptorWrites.hpp
+++ b/include/vkcv/DescriptorWrites.hpp
@@ -20,16 +20,15 @@ namespace vkcv {
 		uint32_t	mipLevel;
 	};
 
-	struct UniformBufferDescriptorWrite {
-		inline UniformBufferDescriptorWrite(uint32_t binding, BufferHandle buffer) : binding(binding), buffer(buffer) {};
-		uint32_t		binding;
-		BufferHandle	buffer;
-	};
-
-	struct StorageBufferDescriptorWrite {
-		inline StorageBufferDescriptorWrite(uint32_t binding, BufferHandle buffer) : binding(binding), buffer(buffer) {};
+	struct BufferDescriptorWrite {
+		inline BufferDescriptorWrite(uint32_t binding, BufferHandle buffer, bool dynamic = false,
+									 uint32_t offset = 0, uint32_t size = 0) :
+		binding(binding), buffer(buffer), dynamic(dynamic), offset(offset), size(size) {};
 		uint32_t		binding;
 		BufferHandle	buffer;
+		bool 			dynamic;
+		uint32_t 		offset;
+		uint32_t 		size;
 	};
 
 	struct SamplerDescriptorWrite {
@@ -41,8 +40,8 @@ namespace vkcv {
 	struct DescriptorWrites {
 		std::vector<SampledImageDescriptorWrite>		sampledImageWrites;
 		std::vector<StorageImageDescriptorWrite>		storageImageWrites;
-		std::vector<UniformBufferDescriptorWrite>	    uniformBufferWrites;
-		std::vector<StorageBufferDescriptorWrite>	    storageBufferWrites;
+		std::vector<BufferDescriptorWrite>	    		uniformBufferWrites;
+		std::vector<BufferDescriptorWrite>	    		storageBufferWrites;
 		std::vector<SamplerDescriptorWrite>			    samplerWrites;
 	};
 }
\ No newline at end of file
diff --git a/include/vkcv/DrawcallRecording.hpp b/include/vkcv/DrawcallRecording.hpp
index 9f162a499a38d5633703f70eec8a8682e3328d72..37cf02d9102fcab5abd10ada711f67b721bcb52b 100644
--- a/include/vkcv/DrawcallRecording.hpp
+++ b/include/vkcv/DrawcallRecording.hpp
@@ -2,6 +2,7 @@
 #include <vulkan/vulkan.hpp>
 #include <vkcv/Handles.hpp>
 #include <vkcv/DescriptorConfig.hpp>
+#include <vkcv/PushConstants.hpp>
 
 namespace vkcv {
     struct VertexBufferBinding {
@@ -12,28 +13,41 @@ namespace vkcv {
         vk::Buffer      buffer;
     };
 
+    enum class IndexBitCount{
+        Bit16,
+        Bit32
+    };
+
     struct DescriptorSetUsage {
-        inline DescriptorSetUsage(uint32_t setLocation, vk::DescriptorSet vulkanHandle) noexcept
-            : setLocation(setLocation), vulkanHandle(vulkanHandle) {}
+        inline DescriptorSetUsage(uint32_t setLocation, vk::DescriptorSet vulkanHandle,
+								  const std::vector<uint32_t>& dynamicOffsets = {}) noexcept
+            : setLocation(setLocation), vulkanHandle(vulkanHandle), dynamicOffsets(dynamicOffsets) {}
 
-        const uint32_t          setLocation;
-        const vk::DescriptorSet vulkanHandle;
+        const uint32_t          	setLocation;
+        const vk::DescriptorSet 	vulkanHandle;
+        const std::vector<uint32_t> dynamicOffsets;
     };
 
     struct Mesh {
-        inline Mesh(std::vector<VertexBufferBinding> vertexBufferBindings, vk::Buffer indexBuffer, size_t indexCount) noexcept
-            : vertexBufferBindings(vertexBufferBindings), indexBuffer(indexBuffer), indexCount(indexCount){}
+
+        inline Mesh(){}
+
+        inline Mesh(
+            std::vector<VertexBufferBinding>    vertexBufferBindings,
+            vk::Buffer                          indexBuffer,
+            size_t                              indexCount,
+            IndexBitCount                       indexBitCount = IndexBitCount::Bit16) noexcept
+            :
+            vertexBufferBindings(vertexBufferBindings),
+            indexBuffer(indexBuffer),
+            indexCount(indexCount),
+            indexBitCount(indexBitCount) {}
 
         std::vector<VertexBufferBinding>    vertexBufferBindings;
         vk::Buffer                          indexBuffer;
         size_t                              indexCount;
-    };
-
-    struct PushConstantData {
-        inline PushConstantData(void* data, size_t sizePerDrawcall) : data(data), sizePerDrawcall(sizePerDrawcall) {}
+        IndexBitCount                       indexBitCount;
 
-        void* data;
-        size_t  sizePerDrawcall;
     };
 
     struct DrawcallInfo {
@@ -49,7 +63,24 @@ namespace vkcv {
         const DrawcallInfo      &drawcall,
         vk::CommandBuffer       cmdBuffer,
         vk::PipelineLayout      pipelineLayout,
-        const PushConstantData  &pushConstantData,
+        const PushConstants     &pushConstants,
         const size_t            drawcallIndex);
 
-}
\ No newline at end of file
+    void InitMeshShaderDrawFunctions(vk::Device device);
+
+    struct MeshShaderDrawcall {
+        inline MeshShaderDrawcall(const std::vector<DescriptorSetUsage> descriptorSets, uint32_t taskCount)
+            : descriptorSets(descriptorSets), taskCount(taskCount) {}
+
+        std::vector<DescriptorSetUsage> descriptorSets;
+        uint32_t                        taskCount;
+    };
+
+    void recordMeshShaderDrawcall(
+        vk::CommandBuffer                       cmdBuffer,
+        vk::PipelineLayout                      pipelineLayout,
+        const PushConstants&                 pushConstantData,
+        const uint32_t                          pushConstantOffset,
+        const MeshShaderDrawcall&               drawcall,
+        const uint32_t                          firstTask);
+}
diff --git a/include/vkcv/Event.hpp b/include/vkcv/Event.hpp
index da5cbc72fbb3eee3a71a35c1da6fe32dff06b057..604e3a444dc3bffd2841cb69cd99746d59af523d 100644
--- a/include/vkcv/Event.hpp
+++ b/include/vkcv/Event.hpp
@@ -1,7 +1,12 @@
 #pragma once
 
 #include <functional>
+
+#ifndef __MINGW32__
 #include <mutex>
+#endif
+
+#include <vector>
 
 namespace vkcv {
 	
@@ -27,7 +32,10 @@ namespace vkcv {
     private:
         std::vector< event_function<T...> > m_functions;
         uint32_t m_id_counter;
+	
+#ifndef __MINGW32__
 		std::mutex m_mutex;
+#endif
 
     public:
 
@@ -75,14 +83,18 @@ namespace vkcv {
          * locks the event so its function handles won't be called
          */
         void lock() {
+#ifndef __MINGW32__
 			m_mutex.lock();
+#endif
         }
 	
 		/**
 		* unlocks the event so its function handles can be called after locking
 		*/
         void unlock() {
+#ifndef __MINGW32__
 			m_mutex.unlock();
+#endif
         }
 
         explicit event(bool locked = false) {
diff --git a/include/vkcv/File.hpp b/include/vkcv/File.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..06f1c48593853147140b2c8c68c675d52c9dfaec
--- /dev/null
+++ b/include/vkcv/File.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <filesystem>
+
+namespace vkcv {
+	
+	std::filesystem::path generateTemporaryFilePath();
+	
+	std::filesystem::path generateTemporaryDirectoryPath();
+	
+}
diff --git a/include/vkcv/Image.hpp b/include/vkcv/Image.hpp
index 85ab2b81e2718b3890ba361c988d5db0e40e84c7..3fca76f70315c0e08e404d7acd8c2010a3501c24 100644
--- a/include/vkcv/Image.hpp
+++ b/include/vkcv/Image.hpp
@@ -31,21 +31,21 @@ namespace vkcv {
 		uint32_t getDepth() const;
 
 		[[nodiscard]]
-		vkcv::ImageHandle getHandle() const;
+		const vkcv::ImageHandle& getHandle() const;
 
 		[[nodiscard]]
 		uint32_t getMipCount() const;
 
 		void switchLayout(vk::ImageLayout newLayout);
 		
-		void fill(void* data, size_t size = SIZE_MAX);
+		void fill(const void* data, size_t size = SIZE_MAX);
 		void generateMipChainImmediate();
 		void recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream);
 	private:
 	    // TODO: const qualifier removed, very hacky!!!
 	    //  Else you cannot recreate an image. Pls fix.
 		ImageManager*       m_manager;
-		ImageHandle   m_handle;
+		ImageHandle   		m_handle;
 
 		Image(ImageManager* manager, const ImageHandle& handle);
 		
diff --git a/include/vkcv/Logger.hpp b/include/vkcv/Logger.hpp
index d484711f642506926b1281a830fb2c9caf8240a2..bb60561e80baadfcac4956223d9313893547068f 100644
--- a/include/vkcv/Logger.hpp
+++ b/include/vkcv/Logger.hpp
@@ -5,6 +5,7 @@
 namespace vkcv {
 	
 	enum class LogLevel {
+		RAW_INFO,
 		INFO,
 		WARNING,
 		ERROR
@@ -12,6 +13,7 @@ namespace vkcv {
 	
 	constexpr auto getLogOutput(LogLevel level) {
 		switch (level) {
+			case LogLevel::RAW_INFO:
 			case LogLevel::INFO:
 				return stdout;
 			default:
@@ -21,6 +23,7 @@ namespace vkcv {
 	
 	constexpr const char* getLogName(LogLevel level) {
 		switch (level) {
+			case LogLevel::RAW_INFO:
 			case LogLevel::INFO:
 				return "INFO";
 			case LogLevel::WARNING:
@@ -41,24 +44,35 @@ namespace vkcv {
 #define __PRETTY_FUNCTION__ __FUNCSIG__
 #endif
 
-#define vkcv_log(level, ...) {      \
-  char output_message [             \
-    VKCV_DEBUG_MESSAGE_LEN          \
-  ];                                \
-  snprintf(                         \
-    output_message,                 \
-    VKCV_DEBUG_MESSAGE_LEN,         \
-    __VA_ARGS__                     \
-  );                                \
-  fprintf(                          \
-    getLogOutput(level),            \
-    "[%s]: %s [%s, line %d: %s]\n", \
-  	vkcv::getLogName(level),        \
-    output_message,                 \
-    __FILE__,                       \
-    __LINE__,                       \
-    __PRETTY_FUNCTION__             \
-  );                                \
+#define vkcv_log(level, ...) {             \
+  char output_message [                    \
+    VKCV_DEBUG_MESSAGE_LEN                 \
+  ];                                       \
+  snprintf(                                \
+    output_message,                        \
+    VKCV_DEBUG_MESSAGE_LEN,                \
+    __VA_ARGS__                            \
+  );                                       \
+  auto output = getLogOutput(level);       \
+  if (level != vkcv::LogLevel::RAW_INFO) { \
+    fprintf(                               \
+      output,                              \
+      "[%s]: %s [%s, line %d: %s]\n",      \
+      vkcv::getLogName(level),             \
+      output_message,                      \
+      __FILE__,                            \
+      __LINE__,                            \
+      __PRETTY_FUNCTION__                  \
+    );                                     \
+  } else {                                 \
+    fprintf(                               \
+      output,                              \
+      "[%s]: %s\n",                        \
+      vkcv::getLogName(level),             \
+      output_message                       \
+    );                                     \
+  }                                        \
+  fflush(output);                          \
 }
 
 #else
diff --git a/include/vkcv/PushConstants.hpp b/include/vkcv/PushConstants.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d974fbe6241daf948b13929305fb24aff5ec06f5
--- /dev/null
+++ b/include/vkcv/PushConstants.hpp
@@ -0,0 +1,93 @@
+#pragma once
+
+#include <vector>
+#include <vulkan/vulkan.hpp>
+
+#include "Logger.hpp"
+
+namespace vkcv {
+	
+	class PushConstants {
+	private:
+		std::vector<uint8_t> m_data;
+		size_t m_sizePerDrawcall;
+		
+	public:
+		template<typename T>
+		PushConstants() : PushConstants(sizeof(T)) {}
+		
+		explicit PushConstants(size_t sizePerDrawcall) :
+		m_data(),
+		m_sizePerDrawcall(sizePerDrawcall) {}
+		
+		PushConstants(const PushConstants& other) = default;
+		PushConstants(PushConstants&& other) = default;
+		
+		~PushConstants() = default;
+		
+		PushConstants& operator=(const PushConstants& other) = default;
+		PushConstants& operator=(PushConstants&& other) = default;
+		
+		[[nodiscard]]
+		size_t getSizePerDrawcall() const {
+			return m_sizePerDrawcall;
+		}
+		
+		[[nodiscard]]
+		size_t getFullSize() const {
+			return m_data.size();
+		}
+		
+		[[nodiscard]]
+		size_t getDrawcallCount() const {
+			return (m_data.size() / m_sizePerDrawcall);
+		}
+		
+		void clear() {
+			m_data.clear();
+		}
+		
+		template<typename T = uint8_t>
+		bool appendDrawcall(const T& value) {
+			if (sizeof(T) != m_sizePerDrawcall) {
+				vkcv_log(LogLevel::WARNING, "Size (%lu) of value does not match the specified size per drawcall (%lu)",
+						 sizeof(value), m_sizePerDrawcall);
+				return false;
+			}
+			
+			const size_t offset = m_data.size();
+			m_data.resize(offset + sizeof(value));
+			std::memcpy(m_data.data() + offset, &value, sizeof(value));
+			return true;
+		}
+		
+		template<typename T = uint8_t>
+		T& getDrawcall(size_t index) {
+			const size_t offset = (index * m_sizePerDrawcall);
+			return *reinterpret_cast<T*>(m_data.data() + offset);
+		}
+		
+		template<typename T = uint8_t>
+		const T& getDrawcall(size_t index) const {
+			const size_t offset = (index * m_sizePerDrawcall);
+			return *reinterpret_cast<const T*>(m_data.data() + offset);
+		}
+		
+		[[nodiscard]]
+		const void* getDrawcallData(size_t index) const {
+			const size_t offset = (index * m_sizePerDrawcall);
+			return reinterpret_cast<const void*>(m_data.data() + offset);
+		}
+		
+		[[nodiscard]]
+		const void* getData() const {
+			if (m_data.empty()) {
+				return nullptr;
+			} else {
+				return m_data.data();
+			}
+		}
+		
+	};
+	
+}
diff --git a/include/vkcv/QueueManager.hpp b/include/vkcv/QueueManager.hpp
index ac043b2d351014ea79fcae0d0fc439bb64a87b72..0919d20d8e07fee67ceb2f393c29b4a53c51b857 100644
--- a/include/vkcv/QueueManager.hpp
+++ b/include/vkcv/QueueManager.hpp
@@ -32,8 +32,8 @@ namespace vkcv {
         const std::vector<Queue> &getTransferQueues() const;
 
         static void queueCreateInfosQueueHandles(vk::PhysicalDevice &physicalDevice,
-                std::vector<float> &queuePriorities,
-                std::vector<vk::QueueFlagBits> &queueFlags,
+                const std::vector<float> &queuePriorities,
+                const std::vector<vk::QueueFlagBits> &queueFlags,
                 std::vector<vk::DeviceQueueCreateInfo> &queueCreateInfos,
                 std::vector<std::pair<int, int>> &queuePairsGraphics,
                 std::vector<std::pair<int, int>> &queuePairsCompute,
diff --git a/include/vkcv/ShaderStage.hpp b/include/vkcv/ShaderStage.hpp
index d671b87b55ac7a5a8926e479c77fa991dd90c665..773c8ca34e17e45b81deeca1f38a2b1be8a6821b 100644
--- a/include/vkcv/ShaderStage.hpp
+++ b/include/vkcv/ShaderStage.hpp
@@ -3,7 +3,7 @@
 #include <vulkan/vulkan.hpp>
 
 namespace vkcv {
-	
+
 	enum class ShaderStage : VkShaderStageFlags {
 		VERTEX = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eVertex),
 		TESS_CONTROL = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eTessellationControl),
@@ -34,5 +34,4 @@ namespace vkcv {
 	constexpr ShaderStages operator~(ShaderStage stage) noexcept {
 		return ~(ShaderStages(stage));
 	}
-	
 }
diff --git a/lib/VulkanMemoryAllocator-Hpp b/lib/VulkanMemoryAllocator-Hpp
new file mode 160000
index 0000000000000000000000000000000000000000..3a61240a5354ce56c222969a69825aabb6ba0a21
--- /dev/null
+++ b/lib/VulkanMemoryAllocator-Hpp
@@ -0,0 +1 @@
+Subproject commit 3a61240a5354ce56c222969a69825aabb6ba0a21
diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt
index 28b2184b2a83515a514f1428733bcf8cf1499633..4b576e7119ebe769eafd1b6abb033b4fb02a3ec1 100644
--- a/modules/CMakeLists.txt
+++ b/modules/CMakeLists.txt
@@ -1,8 +1,11 @@
 
 # Add new modules here:
 add_subdirectory(asset_loader)
-add_subdirectory(material)
 add_subdirectory(camera)
 add_subdirectory(gui)
+add_subdirectory(material)
+add_subdirectory(meshlet)
+add_subdirectory(scene)
 add_subdirectory(shader_compiler)
 add_subdirectory(testing)
+add_subdirectory(upscaling)
diff --git a/modules/asset_loader/CMakeLists.txt b/modules/asset_loader/CMakeLists.txt
index c5a1fd0eb9620d3a95af1c52a9e7456337047c7b..870c16279b1578224a966a4a123a465413333555 100644
--- a/modules/asset_loader/CMakeLists.txt
+++ b/modules/asset_loader/CMakeLists.txt
@@ -31,10 +31,10 @@ include(config/FX_GLTF.cmake)
 include(config/STB.cmake)
 
 # link the required libraries to the module
-target_link_libraries(vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv)
+target_link_libraries(vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv ${vkcv_libraries})
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(vkcv_asset_loader SYSTEM BEFORE PRIVATE ${vkcv_asset_loader_includes})
+target_include_directories(vkcv_asset_loader SYSTEM BEFORE PRIVATE ${vkcv_asset_loader_includes} ${vkcv_includes})
 
 # add the own include directory for public headers
 target_include_directories(vkcv_asset_loader BEFORE PUBLIC ${vkcv_asset_loader_include})
diff --git a/modules/asset_loader/include/vkcv/asset/asset_loader.hpp b/modules/asset_loader/include/vkcv/asset/asset_loader.hpp
index 471870fb1e5af3d3c448a66611d9754db9597f85..25d7d5b36d00bad8587fbe082f8ebd041d84fde6 100644
--- a/modules/asset_loader/include/vkcv/asset/asset_loader.hpp
+++ b/modules/asset_loader/include/vkcv/asset/asset_loader.hpp
@@ -11,17 +11,7 @@
 #include <cstdint>
 #include <filesystem>
 
-/** These macros define limits of the following structs. Implementations can
- * test against these limits when performing sanity checks. The main constraint
- * expressed is that of the data type: Material indices are identified by a
- * uint8_t in the VertexGroup struct, so there can't be more than UINT8_MAX
- * materials in the mesh. Should these limits be too narrow, the data type has
- * to be changed, but the current ones should be generous enough for most use
- * cases. */
-#define MAX_MATERIALS_PER_MESH UINT8_MAX
-#define MAX_VERTICES_PER_VERTEX_GROUP UINT32_MAX
-
-/** LOADING MESHES
+/* LOADING MESHES
  * The description of meshes is a hierarchy of structures with the Mesh at the
  * top.
  *
@@ -46,53 +36,89 @@
 
 namespace vkcv::asset {
 
-/** This enum matches modes in fx-gltf, the library returns a standard mode
- * (TRIANGLES) if no mode is given in the file. */
+/**
+ * These return codes are limited to the asset loader module. If unified return
+ * codes are defined for the vkcv framework, these will be used instead.
+ */
+#define ASSET_ERROR 0
+#define ASSET_SUCCESS 1
+
+/**
+ * This enum matches modes in fx-gltf, the library returns a standard mode
+ * (TRIANGLES) if no mode is given in the file.
+ */
 enum class PrimitiveMode : uint8_t {
-	POINTS=0, LINES, LINELOOP, LINESTRIP, TRIANGLES, TRIANGLESTRIP,
-	TRIANGLEFAN
+	POINTS = 0,
+	LINES = 1,
+	LINELOOP = 2,
+	LINESTRIP = 3,
+	TRIANGLES = 4,
+	TRIANGLESTRIP = 5,
+	TRIANGLEFAN = 6
 };
 
-/** The indices in the index buffer can be of different bit width. */
-enum class IndexType : uint8_t { UNDEFINED=0, UINT8=1, UINT16=2, UINT32=3 };
-
-typedef struct {
-	// TODO define struct for samplers (low priority)
-	// NOTE: glTF defines samplers based on OpenGL, which can not be
-	// directly translated to Vulkan. Specifically, OpenGL (and glTF)
-	// define a different set of Min/Mag-filters than Vulkan.
-} Sampler;
-
-/** struct for defining the loaded texture */
-typedef struct {
-	int sampler;		// index into the sampler array of the Scene
-	uint8_t channels;	// number of channels
-	uint16_t w, h;		// width and height of the texture
-	std::vector<uint8_t> data;	// binary data of the decoded texture
-} Texture;
+/**
+ * The indices in the index buffer can be of different bit width.
+ */
+enum class IndexType : uint8_t {
+	UNDEFINED=0,
+	UINT8=1,
+	UINT16=2,
+	UINT32=3
+};
 
-/** The asset loader module only supports the PBR-MetallicRoughness model for
- * materials.*/
-typedef struct {
-	uint16_t textureMask;	// bit mask with active texture targets
-	// Indices into the Array.textures array
-	int baseColor, metalRough, normal, occlusion, emissive;
-	// Scaling factors for each texture target
-	struct { float r, g, b, a; } baseColorFactor;
-	float metallicFactor, roughnessFactor;
-	float normalScale;
-	float occlusionStrength;
-	struct { float r, g, b; } emissiveFactor;
-} Material;
+/**
+ * This struct defines a sampler for a texture object. All values here can
+ * directly be passed to VkSamplerCreateInfo.
+ * NOTE that glTF defines samplers based on OpenGL, which can not be directly
+ * translated to Vulkan. The vkcv::asset::Sampler struct defined here adheres
+ * to the Vulkan spec, having alerady translated the flags from glTF to Vulkan.
+ * Since glTF does not specify border sampling for more than two dimensions,
+ * the addressModeW is hardcoded to a default: VK_SAMPLER_ADDRESS_MODE_REPEAT.
+ */
+struct Sampler {
+	int minFilter, magFilter;
+	int mipmapMode;
+	float minLOD, maxLOD;
+	int addressModeU, addressModeV, addressModeW;
+};
 
-/** Flags for the bit-mask in the Material struct. To check if a material has a
+/**
+ * This struct describes a (partially) loaded texture.
+ * The data member is not populated after calling probeScene() but only when
+ * calling loadMesh(), loadScene() or loadTexture(). Note that textures are
+ * currently always loaded with 4 channels as RGBA, even if the image has just
+ * RGB or is grayscale. In the case where the glTF-file does not provide a URI
+ * but references a buffer view for the raw data, the path member will be empty
+ * even though the rest is initialized properly.
+ * NOTE: Loading textures without URI is untested.
+ */
+struct Texture {
+	std::filesystem::path path;	// URI to the encoded texture data
+	int sampler;			// index into the sampler array of the Scene
+	
+	union { int width; int w; };
+	union { int height; int h; };
+	int channels;
+	
+	std::vector<uint8_t> data;	// binary data of the decoded texture
+};
+
+/**
+ * Flags for the bit-mask in the Material struct. To check if a material has a
  * certain texture target, you can use the hasTexture() function below, passing
- * the material struct and the enum. */
+ * the material struct and the enum.
+ */
 enum class PBRTextureTarget {
-	baseColor=1, metalRough=2, normal=4, occlusion=8, emissive=16
+	baseColor=1,
+	metalRough=2,
+	normal=4,
+	occlusion=8,
+	emissive=16
 };
 
-/** This macro translates the index of an enum in the defined order to an
+/**
+ * This macro translates the index of an enum in the defined order to an
  * integer with a single bit set in the corresponding place. It is used for
  * working with the bitmask of texture targets ("textureMask") in the Material
  * struct:
@@ -103,100 +129,196 @@ enum class PBRTextureTarget {
  * contact with bit-level operations. */
 #define bitflag(ENUM) (0x1u << ((unsigned)(ENUM)))
 
-/** To signal that a certain texture target is active in a Material struct, its
- * bit is set in the textureMask. You can use this function to check that:
- * Material mat = ...;
- * if (materialHasTexture(&mat, baseColor)) {...} */
-bool materialHasTexture(const Material *const m, const PBRTextureTarget t);
+/**
+ * The asset loader module only supports the PBR-MetallicRoughness model for
+ * materials.
+ */
+struct Material {
+	uint16_t textureMask;	// bit mask with active texture targets
+	
+	// Indices into the Scene.textures vector
+	int baseColor, metalRough, normal, occlusion, emissive;
+	
+	// Scaling factors for each texture target
+	struct { float r, g, b, a; } baseColorFactor;
+	float metallicFactor, roughnessFactor;
+	float normalScale;
+	float occlusionStrength;
+	struct { float r, g, b; } emissiveFactor;
+
+	/**
+	 * To signal that a certain texture target is active in this Material
+	 * struct, its bit is set in the textureMask. You can use this function
+	 * to check that:
+	 * if (myMaterial.hasTexture(baseColor)) {...}
+	 *
+	 * @param t The target to query for
+	 * @return Boolean to signal whether the texture target is active in
+	 * the material.
+	 */
+	bool hasTexture(PBRTextureTarget target) const;
+};
 
-/** With these enums, 0 is reserved to signal uninitialized or invalid data. */
+/* With these enums, 0 is reserved to signal uninitialized or invalid data. */
 enum class PrimitiveType : uint32_t {
     UNDEFINED = 0,
     POSITION = 1,
     NORMAL = 2,
     TEXCOORD_0 = 3,
     TEXCOORD_1 = 4,
-    TANGENT = 5
+    TANGENT = 5,
+    COLOR_0 = 6,
+    COLOR_1 = 7,
+    JOINTS_0 = 8,
+    WEIGHTS_0 = 9
 };
 
-/** These integer values are used the same way in OpenGL, Vulkan and glTF. This
+/**
+ * These integer values are used the same way in OpenGL, Vulkan and glTF. This
  * enum is not needed for translation, it's only for the programmers
  * convenience (easier to read in if/switch statements etc). While this enum
  * exists in (almost) the same definition in the fx-gltf library, we want to
- * avoid exposing that dependency, thus it is re-defined here. */
+ * avoid exposing that dependency, thus it is re-defined here.
+ */
 enum class ComponentType : uint16_t {
-    NONE = 0, INT8 = 5120, UINT8 = 5121, INT16 = 5122, UINT16 = 5123,
-    UINT32 = 5125, FLOAT32 = 5126
+    NONE = 0,
+    INT8 = 5120,
+    UINT8 = 5121,
+    INT16 = 5122,
+    UINT16 = 5123,
+    UINT32 = 5125,
+    FLOAT32 = 5126
 };
 
-/** This struct describes one vertex attribute of a vertex buffer. */
-typedef struct {
+/**
+ * This struct describes one vertex attribute of a vertex buffer.
+ */
+struct VertexAttribute {
     PrimitiveType type;			// POSITION, NORMAL, ...
+    
     uint32_t offset;			// offset in bytes
     uint32_t length;			// length of ... in bytes
     uint32_t stride;			// stride in bytes
-    ComponentType componentType;		// eg. 5126 for float
-    uint8_t  componentCount;	// eg. 3 for vec3
-} VertexAttribute;
+    
+    ComponentType componentType;	// eg. 5126 for float
+    uint8_t  componentCount;		// eg. 3 for vec3
+};
 
-/** This struct represents one (possibly the only) part of a mesh. There is
+/**
+ * This struct represents one (possibly the only) part of a mesh. There is
  * always one vertexBuffer and zero or one indexBuffer (indexed rendering is
  * common but not always used). If there is no index buffer, this is indicated
  * by indexBuffer.data being empty. Each vertex buffer can have one or more
- * vertex attributes. */
-typedef struct {
+ * vertex attributes.
+ */
+struct VertexGroup {
 	enum PrimitiveMode mode;	// draw as points, lines or triangle?
-	size_t numIndices, numVertices;
+	size_t numIndices;
+	size_t numVertices;
+	
 	struct {
 		enum IndexType type;	// data type of the indices
 		std::vector<uint8_t> data; // binary data of the index buffer
 	} indexBuffer;
+	
 	struct {
 		std::vector<uint8_t> data; // binary data of the vertex buffer
 		std::vector<VertexAttribute> attributes; // description of one
 	} vertexBuffer;
+	
 	struct { float x, y, z; } min;	// bounding box lower left
 	struct { float x, y, z; } max;	// bounding box upper right
+	
 	int materialIndex;		// index to one of the materials
-} VertexGroup;
+};
 
-/** This struct represents a single mesh as it was loaded from a glTF file. It
+/**
+ * This struct represents a single mesh as it was loaded from a glTF file. It
  * consists of at least one VertexGroup, which then references other resources
- * such as Materials. */
-typedef struct {
+ * such as Materials.
+ */
+struct Mesh {
 	std::string name;
 	std::array<float, 16> modelMatrix;
 	std::vector<int> vertexGroups;
-} Mesh;
+};
 
-/** The scene struct is simply a collection of objects in the scene as well as
+/**
+ * The scene struct is simply a collection of objects in the scene as well as
  * the resources used by those objects.
- * For now the only type of object are the meshes and they are represented in a
- * flat array.
- * Note that parent-child relations are not yet possible. */
-typedef struct {
+ * Note that parent-child relations are not yet possible.
+ */
+struct Scene {
 	std::vector<Mesh> meshes;
 	std::vector<VertexGroup> vertexGroups;
 	std::vector<Material> materials;
 	std::vector<Texture> textures;
 	std::vector<Sampler> samplers;
-} Scene;
+	std::vector<std::string> uris;
+};
 
 /**
- * Load every mesh from the glTF file, as well as materials and textures.
+ * Parse the given glTF file and create a shallow description of the content.
+ * Only the meta-data of the objects in the scene is loaded, not the binary
+ * content. The rationale is to provide a means of probing the content of a
+ * glTF file without the costly process of loading and decoding large amounts
+ * of data. The returned Scene struct can be used to search for specific meshes
+ * in the scene, that can then be loaded on their own using the loadMesh()
+ * function. Note that the Scene struct received as output argument will be
+ * overwritten by this function.
+ * After this function completes, the returned Scene struct is completely
+ * initialized and all information is final, except for the missing binary
+ * data. This means that indices to vectors will remain valid even when the
+ * shallow scene struct is filled with data by loadMesh().
+ * Note that for URIs only (local) filesystem paths are supported, no
+ * URLs using network protocols etc.
  *
- * @param path must be the path to a glTF or glb file.
+ * @param path	must be the path to a glTF- or glb-file.
+ * @param scene	is a reference to a Scene struct that will be filled with the
+ * 	meta-data of all objects described in the glTF file.
+ * @return ASSET_ERROR on failure, otherwise ASSET_SUCCESS
+ */
+int probeScene(const std::filesystem::path &path, Scene &scene);
+
+/**
+ * This function loads a single mesh from the given file and adds it to the
+ * given scene. The scene must already be initialized (via probeScene()).
+ * The mesh_index refers to the Scenes meshes array and identifies the mesh to
+ * load. To find the mesh you want, iterate over the probed scene and check the
+ * meshes details (eg. name).
+ * Besides the mesh, this function will also add any associated data to the
+ * Scene struct such as Materials and Textures required by the Mesh.
+ *
+ * @param path	must be the path to a glTF- or glb-file.
+ * @param scene	is the scene struct to which the results will be written.
+ * @return ASSET_ERROR on failure, otherwise ASSET_SUCCESS
+ */
+int loadMesh(Scene &scene, int mesh_index);
+
+/**
+ * Load every mesh from the glTF file, as well as materials, textures and other
+ * associated objects.
+ *
+ * @param path	must be the path to a glTF- or glb-file.
  * @param scene is a reference to a Scene struct that will be filled with the
  * 	content of the glTF file being loaded.
- * */
-int loadScene(const std::string &path, Scene &scene);
-
-struct TextureData {
-    int width;
-    int height;
-    int componentCount;
-    std::vector<char*> data;
-};
-TextureData loadTexture(const std::filesystem::path& path);
+ * @return ASSET_ERROR on failure, otherwise ASSET_SUCCESS
+ */
+int loadScene(const std::filesystem::path &path, Scene &scene);
+
+/**
+ * Simply loads a single image at the given path and returns a Texture
+ * struct describing it. This is for special use cases only (eg.
+ * loading a font atlas) and not meant to be used for regular assets.
+ * The sampler is set to -1, signalling that this Texture was loaded
+ * outside the context of a glTF-file.
+ * If there was an error loading or decoding the image, the returned struct
+ * will be cleared to all 0 with path and data being empty; make sure to always
+ * check that !data.empty() before using the struct.
+ *
+ * @param path	must be the path to an image file.
+ * @return	Texture struct describing the loaded image.
+ */
+Texture loadTexture(const std::filesystem::path& path);
 
-}
+}	// end namespace vkcv::asset
diff --git a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
index e3d3072543bd33e1f5a67ae7dac61d229005947a..571d965a400de7197b6fb46f163c4099a5b353f1 100644
--- a/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
+++ b/modules/asset_loader/src/vkcv/asset/asset_loader.cpp
@@ -1,387 +1,784 @@
 #include "vkcv/asset/asset_loader.hpp"
 #include <iostream>
 #include <string.h>	// memcpy(3)
+#include <set>
 #include <stdlib.h>	// calloc(3)
+#include <vulkan/vulkan.hpp>
 #include <fx/gltf.h>
 #include <stb_image.h>
 #include <vkcv/Logger.hpp>
 #include <algorithm>
 
+
 namespace vkcv::asset {
 
-/**
-* convert the accessor type from the fx-gltf library to an unsigned int
-* @param type
-* @return unsigned integer representation
-*/
-// TODO Return proper error code (we need to define those as macros or enums,
-// will discuss during the next core meeting if that should happen on the scope
-// of the vkcv framework or just this module)
-uint8_t convertTypeToInt(const fx::gltf::Accessor::Type type) {
-	switch (type) {
-	case fx::gltf::Accessor::Type::None :
-		return 0;
-	case fx::gltf::Accessor::Type::Scalar :
-		return 1;
-	case fx::gltf::Accessor::Type::Vec2 :
-		return 2;
-	case fx::gltf::Accessor::Type::Vec3 :
-		return 3;
-	case fx::gltf::Accessor::Type::Vec4 :
-		return 4;
-	default: return 10; // TODO add cases for matrices (or maybe change the type in the struct itself)
+	/**
+	 * This function unrolls nested exceptions via recursion and prints them
+	 * @param e	The exception being thrown
+	 * @param path	The path to the file that was responsible for the exception
+	 */
+	static void recurseExceptionPrint(const std::exception& e, const std::string &path) {
+		vkcv_log(LogLevel::ERROR, "Loading file %s: %s", path.c_str(), e.what());
+		
+		try {
+			std::rethrow_if_nested(e);
+		} catch (const std::exception& nested) {
+			recurseExceptionPrint(nested, path);
+		}
 	}
-}
 
-/**
- * This function unrolls nested exceptions via recursion and prints them
- * @param e error code
- * @param path path to file that is responsible for error
- */
-void print_what (const std::exception& e, const std::string &path) {
-	vkcv_log(LogLevel::ERROR, "Loading file %s: %s",
-			 path.c_str(), e.what());
+	/**
+	 * Returns the component count for an accessor type of the fx-gltf library.
+	 * @param type	The accessor type
+	 * @return	An unsigned integer count
+	 */
+	static uint32_t getComponentCount(const fx::gltf::Accessor::Type type) {
+		switch (type) {
+		case fx::gltf::Accessor::Type::Scalar:
+			return 1;
+		case fx::gltf::Accessor::Type::Vec2:
+			return 2;
+		case fx::gltf::Accessor::Type::Vec3:
+			return 3;
+		case fx::gltf::Accessor::Type::Vec4:
+		case fx::gltf::Accessor::Type::Mat2:
+			return 4;
+		case fx::gltf::Accessor::Type::Mat3:
+			return 9;
+		case fx::gltf::Accessor::Type::Mat4:
+			return 16;
+		case fx::gltf::Accessor::Type::None:
+		default:
+			return 0;
+		}
+	}
 	
-	try {
-		std::rethrow_if_nested(e);
-	} catch (const std::exception& nested) {
-		print_what(nested, path);
+	static uint32_t getComponentSize(ComponentType type) {
+		switch (type) {
+			case ComponentType::INT8:
+			case ComponentType::UINT8:
+				return 1;
+			case ComponentType::INT16:
+			case ComponentType::UINT16:
+				return 2;
+			case ComponentType::UINT32:
+			case ComponentType::FLOAT32:
+				return 4;
+			default:
+				return 0;
+		}
 	}
-}
 
-/** Translate the component type used in the index accessor of fx-gltf to our
- * enum for index type. The reason we have defined an incompatible enum that
- * needs translation is that only a subset of component types is valid for
- * indices and we want to catch these incompatibilities here. */
-enum IndexType getIndexType(const enum fx::gltf::Accessor::ComponentType &t)
-{
-	switch (t) {
-	case fx::gltf::Accessor::ComponentType::UnsignedByte:
-		return IndexType::UINT8;
-	case fx::gltf::Accessor::ComponentType::UnsignedShort:
-		return IndexType::UINT16;
-	case fx::gltf::Accessor::ComponentType::UnsignedInt:
-		return IndexType::UINT32;
-	default:
-		vkcv_log(LogLevel::ERROR, "Index type not supported: %u", static_cast<uint16_t>(t));
-		return IndexType::UNDEFINED;
+	/**
+	 * Translate the component type used in the index accessor of fx-gltf to our
+	 * enum for index type. The reason we have defined an incompatible enum that
+	 * needs translation is that only a subset of component types is valid for
+	 * indices and we want to catch these incompatibilities here.
+	 * @param t	The component type
+	 * @return 	The vkcv::IndexType enum representation
+	 */
+	enum IndexType getIndexType(const enum fx::gltf::Accessor::ComponentType &type) {
+		switch (type) {
+		case fx::gltf::Accessor::ComponentType::UnsignedByte:
+			return IndexType::UINT8;
+		case fx::gltf::Accessor::ComponentType::UnsignedShort:
+			return IndexType::UINT16;
+		case fx::gltf::Accessor::ComponentType::UnsignedInt:
+			return IndexType::UINT32;
+		default:
+			vkcv_log(LogLevel::ERROR, "Index type not supported: %u", static_cast<uint16_t>(type));
+			return IndexType::UNDEFINED;
+		}
 	}
-}
-
-/**
- * This function computes the modelMatrix out of the data given in the gltf file. It also checks, whether a modelMatrix was given.
- * @param translation possible translation vector (default 0,0,0)
- * @param scale possible scale vector (default 1,1,1)
- * @param rotation possible rotation, given in quaternion (default 0,0,0,1)
- * @param matrix possible modelmatrix (default identity)
- * @return model Matrix as an array of floats
- */
-std::array<float, 16> computeModelMatrix(std::array<float, 3> translation, std::array<float, 3> scale, std::array<float, 4> rotation, std::array<float, 16> matrix){
-    std::array<float, 16> modelMatrix = {1,0,0,0,
-                                         0,1,0,0,
-                                         0,0,1,0,
-                                         0,0,0,1};
-    if (matrix != modelMatrix){
-        return matrix;
-    } else {
-        // translation
-        modelMatrix[3] = translation[0];
-        modelMatrix[7] = translation[1];
-        modelMatrix[11] = translation[2];
-        // rotation and scale
-        auto a = rotation[0];
-        auto q1 = rotation[1];
-        auto q2 = rotation[2];
-        auto q3 = rotation[3];
-
-        modelMatrix[0] = (2 * (a * a + q1 * q1) - 1) * scale[0];
-        modelMatrix[1] = (2 * (q1 * q2 - a * q3)) * scale[1];
-        modelMatrix[2] = (2 * (q1 * q3 + a * q2)) * scale[2];
-
-        modelMatrix[4] = (2 * (q1 * q2 + a * q3)) * scale[0];
-        modelMatrix[5] = (2 * (a * a + q2 * q2) - 1) * scale[1];
-        modelMatrix[6] = (2 * (q2 * q3 - a * q1)) * scale[2];
-
-        modelMatrix[8] = (2 * (q1 * q3 - a * q2)) * scale[0];
-        modelMatrix[9] = (2 * (q2 * q3 + a * q1)) * scale[1];
-        modelMatrix[10] = (2 * (a * a + q3 * q3) - 1) * scale[2];
-
-        // flip y, because GLTF uses y up, but vulkan -y up
-        modelMatrix[5] *= -1;
-
-        return modelMatrix;
-    }
-
-}
-
-bool materialHasTexture(const Material *const m, const PBRTextureTarget t)
-{
-	return m->textureMask & bitflag(t);
-}
-
-int loadScene(const std::string &path, Scene &scene){
-    fx::gltf::Document sceneObjects;
-
-    try {
-        if (path.rfind(".glb", (path.length()-4)) != std::string::npos) {
-            sceneObjects = fx::gltf::LoadFromBinary(path);
-        } else {
-            sceneObjects = fx::gltf::LoadFromText(path);
-        }
-    } catch (const std::system_error &err) {
-        print_what(err, path);
-        return 0;
-    } catch (const std::exception &e) {
-        print_what(e, path);
-        return 0;
-    }
-    size_t pos = path.find_last_of("/");
-    auto dir = path.substr(0, pos);
-
-    // file has to contain at least one mesh
-    if (sceneObjects.meshes.size() == 0) return 0;
-
-    fx::gltf::Accessor posAccessor;
-    std::vector<VertexAttribute> vertexAttributes;
-    std::vector<Material> materials;
-    std::vector<Texture> textures;
-    std::vector<Sampler> samplers;
-    std::vector<Mesh> meshes;
-    std::vector<VertexGroup> vertexGroups;
-    int groupCount = 0;
-
-    Mesh mesh = {};
-
-    for(int i = 0; i < sceneObjects.meshes.size(); i++){
-        std::vector<int> vertexGroupsIndices;
-        fx::gltf::Mesh const &objectMesh = sceneObjects.meshes[i];
-
-        for(int j = 0; j < objectMesh.primitives.size(); j++){
-            fx::gltf::Primitive const &objectPrimitive = objectMesh.primitives[j];
-            vertexAttributes.clear();
-            vertexAttributes.reserve(objectPrimitive.attributes.size());
-
-            for (auto const & attrib : objectPrimitive.attributes) {
-
-                fx::gltf::Accessor accessor =  sceneObjects.accessors[attrib.second];
-                VertexAttribute attribute;
-
-                if (attrib.first == "POSITION") {
-                    attribute.type = PrimitiveType::POSITION;
-                    posAccessor = accessor;
-                } else if (attrib.first == "NORMAL") {
-                    attribute.type = PrimitiveType::NORMAL;
-                } else if (attrib.first == "TEXCOORD_0") {
-                    attribute.type = PrimitiveType::TEXCOORD_0;
-                }
-                else if (attrib.first == "TEXCOORD_1") {
-                    attribute.type = PrimitiveType::TEXCOORD_1;
-                } else if (attrib.first == "TANGENT") {
-                    attribute.type = PrimitiveType::TANGENT;
-                } else {
-                    return 0;
-                }
-
-                attribute.offset = sceneObjects.bufferViews[accessor.bufferView].byteOffset;
-                attribute.length = sceneObjects.bufferViews[accessor.bufferView].byteLength;
-                attribute.stride = sceneObjects.bufferViews[accessor.bufferView].byteStride;
-		        attribute.componentType = static_cast<ComponentType>(accessor.componentType);
-
-                if (convertTypeToInt(accessor.type) != 10) {
-                    attribute.componentCount = convertTypeToInt(accessor.type);
-                } else {
-                    return 0;
-                }
-
-                vertexAttributes.push_back(attribute);
-            }
-
-            IndexType indexType;
-            std::vector<uint8_t> indexBufferData = {};
-            if (objectPrimitive.indices >= 0){ // if there is no index buffer, -1 is returned from fx-gltf
-                const fx::gltf::Accessor &indexAccessor = sceneObjects.accessors[objectPrimitive.indices];
-                const fx::gltf::BufferView &indexBufferView = sceneObjects.bufferViews[indexAccessor.bufferView];
-                const fx::gltf::Buffer &indexBuffer = sceneObjects.buffers[indexBufferView.buffer];
-
-                indexBufferData.resize(indexBufferView.byteLength);
-                {
-                    const size_t off = indexBufferView.byteOffset;
-                    const void *const ptr = ((char*)indexBuffer.data.data()) + off;
-                    if (!memcpy(indexBufferData.data(), ptr, indexBufferView.byteLength)) {
-                        vkcv_log(LogLevel::ERROR, "Copying index buffer data");
-                        return 0;
-                    }
-                }
-
-                indexType = getIndexType(indexAccessor.componentType);
-                if (indexType == IndexType::UNDEFINED){
-                    vkcv_log(LogLevel::ERROR, "Index Type undefined.");
-                    return 0;
-                }
-            }
-
-            const fx::gltf::BufferView&	vertexBufferView	= sceneObjects.bufferViews[posAccessor.bufferView];
-            const fx::gltf::Buffer&		vertexBuffer		= sceneObjects.buffers[vertexBufferView.buffer];
-
-            // only copy relevant part of vertex data
-            uint32_t relevantBufferOffset = std::numeric_limits<uint32_t>::max();
-            uint32_t relevantBufferEnd = 0;
-            for (const auto &attribute : vertexAttributes) {
-                relevantBufferOffset = std::min(attribute.offset, relevantBufferOffset);
-                const uint32_t attributeEnd = attribute.offset + attribute.length;
-                relevantBufferEnd = std::max(relevantBufferEnd, attributeEnd);    // TODO: need to incorporate stride?
-            }
-            const uint32_t relevantBufferSize = relevantBufferEnd - relevantBufferOffset;
-
-            // FIXME: This only works when all vertex attributes are in one buffer
-            std::vector<uint8_t> vertexBufferData;
-            vertexBufferData.resize(relevantBufferSize);
-            {
-                const void *const ptr = ((char*)vertexBuffer.data.data()) + relevantBufferOffset;
-                if (!memcpy(vertexBufferData.data(), ptr, relevantBufferSize)) {
-                    vkcv_log(LogLevel::ERROR, "Copying vertex buffer data");
-                    return 0;
-                }
-            }
-
-            // make vertex attributes relative to copied section
-            for (auto &attribute : vertexAttributes) {
-                attribute.offset -= relevantBufferOffset;
-            }
-
-            const size_t numVertexGroups = objectMesh.primitives.size();
-            vertexGroups.reserve(numVertexGroups);
-
-            vertexGroups.push_back({
-                static_cast<PrimitiveMode>(objectPrimitive.mode),
-                sceneObjects.accessors[objectPrimitive.indices].count,
-                posAccessor.count,
-                {indexType, indexBufferData},
-                {vertexBufferData, vertexAttributes},
-                {posAccessor.min[0], posAccessor.min[1], posAccessor.min[2]},
-                {posAccessor.max[0], posAccessor.max[1], posAccessor.max[2]},
-                static_cast<uint8_t>(objectPrimitive.material)
-            });
-
-            vertexGroupsIndices.push_back(groupCount);
-            groupCount++;
-        }
-
-        mesh.name = sceneObjects.meshes[i].name;
-        mesh.vertexGroups = vertexGroupsIndices;
 
-        meshes.push_back(mesh);
-    }
-
-    for(int m = 0; m < sceneObjects.nodes.size(); m++) {
-        meshes[sceneObjects.nodes[m].mesh].modelMatrix = computeModelMatrix(sceneObjects.nodes[m].translation,
-                                                                            sceneObjects.nodes[m].scale,
-                                                                            sceneObjects.nodes[m].rotation,
-                                                                            sceneObjects.nodes[m].matrix);
-    }
-
-    if (sceneObjects.textures.size() > 0){
-        textures.reserve(sceneObjects.textures.size());
-
-        for(int k = 0; k < sceneObjects.textures.size(); k++){
-            const fx::gltf::Texture &tex = sceneObjects.textures[k];
-            const fx::gltf::Image &img = sceneObjects.images[tex.source];
-            std::string img_uri = dir + "/" + img.uri;
-            int w, h, c;
-            uint8_t *data = stbi_load(img_uri.c_str(), &w, &h, &c, 4);
-            c = 4;	// FIXME hardcoded to always have RGBA channel layout
-            if (!data) {
-                vkcv_log(LogLevel::ERROR, "Loading texture image data.")
-                return 0;
-            }
-            const size_t byteLen = w * h * c;
-
-            std::vector<uint8_t> imgdata;
-            imgdata.resize(byteLen);
-            if (!memcpy(imgdata.data(), data, byteLen)) {
-                vkcv_log(LogLevel::ERROR, "Copying texture image data")
-                free(data);
-                return 0;
-            }
-            free(data);
+	/**
+	 * This function fills the array of vertex attributes of a VertexGroup (usually
+	 * part of a vkcv::asset::Mesh) object based on the description of attributes
+	 * for a fx::gltf::Primitive.
+	 *
+	 * @param src	The description of attribute objects from the fx-gltf library
+	 * @param gltf	The main glTF document
+	 * @param dst	The array of vertex attributes stored in an asset::Mesh object
+	 * @return	ASSET_ERROR when at least one VertexAttribute could not be
+	 * 		constructed properly, otherwise ASSET_SUCCESS
+	 */
+	static int loadVertexAttributes(const fx::gltf::Attributes &src,
+									const std::vector<fx::gltf::Accessor> &accessors,
+									const std::vector<fx::gltf::BufferView> &bufferViews,
+									std::vector<VertexAttribute> &dst) {
+		for (const auto &attrib : src) {
+			VertexAttribute att;
+			
+			if (attrib.first == "POSITION") {
+				att.type = PrimitiveType::POSITION;
+			} else if (attrib.first == "NORMAL") {
+				att.type = PrimitiveType::NORMAL;
+			} else if (attrib.first == "TANGENT") {
+				att.type = PrimitiveType::TANGENT;
+			} else if (attrib.first == "TEXCOORD_0") {
+				att.type = PrimitiveType::TEXCOORD_0;
+			} else if (attrib.first == "TEXCOORD_1") {
+				att.type = PrimitiveType::TEXCOORD_1;
+			} else if (attrib.first == "COLOR_0") {
+				att.type = PrimitiveType::COLOR_0;
+			} else if (attrib.first == "COLOR_1") {
+				att.type = PrimitiveType::COLOR_1;
+			} else if (attrib.first == "JOINTS_0") {
+				att.type = PrimitiveType::JOINTS_0;
+			} else if (attrib.first == "WEIGHTS_0") {
+				att.type = PrimitiveType::WEIGHTS_0;
+			} else {
+				att.type = PrimitiveType::UNDEFINED;
+			}
+			
+			if (att.type != PrimitiveType::UNDEFINED) {
+				const fx::gltf::Accessor &accessor = accessors[attrib.second];
+				const fx::gltf::BufferView &buf = bufferViews[accessor.bufferView];
+				
+				att.offset = buf.byteOffset;
+				att.length = buf.byteLength;
+				att.stride = buf.byteStride;
+				att.componentType = static_cast<ComponentType>(accessor.componentType);
+				att.componentCount = getComponentCount(accessor.type);
+				
+				/* Assume tightly packed stride as not explicitly provided */
+				if (att.stride == 0) {
+					att.stride = att.componentCount * getComponentSize(att.componentType);
+				}
+			}
+			
+			if ((att.type == PrimitiveType::UNDEFINED) ||
+				(att.componentCount == 0)) {
+				return ASSET_ERROR;
+			}
+			
+			dst.push_back(att);
+		}
+		
+		return ASSET_SUCCESS;
+	}
 
-            textures.push_back({
-                0,
-                static_cast<uint8_t>(c),
-                static_cast<uint16_t>(w),
-                static_cast<uint16_t>(h),
-                imgdata
-            });
+	/**
+	 * This function calculates the modelMatrix out of the data given in the gltf file.
+	 * It also checks, whether a modelMatrix was given.
+	 *
+	 * @param translation possible translation vector (default 0,0,0)
+	 * @param scale possible scale vector (default 1,1,1)
+	 * @param rotation possible rotation, given in quaternion (default 0,0,0,1)
+	 * @param matrix possible modelmatrix (default identity)
+	 * @return model Matrix as an array of floats
+	 */
+	static std::array<float, 16> calculateModelMatrix(const std::array<float, 3>& translation,
+													  const std::array<float, 3>& scale,
+													  const std::array<float, 4>& rotation,
+													  const std::array<float, 16>& matrix){
+		std::array<float, 16> modelMatrix = {
+				1,0,0,0,
+				0,1,0,0,
+				0,0,1,0,
+				0,0,0,1
+		};
+		
+		if (matrix != modelMatrix){
+			return matrix;
+		} else {
+			// translation
+			modelMatrix[3] = translation[0];
+			modelMatrix[7] = translation[1];
+			modelMatrix[11] = translation[2];
+			
+			// rotation and scale
+			auto a = rotation[0];
+			auto q1 = rotation[1];
+			auto q2 = rotation[2];
+			auto q3 = rotation[3];
+	
+			modelMatrix[0] = (2 * (a * a + q1 * q1) - 1) * scale[0];
+			modelMatrix[1] = (2 * (q1 * q2 - a * q3)) * scale[1];
+			modelMatrix[2] = (2 * (q1 * q3 + a * q2)) * scale[2];
+	
+			modelMatrix[4] = (2 * (q1 * q2 + a * q3)) * scale[0];
+			modelMatrix[5] = (2 * (a * a + q2 * q2) - 1) * scale[1];
+			modelMatrix[6] = (2 * (q2 * q3 - a * q1)) * scale[2];
+	
+			modelMatrix[8] = (2 * (q1 * q3 - a * q2)) * scale[0];
+			modelMatrix[9] = (2 * (q2 * q3 + a * q1)) * scale[1];
+			modelMatrix[10] = (2 * (a * a + q3 * q3) - 1) * scale[2];
+	
+			// flip y, because GLTF uses y up, but vulkan -y up
+			modelMatrix[5] *= -1;
+	
+			return modelMatrix;
+		}
+	}
 
-        }
-    }
+	bool Material::hasTexture(const PBRTextureTarget target) const {
+		return textureMask & bitflag(target);
+	}
 
-    if (sceneObjects.materials.size() > 0){
-        materials.reserve(sceneObjects.materials.size());
+	/**
+	 * This function translates a given fx-gltf-sampler-wrapping-mode-enum to its vulkan sampler-adress-mode counterpart.
+	 * @param mode: wrapping mode of a sampler given as fx-gltf-enum
+	 * @return int vulkan-enum representing the same wrapping mode
+	 */
+	static int translateSamplerMode(const fx::gltf::Sampler::WrappingMode mode) {
+		switch (mode) {
+		case fx::gltf::Sampler::WrappingMode::ClampToEdge:
+			return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
+		case fx::gltf::Sampler::WrappingMode::MirroredRepeat:
+			return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
+		case fx::gltf::Sampler::WrappingMode::Repeat:
+		default:
+			return VK_SAMPLER_ADDRESS_MODE_REPEAT;
+		}
+	}
 
-        for (int l = 0; l < sceneObjects.materials.size(); l++){
-            fx::gltf::Material material = sceneObjects.materials[l];
-	    // TODO I think we shouldn't set the index for a texture target if
-	    // it isn't defined. So we need to test first if there is a normal
-	    // texture before assigning material.normalTexture.index.
-	    // About the bitmask: If a normal texture is there, modify the
-	    // materials textureMask like this:
-	    // 		mat.textureMask |= bitflag(asset::normal);
-            materials.push_back({
-               0,
-               material.pbrMetallicRoughness.baseColorTexture.index,
-               material.pbrMetallicRoughness.metallicRoughnessTexture.index,
-               material.normalTexture.index,
-               material.occlusionTexture.index,
-               material.emissiveTexture.index,
-               {
-                   material.pbrMetallicRoughness.baseColorFactor[0],
-                   material.pbrMetallicRoughness.baseColorFactor[1],
-                   material.pbrMetallicRoughness.baseColorFactor[2],
-                   material.pbrMetallicRoughness.baseColorFactor[3]
-               },
-               material.pbrMetallicRoughness.metallicFactor,
-               material.pbrMetallicRoughness.roughnessFactor,
-               material.normalTexture.scale,
-               material.occlusionTexture.strength,
-               {
-                   material.emissiveFactor[0],
-                   material.emissiveFactor[1],
-                   material.emissiveFactor[2]
-               }
+	/**
+	 * If the glTF doesn't define samplers, we use the defaults defined by fx-gltf.
+	 * The following are details about the glTF/OpenGL to Vulkan translation.
+	 * magFilter (VkFilter?):
+	 * 	GL_NEAREST -> VK_FILTER_NEAREST
+	 * 	GL_LINEAR -> VK_FILTER_LINEAR
+	 * minFilter (VkFilter?):
+	 * mipmapMode (VkSamplerMipmapMode?):
+	 * Vulkans minFilter and mipmapMode combined correspond to OpenGLs
+	 * GL_minFilter_MIPMAP_mipmapMode:
+	 * 	GL_NEAREST_MIPMAP_NEAREST:
+	 * 		minFilter=VK_FILTER_NEAREST
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
+	 * 	GL_LINEAR_MIPMAP_NEAREST:
+	 * 		minFilter=VK_FILTER_LINEAR
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
+	 * 	GL_NEAREST_MIPMAP_LINEAR:
+	 * 		minFilter=VK_FILTER_NEAREST
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_LINEAR
+	 * 	GL_LINEAR_MIPMAP_LINEAR:
+	 * 		minFilter=VK_FILTER_LINEAR
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_LINEAR
+	 * The modes of GL_LINEAR and GL_NEAREST have to be emulated using
+	 * mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST with specific minLOD and maxLOD:
+	 * 	GL_LINEAR:
+	 * 		minFilter=VK_FILTER_LINEAR
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
+	 * 		minLOD=0, maxLOD=0.25
+	 * 	GL_NEAREST:
+	 * 		minFilter=VK_FILTER_NEAREST
+	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
+	 * 		minLOD=0, maxLOD=0.25
+	 * Setting maxLOD=0 causes magnification to always be performed (using
+	 * the defined magFilter), this may be valid if the min- and magFilter
+	 * are equal, otherwise it won't be the expected behaviour from OpenGL
+	 * and glTF; instead using maxLod=0.25 allows the minFilter to be
+	 * performed while still always rounding to the base level.
+	 * With other modes, minLOD and maxLOD default to:
+	 * 	minLOD=0
+	 * 	maxLOD=VK_LOD_CLAMP_NONE
+	 * wrapping:
+	 * gltf has wrapS, wrapT with {clampToEdge, MirroredRepeat, Repeat} while
+	 * Vulkan has addressModeU, addressModeV, addressModeW with values
+	 * VK_SAMPLER_ADDRESS_MODE_{REPEAT,MIRRORED_REPEAT,CLAMP_TO_EDGE,
+	 * 			    CAMP_TO_BORDER,MIRROR_CLAMP_TO_EDGE}
+	 * Translation from glTF to Vulkan is straight forward for the 3 existing
+	 * modes, default is repeat, the other modes aren't available.
+	 */
+	static vkcv::asset::Sampler loadSampler(const fx::gltf::Sampler &src) {
+		Sampler dst;
+		
+		dst.minLOD = 0;
+		dst.maxLOD = VK_LOD_CLAMP_NONE;
+	
+		switch (src.minFilter) {
+		case fx::gltf::Sampler::MinFilter::None:
+		case fx::gltf::Sampler::MinFilter::Nearest:
+			dst.minFilter = VK_FILTER_NEAREST;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+			dst.maxLOD = 0.25;
+			break;
+		case fx::gltf::Sampler::MinFilter::Linear:
+			dst.minFilter = VK_FILTER_LINEAR;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+			dst.maxLOD = 0.25;
+			break;
+		case fx::gltf::Sampler::MinFilter::NearestMipMapNearest:
+			dst.minFilter = VK_FILTER_NEAREST;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+			break;
+		case fx::gltf::Sampler::MinFilter::LinearMipMapNearest:
+			dst.minFilter = VK_FILTER_LINEAR;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+			break;
+		case fx::gltf::Sampler::MinFilter::NearestMipMapLinear:
+			dst.minFilter = VK_FILTER_NEAREST;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
+			break;
+		case fx::gltf::Sampler::MinFilter::LinearMipMapLinear:
+			dst.minFilter = VK_FILTER_LINEAR;
+			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
+			break;
+		default:
+			break;
+		}
+	
+		switch (src.magFilter) {
+		case fx::gltf::Sampler::MagFilter::None:
+		case fx::gltf::Sampler::MagFilter::Nearest:
+			dst.magFilter = VK_FILTER_NEAREST;
+			break;
+		case fx::gltf::Sampler::MagFilter::Linear:
+			dst.magFilter = VK_FILTER_LINEAR;
+			break;
+		default:
+			break;
+		}
+	
+		dst.addressModeU = translateSamplerMode(src.wrapS);
+		dst.addressModeV = translateSamplerMode(src.wrapT);
+		
+		// There is no information about wrapping for a third axis in glTF and
+		// we have to hardcode this value.
+		dst.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+		
+		return dst;
+	}
 
-            });
-        }
-    }
+	/**
+	 * Initializes vertex groups of a Mesh, including copying the data to
+	 * index- and vertex-buffers.
+	 */
+	static int loadVertexGroups(const fx::gltf::Mesh &objectMesh,
+								const fx::gltf::Document &sceneObjects,
+								Scene &scene, Mesh &mesh) {
+		mesh.vertexGroups.reserve(objectMesh.primitives.size());
+	
+		for (const auto &objectPrimitive : objectMesh.primitives) {
+			VertexGroup vertexGroup;
+			
+			vertexGroup.vertexBuffer.attributes.reserve(
+					objectPrimitive.attributes.size()
+			);
+	
+			if (ASSET_SUCCESS != loadVertexAttributes(
+					objectPrimitive.attributes,
+					sceneObjects.accessors,
+					sceneObjects.bufferViews,
+					vertexGroup.vertexBuffer.attributes)) {
+				vkcv_log(LogLevel::ERROR, "Failed to get vertex attributes of '%s'",
+						 mesh.name.c_str());
+				return ASSET_ERROR;
+			}
+			
+			// The accessor for the position attribute is used for
+			// 1) getting the vertex buffer view which is only needed to get
+			//    the vertex buffer
+			// 2) getting the vertex count for the VertexGroup
+			// 3) getting the min/max of the bounding box for the VertexGroup
+			fx::gltf::Accessor posAccessor;
+			bool noPosition = true;
+			
+			for (auto const& attrib : objectPrimitive.attributes) {
+				if (attrib.first == "POSITION") {
+					posAccessor = sceneObjects.accessors[attrib.second];
+					noPosition = false;
+					break;
+				}
+			}
+			
+			if (noPosition) {
+				vkcv_log(LogLevel::ERROR, "Position attribute not found from '%s'",
+						 mesh.name.c_str());
+				return ASSET_ERROR;
+			}
+	
+			const fx::gltf::Accessor& indexAccessor = sceneObjects.accessors[objectPrimitive.indices];
+			
+			int indexBufferURI;
+			if (objectPrimitive.indices >= 0) { // if there is no index buffer, -1 is returned from fx-gltf
+				const fx::gltf::BufferView& indexBufferView = sceneObjects.bufferViews[indexAccessor.bufferView];
+				const fx::gltf::Buffer& indexBuffer = sceneObjects.buffers[indexBufferView.buffer];
+				
+				// Because the buffers are already preloaded into the memory by the gltf-library,
+				// it makes no sense to load them later on manually again into memory.
+				vertexGroup.indexBuffer.data.resize(indexBufferView.byteLength);
+				memcpy(vertexGroup.indexBuffer.data.data(),
+					   indexBuffer.data.data() + indexBufferView.byteOffset,
+					   indexBufferView.byteLength);
+			} else {
+				indexBufferURI = -1;
+			}
+			
+			vertexGroup.indexBuffer.type = getIndexType(indexAccessor.componentType);
+			
+			if (IndexType::UNDEFINED == vertexGroup.indexBuffer.type) {
+				vkcv_log(LogLevel::ERROR, "Index Type undefined or not supported.");
+				return ASSET_ERROR;
+			}
+	
+			if (posAccessor.bufferView >= sceneObjects.bufferViews.size()) {
+				vkcv_log(LogLevel::ERROR, "Access to bufferView out of bounds: %lu",
+						posAccessor.bufferView);
+				return ASSET_ERROR;
+			}
+			const fx::gltf::BufferView& vertexBufferView = sceneObjects.bufferViews[posAccessor.bufferView];
+			if (vertexBufferView.buffer >= sceneObjects.buffers.size()) {
+				vkcv_log(LogLevel::ERROR, "Access to buffer out of bounds: %lu",
+						vertexBufferView.buffer);
+				return ASSET_ERROR;
+			}
+			const fx::gltf::Buffer& vertexBuffer = sceneObjects.buffers[vertexBufferView.buffer];
+			
+			// only copy relevant part of vertex data
+			uint32_t relevantBufferOffset = std::numeric_limits<uint32_t>::max();
+			uint32_t relevantBufferEnd = 0;
+			
+			for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
+				relevantBufferOffset = std::min(attribute.offset, relevantBufferOffset);
+				relevantBufferEnd = std::max(relevantBufferEnd, attribute.offset + attribute.length);
+			}
+			
+			const uint32_t relevantBufferSize = relevantBufferEnd - relevantBufferOffset;
+			
+			vertexGroup.vertexBuffer.data.resize(relevantBufferSize);
+			memcpy(vertexGroup.vertexBuffer.data.data(),
+				   vertexBuffer.data.data() + relevantBufferOffset,
+				   relevantBufferSize);
+			
+			// make vertex attributes relative to copied section
+			for (auto& attribute : vertexGroup.vertexBuffer.attributes) {
+				attribute.offset -= relevantBufferOffset;
+			}
+			
+			vertexGroup.mode = static_cast<PrimitiveMode>(objectPrimitive.mode);
+			vertexGroup.numIndices = sceneObjects.accessors[objectPrimitive.indices].count;
+			vertexGroup.numVertices = posAccessor.count;
+			
+			memcpy(&(vertexGroup.min), posAccessor.min.data(), sizeof(vertexGroup.min));
+			memcpy(&(vertexGroup.max), posAccessor.max.data(), sizeof(vertexGroup.max));
+			
+			vertexGroup.materialIndex = static_cast<uint8_t>(objectPrimitive.material);
+			
+			mesh.vertexGroups.push_back(static_cast<int>(scene.vertexGroups.size()));
+			scene.vertexGroups.push_back(vertexGroup);
+		}
+		
+		return ASSET_SUCCESS;
+	}
 
-    scene = {
-            meshes,
-            vertexGroups,
-            materials,
-            textures,
-            samplers
-    };
+	/**
+	 * Returns an integer with specific bits set corresponding to the
+	 * textures that appear in the given material. This mask is used in the
+	 * vkcv::asset::Material struct and can be tested via the hasTexture
+	 * method.
+	 */
+	static uint16_t generateTextureMask(fx::gltf::Material &material) {
+		uint16_t textureMask = 0;
+		
+		if (material.pbrMetallicRoughness.baseColorTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::baseColor);
+		}
+		if (material.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::metalRough);
+		}
+		if (material.normalTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::normal);
+		}
+		if (material.occlusionTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::occlusion);
+		}
+		if (material.emissiveTexture.index >= 0) {
+			textureMask |= bitflag(asset::PBRTextureTarget::emissive);
+		}
+		
+		return textureMask;
+	}
 
-    return 1;
-}
+	int probeScene(const std::filesystem::path& path, Scene& scene) {
+		fx::gltf::Document sceneObjects;
+	
+		try {
+			if (path.extension() == ".glb") {
+				sceneObjects = fx::gltf::LoadFromBinary(path.string());
+			} else {
+				sceneObjects = fx::gltf::LoadFromText(path.string());
+			}
+		} catch (const std::system_error& err) {
+			recurseExceptionPrint(err, path.string());
+			return ASSET_ERROR;
+		} catch (const std::exception& e) {
+			recurseExceptionPrint(e, path.string());
+			return ASSET_ERROR;
+		}
+		
+		const auto directory = path.parent_path();
+		
+		scene.meshes.clear();
+		scene.vertexGroups.clear();
+		scene.materials.clear();
+		scene.textures.clear();
+		scene.samplers.clear();
+	
+		// file has to contain at least one mesh
+		if (sceneObjects.meshes.empty()) {
+			vkcv_log(LogLevel::ERROR, "No meshes found! (%s)", path.c_str());
+			return ASSET_ERROR;
+		} else {
+			scene.meshes.reserve(sceneObjects.meshes.size());
+			
+			for (size_t i = 0; i < sceneObjects.meshes.size(); i++) {
+				Mesh mesh;
+				mesh.name = sceneObjects.meshes[i].name;
+				
+				if (loadVertexGroups(sceneObjects.meshes[i], sceneObjects, scene, mesh) != ASSET_SUCCESS) {
+					vkcv_log(LogLevel::ERROR, "Failed to load vertex groups of '%s'! (%s)",
+							 mesh.name.c_str(), path.c_str());
+					return ASSET_ERROR;
+				}
+				
+				scene.meshes.push_back(mesh);
+			}
+			
+			// This only works if the node has a mesh and it only loads the meshes and ignores cameras and lights
+			for (const auto& node : sceneObjects.nodes) {
+				if ((node.mesh >= 0) && (node.mesh < scene.meshes.size())) {
+					scene.meshes[node.mesh].modelMatrix = calculateModelMatrix(
+							node.translation,
+							node.scale,
+							node.rotation,
+							node.matrix
+					);
+				}
+			}
+		}
+		
+		if (sceneObjects.samplers.empty()) {
+			vkcv_log(LogLevel::WARNING, "No samplers found! (%s)", path.c_str());
+		} else {
+			scene.samplers.reserve(sceneObjects.samplers.size());
+			
+			for (const auto &samplerObject : sceneObjects.samplers) {
+				scene.samplers.push_back(loadSampler(samplerObject));
+			}
+		}
+		
+		if (sceneObjects.textures.empty()) {
+			vkcv_log(LogLevel::WARNING, "No textures found! (%s)", path.c_str());
+		} else {
+			scene.textures.reserve(sceneObjects.textures.size());
+			
+			for (const auto& textureObject : sceneObjects.textures) {
+				Texture texture;
+				
+				if (textureObject.sampler < 0) {
+					texture.sampler = -1;
+				} else
+				if (static_cast<size_t>(textureObject.sampler) >= scene.samplers.size()) {
+					vkcv_log(LogLevel::ERROR, "Sampler of texture '%s' missing (%s) %d",
+							 textureObject.name.c_str(), path.c_str());
+					return ASSET_ERROR;
+				} else {
+					texture.sampler = textureObject.sampler;
+				}
+				
+				if ((textureObject.source < 0) ||
+					(static_cast<size_t>(textureObject.source) >= sceneObjects.images.size())) {
+					vkcv_log(LogLevel::ERROR, "Failed to load texture '%s' (%s)",
+							 textureObject.name.c_str(), path.c_str());
+					return ASSET_ERROR;
+				}
+				
+				const auto& image = sceneObjects.images[textureObject.source];
+				
+				if (image.uri.empty()) {
+					const fx::gltf::BufferView bufferView = sceneObjects.bufferViews[image.bufferView];
+					
+					texture.path.clear();
+					texture.data.resize(bufferView.byteLength);
+					memcpy(texture.data.data(),
+						   sceneObjects.buffers[bufferView.buffer].data.data() + bufferView.byteOffset,
+						   bufferView.byteLength);
+				} else {
+					texture.path = directory / image.uri;
+				}
+				
+				scene.textures.push_back(texture);
+			}
+		}
+		
+		if (sceneObjects.materials.empty()) {
+			vkcv_log(LogLevel::WARNING, "No materials found! (%s)", path.c_str());
+		} else {
+			scene.materials.reserve(sceneObjects.materials.size());
+			
+			for (auto material : sceneObjects.materials) {
+				scene.materials.push_back({
+						generateTextureMask(material),
+						material.pbrMetallicRoughness.baseColorTexture.index,
+						material.pbrMetallicRoughness.metallicRoughnessTexture.index,
+						material.normalTexture.index,
+						material.occlusionTexture.index,
+						material.emissiveTexture.index,
+						{
+								material.pbrMetallicRoughness.baseColorFactor[0],
+								material.pbrMetallicRoughness.baseColorFactor[1],
+								material.pbrMetallicRoughness.baseColorFactor[2],
+								material.pbrMetallicRoughness.baseColorFactor[3]
+						},
+						material.pbrMetallicRoughness.metallicFactor,
+						material.pbrMetallicRoughness.roughnessFactor,
+						material.normalTexture.scale,
+						material.occlusionTexture.strength,
+						{
+								material.emissiveFactor[0],
+								material.emissiveFactor[1],
+								material.emissiveFactor[2]
+						}
+				});
+			}
+		}
+	
+		return ASSET_SUCCESS;
+	}
+	
+	/**
+	 * Loads and decodes the textures data based on the textures file path.
+	 * The path member is the only one that has to be initialized before
+	 * calling this function, the others (width, height, channels, data)
+	 * are set by this function and the sampler is of no concern here.
+	 */
+	static int loadTextureData(Texture& texture) {
+		if ((texture.width > 0) && (texture.height > 0) && (texture.channels > 0) &&
+			(!texture.data.empty())) {
+			return ASSET_SUCCESS; // Texture data was loaded already!
+		}
+		
+		uint8_t* data;
+		
+		if (texture.path.empty()) {
+			data = stbi_load_from_memory(
+					reinterpret_cast<uint8_t*>(texture.data.data()),
+					static_cast<int>(texture.data.size()),
+					&texture.width,
+					&texture.height,
+					&texture.channels, 4
+			);
+		} else {
+			data = stbi_load(
+					texture.path.string().c_str(),
+					&texture.width,
+					&texture.height,
+					&texture.channels, 4
+			);
+		}
+		
+		if (!data) {
+			vkcv_log(LogLevel::ERROR, "Texture could not be loaded from '%s'",
+					 texture.path.c_str());
+			
+			texture.width = 0;
+			texture.height = 0;
+			texture.channels = 0;
+			return ASSET_ERROR;
+		}
+		
+		texture.data.resize(texture.width * texture.height * 4);
+		memcpy(texture.data.data(), data, texture.data.size());
+		stbi_image_free(data);
+	
+		return ASSET_SUCCESS;
+	}
 
-TextureData loadTexture(const std::filesystem::path& path) {
-    TextureData texture;
-    
-    uint8_t* data = stbi_load(path.string().c_str(), &texture.width, &texture.height, &texture.componentCount, 4);
-    
-    if (!data) {
-		vkcv_log(LogLevel::ERROR, "Texture could not be loaded from '%s'", path.c_str());
-    	
-    	texture.width = 0;
-    	texture.height = 0;
-    	texture.componentCount = 0;
-    	return texture;
-    }
-    
-    texture.data.resize(texture.width * texture.height * 4);
-    memcpy(texture.data.data(), data, texture.data.size());
-    return texture;
-}
+	int loadMesh(Scene &scene, int index) {
+		if ((index < 0) || (static_cast<size_t>(index) >= scene.meshes.size())) {
+			vkcv_log(LogLevel::ERROR, "Mesh index out of range: %d", index);
+			return ASSET_ERROR;
+		}
+		
+		const Mesh &mesh = scene.meshes[index];
+		
+		for (const auto& vg : mesh.vertexGroups) {
+			const VertexGroup &vertexGroup = scene.vertexGroups[vg];
+			const Material& material = scene.materials[vertexGroup.materialIndex];
+			
+			if (material.hasTexture(PBRTextureTarget::baseColor)) {
+				const int result = loadTextureData(scene.textures[material.baseColor]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading baseColor texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+			
+			if (material.hasTexture(PBRTextureTarget::metalRough)) {
+				const int result = loadTextureData(scene.textures[material.metalRough]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading metalRough texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+			
+			if (material.hasTexture(PBRTextureTarget::normal)) {
+				const int result = loadTextureData(scene.textures[material.normal]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading normal texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+			
+			if (material.hasTexture(PBRTextureTarget::occlusion)) {
+				const int result = loadTextureData(scene.textures[material.occlusion]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading occlusion texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+			
+			if (material.hasTexture(PBRTextureTarget::emissive)) {
+				const int result = loadTextureData(scene.textures[material.emissive]);
+				if (ASSET_SUCCESS != result) {
+					vkcv_log(LogLevel::ERROR, "Failed loading emissive texture of mesh '%s'",
+							 mesh.name.c_str())
+					return result;
+				}
+			}
+		}
+	
+		return ASSET_SUCCESS;
+	}
+	
+	int loadScene(const std::filesystem::path &path, Scene &scene) {
+		int result = probeScene(path, scene);
+		
+		if (result != ASSET_SUCCESS) {
+			vkcv_log(LogLevel::ERROR, "Loading scene failed '%s'",
+					 path.c_str());
+			return result;
+		}
+		
+		for (size_t i = 0; i < scene.meshes.size(); i++) {
+			result = loadMesh(scene, static_cast<int>(i));
+			
+			if (result != ASSET_SUCCESS) {
+				vkcv_log(LogLevel::ERROR, "Loading mesh with index %d failed '%s'",
+						 static_cast<int>(i), path.c_str());
+				return result;
+			}
+		}
+		
+		return ASSET_SUCCESS;
+	}
+	
+	Texture loadTexture(const std::filesystem::path& path) {
+		Texture texture;
+		texture.path = path;
+		texture.sampler = -1;
+		if (loadTextureData(texture) != ASSET_SUCCESS) {
+			texture.path.clear();
+			texture.w = texture.h = texture.channels = 0;
+			texture.data.clear();
+		}
+		return texture;
+	}
 
 }
diff --git a/modules/camera/config/GLM.cmake b/modules/camera/config/GLM.cmake
index efd6444451100b912aa0b5b4a532dc8f448b0b40..f256ccade8c4f44a89744bb7875371324cf2369d 100644
--- a/modules/camera/config/GLM.cmake
+++ b/modules/camera/config/GLM.cmake
@@ -4,18 +4,20 @@ find_package(glm QUIET)
 if (glm_FOUND)
     list(APPEND vkcv_camera_includes ${GLM_INCLUDE_DIRS})
     list(APPEND vkcv_camera_libraries glm)
-
-    list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
-    list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
 else()
     if (EXISTS "${vkcv_camera_lib_path}/glm")
         add_subdirectory(${vkcv_camera_lib}/glm)
-        
+
+        list(APPEND vkcv_camera_includes ${vkcv_camera_lib_path}/glm)
         list(APPEND vkcv_camera_libraries glm)
-        
-        list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
-        list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
     else()
         message(WARNING "GLM is required..! Update the submodules!")
     endif ()
 endif ()
+
+list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
+list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
+
+if ((WIN32) AND (${CMAKE_SIZEOF_VOID_P} MATCHES 4))
+    list(APPEND vkcv_camera_definitions GLM_ENABLE_EXPERIMENTAL)
+endif()
diff --git a/modules/camera/include/vkcv/camera/Camera.hpp b/modules/camera/include/vkcv/camera/Camera.hpp
index 9d85df7dce6d043630fd9d39287cace8530dbd6a..8a8c5df5d74cf1402bd8810172657ba77ddb2d56 100644
--- a/modules/camera/include/vkcv/camera/Camera.hpp
+++ b/modules/camera/include/vkcv/camera/Camera.hpp
@@ -3,6 +3,8 @@
 #include <glm/glm.hpp>
 #include <glm/gtc/matrix_transform.hpp>
 #include <glm/gtc/matrix_access.hpp>
+#include <glm/vec3.hpp>
+#include <glm/mat4x4.hpp>
 
 namespace vkcv::camera {
 
@@ -20,9 +22,6 @@ namespace vkcv::camera {
 		glm::vec3 m_up;
         glm::vec3 m_position;
         glm::vec3 m_center;
-
-        float m_pitch;
-        float m_yaw;
 	
 		/**
 		 * @brief Sets the view matrix of the camera to @p view
@@ -75,7 +74,7 @@ namespace vkcv::camera {
          * @brief Gets the current projection of the camera
          * @return The current projection matrix
          */
-        glm::mat4 getProjection() const;
+        const glm::mat4& getProjection() const;
 
         /**
          * @brief Gets the model-view-projection matrix of the camera with y-axis-correction applied
@@ -156,6 +155,20 @@ namespace vkcv::camera {
          * @param[in] center The new center point.
          */
         void setCenter(const glm::vec3& center);
+        
+        /**
+         * @brief Gets the angles of the camera.
+         * @param[out] pitch The pitch value in radians
+         * @param[out] yaw The yaw value in radians
+         */
+		void getAngles(float& pitch, float& yaw);
+  
+		/**
+		 * @brief Sets the angles of the camera.
+		 * @param pitch The new pitch value in radians
+		 * @param yaw The new yaw value in radians
+		 */
+		void setAngles(float pitch, float yaw);
 
         /**
          * @brief Gets the pitch value of the camera in degrees.
diff --git a/modules/camera/include/vkcv/camera/PilotCameraController.hpp b/modules/camera/include/vkcv/camera/PilotCameraController.hpp
index 2b64cdc0dd3045714aba7b3b7c6241af2337c706..67388818a59b66775598e9d4257fa4c36646332a 100644
--- a/modules/camera/include/vkcv/camera/PilotCameraController.hpp
+++ b/modules/camera/include/vkcv/camera/PilotCameraController.hpp
@@ -29,42 +29,6 @@ namespace vkcv::camera {
         float m_fov_min;
         float m_fov_max;
 
-        /**
-         * @brief Indicates forward movement of the camera depending on the performed @p action.
-         * @param[in] action The performed action.
-         */
-        void moveForward(int action);
-
-        /**
-         * @brief Indicates backward movement of the camera depending on the performed @p action.
-         * @param[in] action The performed action.
-         */
-        void moveBackward(int action);
-
-        /**
-         * @brief Indicates left movement of the camera depending on the performed @p action.
-         * @param[in] action The performed action.
-         */
-        void moveLeft(int action);
-
-        /**
-         * @brief Indicates right movement of the camera depending on the performed @p action.
-         * @param[in] action The performed action.
-         */
-        void moveRight(int action);
-
-        /**
-         * @brief Indicates upward movement of the camera depending on the performed @p action.
-         * @param[in] action The performed action.
-         */
-        void moveUpward(int action);
-
-        /**
-         * @brief Indicates downward movement of the camera depending on the performed @p action.
-         * @param[in] action The performed action.
-         */
-        void moveDownward(int action);
-
     public:
 
         /**
diff --git a/modules/camera/include/vkcv/camera/TrackballCameraController.hpp b/modules/camera/include/vkcv/camera/TrackballCameraController.hpp
index 4166bda9f6cb62e4c8f1b650557b00c6ec94b2a1..20336f7a10644c73e451c5106f37545e84eb27f7 100644
--- a/modules/camera/include/vkcv/camera/TrackballCameraController.hpp
+++ b/modules/camera/include/vkcv/camera/TrackballCameraController.hpp
@@ -14,6 +14,8 @@ namespace vkcv::camera {
         float m_cameraSpeed;
         float m_scrollSensitivity;
         float m_radius;
+        float m_pitch;
+        float m_yaw;
 
         /**
          * @brief Updates the current radius of @p camera in respect to the @p offset.
diff --git a/modules/camera/src/vkcv/camera/Camera.cpp b/modules/camera/src/vkcv/camera/Camera.cpp
index 3541b1a5bc1253c6b0f2b044d757341855a5e900..87d09aa9a6e3e7dc80d5de9a95f3e1e3b72e9205 100644
--- a/modules/camera/src/vkcv/camera/Camera.cpp
+++ b/modules/camera/src/vkcv/camera/Camera.cpp
@@ -10,8 +10,6 @@ namespace vkcv::camera {
 			glm::vec3(0.0f, 0.0f, 0.0f),
 			glm::vec3(0.0f, 1.0f, 0.0f)
 		);
-  
-		setFront(glm::normalize(m_center - m_position));
     }
 
     Camera::~Camera() = default;
@@ -44,20 +42,20 @@ namespace vkcv::camera {
         0.0f, 0.0f, 0.0f, 1.0f
     );
 
-    glm::mat4 Camera::getProjection() const {
-        return y_correction * m_projection;
+    const glm::mat4& Camera::getProjection() const {
+        return m_projection;
     }
 
     void Camera::setProjection(const glm::mat4& projection) {
-        m_projection = glm::inverse(y_correction) * projection;
+        m_projection = y_correction * projection;
     }
 
     glm::mat4 Camera::getMVP() const {
-        return y_correction * m_projection * m_view;
+        return m_projection * m_view;
     }
 
     float Camera::getFov() const {
-    	const float tanHalfFovy = 1.0f / m_projection[1][1];
+    	const float tanHalfFovy = -1.0f / m_projection[1][1];
     	float halfFovy = std::atan(tanHalfFovy);
     	
     	if (halfFovy < 0) {
@@ -73,7 +71,7 @@ namespace vkcv::camera {
 
     float Camera::getRatio() const {
     	const float aspectProduct = 1.0f / m_projection[0][0];
-		const float tanHalfFovy = 1.0f / m_projection[1][1];
+		const float tanHalfFovy = -1.0f / m_projection[1][1];
 		
         return aspectProduct / tanHalfFovy;
     }
@@ -93,16 +91,11 @@ namespace vkcv::camera {
     }
 
     glm::vec3 Camera::getFront() const {
-        glm::vec3 direction;
-        direction.x = std::sin(glm::radians(m_yaw)) * std::cos(glm::radians(m_pitch));
-        direction.y = std::sin(glm::radians(m_pitch));
-        direction.z = std::cos(glm::radians(m_yaw)) * std::cos(glm::radians(m_pitch));
-        return glm::normalize(direction);
+        return glm::normalize(m_center - m_position);
     }
     
     void Camera::setFront(const glm::vec3 &front) {
-		m_pitch = std::atan2(front.y, std::sqrt(front.x * front.x + front.z * front.z));
-		m_yaw = std::atan2(front.x, front.z);
+		setCenter(m_position + front);
     }
 
     const glm::vec3& Camera::getPosition() const {
@@ -128,21 +121,47 @@ namespace vkcv::camera {
 	void Camera::setUp(const glm::vec3 &up) {
 		lookAt(m_position, m_center, up);
 	}
-
-    float Camera::getPitch() const {
-        return m_pitch;
+	
+	void Camera::getAngles(float& pitch, float& yaw) {
+		const auto front = getFront();
+		
+		pitch = std::atan2(front[1], std::sqrt(
+				front[0] * front[0] + front[2] * front[2]
+		));
+		
+		yaw = std::atan2(front[0], front[2]);
+	}
+	
+	void Camera::setAngles(float pitch, float yaw) {
+		float cosPitch = std::cos(pitch);
+		
+		setFront(glm::vec3(
+				std::sin(yaw) * cosPitch,
+				std::sin(pitch),
+				std::cos(yaw) * cosPitch
+		));
+	}
+	
+	float Camera::getPitch() const {
+    	const auto front = getFront();
+    	
+        return glm::degrees(std::atan2(front[1], std::sqrt(
+        		front[0] * front[0] + front[2] * front[2]
+		)));
     }
 
     void Camera::setPitch(float pitch) {
-        m_pitch = pitch;
+		setAngles(glm::radians(pitch), glm::radians(getYaw()));
     }
 
     float Camera::getYaw() const {
-        return m_yaw;
+		const auto front = getFront();
+	
+		return glm::degrees(std::atan2(front[0], front[2]));
     }
 
     void Camera::setYaw(float yaw) {
-        m_yaw = yaw;
+		setAngles(glm::radians(getPitch()), glm::radians(yaw));
     }
 
 }
\ No newline at end of file
diff --git a/modules/camera/src/vkcv/camera/CameraManager.cpp b/modules/camera/src/vkcv/camera/CameraManager.cpp
index f129f3a248325957cb56470e2547a0146bc7c971..c8aa4f7e0e493a2aaf5bfd6d93768e169cd255b9 100644
--- a/modules/camera/src/vkcv/camera/CameraManager.cpp
+++ b/modules/camera/src/vkcv/camera/CameraManager.cpp
@@ -52,8 +52,8 @@ namespace vkcv::camera {
     }
 
     void CameraManager::mouseMoveCallback(double x, double y){
-        auto xoffset = static_cast<float>(x - m_lastX);
-		auto yoffset = static_cast<float>(y - m_lastY);
+        auto xoffset = static_cast<float>(x - m_lastX) / m_window.getWidth();
+		auto yoffset = static_cast<float>(y - m_lastY) / m_window.getHeight();
         m_lastX = x;
         m_lastY = y;
 		getActiveController().mouseMoveCallback(xoffset, yoffset, getActiveCamera());
diff --git a/modules/camera/src/vkcv/camera/PilotCameraController.cpp b/modules/camera/src/vkcv/camera/PilotCameraController.cpp
index 5460858ab48d81252787b3c0141dd72982faca7d..1c7bb12679e57c9221465452f2fc41a539b6b2a0 100644
--- a/modules/camera/src/vkcv/camera/PilotCameraController.cpp
+++ b/modules/camera/src/vkcv/camera/PilotCameraController.cpp
@@ -50,12 +50,11 @@ namespace vkcv::camera {
         }
 
         // handle yaw rotation
-        float yaw = camera.getYaw() + static_cast<float>(xOffset);
-        yaw += 360.0f * (yaw < -180.0f) - 360.0f * (yaw > 180.0f);
+        float yaw = camera.getYaw() + static_cast<float>(xOffset) * 90.0f * m_cameraSpeed;
         camera.setYaw(yaw);
 
         // handle pitch rotation
-        float pitch = camera.getPitch() - static_cast<float>(yOffset);
+        float pitch = camera.getPitch() - static_cast<float>(yOffset) * 90.0f * m_cameraSpeed;
         pitch = glm::clamp(pitch, -89.0f, 89.0f);
         camera.setPitch(pitch);
     }
@@ -83,22 +82,22 @@ namespace vkcv::camera {
     void PilotCameraController::keyCallback(int key, int scancode, int action, int mods, Camera &camera) {
         switch (key) {
             case GLFW_KEY_W:
-                moveForward(action);
+            	m_forward = static_cast<bool>(action);
                 break;
             case GLFW_KEY_S:
-                moveBackward(action);
+            	m_backward = static_cast<bool>(action);
                 break;
             case GLFW_KEY_A:
-                moveLeft(action);
+            	m_left = static_cast<bool>(action);
                 break;
             case GLFW_KEY_D:
-                moveRight(action);
+            	m_right = static_cast<bool>(action);
                 break;
             case GLFW_KEY_E:
-                moveUpward(action);
+            	m_upward = static_cast<bool>(action);
                 break;
             case GLFW_KEY_Q:
-                moveDownward(action);
+            	m_downward = static_cast<bool>(action);
                 break;
             default:
                 break;
@@ -110,31 +109,25 @@ namespace vkcv::camera {
     }
 
     void PilotCameraController::mouseMoveCallback(double xoffset, double yoffset, Camera &camera) {
-        if(!m_rotationActive){
-            return;
-        }
-
-        float sensitivity = 0.05f;
-        xoffset *= sensitivity;
-        yoffset *= sensitivity;
+    	xoffset *= static_cast<float>(m_rotationActive);
+    	yoffset *= static_cast<float>(m_rotationActive);
 
         panView(xoffset , yoffset, camera);
     }
 
     void PilotCameraController::mouseButtonCallback(int button, int action, int mods, Camera &camera) {
-        if(button == GLFW_MOUSE_BUTTON_2 && m_rotationActive == false && action == GLFW_PRESS){
-            m_rotationActive = true;
-        }
-        else if(button == GLFW_MOUSE_BUTTON_2 && m_rotationActive == true && action == GLFW_RELEASE){
-            m_rotationActive = false;
-        }
+    	if (button == GLFW_MOUSE_BUTTON_2) {
+    		if (m_rotationActive != (action == GLFW_PRESS)) {
+    			m_rotationActive = (action == GLFW_PRESS);
+    		}
+    	}
     }
 
     void PilotCameraController::gamepadCallback(int gamepadIndex, Camera &camera, double frametime) {
         GLFWgamepadstate gamepadState;
         glfwGetGamepadState(gamepadIndex, &gamepadState);
 
-        float sensitivity = 100.0f;
+        float sensitivity = 1.0f;
         double threshold = 0.1;
 
         // handle rotations
@@ -163,29 +156,4 @@ namespace vkcv::camera {
                      * -copysign(1.0, stickLeftX);
     }
 
-
-    void PilotCameraController::moveForward(int action){
-        m_forward = static_cast<bool>(action);
-    }
-
-    void PilotCameraController::moveBackward(int action){
-        m_backward = static_cast<bool>(action);
-    }
-
-    void PilotCameraController::moveLeft(int action){
-        m_left = static_cast<bool>(action);
-    }
-
-    void PilotCameraController::moveRight(int action){
-        m_right = static_cast<bool>(action);
-    }
-
-    void PilotCameraController::moveUpward(int action){
-        m_upward = static_cast<bool>(action);
-    }
-
-    void PilotCameraController::moveDownward(int action){
-        m_downward = static_cast<bool>(action);
-    }
-
 }
\ No newline at end of file
diff --git a/modules/camera/src/vkcv/camera/TrackballCameraController.cpp b/modules/camera/src/vkcv/camera/TrackballCameraController.cpp
index cdd66cdb7fdd650d5112fe7bb4738f1fcded7783..8de2beb87d8f29415db611bfe0d17c5efd57a2a3 100644
--- a/modules/camera/src/vkcv/camera/TrackballCameraController.cpp
+++ b/modules/camera/src/vkcv/camera/TrackballCameraController.cpp
@@ -8,6 +8,8 @@ namespace vkcv::camera {
         m_radius = 3.0f;
         m_cameraSpeed = 2.5f;
         m_scrollSensitivity = 0.2f;
+        m_pitch = 0.0f;
+        m_yaw = 0.0f;
     }
 
     void TrackballCameraController::setRadius(const float radius) {
@@ -21,14 +23,10 @@ namespace vkcv::camera {
         }
 
         // handle yaw rotation
-        float yaw = camera.getYaw() + static_cast<float>(xOffset) * m_cameraSpeed;
-        yaw += 360.0f * (yaw < 0.0f) - 360.0f * (yaw > 360.0f);
-        camera.setYaw(yaw);
+        m_yaw = m_yaw + static_cast<float>(xOffset) * 90.0f * m_cameraSpeed;
 
         // handle pitch rotation
-        float pitch = camera.getPitch() + static_cast<float>(yOffset) * m_cameraSpeed;
-        pitch += 360.0f * (pitch < 0.0f) - 360.0f * (pitch > 360.0f);
-        camera.setPitch(pitch);
+        m_pitch = m_pitch + static_cast<float>(yOffset) * 90.0f * m_cameraSpeed;
     }
 
     void TrackballCameraController::updateRadius(double offset, Camera &camera) {
@@ -44,14 +42,11 @@ namespace vkcv::camera {
     }
 
     void TrackballCameraController::updateCamera(double deltaTime, Camera &camera) {
-		float yaw = camera.getYaw();
-		float pitch = camera.getPitch();
-		
 		const glm::vec3 yAxis = glm::vec3(0.0f, 1.0f, 0.0f);
 		const glm::vec3 xAxis = glm::vec3(1.0f, 0.0f, 0.0f);
 	
-		const glm::mat4 rotationY = glm::rotate(glm::mat4(1.0f), glm::radians(yaw), yAxis);
-		const glm::mat4 rotationX = glm::rotate(rotationY, -glm::radians(pitch), xAxis);
+		const glm::mat4 rotationY = glm::rotate(glm::mat4(1.0f), glm::radians(m_yaw), yAxis);
+		const glm::mat4 rotationX = glm::rotate(rotationY, -glm::radians(m_pitch), xAxis);
 		const glm::vec3 translation = glm::vec3(
 				rotationX * glm::vec4(0.0f, 0.0f, m_radius, 0.0f)
 		);
@@ -72,15 +67,10 @@ namespace vkcv::camera {
     }
 
     void TrackballCameraController::mouseMoveCallback(double xoffset, double yoffset, Camera &camera) {
-        if(!m_rotationActive){
-            return;
-        }
-
-        float sensitivity = 0.025f;
-        xoffset *= sensitivity;
-        yoffset *= sensitivity;
+        xoffset *= static_cast<float>(m_rotationActive);
+        yoffset *= static_cast<float>(m_rotationActive);
 
-        panView(xoffset , yoffset, camera);
+        panView(xoffset, yoffset, camera);
     }
 
     void TrackballCameraController::mouseButtonCallback(int button, int action, int mods, Camera &camera) {
@@ -96,7 +86,7 @@ namespace vkcv::camera {
         GLFWgamepadstate gamepadState;
         glfwGetGamepadState(gamepadIndex, &gamepadState);
 
-        float sensitivity = 100.0f;
+        float sensitivity = 1.0f;
         double threshold = 0.1;
 
         // handle rotations
diff --git a/modules/material/CMakeLists.txt b/modules/material/CMakeLists.txt
index d5b654cc6d00ce77d93b4666f48b7a5097e674b3..ed3804531d36f9850bbb5d334e4fed9b43d92434 100644
--- a/modules/material/CMakeLists.txt
+++ b/modules/material/CMakeLists.txt
@@ -12,8 +12,6 @@ set(vkcv_material_include ${PROJECT_SOURCE_DIR}/include)
 set(vkcv_material_sources
 		${vkcv_material_include}/vkcv/material/Material.hpp
 		${vkcv_material_source}/vkcv/material/Material.cpp
-		${vkcv_material_include}/vkcv/material/PBRMaterial.hpp
-		${vkcv_material_source}/vkcv/material/PBRMaterial.cpp
 )
 
 # adding source files to the module
diff --git a/modules/material/include/vkcv/material/Material.hpp b/modules/material/include/vkcv/material/Material.hpp
index 00b492072fa4ef8b7b41f70202d515ee4ac828fa..9b54d99828eca3738fed9ff1c4078ca9f87eaefa 100644
--- a/modules/material/include/vkcv/material/Material.hpp
+++ b/modules/material/include/vkcv/material/Material.hpp
@@ -1,14 +1,70 @@
 #pragma once
+
+#include <vector>
+
+#include <vkcv/Core.hpp>
 #include <vkcv/Handles.hpp>
 
 namespace vkcv::material {
-
+	
+	enum class MaterialType {
+		PBR_MATERIAL = 1,
+		
+		UNKNOWN = 0
+	};
+	
 	class Material {
 	private:
+		struct Texture {
+			ImageHandle m_Image;
+			SamplerHandle m_Sampler;
+			std::vector<float> m_Factors;
+		};
+		
+		MaterialType m_Type;
+		DescriptorSetHandle m_DescriptorSet;
+		std::vector<Texture> m_Textures;
+		
 	public:
-		const DescriptorSetHandle m_DescriptorSetHandle;
-	protected:
-		Material(const DescriptorSetHandle& setHandle); 
+		Material();
+		~Material() = default;
+		
+		Material(const Material& other) = default;
+		Material(Material&& other) = default;
+		
+		Material& operator=(const Material& other) = default;
+		Material& operator=(Material&& other) = default;
+		
+		[[nodiscard]]
+		MaterialType getType() const;
+		
+		[[nodiscard]]
+		const DescriptorSetHandle& getDescriptorSet() const;
+		
+		explicit operator bool() const;
+		
+		bool operator!() const;
+		
+		static const std::vector<DescriptorBinding>& getDescriptorBindings(MaterialType type);
+		
+		static Material createPBR(Core &core,
+								  const ImageHandle &colorImg,
+								  const SamplerHandle &colorSmp,
+								  const ImageHandle &normalImg,
+								  const SamplerHandle &normalSmp,
+								  const ImageHandle &metRoughImg,
+								  const SamplerHandle &metRoughSmp,
+								  const ImageHandle &occlusionImg,
+								  const SamplerHandle &occlusionSmp,
+								  const ImageHandle &emissiveImg,
+								  const SamplerHandle &emissiveSmp,
+								  const float baseColorFactor [4],
+								  float metallicFactor,
+								  float roughnessFactor,
+								  float normalScale,
+								  float occlusionStrength,
+								  const float emissiveFactor [3]);
+	
 	};
 	
 }
diff --git a/modules/material/include/vkcv/material/PBRMaterial.hpp b/modules/material/include/vkcv/material/PBRMaterial.hpp
deleted file mode 100644
index 09a5214b0e748a09ef8caefe5bf2b1a69ecbd8e1..0000000000000000000000000000000000000000
--- a/modules/material/include/vkcv/material/PBRMaterial.hpp
+++ /dev/null
@@ -1,103 +0,0 @@
-#pragma once
-
-#include <vector>
-
-#include <vkcv/DescriptorConfig.hpp>
-#include <vkcv/Core.hpp>
-
-
-#include "Material.hpp"
-
-namespace vkcv::material
-{
-    class PBRMaterial : Material
-    {
-    private:
-        struct vec3 {
-            float x, y, z;
-        };
-        struct vec4 {
-            float x, y, z, a;
-        };
-        PBRMaterial(const ImageHandle& colorImg,
-            const SamplerHandle& colorSmp,
-            const ImageHandle& normalImg,
-            const SamplerHandle& normalSmp,
-            const ImageHandle& metRoughImg,
-            const SamplerHandle& metRoughSmp,
-            const ImageHandle& occlusionImg,
-            const SamplerHandle& occlusionSmp,
-            const ImageHandle& emissiveImg,
-            const SamplerHandle& emissiveSmp,
-            const DescriptorSetHandle& setHandle,
-            vec4 baseColorFactor,
-            float metallicFactor,
-            float roughnessFactor,
-            float normalScale,
-            float occlusionStrength,
-            vec3 emissiveFactor) noexcept;
-
-
-    public:
-        PBRMaterial() = delete;
-        
-        const ImageHandle   m_ColorTexture;
-        const SamplerHandle m_ColorSampler;
-
-        const ImageHandle   m_NormalTexture;
-        const SamplerHandle m_NormalSampler;
-
-        const ImageHandle   m_MetRoughTexture;
-        const SamplerHandle m_MetRoughSampler;
-
-        const ImageHandle m_OcclusionTexture;
-        const SamplerHandle m_OcclusionSampler;
-
-        const ImageHandle m_EmissiveTexture;
-        const SamplerHandle m_EmissiveSampler;
-
-        //
-        const vec4 m_BaseColorFactor;
-		const float m_MetallicFactor;
-		const float m_RoughnessFactor;
-		const float m_NormalScale;
-		const float m_OcclusionStrength;
-		const vec3 m_EmissiveFactor;
-
-        /*
-        * Returns the material's necessary descriptor bindings which serves as its descriptor layout
-        * The binding is in the following order:
-        * 0 - diffuse texture
-        * 1 - diffuse sampler
-        * 2 - normal texture
-        * 3 - normal sampler
-        * 4 - metallic roughness texture
-        * 5 - metallic roughness sampler
-        * 6 - occlusion texture
-        * 7 - occlusion sampler
-        * 8 - emissive texture
-        * 9 - emissive sampler
-        */
-        static std::vector<DescriptorBinding> getDescriptorBindings() noexcept;
-
-        static PBRMaterial create(
-            vkcv::Core* core,
-            ImageHandle          &colorImg,
-            SamplerHandle        &colorSmp,
-            ImageHandle          &normalImg,
-            SamplerHandle        &normalSmp,
-            ImageHandle          &metRoughImg,
-            SamplerHandle        &metRoughSmp,
-			ImageHandle			&occlusionImg,
-			SamplerHandle		&occlusionSmp,
-			ImageHandle			&emissiveImg,
-			SamplerHandle		&emissiveSmp,
-            vec4 baseColorFactor,
-            float metallicFactor,
-            float roughnessFactor,
-            float normalScale,
-            float occlusionStrength,
-            vec3 emissiveFactor);
-
-    };
-}
\ No newline at end of file
diff --git a/modules/material/src/vkcv/material/Material.cpp b/modules/material/src/vkcv/material/Material.cpp
index 9168bcfbf924e9868ceaaff74aef5d3c6b99739c..409db0b9dd83f91b6a2afbb48d74933ab1a483fc 100644
--- a/modules/material/src/vkcv/material/Material.cpp
+++ b/modules/material/src/vkcv/material/Material.cpp
@@ -2,11 +2,185 @@
 #include "vkcv/material/Material.hpp"
 
 namespace vkcv::material {
+	
+	Material::Material() {
+		m_Type = MaterialType::UNKNOWN;
+	}
 
-	//TODO
-
-	Material::Material(const DescriptorSetHandle& setHandle) : m_DescriptorSetHandle(setHandle)
+	MaterialType Material::getType() const {
+		return m_Type;
+	}
+	
+	const DescriptorSetHandle & Material::getDescriptorSet() const {
+		return m_DescriptorSet;
+	}
+	
+	Material::operator bool() const {
+		return (m_Type != MaterialType::UNKNOWN);
+	}
+	
+	bool Material::operator!() const {
+		return (m_Type == MaterialType::UNKNOWN);
+	}
+	
+	const std::vector<DescriptorBinding>& Material::getDescriptorBindings(MaterialType type)
 	{
+		static std::vector<DescriptorBinding> pbr_bindings;
+		static std::vector<DescriptorBinding> default_bindings;
+		
+		switch (type) {
+			case MaterialType::PBR_MATERIAL:
+				if (pbr_bindings.empty()) {
+					pbr_bindings.emplace_back(0, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+					pbr_bindings.emplace_back(1, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+					pbr_bindings.emplace_back(2, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+					pbr_bindings.emplace_back(3, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+					pbr_bindings.emplace_back(4, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+					pbr_bindings.emplace_back(5, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+					pbr_bindings.emplace_back(6, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+					pbr_bindings.emplace_back(7, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+					pbr_bindings.emplace_back(8, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
+					pbr_bindings.emplace_back(9, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
+				}
+				
+				return pbr_bindings;
+			default:
+				return default_bindings;
+		}
+	}
+	
+	static void fillImage(Image& image, float data [4]) {
+		std::vector<float> vec (image.getWidth() * image.getHeight() * image.getDepth() * 4);
+		
+		for (size_t i = 0; i < vec.size(); i++) {
+			vec[i] = data[i % 4];
+		}
+		
+		image.fill(data);
+	}
+	
+	Material Material::createPBR(Core &core,
+								 const ImageHandle &colorImg, const SamplerHandle &colorSmp,
+								 const ImageHandle &normalImg, const SamplerHandle &normalSmp,
+								 const ImageHandle &metRoughImg, const SamplerHandle &metRoughSmp,
+								 const ImageHandle &occlusionImg, const SamplerHandle &occlusionSmp,
+								 const ImageHandle &emissiveImg, const SamplerHandle &emissiveSmp,
+								 const float baseColorFactor [4],
+								 float metallicFactor,
+								 float roughnessFactor,
+								 float normalScale,
+								 float occlusionStrength,
+								 const float emissiveFactor [3]) {
+		ImageHandle images [5] = {
+				colorImg, normalImg, metRoughImg, occlusionImg, emissiveImg
+		};
+		
+		SamplerHandle samplers [5] = {
+				colorSmp, normalSmp, metRoughSmp, occlusionSmp, emissiveSmp
+		};
+		
+		if (!colorImg) {
+			vkcv::Image defaultColor = core.createImage(vk::Format::eR8G8B8A8Srgb, 2, 2);
+			float colorData [4] = { 228, 51, 255, 1 };
+			fillImage(defaultColor, colorData);
+			images[0] = defaultColor.getHandle();
+		}
+		
+		if (!normalImg) {
+			vkcv::Image defaultNormal = core.createImage(vk::Format::eR8G8B8A8Srgb, 2, 2);
+			float normalData [4] = { 0, 0, 1, 0 };
+			fillImage(defaultNormal, normalData);
+			images[1] = defaultNormal.getHandle();
+		}
+		
+		if (!metRoughImg) {
+			vkcv::Image defaultRough = core.createImage(vk::Format::eR8G8B8A8Srgb, 2, 2);
+			float roughData [4] = { 228, 51, 255, 1 };
+			fillImage(defaultRough, roughData);
+			images[2] = defaultRough.getHandle();
+		}
+		
+		if (!occlusionImg) {
+			vkcv::Image defaultOcclusion = core.createImage(vk::Format::eR8G8B8A8Srgb, 2, 2);
+			float occlusionData [4] = { 228, 51, 255, 1 };
+			fillImage(defaultOcclusion, occlusionData);
+			images[3] = defaultOcclusion.getHandle();
+		}
+		
+		if (!emissiveImg) {
+			vkcv::Image defaultEmissive = core.createImage(vk::Format::eR8G8B8A8Srgb, 2, 2);
+			float emissiveData [4] = { 0, 0, 0, 1 };
+			fillImage(defaultEmissive, emissiveData);
+			images[4] = defaultEmissive.getHandle();
+		}
+		
+		if (!colorSmp) {
+			samplers[0] = core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT
+			);
+		}
+		
+		if (!normalSmp) {
+			samplers[1] = core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT
+			);
+		}
+		
+		if (!metRoughSmp) {
+			samplers[2] = core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT
+			);
+		}
+		
+		if (!occlusionSmp) {
+			samplers[3] = core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT
+			);
+		}
+		
+		if (!emissiveSmp) {
+			samplers[4] = core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT
+			);
+		}
+		
+		Material material;
+		material.m_Type = MaterialType::PBR_MATERIAL;
+		
+		const auto& bindings = getDescriptorBindings(material.m_Type);
+		material.m_DescriptorSet = core.createDescriptorSet(bindings);;
+		
+		material.m_Textures.reserve(bindings.size());
+		material.m_Textures.push_back({ images[0], samplers[0], std::vector<float>(baseColorFactor, baseColorFactor+4) });
+		material.m_Textures.push_back({ images[1], samplers[1], { normalScale } });
+		material.m_Textures.push_back({ images[2], samplers[2], { metallicFactor, roughnessFactor } });
+		material.m_Textures.push_back({ images[3], samplers[3], { occlusionStrength } });
+		material.m_Textures.push_back({ images[4], samplers[4], std::vector<float>(emissiveFactor, emissiveFactor+3) });
+		
+		vkcv::DescriptorWrites setWrites;
+		
+		for (size_t i = 0; i < material.m_Textures.size(); i++) {
+			setWrites.sampledImageWrites.emplace_back(i * 2, material.m_Textures[i].m_Image);
+			setWrites.samplerWrites.emplace_back(i * 2 + 1, material.m_Textures[i].m_Sampler);
+		}
+		
+		core.writeDescriptorSet(material.m_DescriptorSet, setWrites);
+		return material;
 	}
 
 }
diff --git a/modules/material/src/vkcv/material/PBRMaterial.cpp b/modules/material/src/vkcv/material/PBRMaterial.cpp
deleted file mode 100644
index d27e755c06a39e369d22efc997a0b411d067c132..0000000000000000000000000000000000000000
--- a/modules/material/src/vkcv/material/PBRMaterial.cpp
+++ /dev/null
@@ -1,194 +0,0 @@
-#include "vkcv/material/PBRMaterial.hpp"
-
-
-namespace vkcv::material
-{
-    PBRMaterial::PBRMaterial(
-        const ImageHandle& colorImg,
-        const SamplerHandle& colorSmp,
-        const ImageHandle& normalImg,
-        const SamplerHandle& normalSmp,
-        const ImageHandle& metRoughImg,
-        const SamplerHandle& metRoughSmp,
-        const ImageHandle& occlusionImg,
-        const SamplerHandle& occlusionSmp,
-        const ImageHandle& emissiveImg,
-        const SamplerHandle& emissiveSmp,
-        const DescriptorSetHandle& setHandle,
-        vec4 baseColorFactor,
-        float metallicFactor,
-        float roughnessFactor,
-        float normalScale,
-        float occlusionStrength,
-        vec3 emissiveFactor) noexcept :
-        m_ColorTexture(colorImg),
-        m_ColorSampler(colorSmp),
-        m_NormalTexture(normalImg),
-        m_NormalSampler(normalSmp),
-        m_MetRoughTexture(metRoughImg),
-        m_MetRoughSampler(metRoughSmp),
-        m_OcclusionTexture(occlusionImg),
-        m_OcclusionSampler(occlusionSmp),
-        m_EmissiveTexture(emissiveImg),
-        m_EmissiveSampler(emissiveSmp),
-        Material(setHandle),
-        m_BaseColorFactor(baseColorFactor),
-        m_MetallicFactor(metallicFactor),
-        m_RoughnessFactor(roughnessFactor),
-        m_NormalScale(normalScale),
-        m_OcclusionStrength(occlusionStrength),
-        m_EmissiveFactor(emissiveFactor)
-    {
-    }
-
-    std::vector<DescriptorBinding> PBRMaterial::getDescriptorBindings() noexcept
-    {
-		static std::vector<DescriptorBinding> bindings;
-		
-		if (bindings.empty()) {
-			bindings.emplace_back(0, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
-			bindings.emplace_back(1, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
-			bindings.emplace_back(2, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
-			bindings.emplace_back(3, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
-			bindings.emplace_back(4, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
-			bindings.emplace_back(5, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
-			bindings.emplace_back(6, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
-			bindings.emplace_back(7, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
-			bindings.emplace_back(8, DescriptorType::IMAGE_SAMPLED, 1, ShaderStage::FRAGMENT);
-			bindings.emplace_back(9, DescriptorType::SAMPLER, 1, ShaderStage::FRAGMENT);
-		}
-    	
-        return bindings;
-    }
-
-    PBRMaterial PBRMaterial::create(
-        vkcv::Core* core,
-        ImageHandle& colorImg,
-        SamplerHandle& colorSmp,
-        ImageHandle& normalImg,
-        SamplerHandle& normalSmp,
-        ImageHandle& metRoughImg,
-        SamplerHandle& metRoughSmp,
-        ImageHandle& occlusionImg,
-        SamplerHandle& occlusionSmp,
-        ImageHandle& emissiveImg,
-        SamplerHandle& emissiveSmp,
-        vec4 baseColorFactor,
-        float metallicFactor,
-        float roughnessFactor,
-        float normalScale,
-        float occlusionStrength,
-        vec3 emissiveFactor)
-    {
-        //Test if Images and samplers valid, if not use default
-         if (!colorImg) {
-            vkcv::Image defaultColor = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
-            vec4 colorData{ 228, 51, 255,1 };
-            defaultColor.fill(&colorData);
-            colorImg = defaultColor.getHandle();
-        }
-        if (!normalImg) {
-            vkcv::Image defaultNormal = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
-            vec4 normalData{ 0, 0, 1,0 };
-            defaultNormal.fill(&normalData);
-            normalImg = defaultNormal.getHandle();
-        }
-        if (!metRoughImg) {
-            vkcv::Image defaultRough = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
-            vec4 roughData{ 228, 51, 255,1 };
-            defaultRough.fill(&roughData);
-            metRoughImg = defaultRough.getHandle();
-        }
-        if (!occlusionImg) {
-            vkcv::Image defaultOcclusion = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
-            vec4 occlusionData{ 228, 51, 255,1 };
-            defaultOcclusion.fill(&occlusionData);
-            occlusionImg = defaultOcclusion.getHandle();
-        }
-        if (!emissiveImg) {
-            vkcv::Image defaultEmissive = core->createImage(vk::Format::eR8G8B8A8Srgb, 1, 1);
-            vec4 emissiveData{ 0, 0, 0,1 };
-            defaultEmissive.fill(&emissiveData);
-            emissiveImg = defaultEmissive.getHandle();
-        }
-        if (!colorSmp) {            
-            colorSmp = core->createSampler(
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerMipmapMode::LINEAR,
-                vkcv::SamplerAddressMode::REPEAT
-            );            
-        }
-        if (!normalSmp) {            
-            normalSmp = core->createSampler(
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerMipmapMode::LINEAR,
-                vkcv::SamplerAddressMode::REPEAT
-            );            
-        }
-        if (!metRoughSmp) {
-            metRoughSmp = core->createSampler(
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerMipmapMode::LINEAR,
-                vkcv::SamplerAddressMode::REPEAT
-            );            
-        }
-        if (!occlusionSmp) {
-            occlusionSmp = core->createSampler(
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerMipmapMode::LINEAR,
-                vkcv::SamplerAddressMode::REPEAT
-            );
-        }
-        if (!emissiveSmp) {
-            emissiveSmp = core->createSampler(
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerFilterType::LINEAR,
-                vkcv::SamplerMipmapMode::LINEAR,
-                vkcv::SamplerAddressMode::REPEAT
-            );
-        }
-        
-
-
-        //create descriptorset
-        vkcv::DescriptorSetHandle descriptorSetHandle = core->createDescriptorSet(getDescriptorBindings());
-        //writes
-        vkcv::DescriptorWrites setWrites;
-        setWrites.sampledImageWrites = {
-            vkcv::SampledImageDescriptorWrite(0, colorImg),
-            vkcv::SampledImageDescriptorWrite(2, normalImg),
-            vkcv::SampledImageDescriptorWrite(4, metRoughImg),
-            vkcv::SampledImageDescriptorWrite(6, occlusionImg),
-            vkcv::SampledImageDescriptorWrite(8, emissiveImg) };
-        setWrites.samplerWrites = {
-            vkcv::SamplerDescriptorWrite(1, colorSmp),
-            vkcv::SamplerDescriptorWrite(3, normalSmp),
-            vkcv::SamplerDescriptorWrite(5, metRoughSmp),
-            vkcv::SamplerDescriptorWrite(7, occlusionSmp),
-            vkcv::SamplerDescriptorWrite(9, emissiveSmp) };
-        core->writeDescriptorSet(descriptorSetHandle, setWrites);
-
-        return PBRMaterial(
-            colorImg,
-            colorSmp,
-            normalImg,
-            normalSmp,
-            metRoughImg,
-            metRoughSmp,
-            occlusionImg,
-            occlusionSmp,
-            emissiveImg,
-            emissiveSmp,
-            descriptorSetHandle,
-            baseColorFactor,
-            metallicFactor,
-            roughnessFactor,
-            normalScale,
-            occlusionStrength,
-            emissiveFactor);
-    }
-}
\ No newline at end of file
diff --git a/modules/meshlet/CMakeLists.txt b/modules/meshlet/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d576466d3d125d3a19640088a9b5725ac7a46b97
--- /dev/null
+++ b/modules/meshlet/CMakeLists.txt
@@ -0,0 +1,36 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_meshlet)
+
+# setting c++ standard for the module
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_meshlet_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_meshlet_include ${PROJECT_SOURCE_DIR}/include)
+
+# Add source and header files to the module
+set(vkcv_meshlet_sources
+		${vkcv_meshlet_include}/vkcv/meshlet/Meshlet.hpp
+		${vkcv_meshlet_source}/vkcv/meshlet/Meshlet.cpp
+
+		${vkcv_meshlet_include}/vkcv/meshlet/Tipsify.hpp
+		${vkcv_meshlet_source}/vkcv/meshlet/Tipsify.cpp
+
+		${vkcv_meshlet_include}/vkcv/meshlet/Forsyth.hpp
+		${vkcv_meshlet_source}/vkcv/meshlet/Forsyth.cpp)
+
+# adding source files to the module
+add_library(vkcv_meshlet STATIC ${vkcv_meshlet_sources})
+
+
+# link the required libraries to the module
+target_link_libraries(vkcv_meshlet vkcv ${vkcv_libraries})
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_meshlet SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_meshlet BEFORE PUBLIC ${vkcv_meshlet_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(vkcv_meshlet vkcv vkcv_asset_loader vkcv_camera)
diff --git a/modules/meshlet/include/vkcv/meshlet/Forsyth.hpp b/modules/meshlet/include/vkcv/meshlet/Forsyth.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..43dc9a3b6bb81ea915268de7a7b53b18efd27638
--- /dev/null
+++ b/modules/meshlet/include/vkcv/meshlet/Forsyth.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "Meshlet.hpp"
+
+namespace vkcv::meshlet
+{
+ /**
+  * Reorders the index buffer, simulating a LRU cache, so that vertices are grouped together in close triangle patches
+  * @param idxBuf current IndexBuffer
+  * @param vertexCount of the mesh
+  * @return new reordered index buffer to replace the input index buffer
+  * References:
+  * https://tomforsyth1000.github.io/papers/fast_vert_cache_opt.html
+  * https://www.martin.st/thesis/efficient_triangle_reordering.pdf
+  * https://github.com/vivkin/forsyth/blob/master/forsyth.h
+  */
+ VertexCacheReorderResult forsythReorder(const std::vector<uint32_t> &idxBuf, const size_t vertexCount);
+}
diff --git a/modules/meshlet/include/vkcv/meshlet/Meshlet.hpp b/modules/meshlet/include/vkcv/meshlet/Meshlet.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9900dffaf28c85753d367ba79bbdf5c19a2cf479
--- /dev/null
+++ b/modules/meshlet/include/vkcv/meshlet/Meshlet.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <vector>
+#include <map>
+#include <glm/glm.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+
+namespace vkcv::meshlet {
+
+    struct Vertex {
+        glm::vec3   position;
+        float       padding0;
+        glm::vec3   normal;
+        float       padding1;
+    };
+
+    struct Meshlet {
+        uint32_t    vertexOffset;
+        uint32_t    vertexCount;
+        uint32_t    indexOffset;
+        uint32_t    indexCount;
+        glm::vec3   meanPosition;
+        float       boundingSphereRadius;
+    };
+
+    struct VertexCacheReorderResult {
+        /**
+         * @param indexBuffer new indexBuffer
+         * @param skippedIndices indices that have a spacial break
+         */
+        VertexCacheReorderResult(const std::vector<uint32_t> indexBuffer, const std::vector<uint32_t> skippedIndices)
+                :indexBuffer(indexBuffer), skippedIndices(skippedIndices) {}
+
+        std::vector<uint32_t> indexBuffer;
+        std::vector<uint32_t>  skippedIndices;
+    };
+
+    struct MeshShaderModelData {
+        std::vector<Vertex>     vertices;
+        std::vector<uint32_t>   localIndices;
+        std::vector<Meshlet>    meshlets;
+    };
+
+    std::vector<Vertex> convertToVertices(
+            const std::vector<uint8_t>&         vertexData,
+            const uint64_t                      vertexCount,
+            const vkcv::asset::VertexAttribute& positionAttribute,
+            const vkcv::asset::VertexAttribute& normalAttribute);
+
+    MeshShaderModelData createMeshShaderModelData(
+            const std::vector<Vertex>&      inVertices,
+            const std::vector<uint32_t>&    inIndices,
+            const std::vector<uint32_t>& deadEndIndices = {});
+
+    std::vector<uint32_t> assetLoaderIndicesTo32BitIndices(
+            const std::vector<uint8_t>& indexData,
+            vkcv::asset::IndexType indexType);
+
+}
\ No newline at end of file
diff --git a/modules/meshlet/include/vkcv/meshlet/Tipsify.hpp b/modules/meshlet/include/vkcv/meshlet/Tipsify.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6fb4b37d9c17c82642c3b5e7667c3e8acc50b8c0
--- /dev/null
+++ b/modules/meshlet/include/vkcv/meshlet/Tipsify.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "Meshlet.hpp"
+#include <algorithm>
+#include <iostream>
+
+namespace vkcv::meshlet {
+    /**
+     * reorders the IndexBuffer, so all usages of vertices to triangle are as close as possible
+     * @param indexBuffer32Bit current IndexBuffer
+     * @param vertexCount of the mesh
+     * @param cacheSize of the priority cache <br>
+     * Recommended: 20. Keep the value between 5 and 50 <br>
+     * low:         more random and patchy<br>
+     * high:        closer vertices have higher chance -> leads to sinuous lines
+     * @return new IndexBuffer that replaces the input IndexBuffer, and the indices that are skipped
+     *
+     * https://gfx.cs.princeton.edu/pubs/Sander_2007_%3ETR/tipsy.pdf
+     * https://www.martin.st/thesis/efficient_triangle_reordering.pdf
+     */
+    VertexCacheReorderResult tipsifyMesh(const std::vector<uint32_t> &indexBuffer32Bit,
+                                         const int vertexCount, const unsigned int cacheSize = 20);
+}
\ No newline at end of file
diff --git a/modules/meshlet/src/vkcv/meshlet/Forsyth.cpp b/modules/meshlet/src/vkcv/meshlet/Forsyth.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fd0f160d65b8db81102f9eb6a9d60cf735999d44
--- /dev/null
+++ b/modules/meshlet/src/vkcv/meshlet/Forsyth.cpp
@@ -0,0 +1,317 @@
+#include "vkcv/meshlet/Forsyth.hpp"
+#include <vkcv/Logger.hpp>
+#include <array>
+#include <cmath>
+
+namespace vkcv::meshlet
+{
+    /*
+     * CACHE AND VALENCE
+     * SIZE AND SCORE CONSTANTS
+     * CHANGE AS NEEDED
+     */
+
+    // set these to adjust performance and result quality
+    const size_t VERTEX_CACHE_SIZE = 8;
+    const size_t CACHE_FUNCTION_LENGTH = 32;
+
+    // score function constants
+    const float CACHE_DECAY_POWER = 1.5f;
+    const float LAST_TRI_SCORE = 0.75f;
+
+    const float VALENCE_BOOST_SCALE = 2.0f;
+    const float VALENCE_BOOST_POWER = 0.5f;
+
+    // sizes for precalculated tables
+    // make sure that cache score is always >= vertex_cache_size
+    const size_t CACHE_SCORE_TABLE_SIZE = 32;
+    const size_t VALENCE_SCORE_TABLE_SIZE = 32;
+
+    // precalculated tables
+    std::array<float, CACHE_SCORE_TABLE_SIZE> cachePositionScore = {};
+    std::array<float, VALENCE_SCORE_TABLE_SIZE> valenceScore = {};
+
+    // function to populate the cache position and valence score tables
+    void initScoreTables()
+    {
+        for(size_t i = 0; i < CACHE_SCORE_TABLE_SIZE; i++)
+        {
+            float score = 0.0f;
+            if (i < 3)
+            {
+                score = LAST_TRI_SCORE;
+            }
+            else
+            {
+                const float scaler = 1.0f / static_cast<float>(CACHE_FUNCTION_LENGTH - 3);
+                score = 1.0f - (i - 3) * scaler;
+                score = std::pow(score, CACHE_DECAY_POWER);
+            }
+            cachePositionScore[i] = score;
+        }
+
+        for(size_t i = 0; i < VALENCE_SCORE_TABLE_SIZE; i++)
+        {
+            const float valenceBoost = std::pow(i, -VALENCE_BOOST_POWER);
+            const float score = VALENCE_BOOST_SCALE * valenceBoost;
+
+            valenceScore[i] = score;
+        }
+    }
+
+    /**
+     * Return the vertex' score, depending on its current active triangle count and cache position
+     * Add a valence boost to score, if active triangles are below VALENCE_SCORE_TABLE_SIZE
+     * @param numActiveTris the active triangles on this vertex
+     * @param cachePos the vertex' position in the cache
+     * @return vertex' score
+     */
+    float findVertexScore(uint32_t numActiveTris, int32_t cachePos)
+    {
+        if(numActiveTris == 0)
+            return 0.0f;
+
+        float score = 0.0f;
+
+        if (cachePos >= 0)
+            score = cachePositionScore[cachePos];
+
+        if (numActiveTris < VALENCE_SCORE_TABLE_SIZE)
+            score += valenceScore[numActiveTris];
+
+        return score;
+    }
+
+    VertexCacheReorderResult forsythReorder(const std::vector<uint32_t> &idxBuf, const size_t vertexCount)
+    {
+        std::vector<uint32_t> skippedIndices;
+
+        initScoreTables();
+
+        // get the total triangle count from the index buffer
+        const size_t triangleCount = idxBuf.size() / 3;
+
+        // per-vertex active triangle count
+        std::vector<uint8_t> numActiveTris(vertexCount, 0);
+        // iterate over indices, count total occurrences of each vertex
+        for(const auto index : idxBuf)
+        {
+            if(numActiveTris[index] == UINT8_MAX)
+            {
+                vkcv_log(LogLevel::ERROR, "Unsupported mesh.");
+                vkcv_log(LogLevel::ERROR, "Vertex shared by too many triangles.");
+                return VertexCacheReorderResult({}, {});
+            }
+
+            numActiveTris[index]++;
+        }
+
+
+        // allocate remaining vectors
+        /**
+         * offsets: contains the vertices' offset into the triangleIndices vector
+         * Offset itself is the sum of triangles required by the previous vertices
+         *
+         * lastScore: the vertices' most recent calculated score
+         *
+         * cacheTag: the vertices' most recent cache score
+         *
+         * triangleAdded: boolean flags to denote whether a triangle has been processed or not
+         *
+         * triangleScore: total score of the three vertices making up the triangle
+         *
+         * triangleIndices: indices for the triangles
+         */
+        std::vector<uint32_t> offsets(vertexCount, 0);
+        std::vector<float> lastScore(vertexCount, 0.0f);
+        std::vector<int8_t> cacheTag(vertexCount, -1);
+
+        std::vector<bool> triangleAdded(triangleCount, false);
+        std::vector<float> triangleScore(triangleCount, 0.0f);
+
+        std::vector<int32_t> triangleIndices(idxBuf.size(), 0);
+
+
+        // sum the number of active triangles for all previous vertices
+        // null the number of active triangles afterwards for recalculation in second loop
+        uint32_t sum = 0;
+        for(size_t i = 0; i < vertexCount; i++)
+        {
+            offsets[i] = sum;
+            sum += numActiveTris[i];
+            numActiveTris[i] = 0;
+        }
+        // create the triangle indices, using the newly calculated offsets, and increment numActiveTris
+        // every vertex should be referenced by a triangle index now
+        for(size_t i = 0; i < triangleCount; i++)
+        {
+            for(size_t j = 0; j < 3; j++)
+            {
+                uint32_t v = idxBuf[3 * i + j];
+                triangleIndices[offsets[v] + numActiveTris[v]] = static_cast<int32_t>(i);
+                numActiveTris[v]++;
+            }
+        }
+
+        // calculate and initialize the triangle score, by summing the vertices' score
+        for (size_t i = 0; i < vertexCount; i++)
+        {
+            lastScore[i] = findVertexScore(numActiveTris[i], static_cast<int32_t>(cacheTag[i]));
+
+            for(size_t j = 0; j < numActiveTris[i]; j++)
+            {
+                triangleScore[triangleIndices[offsets[i] + j]] += lastScore[i];
+            }
+        }
+
+        // find best triangle to start reordering with
+        int32_t bestTriangle = -1;
+        float   bestScore    = -1.0f;
+        for(size_t i = 0; i < triangleCount; i++)
+        {
+            if(triangleScore[i] > bestScore)
+            {
+                bestScore = triangleScore[i];
+                bestTriangle = static_cast<int32_t>(i);
+            }
+        }
+
+        // allocate output triangles
+        std::vector<int32_t> outTriangles(triangleCount, 0);
+        uint32_t outPos = 0;
+
+        // initialize cache (with -1)
+        std::array<int32_t, VERTEX_CACHE_SIZE + 3> cache = {};
+        for(auto &element : cache)
+        {
+            element = -1;
+        }
+
+        uint32_t scanPos = 0;
+
+        // begin reordering routine
+        // output the currently best triangle, as long as there are triangles left to output
+        while(bestTriangle >= 0)
+        {
+            // mark best triangle as added
+            triangleAdded[bestTriangle] = true;
+            // output this triangle
+            outTriangles[outPos++] = bestTriangle;
+
+            // push best triangle's vertices into the cache
+            for(size_t i = 0; i < 3; i++)
+            {
+                uint32_t v = idxBuf[3 * bestTriangle + i];
+
+                // get vertex' cache position, if its -1, set its position to the end
+                int8_t endPos = cacheTag[v];
+                if(endPos < 0)
+                    endPos = static_cast<int8_t>(VERTEX_CACHE_SIZE + i);
+
+                // shift vertices' cache entries forward by one
+                for(int8_t j = endPos; j > i; j--)
+                {
+                    cache[j] = cache[j - 1];
+
+                    // if cache slot is valid vertex,
+                    // update the vertex cache tag accordingly
+                    if (cache[j] >= 0)
+                        cacheTag[cache[j]]++;
+                }
+
+                // insert current vertex into its new target slot
+                cache[i] = static_cast<int32_t>(v);
+                cacheTag[v] = static_cast<int8_t>(i);
+
+                // find current triangle in the list of active triangles
+                // remove it by moving the last triangle into the slot the current triangle is holding.
+                for (size_t j = 0; j < numActiveTris[v]; j++)
+                {
+                    if(triangleIndices[offsets[v] + j] == bestTriangle)
+                    {
+                        triangleIndices[offsets[v] + j] = triangleIndices[offsets[v] + numActiveTris[v] - 1];
+                        break;
+                    }
+                }
+                // shorten the list
+                numActiveTris[v]--;
+            }
+
+            // update scores of all triangles in cache
+            for (size_t i = 0; i < cache.size(); i++)
+            {
+                int32_t v = cache[i];
+                if (v < 0)
+                    break;
+
+                // this vertex has been pushed outside of the actual cache
+                if(i >= VERTEX_CACHE_SIZE)
+                {
+                    cacheTag[v] = -1;
+                    cache[i] = -1;
+                }
+
+                float newScore = findVertexScore(numActiveTris[v], cacheTag[v]);
+                float diff = newScore - lastScore[v];
+
+                for(size_t j = 0; j < numActiveTris[v]; j++)
+                {
+                    triangleScore[triangleIndices[offsets[v] + j]] += diff;
+                }
+                lastScore[v] = newScore;
+            }
+
+            // find best triangle reference by vertices in cache
+            bestTriangle = -1;
+            bestScore = -1.0f;
+            for(size_t i = 0; i < VERTEX_CACHE_SIZE; i++)
+            {
+                if (cache[i] < 0)
+                    break;
+
+                int32_t v = cache[i];
+                for(size_t j = 0; j < numActiveTris[v]; j++)
+                {
+                    int32_t t = triangleIndices[offsets[v] + j];
+                    if(triangleScore[t] > bestScore)
+                    {
+                        bestTriangle = t;
+                        bestScore = triangleScore[t];
+                    }
+                }
+            }
+
+            // if no triangle was found at all, continue scanning whole list of triangles
+            if (bestTriangle < 0)
+            {
+                for(; scanPos < triangleCount; scanPos++)
+                {
+                    if(!triangleAdded[scanPos])
+                    {
+                        bestTriangle = scanPos;
+
+                        skippedIndices.push_back(3 * outPos);
+
+                        break;
+                    }
+                }
+            }
+        }
+
+
+        // convert triangle index array into full triangle list
+        std::vector<uint32_t> outIndices(idxBuf.size(), 0);
+        outPos = 0;
+        for(size_t i = 0; i < triangleCount; i++)
+        {
+            int32_t t = outTriangles[i];
+            for(size_t j = 0; j < 3; j++)
+            {
+                int32_t v = idxBuf[3 * t + j];
+                outIndices[outPos++] = static_cast<uint32_t>(v);
+            }
+        }
+
+        return VertexCacheReorderResult(outIndices, skippedIndices);
+    }
+}
\ No newline at end of file
diff --git a/modules/meshlet/src/vkcv/meshlet/Meshlet.cpp b/modules/meshlet/src/vkcv/meshlet/Meshlet.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..abcad7207ed5a6f80cb292ab2f7e855d3b4c7797
--- /dev/null
+++ b/modules/meshlet/src/vkcv/meshlet/Meshlet.cpp
@@ -0,0 +1,167 @@
+
+#include "vkcv/meshlet/Meshlet.hpp"
+#include <vkcv/Logger.hpp>
+#include <cassert>
+#include <iostream>
+
+namespace vkcv::meshlet {
+
+std::vector<vkcv::meshlet::Vertex> convertToVertices(
+        const std::vector<uint8_t>&         vertexData,
+        const uint64_t                      vertexCount,
+        const vkcv::asset::VertexAttribute& positionAttribute,
+        const vkcv::asset::VertexAttribute& normalAttribute) {
+
+    assert(positionAttribute.type   == vkcv::asset::PrimitiveType::POSITION);
+    assert(normalAttribute.type     == vkcv::asset::PrimitiveType::NORMAL);
+
+    std::vector<vkcv::meshlet::Vertex> vertices;
+    vertices.reserve(vertexCount);
+
+    const size_t positionStepSize   = positionAttribute.stride == 0 ? sizeof(glm::vec3) : positionAttribute.stride;
+    const size_t normalStepSize     = normalAttribute.stride   == 0 ? sizeof(glm::vec3) : normalAttribute.stride;
+
+    for (int i = 0; i < vertexCount; i++) {
+        Vertex v;
+
+        const size_t positionOffset = positionAttribute.offset  + positionStepSize  * i;
+        const size_t normalOffset   = normalAttribute.offset    + normalStepSize    * i;
+
+        v.position  = *reinterpret_cast<const glm::vec3*>(&(vertexData[positionOffset]));
+        v.normal    = *reinterpret_cast<const glm::vec3*>(&(vertexData[normalOffset]));
+        vertices.push_back(v);
+    }
+    return vertices;
+}
+
+MeshShaderModelData createMeshShaderModelData(
+        const std::vector<Vertex>&      inVertices,
+        const std::vector<uint32_t>&    inIndices,
+        const std::vector<uint32_t>&    deadEndIndices) {
+
+    MeshShaderModelData data;
+    size_t currentIndex = 0;
+
+    const size_t maxVerticesPerMeshlet = 64;
+    const size_t maxIndicesPerMeshlet  = 126 * 3;
+
+    bool indicesAreLeft = true;
+
+    size_t deadEndIndicesIndex = 0;
+
+    while (indicesAreLeft) {
+        Meshlet meshlet;
+
+        meshlet.indexCount  = 0;
+        meshlet.vertexCount = 0;
+
+        meshlet.indexOffset  = data.localIndices.size();
+        meshlet.vertexOffset = data.vertices.size();
+
+        std::map<uint32_t, uint32_t> globalToLocalIndexMap;
+        std::vector<uint32_t> globalIndicesOrdered;
+
+        while (true) {
+
+            if (deadEndIndicesIndex < deadEndIndices.size()) {
+                const uint32_t deadEndIndex = deadEndIndices[deadEndIndicesIndex];
+                if (deadEndIndex == currentIndex) {
+                    deadEndIndicesIndex++;
+                    break;
+                }
+            }
+
+            indicesAreLeft = currentIndex + 1 <= inIndices.size();
+            if (!indicesAreLeft) {
+                break;
+            }
+
+            bool enoughSpaceForIndices = meshlet.indexCount + 3 < maxIndicesPerMeshlet;
+            if (!enoughSpaceForIndices) {
+                break;
+            }
+
+            size_t vertexCountToAdd = 0;
+            for (int i = 0; i < 3; i++) {
+                const uint32_t globalIndex = inIndices[currentIndex + i];
+                const bool containsVertex  = globalToLocalIndexMap.find(globalIndex) != globalToLocalIndexMap.end();
+                if (!containsVertex) {
+                    vertexCountToAdd++;
+                }
+            }
+
+            bool enoughSpaceForVertices = meshlet.vertexCount + vertexCountToAdd < maxVerticesPerMeshlet;
+            if (!enoughSpaceForVertices) {
+                break;
+            }
+
+            for (int i = 0; i < 3; i++) {
+                const uint32_t globalIndex = inIndices[currentIndex + i];
+
+                uint32_t localIndex;
+                const bool indexAlreadyExists = globalToLocalIndexMap.find(globalIndex) != globalToLocalIndexMap.end();
+                if (indexAlreadyExists) {
+                    localIndex = globalToLocalIndexMap[globalIndex];
+                }
+                else {
+                    localIndex = globalToLocalIndexMap.size();
+                    globalToLocalIndexMap[globalIndex] = localIndex;
+                    globalIndicesOrdered.push_back(globalIndex);
+                }
+
+                data.localIndices.push_back(localIndex);
+            }
+
+            meshlet.indexCount  += 3;
+            currentIndex        += 3;
+            meshlet.vertexCount += vertexCountToAdd;
+        }
+
+        for (const uint32_t globalIndex : globalIndicesOrdered) {
+            const Vertex v = inVertices[globalIndex];
+            data.vertices.push_back(v);
+        }
+
+        // compute mean position
+        meshlet.meanPosition = glm::vec3(0);
+        const uint32_t meshletLastVertexIndex = meshlet.vertexOffset + meshlet.vertexCount;
+
+        for (uint32_t vertexIndex = meshlet.vertexOffset; vertexIndex < meshletLastVertexIndex; vertexIndex++) {
+            const Vertex& v         = data.vertices[vertexIndex];
+            meshlet.meanPosition    += v.position;
+        }
+        meshlet.meanPosition /= meshlet.vertexCount;
+
+        // compute bounding sphere radius
+        meshlet.boundingSphereRadius = 0.f;
+        for (uint32_t vertexIndex = meshlet.vertexOffset; vertexIndex < meshletLastVertexIndex; vertexIndex++) {
+            const Vertex& v = data.vertices[vertexIndex];
+            const float d                   = glm::distance(v.position, meshlet.meanPosition);
+            meshlet.boundingSphereRadius    = glm::max(meshlet.boundingSphereRadius, d);
+        }
+
+        data.meshlets.push_back(meshlet);
+    }
+
+    return data;
+}
+
+std::vector<uint32_t> assetLoaderIndicesTo32BitIndices(const std::vector<uint8_t>& indexData, vkcv::asset::IndexType indexType) {
+    std::vector<uint32_t> indices;
+    if (indexType == vkcv::asset::IndexType::UINT16) {
+        for (int i = 0; i < indexData.size(); i += 2) {
+            const uint16_t index16Bit = *reinterpret_cast<const uint16_t *>(&(indexData[i]));
+            const uint32_t index32Bit = static_cast<uint32_t>(index16Bit);
+            indices.push_back(index32Bit);
+        }
+    } else if (indexType == vkcv::asset::IndexType::UINT32) {
+        for (int i = 0; i < indexData.size(); i += 4) {
+            const uint32_t index32Bit = *reinterpret_cast<const uint32_t *>(&(indexData[i]));
+            indices.push_back(index32Bit);
+        }
+    } else {
+        vkcv_log(vkcv::LogLevel::ERROR, "Unsupported index type");
+    }
+    return indices;
+}
+}
\ No newline at end of file
diff --git a/modules/meshlet/src/vkcv/meshlet/Tipsify.cpp b/modules/meshlet/src/vkcv/meshlet/Tipsify.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c5762100bc37eccbe3e4f6b4c94e5f0e580c53c7
--- /dev/null
+++ b/modules/meshlet/src/vkcv/meshlet/Tipsify.cpp
@@ -0,0 +1,288 @@
+
+#include <vkcv/Logger.hpp>
+#include "vkcv/meshlet/Tipsify.hpp"
+#include <iostream>
+
+namespace vkcv::meshlet {
+
+    const int maxUsedVertices           = 128;
+
+    /**
+     * modulo operation with maxUsedVertices
+     * @param number for modulo operation
+     * @return number between 0 and maxUsedVertices - 1
+     */
+    int mod( int number ){
+        return (number + maxUsedVertices) % maxUsedVertices;
+    }
+
+    /**
+     * searches for the next VertexIndex that was used before or returns any vertexIndex if no used was found
+     * @param livingTriangles
+     * @param usedVerticeStack
+     * @param usedVerticeCount
+     * @param usedVerticeOffset
+     * @param vertexCount
+     * @param lowestLivingVertexIndex
+     * @param currentTriangleIndex
+     * @param skippedIndices
+     * @return a VertexIndex to be used as fanningVertexIndex
+     */
+    int skipDeadEnd(
+            const std::vector<uint8_t> &livingTriangles,
+            const std::vector<uint32_t> &usedVerticeStack,
+            int &usedVerticeCount,
+            int &usedVerticeOffset,
+            int vertexCount,
+            int &lowestLivingVertexIndex,
+            int &currentTriangleIndex,
+            std::vector<uint32_t> &skippedIndices) {
+
+        // returns the latest vertex used that has a living triangle
+        while (mod(usedVerticeCount) != usedVerticeOffset) {
+            // iterate from the latest to the oldest. + maxUsedVertices to always make it a positive number in the range 0 to maxUsedVertices -1
+            int nextVertex = usedVerticeStack[mod(--usedVerticeCount)];
+
+            if (livingTriangles[nextVertex] > 0) {
+                return nextVertex;
+            }
+        }
+        // returns any vertexIndex since no last used has a living triangle
+        while (lowestLivingVertexIndex + 1 < vertexCount) {
+            lowestLivingVertexIndex++;
+            if (livingTriangles[lowestLivingVertexIndex] > 0) {
+                // add index of the vertex to skippedIndices
+                skippedIndices.push_back(static_cast<uint32_t>(currentTriangleIndex * 3));
+                return lowestLivingVertexIndex;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * searches for the best next candidate as a fanningVertexIndex
+     * @param vertexCount
+     * @param lowestLivingVertexIndex
+     * @param cacheSize
+     * @param possibleCandidates
+     * @param numPossibleCandidates
+     * @param lastTimestampCache
+     * @param currentTimeStamp
+     * @param livingTriangles
+     * @param usedVerticeStack
+     * @param usedVerticeCount
+     * @param usedVerticeOffset
+     * @param currentTriangleIndex
+     * @param skippedIndices
+     * @return a VertexIndex to be used as fanningVertexIndex
+     */
+    int getNextVertexIndex(int vertexCount,
+                           int &lowestLivingVertexIndex,
+                           int cacheSize,
+                           const std::vector<uint32_t> &possibleCandidates,
+                           int numPossibleCandidates,
+                           const std::vector<uint32_t> &lastTimestampCache,
+                           int currentTimeStamp,
+                           const std::vector<uint8_t> &livingTriangles,
+                           const std::vector<uint32_t> &usedVerticeStack,
+                           int &usedVerticeCount,
+                           int &usedVerticeOffset,
+                           int &currentTriangleIndex,
+                           std::vector<uint32_t> &skippedIndices) {
+        int nextVertexIndex = -1;
+        int maxPriority     = -1;
+        // calculates the next possibleCandidates that is recently used
+        for (int j = 0; j < numPossibleCandidates; j++) {
+            int vertexIndex = possibleCandidates[j];
+
+            // the candidate needs to be not fanned out yet
+            if (livingTriangles[vertexIndex] > 0) {
+                int priority = -1;
+
+                // prioritizes recent used vertices, but tries not to pick one that has many triangles -> fills holes better
+                if ( currentTimeStamp - lastTimestampCache[vertexIndex] + 2 * livingTriangles[vertexIndex] <=
+                    cacheSize) {
+                    priority = currentTimeStamp - lastTimestampCache[vertexIndex];
+                }
+                // select the vertexIndex with the highest priority
+                if (priority > maxPriority) {
+                    maxPriority     = priority;
+                    nextVertexIndex = vertexIndex;
+                }
+            }
+        }
+
+        // if no candidate is alive, try and find another one
+        if (nextVertexIndex == -1) {
+            nextVertexIndex = skipDeadEnd(
+                    livingTriangles,
+                    usedVerticeStack,
+                    usedVerticeCount,
+                    usedVerticeOffset,
+                    vertexCount,
+                    lowestLivingVertexIndex,
+                    currentTriangleIndex,
+                    skippedIndices);
+        }
+        return nextVertexIndex;
+    }
+
+    VertexCacheReorderResult tipsifyMesh(
+            const std::vector<uint32_t> &indexBuffer32Bit,
+            const int vertexCount,
+            const unsigned int cacheSize) {
+
+        if (indexBuffer32Bit.empty() || vertexCount <= 0) {
+            vkcv_log(LogLevel::ERROR, "Invalid Input.");
+            return VertexCacheReorderResult(indexBuffer32Bit , {});
+        }
+        int triangleCount = indexBuffer32Bit.size() / 3;
+
+       // dynamic array for vertexOccurrence
+        std::vector<uint8_t> vertexOccurrence(vertexCount, 0);
+        // count the occurrence of a vertex in all among all triangles
+        for (size_t i = 0; i < triangleCount * 3; i++) {
+            vertexOccurrence[indexBuffer32Bit[i]]++;
+        }
+
+        int sum = 0;
+        std::vector<uint32_t> offsetVertexOccurrence(vertexCount + 1, 0);
+        // highest offset for later iteration
+        int maxOffset = 0;
+        // calculate the offset of each vertex from the start
+        for (int i = 0; i < vertexCount; i++) {
+            offsetVertexOccurrence[i]   = sum;
+            sum                         += vertexOccurrence[i];
+
+            if (vertexOccurrence[i] > maxOffset) {
+                maxOffset = vertexOccurrence[i];
+            }
+            // reset for reuse
+            vertexOccurrence[i] = 0;
+        }
+        offsetVertexOccurrence[vertexCount] = sum;
+
+        // vertexIndexToTriangle = which vertex belongs to which triangle
+        std::vector<uint32_t> vertexIndexToTriangle(3 * triangleCount, 0);
+        // vertexOccurrence functions as number of usages in all triangles
+        // lowestLivingVertexIndex = number of a triangle
+        for (int i = 0; i < triangleCount; i++) {
+            // get the pointer to the first vertex of the triangle
+            // this allows us to iterate over the indexBuffer with the first vertex of the triangle as start
+            const uint32_t *vertexIndexOfTriangle = &indexBuffer32Bit[i * 3];
+
+            vertexIndexToTriangle[offsetVertexOccurrence[vertexIndexOfTriangle[0]] + vertexOccurrence[vertexIndexOfTriangle[0]]] = i;
+            vertexOccurrence[vertexIndexOfTriangle[0]]++;
+
+            vertexIndexToTriangle[offsetVertexOccurrence[vertexIndexOfTriangle[1]] + vertexOccurrence[vertexIndexOfTriangle[1]]] = i;
+            vertexOccurrence[vertexIndexOfTriangle[1]]++;
+
+            vertexIndexToTriangle[offsetVertexOccurrence[vertexIndexOfTriangle[2]] + vertexOccurrence[vertexIndexOfTriangle[2]]] = i;
+            vertexOccurrence[vertexIndexOfTriangle[2]]++;
+        }
+
+        // counts if a triangle still uses this vertex
+        std::vector<uint8_t>  livingVertices = vertexOccurrence;
+        std::vector<uint32_t> lastTimestampCache(vertexCount, 0);
+
+        // stack of already used vertices, if it'currentTimeStamp full it will write to 0 again
+        std::vector<uint32_t> usedVerticeStack(maxUsedVertices, 0);
+
+        //currently used vertices
+        int usedVerticeCount     = 0;
+        // offset if maxUsedVertices was reached and it loops back to 0
+        int usedVerticeOffset    = 0;
+
+        // saves if a triangle was emitted (used in the IndexBuffer)
+        std::vector<bool> isEmittedTriangles(triangleCount, false);
+
+        // reordered Triangles that get rewritten to the new IndexBuffer
+        std::vector<uint32_t> reorderedTriangleIndexBuffer(triangleCount, 0);
+
+        // offset to the latest not used triangleIndex
+        int triangleOutputOffset    = 0;
+        // vertexIndex to fan out from (fanning VertexIndex)
+        int currentVertexIndex      = 0;
+        int currentTimeStamp        = cacheSize + 1;
+        int lowestLivingVertexIndex = 0;
+
+        std::vector<uint32_t> possibleCandidates(3 * maxOffset);
+
+        int currentTriangleIndex = 0;
+        // list of vertex indices where a deadEnd was reached
+        // useful to know where the mesh is potentially not contiguous
+        std::vector<uint32_t> skippedIndices;
+
+        // run while not all indices are fanned out, -1 equals all are fanned out
+        while (currentVertexIndex >= 0) {
+            // number of possible candidates for a fanning VertexIndex
+            int numPossibleCandidates   = 0;
+            // offset of currentVertexIndex and the next VertexIndex
+            int startOffset             = offsetVertexOccurrence[currentVertexIndex];
+            int endOffset               = offsetVertexOccurrence[currentVertexIndex + 1];
+            // iterates over every triangle of currentVertexIndex
+            for (int offset = startOffset; offset < endOffset; offset++) {
+                int triangleIndex = vertexIndexToTriangle[offset];
+
+                // checks if the triangle is already emitted
+                if (!isEmittedTriangles[triangleIndex]) {
+
+                    // get the pointer to the first vertex of the triangle
+                    // this allows us to iterate over the indexBuffer with the first vertex of the triangle as start
+                    const uint32_t *vertexIndexOfTriangle        = &indexBuffer32Bit[3 * triangleIndex];
+
+                    currentTriangleIndex++;
+
+                    // save emitted vertexIndexOfTriangle to reorderedTriangleIndexBuffer and set it to emitted
+                    reorderedTriangleIndexBuffer[triangleOutputOffset++]    = triangleIndex;
+                    isEmittedTriangles[triangleIndex]                       = true;
+
+                    // save all vertexIndices of the triangle to reuse as soon as possible
+                    for (int j = 0; j < 3; j++) {
+                        int vertexIndex = vertexIndexOfTriangle[j];
+
+                        //save vertexIndex to reuseStack
+                        usedVerticeStack[mod(usedVerticeCount++)] = vertexIndex;
+
+                        // after looping back increase the start, so it only overrides the oldest vertexIndex
+                        if ((mod(usedVerticeCount)) ==
+                            (mod(usedVerticeOffset))) {
+                            usedVerticeOffset = mod(usedVerticeOffset + 1);
+                        }
+                        // add vertex to next possibleCandidates as fanning vertex
+                        possibleCandidates[numPossibleCandidates++] = vertexIndex;
+
+                        // remove one occurrence of the vertex, since the triangle is used
+                        livingVertices[vertexIndex]--;
+
+                        // writes the timestamp (number of iteration) of the last usage, if it wasn't used within the last cacheSize iterations
+                        if (currentTimeStamp - lastTimestampCache[vertexIndex] > cacheSize) {
+                            lastTimestampCache[vertexIndex] = currentTimeStamp;
+                            currentTimeStamp++;
+                        }
+                    }
+                }
+            }
+
+            // search for the next vertexIndex to fan out
+            currentVertexIndex = getNextVertexIndex(
+                    vertexCount, lowestLivingVertexIndex, cacheSize, possibleCandidates, numPossibleCandidates, lastTimestampCache, currentTimeStamp,
+                    livingVertices, usedVerticeStack, usedVerticeCount, usedVerticeOffset, currentTriangleIndex, skippedIndices);
+        }
+
+        std::vector<uint32_t> reorderedIndexBuffer(3 * triangleCount);
+
+        triangleOutputOffset = 0;
+        // rewriting the TriangleIndexBuffer to the new IndexBuffer
+        for (int i = 0; i < triangleCount; i++) {
+            int triangleIndex = reorderedTriangleIndexBuffer[i];
+            // rewriting the triangle index to vertices
+            for (int j = 0; j < 3; j++) {
+                int vertexIndex = indexBuffer32Bit[(3 * triangleIndex) + j];
+                reorderedIndexBuffer[triangleOutputOffset++] = vertexIndex;
+            }
+        }
+
+        return VertexCacheReorderResult(reorderedIndexBuffer, skippedIndices);
+    }
+}
\ No newline at end of file
diff --git a/modules/scene/CMakeLists.txt b/modules/scene/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5edf9a29ad929b3c07b79d4f1ffcb7f1cf2fcd99
--- /dev/null
+++ b/modules/scene/CMakeLists.txt
@@ -0,0 +1,45 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_scene)
+
+# setting c++ standard for the module
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_scene_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_scene_include ${PROJECT_SOURCE_DIR}/include)
+
+# Add source and header files to the module
+set(vkcv_scene_sources
+		${vkcv_scene_include}/vkcv/scene/Bounds.hpp
+		${vkcv_scene_source}/vkcv/scene/Bounds.cpp
+		
+		${vkcv_scene_include}/vkcv/scene/Frustum.hpp
+		${vkcv_scene_source}/vkcv/scene/Frustum.cpp
+		
+		${vkcv_scene_include}/vkcv/scene/MeshPart.hpp
+		${vkcv_scene_source}/vkcv/scene/MeshPart.cpp
+		
+		${vkcv_scene_include}/vkcv/scene/Mesh.hpp
+		${vkcv_scene_source}/vkcv/scene/Mesh.cpp
+
+		${vkcv_scene_include}/vkcv/scene/Node.hpp
+		${vkcv_scene_source}/vkcv/scene/Node.cpp
+		
+		${vkcv_scene_include}/vkcv/scene/Scene.hpp
+		${vkcv_scene_source}/vkcv/scene/Scene.cpp
+)
+
+# adding source files to the module
+add_library(vkcv_scene STATIC ${vkcv_scene_sources})
+
+# link the required libraries to the module
+target_link_libraries(vkcv_scene vkcv)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_scene SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_material_include} ${vkcv_camera_include})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_scene BEFORE PUBLIC ${vkcv_scene_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(vkcv_scene vkcv vkcv_asset_loader vkcv_material vkcv_camera)
diff --git a/modules/scene/include/vkcv/scene/Bounds.hpp b/modules/scene/include/vkcv/scene/Bounds.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..07cdf88828d786982b0fe8e7919d543557794c42
--- /dev/null
+++ b/modules/scene/include/vkcv/scene/Bounds.hpp
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <array>
+#include <iostream>
+#include <glm/vec3.hpp>
+
+namespace vkcv::scene {
+	
+	class Bounds {
+	private:
+		glm::vec3 m_min;
+		glm::vec3 m_max;
+		
+	public:
+		Bounds();
+		Bounds(const glm::vec3& min, const glm::vec3& max);
+		~Bounds() = default;
+		
+		Bounds(const Bounds& other) = default;
+		Bounds(Bounds&& other) = default;
+		
+		Bounds& operator=(const Bounds& other) = default;
+		Bounds& operator=(Bounds&& other) = default;
+		
+		void setMin(const glm::vec3& min);
+		
+		[[nodiscard]]
+		const glm::vec3& getMin() const;
+		
+		void setMax(const glm::vec3& max);
+		
+		[[nodiscard]]
+		const glm::vec3& getMax() const;
+		
+		void setCenter(const glm::vec3& center);
+		
+		[[nodiscard]]
+		glm::vec3 getCenter() const;
+		
+		void setSize(const glm::vec3& size);
+		
+		[[nodiscard]]
+		glm::vec3 getSize() const;
+		
+		[[nodiscard]]
+		std::array<glm::vec3, 8> getCorners() const;
+		
+		void extend(const glm::vec3& point);
+		
+		[[nodiscard]]
+		bool contains(const glm::vec3& point) const;
+		
+		[[nodiscard]]
+		bool contains(const Bounds& other) const;
+		
+		[[nodiscard]]
+		bool intersects(const Bounds& other) const;
+		
+		[[nodiscard]]
+		explicit operator bool() const;
+		
+		[[nodiscard]]
+		bool operator!() const;
+	
+	};
+	
+	std::ostream& operator << (std::ostream& out, const Bounds& bounds);
+	
+}
diff --git a/modules/scene/include/vkcv/scene/Frustum.hpp b/modules/scene/include/vkcv/scene/Frustum.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..de3917575a9aed32459e6403fab1d6d8fe131b0a
--- /dev/null
+++ b/modules/scene/include/vkcv/scene/Frustum.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <glm/mat4x4.hpp>
+#include "vkcv/scene/Bounds.hpp"
+
+namespace vkcv::scene {
+	
+	Bounds transformBounds(const glm::mat4& transform, const Bounds& bounds, bool* negative_w = nullptr);
+	
+	bool checkFrustum(const glm::mat4& transform, const Bounds& bounds);
+	
+}
diff --git a/modules/scene/include/vkcv/scene/Mesh.hpp b/modules/scene/include/vkcv/scene/Mesh.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..bc82af4bfabed5e8bfc286bc53cd7b89791726fc
--- /dev/null
+++ b/modules/scene/include/vkcv/scene/Mesh.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include <glm/mat4x4.hpp>
+
+#include <vkcv/camera/Camera.hpp>
+
+#include "MeshPart.hpp"
+
+namespace vkcv::scene {
+	
+	typedef typename event_function<const glm::mat4&, const glm::mat4&, PushConstants&, vkcv::DrawcallInfo&>::type RecordMeshDrawcallFunction;
+	
+	class Node;
+	
+	class Mesh {
+		friend class Node;
+		
+	private:
+		Scene& m_scene;
+		std::vector<MeshPart> m_parts;
+		std::vector<DrawcallInfo> m_drawcalls;
+		glm::mat4 m_transform;
+		Bounds m_bounds;
+		
+		explicit Mesh(Scene& scene);
+		
+		void load(const asset::Scene& scene,
+				  const asset::Mesh& mesh);
+		
+		void recordDrawcalls(const glm::mat4& viewProjection,
+							 PushConstants& pushConstants,
+							 std::vector<DrawcallInfo>& drawcalls,
+							 const RecordMeshDrawcallFunction& record);
+		
+		[[nodiscard]]
+		size_t getDrawcallCount() const;
+	
+	public:
+		~Mesh();
+		
+		Mesh(const Mesh& other) = default;
+		Mesh(Mesh&& other) = default;
+		
+		Mesh& operator=(const Mesh& other);
+		Mesh& operator=(Mesh&& other) noexcept;
+		
+		[[nodiscard]]
+		const Bounds& getBounds() const;
+	
+	};
+	
+}
diff --git a/modules/scene/include/vkcv/scene/MeshPart.hpp b/modules/scene/include/vkcv/scene/MeshPart.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0d3467c6b57fcece69eb6f0c609c604fb99907d2
--- /dev/null
+++ b/modules/scene/include/vkcv/scene/MeshPart.hpp
@@ -0,0 +1,54 @@
+#pragma once
+
+#include <vector>
+
+#include <vkcv/Buffer.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/material/Material.hpp>
+
+#include "Bounds.hpp"
+
+namespace vkcv::scene {
+	
+	class Scene;
+	class Mesh;
+	
+	class MeshPart {
+		friend class Mesh;
+	
+	private:
+		Scene& m_scene;
+		BufferHandle m_vertices;
+		std::vector<VertexBufferBinding> m_vertexBindings;
+		BufferHandle m_indices;
+		size_t m_indexCount;
+		Bounds m_bounds;
+		size_t m_materialIndex;
+		
+		explicit MeshPart(Scene& scene);
+		
+		void load(const asset::Scene& scene,
+				  const asset::VertexGroup& vertexGroup,
+				  std::vector<DrawcallInfo>& drawcalls);
+	
+	public:
+		~MeshPart();
+		
+		MeshPart(const MeshPart& other);
+		MeshPart(MeshPart&& other);
+		
+		MeshPart& operator=(const MeshPart& other);
+		MeshPart& operator=(MeshPart&& other) noexcept;
+		
+		[[nodiscard]]
+		const material::Material& getMaterial() const;
+		
+		[[nodiscard]]
+		const Bounds& getBounds() const;
+		
+		explicit operator bool() const;
+		bool operator!() const;
+		
+	};
+
+}
diff --git a/modules/scene/include/vkcv/scene/Node.hpp b/modules/scene/include/vkcv/scene/Node.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1fcca5b9cbecf1064070d7737d008d2b108371db
--- /dev/null
+++ b/modules/scene/include/vkcv/scene/Node.hpp
@@ -0,0 +1,61 @@
+#pragma once
+
+#include <vector>
+
+#include <vkcv/camera/Camera.hpp>
+
+#include "Bounds.hpp"
+#include "Mesh.hpp"
+
+namespace vkcv::scene {
+	
+	class Scene;
+	
+	class Node {
+		friend class Scene;
+		
+	private:
+		Scene& m_scene;
+		
+		std::vector<Mesh> m_meshes;
+		std::vector<Node> m_nodes;
+		Bounds m_bounds;
+		
+		explicit Node(Scene& scene);
+		
+		void addMesh(const Mesh& mesh);
+		
+		void loadMesh(const asset::Scene& asset_scene, const asset::Mesh& asset_mesh);
+		
+		void recordDrawcalls(const glm::mat4& viewProjection,
+							 PushConstants& pushConstants,
+							 std::vector<DrawcallInfo>& drawcalls,
+							 const RecordMeshDrawcallFunction& record);
+		
+		void splitMeshesToSubNodes(size_t maxMeshesPerNode);
+		
+		[[nodiscard]]
+		size_t getDrawcallCount() const;
+		
+		size_t addNode();
+		
+		Node& getNode(size_t index);
+		
+		[[nodiscard]]
+		const Node& getNode(size_t index) const;
+	
+	public:
+		~Node();
+		
+		Node(const Node& other) = default;
+		Node(Node&& other) = default;
+		
+		Node& operator=(const Node& other);
+		Node& operator=(Node&& other) noexcept;
+		
+		[[nodiscard]]
+		const Bounds& getBounds() const;
+		
+	};
+	
+}
diff --git a/modules/scene/include/vkcv/scene/Scene.hpp b/modules/scene/include/vkcv/scene/Scene.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..429c0bcf729f9afb7dd76cdd58c54931862e1a4a
--- /dev/null
+++ b/modules/scene/include/vkcv/scene/Scene.hpp
@@ -0,0 +1,72 @@
+#pragma once
+
+#include <filesystem>
+#include <mutex>
+
+#include <vkcv/Core.hpp>
+#include <vkcv/Event.hpp>
+#include <vkcv/camera/Camera.hpp>
+#include <vkcv/material/Material.hpp>
+
+#include "Node.hpp"
+
+namespace vkcv::scene {
+	
+	class Scene {
+		friend class MeshPart;
+		
+	private:
+		struct Material {
+			size_t m_usages;
+			material::Material m_data;
+		};
+		
+		Core* m_core;
+		
+		std::vector<Material> m_materials;
+		std::vector<Node> m_nodes;
+		
+		explicit Scene(Core* core);
+		
+		size_t addNode();
+		
+		Node& getNode(size_t index);
+		
+		const Node& getNode(size_t index) const;
+		
+		void increaseMaterialUsage(size_t index);
+		
+		void decreaseMaterialUsage(size_t index);
+		
+		void loadMaterial(size_t index, const asset::Scene& scene,
+						  const asset::Material& material);
+		
+	public:
+		~Scene();
+		
+		Scene(const Scene& other);
+		Scene(Scene&& other) noexcept;
+		
+		Scene& operator=(const Scene& other);
+		Scene& operator=(Scene&& other) noexcept;
+		
+		size_t getMaterialCount() const;
+		
+		[[nodiscard]]
+		const material::Material& getMaterial(size_t index) const;
+		
+		void recordDrawcalls(CommandStreamHandle       		  &cmdStream,
+							 const camera::Camera			  &camera,
+							 const PassHandle                 &pass,
+							 const PipelineHandle             &pipeline,
+							 size_t							  pushConstantsSizePerDrawcall,
+							 const RecordMeshDrawcallFunction &record,
+							 const std::vector<ImageHandle>   &renderTargets);
+		
+		static Scene create(Core& core);
+		
+		static Scene load(Core& core, const std::filesystem::path &path);
+		
+	};
+	
+}
\ No newline at end of file
diff --git a/modules/scene/src/vkcv/scene/Bounds.cpp b/modules/scene/src/vkcv/scene/Bounds.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..731d81e928deae4c27f5c857de5b94dc3180888b
--- /dev/null
+++ b/modules/scene/src/vkcv/scene/Bounds.cpp
@@ -0,0 +1,126 @@
+
+#include "vkcv/scene/Bounds.hpp"
+
+namespace vkcv::scene {
+	
+	Bounds::Bounds() :
+	m_min(glm::vec3(0)),
+	m_max(glm::vec3(0)) {}
+	
+	Bounds::Bounds(const glm::vec3 &min, const glm::vec3 &max) :
+	m_min(min),
+	m_max(max)
+	{}
+
+	void Bounds::setMin(const glm::vec3 &min) {
+		m_min = min;
+	}
+	
+	const glm::vec3 & Bounds::getMin() const {
+		return m_min;
+	}
+	
+	void Bounds::setMax(const glm::vec3 &max) {
+		m_max = max;
+	}
+	
+	const glm::vec3 & Bounds::getMax() const {
+		return m_max;
+	}
+	
+	void Bounds::setCenter(const glm::vec3 &center) {
+		const glm::vec3 size = getSize();
+		m_min = center - size / 2.0f;
+		m_max = center + size / 2.0f;
+	}
+	
+	glm::vec3 Bounds::getCenter() const {
+		return (m_min + m_max) / 2.0f;
+	}
+	
+	void Bounds::setSize(const glm::vec3 &size) {
+		const glm::vec3 center = getCenter();
+		m_min = center - size / 2.0f;
+		m_max = center + size / 2.0f;
+	}
+	
+	glm::vec3 Bounds::getSize() const {
+		return (m_max - m_min);
+	}
+	
+	std::array<glm::vec3, 8> Bounds::getCorners() const {
+		return {
+			m_min,
+			glm::vec3(m_min[0], m_min[1], m_max[2]),
+			glm::vec3(m_min[0], m_max[1], m_min[2]),
+			glm::vec3(m_min[0], m_max[1], m_max[2]),
+			glm::vec3(m_max[0], m_min[1], m_min[2]),
+			glm::vec3(m_max[0], m_min[1], m_max[2]),
+			glm::vec3(m_max[0], m_max[1], m_min[2]),
+			m_max
+		};
+	}
+	
+	void Bounds::extend(const glm::vec3 &point) {
+		m_min = glm::vec3(
+				std::min(m_min[0], point[0]),
+				std::min(m_min[1], point[1]),
+				std::min(m_min[2], point[2])
+		);
+		
+		m_max = glm::vec3(
+				std::max(m_max[0], point[0]),
+				std::max(m_max[1], point[1]),
+				std::max(m_max[2], point[2])
+		);
+	}
+	
+	bool Bounds::contains(const glm::vec3 &point) const {
+		return (
+				(point[0] >= m_min[0]) && (point[0] <= m_max[0]) &&
+				(point[1] >= m_min[1]) && (point[1] <= m_max[1]) &&
+				(point[2] >= m_min[2]) && (point[2] <= m_max[2])
+		);
+	}
+	
+	bool Bounds::contains(const Bounds &other) const {
+		return (
+				(other.m_min[0] >= m_min[0]) && (other.m_max[0] <= m_max[0]) &&
+				(other.m_min[1] >= m_min[1]) && (other.m_max[1] <= m_max[1]) &&
+				(other.m_min[2] >= m_min[2]) && (other.m_max[2] <= m_max[2])
+		);
+	}
+	
+	bool Bounds::intersects(const Bounds &other) const {
+		return (
+				(other.m_max[0] >= m_min[0]) && (other.m_min[0] <= m_max[0]) &&
+				(other.m_max[1] >= m_min[1]) && (other.m_min[1] <= m_max[1]) &&
+				(other.m_max[2] >= m_min[2]) && (other.m_min[2] <= m_max[2])
+		);
+	}
+	
+	Bounds::operator bool() const {
+		return (
+				(m_min[0] <= m_max[0]) &&
+				(m_min[1] <= m_max[1]) &&
+				(m_min[2] <= m_max[2])
+		);
+	}
+	
+	bool Bounds::operator!() const {
+		return (
+				(m_min[0] > m_max[0]) ||
+				(m_min[1] > m_max[1]) ||
+				(m_min[2] > m_max[2])
+		);
+	}
+	
+	std::ostream& operator << (std::ostream& out, const Bounds& bounds) {
+		const auto& min = bounds.getMin();
+		const auto& max = bounds.getMax();
+		
+		return out << "[Bounds: (" << min[0] << ", " << min[1] << ", " << min[2] << ") ("
+								   << max[0] << ", " << max[1] << ", " << max[2] << ") ]";
+	}
+	
+}
diff --git a/modules/scene/src/vkcv/scene/Frustum.cpp b/modules/scene/src/vkcv/scene/Frustum.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1f63eb1d07002d24add81872627777048642dcdb
--- /dev/null
+++ b/modules/scene/src/vkcv/scene/Frustum.cpp
@@ -0,0 +1,73 @@
+
+#include "vkcv/scene/Frustum.hpp"
+
+namespace vkcv::scene {
+	
+	static glm::vec3 transformPoint(const glm::mat4& transform, const glm::vec3& point, bool* negative_w) {
+		const glm::vec4 position = transform * glm::vec4(point, 1.0f);
+		
+		
+		/*
+		 * We divide by the absolute of the 4th coorditnate because
+		 * clipping is weird and points have to move to the other
+		 * side of the camera.
+		 *
+		 * We also need to collect if the 4th coordinate was negative
+		 * to know if all corners are behind the camera. So these can
+		 * be culled as well
+		 */
+		if (negative_w) {
+			const float perspective = std::abs(position[3]);
+			
+			*negative_w &= (position[3] < 0.0f);
+			
+			return glm::vec3(
+					position[0] / perspective,
+					position[1] / perspective,
+					position[2] / perspective
+			);
+		} else {
+			return glm::vec3(
+					position[0],
+					position[1],
+					position[2]
+			);
+		}
+	}
+	
+	Bounds transformBounds(const glm::mat4& transform, const Bounds& bounds, bool* negative_w) {
+		const auto corners = bounds.getCorners();
+		
+		if (negative_w) {
+			*negative_w = true;
+		}
+		
+		auto projected = transformPoint(transform, corners[0], negative_w);
+		
+		Bounds result (projected, projected);
+		
+		for (size_t j = 1; j < corners.size(); j++) {
+			projected = transformPoint(transform, corners[j], negative_w);
+			result.extend(projected);
+		}
+		
+		return result;
+	}
+	
+	bool checkFrustum(const glm::mat4& transform, const Bounds& bounds) {
+		static Bounds frustum (
+				glm::vec3(-1.0f, -1.0f, -0.0f),
+				glm::vec3(+1.0f, +1.0f, +1.0f)
+		);
+		
+		bool negative_w;
+		auto box = transformBounds(transform, bounds, &negative_w);
+		
+		if (negative_w) {
+			return false;
+		} else {
+			return box.intersects(frustum);
+		}
+	}
+
+}
diff --git a/modules/scene/src/vkcv/scene/Mesh.cpp b/modules/scene/src/vkcv/scene/Mesh.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..af02aedbd71ba4bdfcc30aa7fdcd82796af904f1
--- /dev/null
+++ b/modules/scene/src/vkcv/scene/Mesh.cpp
@@ -0,0 +1,132 @@
+
+#include "vkcv/scene/Mesh.hpp"
+#include "vkcv/scene/Scene.hpp"
+#include "vkcv/scene/Frustum.hpp"
+
+namespace vkcv::scene {
+	
+	Mesh::Mesh(Scene& scene) :
+	m_scene(scene) {}
+	
+	static glm::mat4 arrayTo4x4Matrix(const std::array<float,16>& array){
+		glm::mat4 matrix;
+		
+		for (int i = 0; i < 4; i++){
+			for (int j = 0; j < 4; j++){
+				matrix[i][j] = array[j * 4 + i];
+			}
+		}
+		
+		return matrix;
+	}
+	
+	void Mesh::load(const asset::Scene &scene, const asset::Mesh &mesh) {
+		m_parts.clear();
+		m_drawcalls.clear();
+		
+		m_transform = arrayTo4x4Matrix(mesh.modelMatrix);
+		
+		for (const auto& vertexGroupIndex : mesh.vertexGroups) {
+			if ((vertexGroupIndex < 0) || (vertexGroupIndex >= scene.vertexGroups.size())) {
+				continue;
+			}
+			
+			MeshPart part (m_scene);
+			part.load(scene, scene.vertexGroups[vertexGroupIndex], m_drawcalls);
+			
+			if (!part) {
+				continue;
+			}
+			
+			auto bounds = transformBounds(m_transform, part.getBounds());
+			
+			if (m_parts.empty()) {
+				m_bounds = bounds;
+			} else {
+				m_bounds.extend(bounds.getMin());
+				m_bounds.extend(bounds.getMax());
+			}
+			
+			m_parts.push_back(part);
+		}
+	}
+	
+	Mesh::~Mesh() {
+		m_drawcalls.clear();
+		m_parts.clear();
+	}
+	
+	Mesh &Mesh::operator=(const Mesh &other) {
+		if (&other == this) {
+			return *this;
+		}
+		
+		m_parts.resize(other.m_parts.size(), MeshPart(m_scene));
+		
+		for (size_t i = 0; i < m_parts.size(); i++) {
+			m_parts[i] = other.m_parts[i];
+		}
+		
+		m_drawcalls = std::vector<DrawcallInfo>(other.m_drawcalls);
+		m_transform = other.m_transform;
+		m_bounds = other.m_bounds;
+		
+		return *this;
+	}
+	
+	Mesh &Mesh::operator=(Mesh &&other) noexcept {
+		m_parts.resize(other.m_parts.size(), MeshPart(m_scene));
+		
+		for (size_t i = 0; i < m_parts.size(); i++) {
+			m_parts[i] = std::move(other.m_parts[i]);
+		}
+		
+		m_drawcalls = std::move(other.m_drawcalls);
+		m_transform = other.m_transform;
+		m_bounds = other.m_bounds;
+		
+		return *this;
+	}
+	
+	void Mesh::recordDrawcalls(const glm::mat4& viewProjection,
+							   PushConstants& pushConstants,
+							   std::vector<DrawcallInfo>& drawcalls,
+							   const RecordMeshDrawcallFunction& record) {
+		const glm::mat4 transform = viewProjection * m_transform;
+		
+		if (!checkFrustum(viewProjection, m_bounds)) {
+			return;
+		}
+		
+		if (m_drawcalls.size() == 1) {
+			drawcalls.push_back(m_drawcalls[0]);
+			
+			if (record) {
+				record(transform, m_transform, pushConstants, drawcalls.back());
+			}
+		} else {
+			for (size_t i = 0; i < m_parts.size(); i++) {
+				const MeshPart& part = m_parts[i];
+				
+				if (!checkFrustum(transform, part.getBounds())) {
+					continue;
+				}
+				
+				drawcalls.push_back(m_drawcalls[i]);
+				
+				if (record) {
+					record(transform, m_transform, pushConstants, drawcalls.back());
+				}
+			}
+		}
+	}
+	
+	size_t Mesh::getDrawcallCount() const {
+		return m_drawcalls.size();
+	}
+	
+	const Bounds& Mesh::getBounds() const {
+		return m_bounds;
+	}
+
+}
diff --git a/modules/scene/src/vkcv/scene/MeshPart.cpp b/modules/scene/src/vkcv/scene/MeshPart.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..46e79897719d5422151ec31837a41f7e58324a71
--- /dev/null
+++ b/modules/scene/src/vkcv/scene/MeshPart.cpp
@@ -0,0 +1,158 @@
+
+#include "vkcv/scene/MeshPart.hpp"
+#include "vkcv/scene/Scene.hpp"
+
+namespace vkcv::scene {
+	
+	MeshPart::MeshPart(Scene& scene) :
+	m_scene(scene),
+	m_vertices(),
+	m_vertexBindings(),
+	m_indices(),
+	m_indexCount(0),
+	m_bounds(),
+	m_materialIndex(std::numeric_limits<size_t>::max()) {}
+	
+	void MeshPart::load(const asset::Scene& scene,
+						const asset::VertexGroup &vertexGroup,
+						std::vector<DrawcallInfo>& drawcalls) {
+		Core& core = *(m_scene.m_core);
+		
+		auto vertexBuffer = core.createBuffer<uint8_t>(
+				BufferType::VERTEX, vertexGroup.vertexBuffer.data.size()
+		);
+		
+		vertexBuffer.fill(vertexGroup.vertexBuffer.data);
+		m_vertices = vertexBuffer.getHandle();
+		
+		auto attributes = vertexGroup.vertexBuffer.attributes;
+		
+		std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
+			return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
+		});
+		
+		for (const auto& attribute : attributes) {
+			m_vertexBindings.emplace_back(attribute.offset, vertexBuffer.getVulkanHandle());
+		}
+		
+		auto indexBuffer = core.createBuffer<uint8_t>(
+				BufferType::INDEX, vertexGroup.indexBuffer.data.size()
+		);
+		
+		indexBuffer.fill(vertexGroup.indexBuffer.data);
+		m_indices = indexBuffer.getHandle();
+		m_indexCount = vertexGroup.numIndices;
+		
+		m_bounds.setMin(glm::vec3(
+				vertexGroup.min.x,
+				vertexGroup.min.y,
+				vertexGroup.min.z
+		));
+		
+		m_bounds.setMax(glm::vec3(
+				vertexGroup.max.x,
+				vertexGroup.max.y,
+				vertexGroup.max.z
+		));
+		
+		if ((vertexGroup.materialIndex >= 0) &&
+			(vertexGroup.materialIndex < scene.materials.size())) {
+			m_materialIndex = vertexGroup.materialIndex;
+			
+			if (!getMaterial()) {
+				m_scene.loadMaterial(m_materialIndex, scene, scene.materials[vertexGroup.materialIndex]);
+			}
+			
+			m_scene.increaseMaterialUsage(m_materialIndex);
+		} else {
+			m_materialIndex = std::numeric_limits<size_t>::max();
+		}
+		
+		if (*this) {
+			const auto& material = getMaterial();
+			const auto& descriptorSet = core.getDescriptorSet(material.getDescriptorSet());
+			
+			drawcalls.push_back(DrawcallInfo(
+					vkcv::Mesh(m_vertexBindings, indexBuffer.getVulkanHandle(), m_indexCount),
+					{ DescriptorSetUsage(0, descriptorSet.vulkanHandle) }
+			));
+		}
+	}
+	
+	MeshPart::~MeshPart() {
+		m_scene.decreaseMaterialUsage(m_materialIndex);
+	}
+	
+	MeshPart::MeshPart(const MeshPart &other) :
+			m_scene(other.m_scene),
+			m_vertices(other.m_vertices),
+			m_vertexBindings(other.m_vertexBindings),
+			m_indices(other.m_indices),
+			m_indexCount(other.m_indexCount),
+			m_bounds(other.m_bounds),
+			m_materialIndex(other.m_materialIndex) {
+		m_scene.increaseMaterialUsage(m_materialIndex);
+	}
+	
+	MeshPart::MeshPart(MeshPart &&other) :
+			m_scene(other.m_scene),
+			m_vertices(other.m_vertices),
+			m_vertexBindings(other.m_vertexBindings),
+			m_indices(other.m_indices),
+			m_indexCount(other.m_indexCount),
+			m_bounds(other.m_bounds),
+			m_materialIndex(other.m_materialIndex) {
+		m_scene.increaseMaterialUsage(m_materialIndex);
+	}
+	
+	MeshPart &MeshPart::operator=(const MeshPart &other) {
+		if (&other == this) {
+			return *this;
+		}
+		
+		m_vertices = other.m_vertices;
+		m_vertexBindings = other.m_vertexBindings;
+		m_indices = other.m_indices;
+		m_indexCount = other.m_indexCount;
+		m_bounds = other.m_bounds;
+		m_materialIndex = other.m_materialIndex;
+		
+		return *this;
+	}
+	
+	MeshPart &MeshPart::operator=(MeshPart &&other) noexcept {
+		m_vertices = other.m_vertices;
+		m_vertexBindings = other.m_vertexBindings;
+		m_indices = other.m_indices;
+		m_indexCount = other.m_indexCount;
+		m_bounds = other.m_bounds;
+		m_materialIndex = other.m_materialIndex;
+		
+		return *this;
+	}
+	
+	const material::Material & MeshPart::getMaterial() const {
+		return m_scene.getMaterial(m_materialIndex);
+	}
+	
+	MeshPart::operator bool() const {
+		return (
+				(getMaterial()) &&
+				(m_vertices) &&
+				(m_indices)
+		);
+	}
+	
+	bool MeshPart::operator!() const {
+		return (
+				(!getMaterial()) ||
+				(!m_vertices) ||
+				(!m_indices)
+		);
+	}
+	
+	const Bounds &MeshPart::getBounds() const {
+		return m_bounds;
+	}
+	
+}
diff --git a/modules/scene/src/vkcv/scene/Node.cpp b/modules/scene/src/vkcv/scene/Node.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..24f62d18e160c7d80f82384829a2130737737ba9
--- /dev/null
+++ b/modules/scene/src/vkcv/scene/Node.cpp
@@ -0,0 +1,189 @@
+
+#include "vkcv/scene/Node.hpp"
+#include "vkcv/scene/Scene.hpp"
+#include "vkcv/scene/Frustum.hpp"
+
+#include <algorithm>
+
+namespace vkcv::scene {
+	
+	Node::Node(Scene& scene) :
+	m_scene(scene),
+	m_meshes(),
+	m_nodes(),
+	m_bounds() {}
+	
+	Node::~Node() {
+		m_nodes.clear();
+		m_meshes.clear();
+	}
+	
+	Node &Node::operator=(const Node &other) {
+		if (&other == this) {
+			return *this;
+		}
+		
+		m_meshes.resize(other.m_meshes.size(), Mesh(m_scene));
+		
+		for (size_t i = 0; i < m_meshes.size(); i++) {
+			m_meshes[i] = other.m_meshes[i];
+		}
+		
+		m_nodes.resize(other.m_nodes.size(), Node(m_scene));
+		
+		for (size_t i = 0; i < m_nodes.size(); i++) {
+			m_nodes[i] = other.m_nodes[i];
+		}
+		
+		m_bounds = other.m_bounds;
+		
+		return *this;
+	}
+	
+	Node &Node::operator=(Node &&other) noexcept {
+		m_meshes.resize(other.m_meshes.size(), Mesh(m_scene));
+		
+		for (size_t i = 0; i < m_meshes.size(); i++) {
+			m_meshes[i] = std::move(other.m_meshes[i]);
+		}
+		
+		m_nodes.resize(other.m_nodes.size(), Node(m_scene));
+		
+		for (size_t i = 0; i < m_nodes.size(); i++) {
+			m_nodes[i] = std::move(other.m_nodes[i]);
+		}
+		
+		m_bounds = other.m_bounds;
+		
+		return *this;
+	}
+	
+	void Node::addMesh(const Mesh& mesh) {
+		if (m_meshes.empty()) {
+			m_bounds = mesh.getBounds();
+		} else {
+			m_bounds.extend(mesh.getBounds().getMin());
+			m_bounds.extend(mesh.getBounds().getMax());
+		}
+		
+		m_meshes.push_back(mesh);
+	}
+	
+	void Node::loadMesh(const asset::Scene &asset_scene, const asset::Mesh &asset_mesh) {
+		Mesh mesh (m_scene);
+		mesh.load(asset_scene, asset_mesh);
+		addMesh(mesh);
+	}
+	
+	size_t Node::addNode() {
+		const Node node (m_scene);
+		const size_t index = m_nodes.size();
+		m_nodes.push_back(node);
+		return index;
+	}
+	
+	Node& Node::getNode(size_t index) {
+		return m_nodes[index];
+	}
+	
+	const Node& Node::getNode(size_t index) const {
+		return m_nodes[index];
+	}
+	
+	void Node::recordDrawcalls(const glm::mat4& viewProjection,
+							   PushConstants& pushConstants,
+							   std::vector<DrawcallInfo>& drawcalls,
+							   const RecordMeshDrawcallFunction& record) {
+		if (!checkFrustum(viewProjection, m_bounds)) {
+			return;
+		}
+		
+		for (auto& mesh : m_meshes) {
+			mesh.recordDrawcalls(viewProjection, pushConstants, drawcalls, record);
+		}
+		
+		for (auto& node : m_nodes) {
+			node.recordDrawcalls(viewProjection, pushConstants, drawcalls, record);
+		}
+	}
+	
+	void Node::splitMeshesToSubNodes(size_t maxMeshesPerNode) {
+		if (m_meshes.size() <= maxMeshesPerNode) {
+			return;
+		}
+		
+		const auto split = m_bounds.getCenter();
+		int axis = 0;
+		
+		const auto size = m_bounds.getSize();
+		
+		if (size[1] > size[0]) {
+			if (size[2] > size[1]) {
+				axis = 2;
+			} else {
+				axis = 1;
+			}
+		} else
+		if (size[2] > size[0]) {
+			axis = 2;
+		}
+		
+		std::vector<size_t> left_meshes;
+		std::vector<size_t> right_meshes;
+		
+		for (size_t i = 0; i < m_meshes.size(); i++) {
+			const auto& bounds = m_meshes[i].getBounds();
+			
+			if (bounds.getMax()[axis] <= split[axis]) {
+				left_meshes.push_back(i);
+			} else
+			if (bounds.getMin()[axis] >= split[axis]) {
+				right_meshes.push_back(i);
+			}
+		}
+		
+		if ((left_meshes.empty()) || (right_meshes.empty())) {
+			return;
+		}
+		
+		const size_t left = addNode();
+		const size_t right = addNode();
+		
+		for (size_t i : left_meshes) {
+			getNode(left).addMesh(m_meshes[i]);
+		}
+		
+		for (size_t i : right_meshes) {
+			getNode(right).addMesh(m_meshes[i]);
+			left_meshes.push_back(i);
+		}
+		
+		std::sort(left_meshes.begin(), left_meshes.end(), std::greater());
+		
+		for (size_t i : left_meshes) {
+			m_meshes.erase(m_meshes.begin() + static_cast<long>(i));
+		}
+		
+		getNode(left).splitMeshesToSubNodes(maxMeshesPerNode);
+		getNode(right).splitMeshesToSubNodes(maxMeshesPerNode);
+	}
+	
+	size_t Node::getDrawcallCount() const {
+		size_t count = 0;
+		
+		for (auto& mesh : m_meshes) {
+			count += mesh.getDrawcallCount();
+		}
+		
+		for (auto& node : m_nodes) {
+			count += node.getDrawcallCount();
+		}
+		
+		return count;
+	}
+	
+	const Bounds& Node::getBounds() const {
+		return m_bounds;
+	}
+
+}
diff --git a/modules/scene/src/vkcv/scene/Scene.cpp b/modules/scene/src/vkcv/scene/Scene.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d6fa2a40a494ef57386e52a306e962a460c66dd6
--- /dev/null
+++ b/modules/scene/src/vkcv/scene/Scene.cpp
@@ -0,0 +1,273 @@
+
+#include "vkcv/scene/Scene.hpp"
+
+#include <vkcv/Logger.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+
+namespace vkcv::scene {
+	
+	Scene::Scene(Core* core) :
+	m_core(core),
+	m_materials(),
+	m_nodes() {}
+	
+	Scene::~Scene() {
+		m_nodes.clear();
+		m_materials.clear();
+	}
+	
+	Scene::Scene(const Scene &other) :
+	m_core(other.m_core),
+	m_materials(other.m_materials),
+	m_nodes() {
+		m_nodes.resize(other.m_nodes.size(), Node(*this));
+		
+		for (size_t i = 0; i < m_nodes.size(); i++) {
+			m_nodes[i] = other.m_nodes[i];
+		}
+	}
+	
+	Scene::Scene(Scene &&other) noexcept :
+	m_core(other.m_core),
+	m_materials(other.m_materials),
+	m_nodes() {
+		m_nodes.resize(other.m_nodes.size(), Node(*this));
+		
+		for (size_t i = 0; i < m_nodes.size(); i++) {
+			m_nodes[i] = std::move(other.m_nodes[i]);
+		}
+	}
+	
+	Scene &Scene::operator=(const Scene &other) {
+		if (&other == this) {
+			return *this;
+		}
+		
+		m_core = other.m_core;
+		m_materials = std::vector<Material>(other.m_materials);
+		
+		m_nodes.resize(other.m_nodes.size(), Node(*this));
+		
+		for (size_t i = 0; i < m_nodes.size(); i++) {
+			m_nodes[i] = other.m_nodes[i];
+		}
+		
+		return *this;
+	}
+	
+	Scene &Scene::operator=(Scene &&other) noexcept {
+		m_core = other.m_core;
+		m_materials = std::move(other.m_materials);
+		
+		m_nodes.resize(other.m_nodes.size(), Node(*this));
+		
+		for (size_t i = 0; i < m_nodes.size(); i++) {
+			m_nodes[i] = std::move(other.m_nodes[i]);
+		}
+		
+		return *this;
+	}
+	
+	size_t Scene::addNode() {
+		const Node node (*this);
+		const size_t index = m_nodes.size();
+		m_nodes.push_back(node);
+		return index;
+	}
+	
+	Node& Scene::getNode(size_t index) {
+		return m_nodes[index];
+	}
+	
+	const Node& Scene::getNode(size_t index) const {
+		return m_nodes[index];
+	}
+	
+	void Scene::increaseMaterialUsage(size_t index) {
+		if (index < m_materials.size()) {
+			m_materials[index].m_usages++;
+		}
+	}
+	
+	void Scene::decreaseMaterialUsage(size_t index) {
+		if ((index < m_materials.size()) && (m_materials[index].m_usages > 0)) {
+			m_materials[index].m_usages--;
+		}
+	}
+	
+	size_t Scene::getMaterialCount() const {
+		return m_materials.size();
+	}
+	
+	const material::Material & Scene::getMaterial(size_t index) const {
+		static material::Material noMaterial;
+		
+		if (index >= m_materials.size()) {
+			return noMaterial;
+		}
+		
+		return m_materials[index].m_data;
+	}
+	
+	void Scene::recordDrawcalls(CommandStreamHandle       		 &cmdStream,
+								const camera::Camera			 &camera,
+								const PassHandle                 &pass,
+								const PipelineHandle             &pipeline,
+								size_t							 pushConstantsSizePerDrawcall,
+								const RecordMeshDrawcallFunction &record,
+								const std::vector<ImageHandle>   &renderTargets) {
+		PushConstants pushConstants (pushConstantsSizePerDrawcall);
+		std::vector<DrawcallInfo> drawcalls;
+		size_t count = 0;
+		
+		const glm::mat4 viewProjection = camera.getMVP();
+		
+		for (auto& node : m_nodes) {
+			count += node.getDrawcallCount();
+			node.recordDrawcalls(viewProjection, pushConstants, drawcalls, record);
+		}
+		
+		vkcv_log(LogLevel::RAW_INFO, "Frustum culling: %lu / %lu", drawcalls.size(), count);
+		
+		m_core->recordDrawcallsToCmdStream(
+				cmdStream,
+				pass,
+				pipeline,
+				pushConstants,
+				drawcalls,
+				renderTargets
+		);
+	}
+	
+	Scene Scene::create(Core& core) {
+		return Scene(&core);
+	}
+	
+	static void loadImage(Core& core, const asset::Scene& asset_scene,
+						  const asset::Texture& asset_texture,
+						  const vk::Format& format,
+						  ImageHandle& image, SamplerHandle& sampler) {
+		asset::Sampler* asset_sampler = nullptr;
+		
+		if ((asset_texture.sampler >= 0) && (asset_texture.sampler < asset_scene.samplers.size())) {
+			//asset_sampler = &(asset_scene.samplers[asset_texture.sampler]); // TODO
+		}
+		
+		Image img = core.createImage(format, asset_texture.w, asset_texture.h);
+		img.fill(asset_texture.data.data());
+		image = img.getHandle();
+		
+		if (asset_sampler) {
+			//sampler = core.createSampler(asset_sampler) // TODO
+		} else {
+			sampler = core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT
+			);
+		}
+	}
+	
+	void Scene::loadMaterial(size_t index, const asset::Scene& scene,
+							 const asset::Material& material) {
+		if (index >= m_materials.size()) {
+			return;
+		}
+		
+		ImageHandle diffuseImg;
+		SamplerHandle diffuseSmp;
+		
+		if ((material.baseColor >= 0) && (material.baseColor < scene.textures.size())) {
+			loadImage(*m_core, scene, scene.textures[material.baseColor], vk::Format::eR8G8B8A8Srgb,
+					  diffuseImg,diffuseSmp);
+		}
+		
+		ImageHandle normalImg;
+		SamplerHandle normalSmp;
+		
+		if ((material.baseColor >= 0) && (material.baseColor < scene.textures.size())) {
+			loadImage(*m_core, scene, scene.textures[material.baseColor], vk::Format::eR8G8B8A8Srgb,
+					  diffuseImg,diffuseSmp);
+		}
+		
+		ImageHandle metalRoughImg;
+		SamplerHandle metalRoughSmp;
+		
+		if ((material.baseColor >= 0) && (material.baseColor < scene.textures.size())) {
+			loadImage(*m_core, scene, scene.textures[material.baseColor], vk::Format::eR8G8B8A8Srgb,
+					  diffuseImg,diffuseSmp);
+		}
+		
+		ImageHandle occlusionImg;
+		SamplerHandle occlusionSmp;
+		
+		if ((material.baseColor >= 0) && (material.baseColor < scene.textures.size())) {
+			loadImage(*m_core, scene, scene.textures[material.baseColor], vk::Format::eR8G8B8A8Srgb,
+					  diffuseImg,diffuseSmp);
+		}
+		
+		ImageHandle emissionImg;
+		SamplerHandle emissionSmp;
+		
+		if ((material.baseColor >= 0) && (material.baseColor < scene.textures.size())) {
+			loadImage(*m_core, scene, scene.textures[material.baseColor], vk::Format::eR8G8B8A8Srgb,
+					  diffuseImg,diffuseSmp);
+		}
+		
+		const float colorFactors [4] = {
+				material.baseColorFactor.r,
+				material.baseColorFactor.g,
+				material.baseColorFactor.b,
+				material.baseColorFactor.a
+		};
+		
+		const float emissionFactors[4] = {
+				material.emissiveFactor.r,
+				material.emissiveFactor.g,
+				material.emissiveFactor.b
+		};
+		
+		m_materials[index].m_data = material::Material::createPBR(
+				*m_core,
+				diffuseImg, diffuseSmp,
+				normalImg, normalSmp,
+				metalRoughImg, metalRoughSmp,
+				occlusionImg, occlusionSmp,
+				emissionImg, emissionSmp,
+				colorFactors,
+				material.normalScale,
+				material.metallicFactor,
+				material.roughnessFactor,
+				material.occlusionStrength,
+				emissionFactors
+		);
+	}
+	
+	Scene Scene::load(Core& core, const std::filesystem::path &path) {
+		asset::Scene asset_scene;
+		
+		if (!asset::loadScene(path.string(), asset_scene)) {
+			vkcv_log(LogLevel::ERROR, "Scene could not be loaded (%s)", path.c_str());
+			return create(core);
+		}
+		
+		Scene scene = create(core);
+		
+		for (const auto& material : asset_scene.materials) {
+			scene.m_materials.push_back({
+				0, material::Material()
+			});
+		}
+		
+		const size_t root = scene.addNode();
+		
+		for (const auto& mesh : asset_scene.meshes) {
+			scene.getNode(root).loadMesh(asset_scene, mesh);
+		}
+		
+		scene.getNode(root).splitMeshesToSubNodes(128);
+		return scene;
+	}
+	
+}
diff --git a/modules/shader_compiler/CMakeLists.txt b/modules/shader_compiler/CMakeLists.txt
index 62e00d5fdc181535fa4f1c981e772e268a116f20..11c1e460575709dd9c9c16fdd02b6b923cc33045 100644
--- a/modules/shader_compiler/CMakeLists.txt
+++ b/modules/shader_compiler/CMakeLists.txt
@@ -10,6 +10,9 @@ set(vkcv_shader_compiler_include ${PROJECT_SOURCE_DIR}/include)
 
 # Add source and header files to the module
 set(vkcv_shader_compiler_sources
+		${vkcv_shader_compiler_include}/vkcv/shader/Compiler.hpp
+		${vkcv_shader_compiler_source}/vkcv/shader/Compiler.cpp
+		
 		${vkcv_shader_compiler_include}/vkcv/shader/GLSLCompiler.hpp
 		${vkcv_shader_compiler_source}/vkcv/shader/GLSLCompiler.cpp
 )
diff --git a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
index d7b7af7178531aea358cecbc8b86a29527173014..5b119ca5c68f997bacfbea6c60d5c965f9a7a54e 100644
--- a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
+++ b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
@@ -1,6 +1,11 @@
 #pragma once
 
+#include <filesystem>
+#include <string>
+#include <unordered_map>
+
 #include <vkcv/Event.hpp>
+#include <vkcv/ShaderStage.hpp>
 
 namespace vkcv::shader {
 	
@@ -8,10 +13,21 @@ namespace vkcv::shader {
 	
 	class Compiler {
 	private:
+	protected:
+		std::unordered_map<std::string, std::string> m_defines;
+		
 	public:
+		virtual bool compileSource(ShaderStage shaderStage, const char* shaderSource,
+								   const ShaderCompiledFunction& compiled,
+								   const std::filesystem::path& includePath) = 0;
+		
 		virtual void compile(ShaderStage shaderStage, const std::filesystem::path& shaderPath,
-							 const ShaderCompiledFunction& compiled, bool update = false) = 0;
+							 const ShaderCompiledFunction& compiled,
+							 const std::filesystem::path& includePath, bool update) = 0;
+		
+		std::string getDefine(const std::string& name) const;
 		
+		void setDefine(const std::string& name, const std::string& value);
 	};
 	
 }
diff --git a/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp b/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
index 7105d93a0c3e153bf3abe1d624d0c13c6f09ac6d..eca84def118625e21df1c645cfc71b6bcddf7393 100644
--- a/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
+++ b/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
@@ -7,7 +7,7 @@
 
 namespace vkcv::shader {
 	
-	class GLSLCompiler {
+	class GLSLCompiler : public Compiler {
 	private:
 	public:
 		GLSLCompiler();
@@ -20,8 +20,13 @@ namespace vkcv::shader {
 		GLSLCompiler& operator=(const GLSLCompiler& other);
 		GLSLCompiler& operator=(GLSLCompiler&& other) = default;
 		
+		bool compileSource(ShaderStage shaderStage, const char* shaderSource,
+						   const ShaderCompiledFunction& compiled,
+						   const std::filesystem::path& includePath);
+		
 		void compile(ShaderStage shaderStage, const std::filesystem::path& shaderPath,
-					 const ShaderCompiledFunction& compiled, bool update = false);
+					 const ShaderCompiledFunction& compiled,
+					 const std::filesystem::path& includePath = "", bool update = false) override;
 		
 	};
 	
diff --git a/modules/shader_compiler/src/vkcv/shader/Compiler.cpp b/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f5ec0435ca8b82dc5f328921f43a39338d1be456
--- /dev/null
+++ b/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
@@ -0,0 +1,14 @@
+
+#include "vkcv/shader/Compiler.hpp"
+
+namespace vkcv::shader {
+	
+	std::string Compiler::getDefine(const std::string &name) const {
+		return m_defines.at(name);
+	}
+	
+	void Compiler::setDefine(const std::string &name, const std::string &value) {
+		m_defines[name] = value;
+	}
+	
+}
diff --git a/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
index ec358188b8e871da6f4d62ffd397f32bfb795ee2..16067aebedfda8793a0096803ba5344275bcbbcd 100644
--- a/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
+++ b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
@@ -2,16 +2,18 @@
 #include "vkcv/shader/GLSLCompiler.hpp"
 
 #include <fstream>
+#include <sstream>
 #include <glslang/SPIRV/GlslangToSpv.h>
 #include <glslang/StandAlone/DirStackFileIncluder.h>
 
+#include <vkcv/File.hpp>
 #include <vkcv/Logger.hpp>
 
 namespace vkcv::shader {
 	
 	static uint32_t s_CompilerCount = 0;
 	
-	GLSLCompiler::GLSLCompiler() {
+	GLSLCompiler::GLSLCompiler() : Compiler() {
 		if (s_CompilerCount == 0) {
 			glslang::InitializeProcess();
 		}
@@ -19,7 +21,7 @@ namespace vkcv::shader {
 		s_CompilerCount++;
 	}
 	
-	GLSLCompiler::GLSLCompiler(const GLSLCompiler &other) {
+	GLSLCompiler::GLSLCompiler(const GLSLCompiler &other) : Compiler(other) {
 		s_CompilerCount++;
 	}
 	
@@ -50,6 +52,10 @@ namespace vkcv::shader {
 				return EShLangFragment;
 			case ShaderStage::COMPUTE:
 				return EShLangCompute;
+			case ShaderStage::TASK:
+				return EShLangTaskNV;
+			case ShaderStage::MESH:
+				return EShLangMeshNV;
 			default:
 				return EShLangCount;
 		}
@@ -197,22 +203,45 @@ namespace vkcv::shader {
 		return true;
 	}
 	
-	void GLSLCompiler::compile(ShaderStage shaderStage, const std::filesystem::path &shaderPath,
-							   const ShaderCompiledFunction& compiled, bool update) {
+	bool GLSLCompiler::compileSource(ShaderStage shaderStage, const char* shaderSource,
+									 const ShaderCompiledFunction &compiled,
+									 const std::filesystem::path& includePath) {
 		const EShLanguage language = findShaderLanguage(shaderStage);
 		
 		if (language == EShLangCount) {
-			vkcv_log(LogLevel::ERROR, "Shader stage not supported (%s)", shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "Shader stage not supported");
+			return false;
 		}
 		
-		const std::vector<char> code = readShaderCode(shaderPath);
-		
 		glslang::TShader shader (language);
 		glslang::TProgram program;
 		
+		std::string source (shaderSource);
+		
+		if (!m_defines.empty()) {
+			std::ostringstream defines;
+			for (const auto& define : m_defines) {
+				defines << "#define " << define.first << " " << define.second << std::endl;
+			}
+
+			size_t pos = source.find("#version") + 8;
+			if (pos >= source.length()) {
+				pos = 0;
+			}
+			
+			const size_t epos = source.find_last_of("#extension", pos) + 10;
+			if (epos < source.length()) {
+				pos = epos;
+			}
+			
+			const auto defines_str = defines.str();
+			
+			pos = source.find('\n', pos) + 1;
+			source = source.insert(pos, defines_str);
+		}
+		
 		const char *shaderStrings [1];
-		shaderStrings[0] = code.data();
+		shaderStrings[0] = source.c_str();
 		
 		shader.setStrings(shaderStrings, 1);
 		
@@ -222,51 +251,53 @@ namespace vkcv::shader {
 		const auto messages = (EShMessages)(
 			EShMsgSpvRules |
 			EShMsgVulkanRules
-			);
+		);
 
 		std::string preprocessedGLSL;
 
 		DirStackFileIncluder includer;
-		includer.pushExternalLocalDirectory(shaderPath.parent_path().string());
+		includer.pushExternalLocalDirectory(includePath.string());
 
-		if (!shader.preprocess(&resources, 100, ENoProfile, false, false, messages, &preprocessedGLSL, includer)) {
-			vkcv_log(LogLevel::ERROR, "Shader parsing failed {\n%s\n%s\n} (%s)",
-				shader.getInfoLog(), shader.getInfoDebugLog(), shaderPath.string().c_str());
-			return;
+		if (!shader.preprocess(&resources, 100, ENoProfile,
+							   false, false,
+							   messages, &preprocessedGLSL, includer)) {
+			vkcv_log(LogLevel::ERROR, "Shader preprocessing failed {\n%s\n%s\n}",
+				shader.getInfoLog(), shader.getInfoDebugLog());
+			return false;
 		}
 		
 		const char* preprocessedCString = preprocessedGLSL.c_str();
 		shader.setStrings(&preprocessedCString, 1);
 
 		if (!shader.parse(&resources, 100, false, messages)) {
-			vkcv_log(LogLevel::ERROR, "Shader parsing failed {\n%s\n%s\n} (%s)",
-					 shader.getInfoLog(), shader.getInfoDebugLog(), shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "Shader parsing failed {\n%s\n%s\n}",
+					 shader.getInfoLog(), shader.getInfoDebugLog());
+			return false;
 		}
 		
 		program.addShader(&shader);
 		
 		if (!program.link(messages)) {
-			vkcv_log(LogLevel::ERROR, "Shader linking failed {\n%s\n%s\n} (%s)",
-					 shader.getInfoLog(), shader.getInfoDebugLog(), shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "Shader linking failed {\n%s\n%s\n}",
+					 shader.getInfoLog(), shader.getInfoDebugLog());
+			return false;
 		}
 		
 		const glslang::TIntermediate* intermediate = program.getIntermediate(language);
 		
 		if (!intermediate) {
-			vkcv_log(LogLevel::ERROR, "No valid intermediate representation (%s)", shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "No valid intermediate representation");
+			return false;
 		}
 		
 		std::vector<uint32_t> spirv;
 		glslang::GlslangToSpv(*intermediate, spirv);
 		
-		const std::filesystem::path tmp_path (std::tmpnam(nullptr));
+		const std::filesystem::path tmp_path = generateTemporaryFilePath();
 		
 		if (!writeSpirvCode(tmp_path, spirv)) {
-			vkcv_log(LogLevel::ERROR, "Spir-V could not be written to disk (%s)", shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "Spir-V could not be written to disk");
+			return false;
 		}
 		
 		if (compiled) {
@@ -274,6 +305,24 @@ namespace vkcv::shader {
 		}
 		
 		std::filesystem::remove(tmp_path);
+		return true;
+	}
+	
+	void GLSLCompiler::compile(ShaderStage shaderStage, const std::filesystem::path &shaderPath,
+							   const ShaderCompiledFunction& compiled,
+							   const std::filesystem::path& includePath, bool update) {
+		const std::vector<char> code = readShaderCode(shaderPath);
+		bool result;
+		
+		if (!includePath.empty()) {
+			result = compileSource(shaderStage, code.data(), compiled, includePath);
+		} else {
+			result = compileSource(shaderStage, code.data(), compiled, shaderPath.parent_path());
+		}
+		
+		if (!result) {
+			vkcv_log(LogLevel::ERROR, "Shader compilation failed: (%s)", shaderPath.string().c_str());
+		}
 		
 		if (update) {
 			// TODO: Shader hot compilation during runtime
diff --git a/modules/upscaling/CMakeLists.txt b/modules/upscaling/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0767e5c4d2d60c001ac9d6792efcd623456284a8
--- /dev/null
+++ b/modules/upscaling/CMakeLists.txt
@@ -0,0 +1,39 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_upscaling)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_upscaling_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_upscaling_include ${PROJECT_SOURCE_DIR}/include)
+
+set(vkcv_upscaling_sources
+		${vkcv_upscaling_include}/vkcv/upscaling/Upscaling.hpp
+		${vkcv_upscaling_source}/vkcv/upscaling/Upscaling.cpp
+		
+		${vkcv_upscaling_include}/vkcv/upscaling/BilinearUpscaling.hpp
+		${vkcv_upscaling_source}/vkcv/upscaling/BilinearUpscaling.cpp
+		
+		${vkcv_upscaling_include}/vkcv/upscaling/FSRUpscaling.hpp
+		${vkcv_upscaling_source}/vkcv/upscaling/FSRUpscaling.cpp
+)
+
+# Setup some path variables to load libraries
+set(vkcv_upscaling_lib lib)
+set(vkcv_upscaling_lib_path ${PROJECT_SOURCE_DIR}/${vkcv_upscaling_lib})
+
+# Check and load FidelityFX_FSR
+include(config/FidelityFX_FSR.cmake)
+
+# adding source files to the project
+add_library(vkcv_upscaling STATIC ${vkcv_upscaling_sources})
+
+# link the required libraries to the module
+target_link_libraries(vkcv_upscaling ${vkcv_upscaling_libraries} vkcv vkcv_shader_compiler)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_upscaling SYSTEM BEFORE PRIVATE ${vkcv_upscaling_includes} ${vkcv_include} ${vkcv_shader_compiler_include})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_upscaling BEFORE PUBLIC ${vkcv_upscaling_include})
diff --git a/modules/upscaling/config/FidelityFX_FSR.cmake b/modules/upscaling/config/FidelityFX_FSR.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..cc52b4189f781f534a933feb7b782b6bec333e5a
--- /dev/null
+++ b/modules/upscaling/config/FidelityFX_FSR.cmake
@@ -0,0 +1,18 @@
+
+if (EXISTS "${vkcv_upscaling_lib_path}/FidelityFX-FSR")
+	include_shader(${vkcv_upscaling_lib_path}/FidelityFX-FSR/ffx-fsr/ffx_a.h ${vkcv_upscaling_include} ${vkcv_upscaling_source})
+	include_shader(${vkcv_upscaling_lib_path}/FidelityFX-FSR/ffx-fsr/ffx_fsr1.h ${vkcv_upscaling_include} ${vkcv_upscaling_source})
+	include_shader(${vkcv_upscaling_lib_path}/FidelityFX-FSR/sample/src/VK/FSR_Pass.glsl ${vkcv_upscaling_include} ${vkcv_upscaling_source})
+	
+	list(APPEND vkcv_upscaling_includes ${vkcv_upscaling_lib}/FidelityFX-FSR/ffx-fsr)
+	
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_source}/ffx_a.h.cxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_source}/ffx_fsr1.h.cxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_source}/FSR_Pass.glsl.cxx)
+	
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/ffx_a.h.hxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/ffx_fsr1.h.hxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/FSR_Pass.glsl.hxx)
+else()
+	message(WARNING "FidelityFX-FSR is required..! Update the submodules!")
+endif ()
diff --git a/modules/upscaling/include/vkcv/upscaling/BilinearUpscaling.hpp b/modules/upscaling/include/vkcv/upscaling/BilinearUpscaling.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..52124dc8e36bee7ef7c00de6afcf3457296a7623
--- /dev/null
+++ b/modules/upscaling/include/vkcv/upscaling/BilinearUpscaling.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "Upscaling.hpp"
+
+namespace vkcv::upscaling {
+	
+	class BilinearUpscaling : public Upscaling {
+	private:
+	public:
+		BilinearUpscaling(Core& core);
+		
+		void recordUpscaling(const CommandStreamHandle& cmdStream,
+							 const ImageHandle& input,
+							 const ImageHandle& output) override;
+	
+	};
+
+}
diff --git a/modules/upscaling/include/vkcv/upscaling/FSRUpscaling.hpp b/modules/upscaling/include/vkcv/upscaling/FSRUpscaling.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2a1338b85e3ee60a33215157aaaa15817f2db97f
--- /dev/null
+++ b/modules/upscaling/include/vkcv/upscaling/FSRUpscaling.hpp
@@ -0,0 +1,78 @@
+#pragma once
+
+#include "Upscaling.hpp"
+
+#include <vkcv/ShaderProgram.hpp>
+
+namespace vkcv::upscaling {
+	
+	enum class FSRQualityMode : int {
+		NONE = 0,
+		ULTRA_QUALITY = 1,
+		QUALITY = 2,
+		BALANCED = 3,
+		PERFORMANCE = 4
+	};
+	
+	void getFSRResolution(FSRQualityMode mode,
+						  uint32_t outputWidth, uint32_t outputHeight,
+						  uint32_t &inputWidth, uint32_t &inputHeight);
+	
+	float getFSRLodBias(FSRQualityMode mode);
+	
+	struct FSRConstants {
+		uint32_t Const0 [4];
+		uint32_t Const1 [4];
+		uint32_t Const2 [4];
+		uint32_t Const3 [4];
+		uint32_t Sample [4];
+	};
+	
+	class FSRUpscaling : public Upscaling {
+	private:
+		PipelineHandle m_easuPipeline;
+		PipelineHandle m_rcasPipeline;
+		
+		DescriptorSetHandle m_easuDescriptorSet;
+		DescriptorSetHandle m_rcasDescriptorSet;
+		
+		Buffer<FSRConstants> m_easuConstants;
+		Buffer<FSRConstants> m_rcasConstants;
+		ImageHandle m_intermediateImage;
+		SamplerHandle m_sampler;
+		
+		bool m_hdr;
+		
+		/**
+		 * Sharpness will calculate the rcasAttenuation value
+		 * which should be between 0.0f and 2.0f (default: 0.25f).
+		 *
+		 * rcasAttenuation = (1.0f - sharpness) * 2.0f
+		 *
+		 * So the default value for sharpness should be 0.875f.
+		 *
+		 * Beware that 0.0f or any negative value of sharpness will
+		 * disable the rcas pass completely.
+		 */
+		float m_sharpness;
+	
+	public:
+		explicit FSRUpscaling(Core& core);
+		
+		void recordUpscaling(const CommandStreamHandle& cmdStream,
+							 const ImageHandle& input,
+							 const ImageHandle& output) override;
+		
+		[[nodiscard]]
+		bool isHdrEnabled() const;
+		
+		void setHdrEnabled(bool enabled);
+		
+		[[nodiscard]]
+		float getSharpness() const;
+		
+		void setSharpness(float sharpness);
+		
+	};
+
+}
diff --git a/modules/upscaling/include/vkcv/upscaling/Upscaling.hpp b/modules/upscaling/include/vkcv/upscaling/Upscaling.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c44e878ad78f0a3359599c76f781371505fd3a85
--- /dev/null
+++ b/modules/upscaling/include/vkcv/upscaling/Upscaling.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <vkcv/Core.hpp>
+#include <vkcv/Handles.hpp>
+
+namespace vkcv::upscaling {
+	
+	class Upscaling {
+	protected:
+		Core& m_core;
+	
+	public:
+		Upscaling(Core& core);
+		
+		~Upscaling() = default;
+		
+		virtual void recordUpscaling(const CommandStreamHandle& cmdStream,
+									 const ImageHandle& input,
+							 		 const ImageHandle& output) = 0;
+	
+	};
+	
+}
diff --git a/modules/upscaling/lib/FidelityFX-FSR b/modules/upscaling/lib/FidelityFX-FSR
new file mode 160000
index 0000000000000000000000000000000000000000..bcffc8171efb80e265991301a49670ed755088dd
--- /dev/null
+++ b/modules/upscaling/lib/FidelityFX-FSR
@@ -0,0 +1 @@
+Subproject commit bcffc8171efb80e265991301a49670ed755088dd
diff --git a/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9c36acf5d050e3f4f19223020357b6c32534a2de
--- /dev/null
+++ b/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp
@@ -0,0 +1,13 @@
+
+#include "vkcv/upscaling/BilinearUpscaling.hpp"
+
+namespace vkcv::upscaling {
+	
+	BilinearUpscaling::BilinearUpscaling(Core &core) : Upscaling(core) {}
+	
+	void BilinearUpscaling::recordUpscaling(const CommandStreamHandle &cmdStream, const ImageHandle &input,
+											const ImageHandle &output) {
+		m_core.recordBlitImage(cmdStream, input, output, SamplerFilterType::LINEAR);
+	}
+
+}
diff --git a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..460a6d0b459fe7d1d2a917a62138fea2e5a40908
--- /dev/null
+++ b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
@@ -0,0 +1,382 @@
+
+#include "vkcv/upscaling/FSRUpscaling.hpp"
+
+#include <stdint.h>
+#include <math.h>
+
+#define A_CPU 1
+#include <ffx_a.h>
+#include <ffx_fsr1.h>
+
+#include "ffx_a.h.hxx"
+#include "ffx_fsr1.h.hxx"
+#include "FSR_Pass.glsl.hxx"
+
+#include <vkcv/File.hpp>
+#include <vkcv/Logger.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+namespace vkcv::upscaling {
+	
+	void getFSRResolution(FSRQualityMode mode,
+						  uint32_t outputWidth, uint32_t outputHeight,
+						  uint32_t &inputWidth, uint32_t &inputHeight) {
+		float scale;
+		
+		switch (mode) {
+			case FSRQualityMode::ULTRA_QUALITY:
+				scale = 1.3f;
+				break;
+			case FSRQualityMode::QUALITY:
+				scale = 1.5f;
+				break;
+			case FSRQualityMode::BALANCED:
+				scale = 1.7f;
+				break;
+			case FSRQualityMode::PERFORMANCE:
+				scale = 2.0f;
+				break;
+			default:
+				scale = 1.0f;
+				break;
+		}
+		
+		inputWidth = static_cast<uint32_t>(
+				std::round(static_cast<float>(outputWidth) / scale)
+		);
+		
+		inputHeight = static_cast<uint32_t>(
+				std::round(static_cast<float>(outputHeight) / scale)
+		);
+	}
+	
+	float getFSRLodBias(FSRQualityMode mode) {
+		switch (mode) {
+			case FSRQualityMode::ULTRA_QUALITY:
+				return -0.38f;
+			case FSRQualityMode::QUALITY:
+				return -0.58f;
+			case FSRQualityMode::BALANCED:
+				return -0.79f;
+			case FSRQualityMode::PERFORMANCE:
+				return -1.0f;
+			default:
+				return 0.0f;
+		}
+	}
+	
+	static std::vector<DescriptorBinding> getDescriptorBindings() {
+		return std::vector<DescriptorBinding>({
+			DescriptorBinding(
+					0, DescriptorType::UNIFORM_BUFFER_DYNAMIC,
+					1, ShaderStage::COMPUTE
+			),
+			DescriptorBinding(
+					1, DescriptorType::IMAGE_SAMPLED,
+					1, ShaderStage::COMPUTE
+			),
+			DescriptorBinding(
+					2, DescriptorType::IMAGE_STORAGE,
+					1, ShaderStage::COMPUTE
+			),
+			DescriptorBinding(
+					3, DescriptorType::SAMPLER,
+					1, ShaderStage::COMPUTE
+			)
+		});
+	}
+	
+	template<typename T>
+	bool checkFeatures(const vk::BaseInStructure* base, vk::StructureType type, bool (*check)(const T& features)) {
+		if (base->sType == type) {
+			return check(*reinterpret_cast<const T*>(base));
+		} else
+		if (base->pNext) {
+			return checkFeatures<T>(base->pNext, type, check);
+		} else {
+			return false;
+		}
+	}
+	
+	static bool checkFloat16(const vk::PhysicalDeviceFloat16Int8FeaturesKHR& features) {
+		return features.shaderFloat16;
+	}
+	
+	static bool check16Storage(const vk::PhysicalDevice16BitStorageFeaturesKHR& features) {
+		return features.storageBuffer16BitAccess;
+	}
+	
+	static bool writeShaderCode(const std::filesystem::path &shaderPath, const std::string& code) {
+		std::ofstream file (shaderPath.string(), std::ios::out);
+		
+		if (!file.is_open()) {
+			vkcv_log(LogLevel::ERROR, "The file could not be opened (%s)", shaderPath.string().c_str());
+			return false;
+		}
+		
+		file.seekp(0);
+		file.write(code.c_str(), static_cast<std::streamsize>(code.length()));
+		file.close();
+		
+		return true;
+	}
+	
+	static bool compileFSRShader(vkcv::shader::GLSLCompiler& compiler,
+								 const shader::ShaderCompiledFunction& compiled) {
+		std::filesystem::path directory = generateTemporaryDirectoryPath();
+		
+		if (!std::filesystem::create_directory(directory)) {
+			vkcv_log(LogLevel::ERROR, "The directory could not be created (%s)", directory.string().c_str());
+			return false;
+		}
+		
+		if (!writeShaderCode(directory / "ffx_a.h", FFX_A_H_SHADER)) {
+			return false;
+		}
+		
+		if (!writeShaderCode(directory / "ffx_fsr1.h", FFX_FSR1_H_SHADER)) {
+			return false;
+		}
+		
+		return compiler.compileSource(vkcv::ShaderStage::COMPUTE,
+									  FSR_PASS_GLSL_SHADER.c_str(),
+									  [&directory, &compiled] (vkcv::ShaderStage shaderStage,
+									  		const std::filesystem::path& path) {
+				if (compiled) {
+					compiled(shaderStage, path);
+				}
+				
+				std::filesystem::remove_all(directory);
+			}, directory
+		);
+	}
+	
+	FSRUpscaling::FSRUpscaling(Core& core) :
+	Upscaling(core),
+	m_easuPipeline(),
+	m_rcasPipeline(),
+	m_easuDescriptorSet(m_core.createDescriptorSet(getDescriptorBindings())),
+	m_rcasDescriptorSet(m_core.createDescriptorSet(getDescriptorBindings())),
+	m_easuConstants(m_core.createBuffer<FSRConstants>(
+			BufferType::UNIFORM,1,
+			BufferMemoryType::HOST_VISIBLE
+	)),
+	m_rcasConstants(m_core.createBuffer<FSRConstants>(
+			BufferType::UNIFORM,1,
+			BufferMemoryType::HOST_VISIBLE
+	)),
+	m_intermediateImage(),
+	m_sampler(m_core.createSampler(
+			SamplerFilterType::LINEAR,
+			SamplerFilterType::LINEAR,
+			SamplerMipmapMode::NEAREST,
+			SamplerAddressMode::CLAMP_TO_EDGE
+	)),
+	m_hdr(false),
+	m_sharpness(0.875f) {
+		vkcv::shader::GLSLCompiler easuCompiler;
+		vkcv::shader::GLSLCompiler rcasCompiler;
+		
+		const auto& features = m_core.getContext().getPhysicalDevice().getFeatures2();
+		const bool float16Support = (
+				checkFeatures<vk::PhysicalDeviceFloat16Int8FeaturesKHR>(
+						reinterpret_cast<const vk::BaseInStructure*>(&features),
+						vk::StructureType::ePhysicalDeviceShaderFloat16Int8FeaturesKHR,
+						checkFloat16
+				) &&
+				checkFeatures<vk::PhysicalDevice16BitStorageFeaturesKHR>(
+						reinterpret_cast<const vk::BaseInStructure*>(&features),
+						vk::StructureType::ePhysicalDevice16BitStorageFeaturesKHR,
+						check16Storage
+				)
+		) || (true); // check doesn't work because chain is empty
+		
+		if (!float16Support) {
+			easuCompiler.setDefine("SAMPLE_SLOW_FALLBACK", "1");
+			rcasCompiler.setDefine("SAMPLE_SLOW_FALLBACK", "1");
+		}
+		
+		easuCompiler.setDefine("SAMPLE_EASU", "1");
+		rcasCompiler.setDefine("SAMPLE_RCAS", "1");
+		
+		{
+			ShaderProgram program;
+			compileFSRShader(easuCompiler, [&program](vkcv::ShaderStage shaderStage,
+					const std::filesystem::path& path) {
+				program.addShader(shaderStage, path);
+			});
+			
+			m_easuPipeline = m_core.createComputePipeline(program, {
+				m_core.getDescriptorSet(m_easuDescriptorSet).layout
+			});
+			
+			DescriptorWrites writes;
+			writes.uniformBufferWrites.emplace_back(
+					0, m_easuConstants.getHandle(),true
+			);
+			
+			writes.samplerWrites.emplace_back(3, m_sampler);
+			
+			m_core.writeDescriptorSet(m_easuDescriptorSet, writes);
+		}
+		
+		{
+			ShaderProgram program;
+			compileFSRShader(rcasCompiler, [&program](vkcv::ShaderStage shaderStage,
+					const std::filesystem::path& path) {
+				program.addShader(shaderStage, path);
+			});
+			
+			m_rcasPipeline = m_core.createComputePipeline(program, {
+				m_core.getDescriptorSet(m_rcasDescriptorSet).layout
+			});
+			
+			DescriptorWrites writes;
+			writes.uniformBufferWrites.emplace_back(
+					0, m_rcasConstants.getHandle(),true
+			);
+			
+			writes.samplerWrites.emplace_back(3, m_sampler);
+			
+			m_core.writeDescriptorSet(m_rcasDescriptorSet, writes);
+		}
+	}
+	
+	void FSRUpscaling::recordUpscaling(const CommandStreamHandle& cmdStream,
+									   const ImageHandle& input,
+									   const ImageHandle& output) {
+		const uint32_t inputWidth = m_core.getImageWidth(input);
+		const uint32_t inputHeight = m_core.getImageHeight(input);
+		
+		const uint32_t outputWidth = m_core.getImageWidth(output);
+		const uint32_t outputHeight = m_core.getImageHeight(output);
+		
+		if ((!m_intermediateImage) ||
+			(outputWidth != m_core.getImageWidth(m_intermediateImage)) ||
+			(outputHeight != m_core.getImageHeight(m_intermediateImage))) {
+			m_intermediateImage = m_core.createImage(
+					m_core.getImageFormat(output),
+					outputWidth, outputHeight,1,
+					false,
+					true
+			).getHandle();
+			
+			m_core.prepareImageForStorage(cmdStream, m_intermediateImage);
+		}
+		
+		const bool rcasEnabled = (
+				(m_sharpness > +0.0f) &&
+				((inputWidth < outputWidth) || (inputHeight < outputHeight))
+		);
+		
+		{
+			FSRConstants consts = {};
+			
+			FsrEasuCon(
+					consts.Const0, consts.Const1, consts.Const2, consts.Const3,
+					static_cast<AF1>(inputWidth), static_cast<AF1>(inputHeight),
+					static_cast<AF1>(inputWidth), static_cast<AF1>(inputHeight),
+					static_cast<AF1>(outputWidth), static_cast<AF1>(outputHeight)
+			);
+			
+			consts.Sample[0] = (((m_hdr) && (!rcasEnabled)) ? 1 : 0);
+			
+			m_easuConstants.fill(&consts);
+		}
+		
+		static const uint32_t threadGroupWorkRegionDim = 16;
+		
+		uint32_t dispatch[3];
+		dispatch[0] = (outputWidth + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
+		dispatch[1] = (outputHeight + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
+		dispatch[2] = 1;
+		
+		m_core.recordBufferMemoryBarrier(cmdStream, m_easuConstants.getHandle());
+		
+		if (rcasEnabled) {
+			{
+				DescriptorWrites writes;
+				writes.sampledImageWrites.emplace_back(1, input);
+				writes.storageImageWrites.emplace_back(2, m_intermediateImage);
+				
+				m_core.writeDescriptorSet(m_easuDescriptorSet, writes);
+			}
+			{
+				DescriptorWrites writes;
+				writes.sampledImageWrites.emplace_back(1, m_intermediateImage);
+				writes.storageImageWrites.emplace_back(2, output);
+				
+				m_core.writeDescriptorSet(m_rcasDescriptorSet, writes);
+			}
+			
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_easuPipeline,
+					dispatch,
+					{DescriptorSetUsage(0, m_core.getDescriptorSet(
+							m_easuDescriptorSet
+					).vulkanHandle, { 0 })},
+					PushConstants(0)
+			);
+			
+			{
+				FSRConstants consts = {};
+				
+				FsrRcasCon(consts.Const0, (1.0f - m_sharpness) * 2.0f);
+				consts.Sample[0] = (m_hdr ? 1 : 0);
+				
+				m_rcasConstants.fill(&consts);
+			}
+			
+			m_core.recordBufferMemoryBarrier(cmdStream, m_rcasConstants.getHandle());
+			m_core.prepareImageForSampling(cmdStream, m_intermediateImage);
+			
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_rcasPipeline,
+					dispatch,
+					{DescriptorSetUsage(0, m_core.getDescriptorSet(
+							m_rcasDescriptorSet
+					).vulkanHandle, { 0 })},
+					PushConstants(0)
+			);
+			
+			m_core.prepareImageForStorage(cmdStream, m_intermediateImage);
+		} else {
+			{
+				DescriptorWrites writes;
+				writes.sampledImageWrites.emplace_back(1, input);
+				writes.storageImageWrites.emplace_back(2, output);
+				
+				m_core.writeDescriptorSet(m_easuDescriptorSet, writes);
+			}
+			
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_easuPipeline,
+					dispatch,
+					{DescriptorSetUsage(0, m_core.getDescriptorSet(
+							m_easuDescriptorSet
+					).vulkanHandle, { 0 })},
+					PushConstants(0)
+			);
+		}
+	}
+	
+	bool FSRUpscaling::isHdrEnabled() const {
+		return m_hdr;
+	}
+	
+	void FSRUpscaling::setHdrEnabled(bool enabled) {
+		m_hdr = enabled;
+	}
+	
+	float FSRUpscaling::getSharpness() const {
+		return m_sharpness;
+	}
+	
+	void FSRUpscaling::setSharpness(float sharpness) {
+		m_sharpness = (sharpness < 0.0f ? 0.0f : (sharpness > 1.0f ? 1.0f : sharpness));
+	}
+	
+}
diff --git a/modules/upscaling/src/vkcv/upscaling/Upscaling.cpp b/modules/upscaling/src/vkcv/upscaling/Upscaling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b0c3dee9b1c799c0e1f07b59b03d3ad46bd453ed
--- /dev/null
+++ b/modules/upscaling/src/vkcv/upscaling/Upscaling.cpp
@@ -0,0 +1,8 @@
+
+#include "vkcv/upscaling/Upscaling.hpp"
+
+namespace vkcv::upscaling {
+	
+	Upscaling::Upscaling(Core &core) : m_core(core) {}
+	
+}
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index 1c6e3afe2347f6ef8ea8a62be7acbe0ea750497d..8010718447b8e72aed8eab42c8eac3e9591986ee 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -1,8 +1,9 @@
 
 # Add new projects/examples here:
-add_subdirectory(bloom)
 add_subdirectory(first_triangle)
 add_subdirectory(first_mesh)
-add_subdirectory(particle_simulation)
 add_subdirectory(first_scene)
+add_subdirectory(particle_simulation)
 add_subdirectory(voxelization)
+add_subdirectory(mesh_shader)
+add_subdirectory(indirect_dispatch)
diff --git a/projects/bloom/.gitignore b/projects/bloom/.gitignore
deleted file mode 100644
index 3643183e0628e666abab193e1dd1d92c1774ac61..0000000000000000000000000000000000000000
--- a/projects/bloom/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-bloom
\ No newline at end of file
diff --git a/projects/bloom/CMakeLists.txt b/projects/bloom/CMakeLists.txt
deleted file mode 100644
index 8171938e7cb430aacce5562af44f628c11c97c54..0000000000000000000000000000000000000000
--- a/projects/bloom/CMakeLists.txt
+++ /dev/null
@@ -1,32 +0,0 @@
-cmake_minimum_required(VERSION 3.16)
-project(bloom)
-
-# setting c++ standard for the project
-set(CMAKE_CXX_STANDARD 17)
-set(CMAKE_CXX_STANDARD_REQUIRED ON)
-
-# this should fix the execution path to load local files from the project
-set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
-
-# adding source files to the project
-add_executable(bloom src/main.cpp)
-
-target_sources(bloom PRIVATE
-		src/BloomAndFlares.cpp
-		src/BloomAndFlares.hpp)
-
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(bloom PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(bloom PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-    
-    # in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(bloom PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
-
-# including headers of dependencies and the VkCV framework
-target_include_directories(bloom SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
-
-# linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(bloom vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler)
diff --git a/projects/bloom/resources/Sponza/Sponza.bin b/projects/bloom/resources/Sponza/Sponza.bin
deleted file mode 100644
index cfedd26ca5a67b6d0a47d44d13a75e14a141717a..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/Sponza.bin
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:4b809f7a17687dc99e6f41ca1ea32c06eded8779bf34d16f1f565d750b0ffd68
-size 6347696
diff --git a/projects/bloom/resources/Sponza/Sponza.gltf b/projects/bloom/resources/Sponza/Sponza.gltf
deleted file mode 100644
index 172ea07e21c94465211c860cd805355704cef230..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/Sponza.gltf
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:5cc0ecad5c4694088ff820e663619c370421afc1323ac487406e8e9b4735d787
-size 713962
diff --git a/projects/bloom/resources/Sponza/background.png b/projects/bloom/resources/Sponza/background.png
deleted file mode 100644
index b64def129da38f4e23d89e21b4af1039008a4327..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/background.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:f5b5f900ff8ed83a31750ec8e428b5b91273794ddcbfc4e4b8a6a7e781f8c686
-size 1417666
diff --git a/projects/bloom/resources/Sponza/chain_texture.png b/projects/bloom/resources/Sponza/chain_texture.png
deleted file mode 100644
index c1e1768cff78e0614ad707eca8602a4c4edab5e5..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/chain_texture.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d8362cfd472880daeaea37439326a4651d1338680ae69bb2513fc6b17c8de7d4
-size 490895
diff --git a/projects/bloom/resources/Sponza/lion.png b/projects/bloom/resources/Sponza/lion.png
deleted file mode 100644
index c49c7f0ed31e762e19284d0d3624fbc47664e56b..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/lion.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:9f882f746c3a9cd51a9c6eedc1189b97668721d91a3fe49232036e789912c652
-size 2088728
diff --git a/projects/bloom/resources/Sponza/spnza_bricks_a_diff.png b/projects/bloom/resources/Sponza/spnza_bricks_a_diff.png
deleted file mode 100644
index cde4c7a6511e9a5f03c63ad996437fcdba3ce2df..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/spnza_bricks_a_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:b94219c2f5f943f3f4715c74e7d1038bf0ab3b3b3216a758eaee67f875df0851
-size 1928829
diff --git a/projects/bloom/resources/Sponza/sponza_arch_diff.png b/projects/bloom/resources/Sponza/sponza_arch_diff.png
deleted file mode 100644
index bcd9bda2918d226039f9e2d03902d377b706fab6..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_arch_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c0df2c8a01b2843b1c792b494f7173cdbc4f834840fc2177af3e5d690fceda57
-size 1596151
diff --git a/projects/bloom/resources/Sponza/sponza_ceiling_a_diff.png b/projects/bloom/resources/Sponza/sponza_ceiling_a_diff.png
deleted file mode 100644
index 59de631ffac4414cabf69b2dc794c46fc187d6cb..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_ceiling_a_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:ab6c187a81aa68f4eba30119e17fce2e4882a9ec320f70c90482dbe9da82b1c6
-size 1872074
diff --git a/projects/bloom/resources/Sponza/sponza_column_a_diff.png b/projects/bloom/resources/Sponza/sponza_column_a_diff.png
deleted file mode 100644
index 01a82432d3f9939bbefe850bdb900f1ff9a3f6db..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_column_a_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:2c291507e2808bb83e160ab4b020689817df273baad3713a9ad19ac15fac6826
-size 1840992
diff --git a/projects/bloom/resources/Sponza/sponza_column_b_diff.png b/projects/bloom/resources/Sponza/sponza_column_b_diff.png
deleted file mode 100644
index 10a660cce2a5a9b8997772c746058ce23e7d45d7..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_column_b_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:2820b0267c4289c6cedbb42721792a57ef244ec2d0935941011c2a7d3fe88a9b
-size 2170433
diff --git a/projects/bloom/resources/Sponza/sponza_column_c_diff.png b/projects/bloom/resources/Sponza/sponza_column_c_diff.png
deleted file mode 100644
index bc46fd979044a938d3adca7601689e71504e48bf..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_column_c_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a0bc993ff59865468ef4530798930c7dfefb07482d71db45bc2a520986b27735
-size 2066950
diff --git a/projects/bloom/resources/Sponza/sponza_curtain_blue_diff.png b/projects/bloom/resources/Sponza/sponza_curtain_blue_diff.png
deleted file mode 100644
index 384c8c2c051160d530eb3ac8b05c9c60752a2d2b..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_curtain_blue_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:b85c6bb3cd5105f48d3812ec8e7a1068521ce69e917300d79e136e19d45422fb
-size 9510905
diff --git a/projects/bloom/resources/Sponza/sponza_curtain_diff.png b/projects/bloom/resources/Sponza/sponza_curtain_diff.png
deleted file mode 100644
index af842e9f5fe18c1f609875e00899a6770fa4488b..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_curtain_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:563c56bdbbee395a6ef7f0c51c8ac9223c162e517b4cdba0d4654e8de27c98d8
-size 9189263
diff --git a/projects/bloom/resources/Sponza/sponza_curtain_green_diff.png b/projects/bloom/resources/Sponza/sponza_curtain_green_diff.png
deleted file mode 100644
index 6c9b6391a199407637fa71033d79fb58b8b4f0d7..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_curtain_green_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:238fe1c7f481388d1c1d578c2da8d411b99e8f0030ab62060a306db333124476
-size 8785458
diff --git a/projects/bloom/resources/Sponza/sponza_details_diff.png b/projects/bloom/resources/Sponza/sponza_details_diff.png
deleted file mode 100644
index 12656686362c3e0a297e060491f33bd7351551f9..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_details_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:cb1223b3bb82f8757e7df25a6891f1239cdd7ec59990340e952fb2d6b7ea570c
-size 1522643
diff --git a/projects/bloom/resources/Sponza/sponza_fabric_blue_diff.png b/projects/bloom/resources/Sponza/sponza_fabric_blue_diff.png
deleted file mode 100644
index 879d16ef84722a4fc13e83a771778de326e4bc54..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_fabric_blue_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:467d290bf5d4b2a017da140ba9e244ed8a8a9be5418a9ac9bcb4ad572ae2d7ab
-size 2229440
diff --git a/projects/bloom/resources/Sponza/sponza_fabric_diff.png b/projects/bloom/resources/Sponza/sponza_fabric_diff.png
deleted file mode 100644
index 3311287a219d2148620b87fe428fea071688d051..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_fabric_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1594f59cc2848db26add47361f4e665e3d8afa147760ed915d839fea42b20287
-size 2267382
diff --git a/projects/bloom/resources/Sponza/sponza_fabric_green_diff.png b/projects/bloom/resources/Sponza/sponza_fabric_green_diff.png
deleted file mode 100644
index de110f369004388dae4cd5067c63428db3a07834..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_fabric_green_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:902b87faab221173bf370cea7c74cb9060b4d870ac6316b190dafded1cb12993
-size 2258220
diff --git a/projects/bloom/resources/Sponza/sponza_flagpole_diff.png b/projects/bloom/resources/Sponza/sponza_flagpole_diff.png
deleted file mode 100644
index 5f6e0812a0df80346318baa3cb50a6888afc58f8..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_flagpole_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:bfffb62e770959c725d0f3db6dc7dbdd46a380ec55ef884dab94d44ca017b438
-size 1425673
diff --git a/projects/bloom/resources/Sponza/sponza_floor_a_diff.png b/projects/bloom/resources/Sponza/sponza_floor_a_diff.png
deleted file mode 100644
index 788ed764f79ba724f04a2d603076a5b85013e188..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_floor_a_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a16f9230fa91f9f31dfca6216ce205f1ef132d44f3b012fbf6efc0fba69770ab
-size 1996838
diff --git a/projects/bloom/resources/Sponza/sponza_roof_diff.png b/projects/bloom/resources/Sponza/sponza_roof_diff.png
deleted file mode 100644
index c5b84261fdd1cc776a94b3ce398c7806b895f9a3..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_roof_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:7fc412138c20da19f8173e53545e771f4652558dff624d4dc67143e40efe562b
-size 2320533
diff --git a/projects/bloom/resources/Sponza/sponza_thorn_diff.png b/projects/bloom/resources/Sponza/sponza_thorn_diff.png
deleted file mode 100644
index 7a9142674a7d4a6f94a48c5152cf0300743b597a..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/sponza_thorn_diff.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a73a17c883cd0d0d67cfda2dc4118400a916366c05b9a5ac465f0c8b30fd9c8e
-size 635001
diff --git a/projects/bloom/resources/Sponza/vase_dif.png b/projects/bloom/resources/Sponza/vase_dif.png
deleted file mode 100644
index 61236a81cb324af8797b05099cd264cefe189e56..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/vase_dif.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:53d06f52bf9e59df4cf00237707cca76c4f692bda61a62b06a30d321311d6dd9
-size 1842101
diff --git a/projects/bloom/resources/Sponza/vase_hanging.png b/projects/bloom/resources/Sponza/vase_hanging.png
deleted file mode 100644
index 36a3cee71d8213225090c74f8c0dce33b9d44378..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/vase_hanging.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a9d10b4f27a3c9a78d5bac882fdd4b6a6987c262f48fa490670fe5e235951e31
-size 1432804
diff --git a/projects/bloom/resources/Sponza/vase_plant.png b/projects/bloom/resources/Sponza/vase_plant.png
deleted file mode 100644
index 7ad95e702e229f1ebd803e5203a266d15f2c07b9..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/vase_plant.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d2087371ff02212fb7014b6daefa191cf5676d2227193fff261a5d02f554cb8e
-size 998089
diff --git a/projects/bloom/resources/Sponza/vase_round.png b/projects/bloom/resources/Sponza/vase_round.png
deleted file mode 100644
index c17953abc000c44b8991e23c136c2b67348f3d1b..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/Sponza/vase_round.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:aa23d48d492d5d4ada2ddb27d1ef22952b214e6eb3b301c65f9d88442723d20a
-size 1871399
diff --git a/projects/bloom/resources/shaders/comp.spv b/projects/bloom/resources/shaders/comp.spv
deleted file mode 100644
index 85c7e74cfc0a89917bf6dd1a7ec449368274c1d3..0000000000000000000000000000000000000000
Binary files a/projects/bloom/resources/shaders/comp.spv and /dev/null differ
diff --git a/projects/bloom/resources/shaders/composite.comp b/projects/bloom/resources/shaders/composite.comp
deleted file mode 100644
index 190bed0657d70e0217bf654820d0b2b2c58f12c2..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/shaders/composite.comp
+++ /dev/null
@@ -1,38 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurImage;
-layout(set=0, binding=1) uniform texture2D                          lensImage;
-layout(set=0, binding=2) uniform sampler                            linearSampler;
-layout(set=0, binding=3, r11f_g11f_b10f) uniform image2D            colorBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(colorBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / textureSize(sampler2D(blurImage, linearSampler), 0);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec4 composite_color = vec4(0.0f);
-
-    vec3 blur_color   = texture(sampler2D(blurImage, linearSampler), UV).rgb;
-    vec3 lens_color   = texture(sampler2D(lensImage, linearSampler), UV).rgb;
-    vec3 main_color   = imageLoad(colorBuffer, pixel_coord).rgb;
-
-    // composite blur and lens features
-    float bloom_weight = 0.25f;
-    float lens_weight  = 0.25f;
-    float main_weight = 1 - (bloom_weight + lens_weight);
-
-    composite_color.rgb = blur_color * bloom_weight +
-                          lens_color * lens_weight  +
-                          main_color * main_weight;
-
-    imageStore(colorBuffer, pixel_coord, composite_color);
-}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/downsample.comp b/projects/bloom/resources/shaders/downsample.comp
deleted file mode 100644
index 2ab00c7c92798769153634f3479c5b7f3fb61d94..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/shaders/downsample.comp
+++ /dev/null
@@ -1,76 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inBlurImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform writeonly image2D  outBlurImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outBlurImage)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outBlurImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-    vec2  UV_offset     = UV + 0.5f * pixel_size;
-
-    vec2 color_fetches[13] = {
-        // center neighbourhood (RED)
-        vec2(-1,  1), // LT
-        vec2(-1, -1), // LB
-        vec2( 1, -1), // RB
-        vec2( 1,  1), // RT
-
-        vec2(-2, 2), // LT
-        vec2( 0, 2), // CT
-        vec2( 2, 2), // RT
-
-        vec2(0 ,-2), // LC
-        vec2(0 , 0), // CC
-        vec2(2,  0), // CR
-
-        vec2(-2, -2), // LB
-        vec2(0 , -2), // CB
-        vec2(2 , -2)  // RB
-    };
-
-    float color_weights[13] = {
-        // 0.5f
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-        1.f/8.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f,
-
-        // 0.25f
-        1.f/16.f,
-        1.f/8.f,
-        1.f/16.f,
-
-        // 0.125f
-        1.f/32.f,
-        1.f/16.f,
-        1.f/32.f
-    };
-
-    vec3 sampled_color = vec3(0.0f);
-
-    for(uint i = 0; i < 13; i++)
-    {
-        vec2 color_fetch = UV_offset + color_fetches[i] * pixel_size;
-        vec3 color = texture(sampler2D(inBlurImage, inImageSampler), color_fetch).rgb;
-        color *= color_weights[i];
-        sampled_color += color;
-    }
-
-    imageStore(outBlurImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/gammaCorrection.comp b/projects/bloom/resources/shaders/gammaCorrection.comp
deleted file mode 100644
index f89ad167c846cca8e80f69d33eda83bd6ed00d46..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/shaders/gammaCorrection.comp
+++ /dev/null
@@ -1,20 +0,0 @@
-#version 440
-
-layout(set=0, binding=0, r11f_g11f_b10f)    uniform image2D inImage;
-layout(set=0, binding=1, rgba8)             uniform image2D outImage;
-
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-void main(){
-
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(inImage)))){
-        return;
-    }
-    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
-    vec3 linearColor = imageLoad(inImage, uv).rgb;
-    // cheap Reinhard tone mapping
-    linearColor = linearColor/(linearColor + 1.0f);
-    vec3 gammaCorrected = pow(linearColor, vec3(1.f / 2.2f));
-    imageStore(outImage, uv, vec4(gammaCorrected, 0.f));
-}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/lensFlares.comp b/projects/bloom/resources/shaders/lensFlares.comp
deleted file mode 100644
index ce27d8850b709f61332d467914ddc944dc63109f..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/shaders/lensFlares.comp
+++ /dev/null
@@ -1,109 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          blurBuffer;
-layout(set=0, binding=1) uniform sampler                            linearSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D            lensBuffer;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-vec3 sampleColorChromaticAberration(vec2 _uv)
-{
-    vec2 toCenter = (vec2(0.5) - _uv);
-
-    vec3    colorScales     = vec3(-1, 0, 1);
-    float   aberrationScale = 0.1;
-    vec3 scaleFactors = colorScales * aberrationScale;
-
-    float r = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.r).r;
-    float g = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.g).g;
-    float b = texture(sampler2D(blurBuffer, linearSampler), _uv + toCenter * scaleFactors.b).b;
-    return vec3(r, g, b);
-}
-
-// _uv assumed to be flipped UV coordinates!
-vec3 ghost_vectors(vec2 _uv)
-{
-    vec2 ghost_vec = (vec2(0.5f) - _uv);
-
-    const uint c_ghost_count = 64;
-    const float c_ghost_spacing = length(ghost_vec) / c_ghost_count;
-
-    ghost_vec *= c_ghost_spacing;
-
-    vec3 ret_color = vec3(0.0f);
-
-    for (uint i = 0; i < c_ghost_count; ++i)
-    {
-        // sample scene color
-        vec2 s_uv = fract(_uv + ghost_vec * vec2(i));
-        vec3 s = sampleColorChromaticAberration(s_uv);
-
-        // tint/weight
-        float d = distance(s_uv, vec2(0.5));
-        float weight = 1.0f - smoothstep(0.0f, 0.75f, d);
-        s *= weight;
-
-        ret_color += s;
-    }
-
-    ret_color /= c_ghost_count;
-    return ret_color;
-}
-
-vec3 halo(vec2 _uv)
-{
-    const float c_aspect_ratio = float(imageSize(lensBuffer).x) / float(imageSize(lensBuffer).y);
-    const float c_radius = 0.6f;
-    const float c_halo_thickness = 0.1f;
-
-    vec2 halo_vec = vec2(0.5) - _uv;
-    //halo_vec.x /= c_aspect_ratio;
-    halo_vec = normalize(halo_vec);
-    //halo_vec.x *= c_aspect_ratio;
-
-
-    //vec2 w_uv = (_uv - vec2(0.5, 0.0)) * vec2(c_aspect_ratio, 1.0) + vec2(0.5, 0.0);
-    vec2 w_uv = _uv;
-    float d = distance(w_uv, vec2(0.5)); // distance to center
-
-    float distance_to_halo = abs(d - c_radius);
-
-    float halo_weight = 0.0f;
-    if(abs(d - c_radius) <= c_halo_thickness)
-    {
-        float distance_to_border = c_halo_thickness - distance_to_halo;
-        halo_weight = distance_to_border / c_halo_thickness;
-
-        //halo_weight = clamp((halo_weight / 0.4f), 0.0f, 1.0f);
-        halo_weight = pow(halo_weight, 2.0f);
-
-
-        //halo_weight = 1.0f;
-    }
-
-    return sampleColorChromaticAberration(_uv + halo_vec) * halo_weight;
-}
-
-
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(lensBuffer)))){
-        return;
-    }
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(lensBuffer);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    vec2 flipped_UV = vec2(1.0f) - UV;
-
-    vec3 color = vec3(0.0f);
-
-    color += ghost_vectors(flipped_UV);
-    color += halo(UV);
-    color  *= 0.5f;
-
-    imageStore(lensBuffer, pixel_coord, vec4(color, 0.0f));
-}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/perMeshResources.inc b/projects/bloom/resources/shaders/perMeshResources.inc
deleted file mode 100644
index 95e4fb7c27009965659d14a9c72acfec950c37e3..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/shaders/perMeshResources.inc
+++ /dev/null
@@ -1,2 +0,0 @@
-layout(set=1, binding=0) uniform texture2D  albedoTexture;
-layout(set=1, binding=1) uniform sampler    textureSampler;
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/shader.frag b/projects/bloom/resources/shaders/shader.frag
deleted file mode 100644
index 3e95b4508f112c1ed9aa4a7050a98fa789dccd09..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/shaders/shader.frag
+++ /dev/null
@@ -1,45 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-#extension GL_GOOGLE_include_directive : enable
-
-#include "perMeshResources.inc"
-
-layout(location = 0) in vec3 passNormal;
-layout(location = 1) in vec2 passUV;
-layout(location = 2) in vec3 passPos;
-
-layout(location = 0) out vec3 outColor;
-
-layout(set=0, binding=0) uniform sunBuffer {
-    vec3 L; float padding;
-    mat4 lightMatrix;
-};
-layout(set=0, binding=1) uniform texture2D  shadowMap;
-layout(set=0, binding=2) uniform sampler    shadowMapSampler;
-
-float shadowTest(vec3 worldPos){
-    vec4 lightPos = lightMatrix * vec4(worldPos, 1);
-    lightPos /= lightPos.w;
-    lightPos.xy = lightPos.xy * 0.5 + 0.5;
-    
-    if(any(lessThan(lightPos.xy, vec2(0))) || any(greaterThan(lightPos.xy, vec2(1)))){
-        return 1;
-    }
-    
-    lightPos.z = clamp(lightPos.z, 0, 1);
-    
-    float shadowMapSample = texture(sampler2D(shadowMap, shadowMapSampler), lightPos.xy).r;
-    float bias = 0.01f;
-    shadowMapSample += bias;
-    return shadowMapSample < lightPos.z ? 0 : 1;
-}
-
-void main()	{
-    vec3 N = normalize(passNormal);
-    vec3 sunColor = vec3(10);
-    vec3 sun = sunColor * clamp(dot(N, L), 0, 1);
-    sun *= shadowTest(passPos);
-    vec3 ambient = vec3(0.05);
-    vec3 albedo = texture(sampler2D(albedoTexture, textureSampler), passUV).rgb;
-	outColor = albedo * (sun + ambient);
-}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/upsample.comp b/projects/bloom/resources/shaders/upsample.comp
deleted file mode 100644
index 0ddeedb5b5af9e476dc19012fed6430544006c0e..0000000000000000000000000000000000000000
--- a/projects/bloom/resources/shaders/upsample.comp
+++ /dev/null
@@ -1,45 +0,0 @@
-#version 450
-#extension GL_ARB_separate_shader_objects : enable
-
-layout(set=0, binding=0) uniform texture2D                          inUpsampleImage;
-layout(set=0, binding=1) uniform sampler                            inImageSampler;
-layout(set=0, binding=2, r11f_g11f_b10f) uniform image2D  outUpsampleImage;
-
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
-
-void main()
-{
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outUpsampleImage)))){
-        return;
-    }
-
-
-    ivec2 pixel_coord   = ivec2(gl_GlobalInvocationID.xy);
-    vec2  pixel_size    = vec2(1.0f) / imageSize(outUpsampleImage);
-    vec2  UV            = pixel_coord.xy * pixel_size;
-
-    const float gauss_kernel[3] = {1.f, 2.f, 1.f};
-    const float gauss_weight = 16.f;
-
-    vec3 sampled_color = vec3(0.f);
-
-    for(int i = -1; i <= 1; i++)
-    {
-        for(int j = -1; j <= 1; j++)
-        {
-            vec2 sample_location = UV + vec2(j, i) * pixel_size;
-            vec3 color = texture(sampler2D(inUpsampleImage, inImageSampler), sample_location).rgb;
-            color *= gauss_kernel[j+1];
-            color *= gauss_kernel[i+1];
-            color /= gauss_weight;
-
-            sampled_color += color;
-        }
-    }
-
-    //vec3 prev_color = imageLoad(outUpsampleImage, pixel_coord).rgb;
-    //float bloomRimStrength = 0.75f; // adjust this to change strength of bloom
-    //sampled_color = mix(prev_color, sampled_color, bloomRimStrength);
-
-    imageStore(outUpsampleImage, pixel_coord, vec4(sampled_color, 1.f));
-}
\ No newline at end of file
diff --git a/projects/bloom/src/BloomAndFlares.cpp b/projects/bloom/src/BloomAndFlares.cpp
deleted file mode 100644
index 6f26db9de0f2c8334b6dd7e5dd6cf4b6f48baedc..0000000000000000000000000000000000000000
--- a/projects/bloom/src/BloomAndFlares.cpp
+++ /dev/null
@@ -1,274 +0,0 @@
-#include "BloomAndFlares.hpp"
-#include <vkcv/shader/GLSLCompiler.hpp>
-
-BloomAndFlares::BloomAndFlares(
-        vkcv::Core *p_Core,
-        vk::Format colorBufferFormat,
-        uint32_t width,
-        uint32_t height) :
-
-        p_Core(p_Core),
-        m_ColorBufferFormat(colorBufferFormat),
-        m_Width(width),
-        m_Height(height),
-        m_LinearSampler(p_Core->createSampler(vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerFilterType::LINEAR,
-                                              vkcv::SamplerMipmapMode::LINEAR,
-                                              vkcv::SamplerAddressMode::CLAMP_TO_EDGE)),
-        m_Blur(p_Core->createImage(colorBufferFormat, width, height, 1, true, true, false)),
-        m_LensFeatures(p_Core->createImage(colorBufferFormat, width, height, 1, false, true, false))
-{
-    vkcv::shader::GLSLCompiler compiler;
-
-    // DOWNSAMPLE
-    vkcv::ShaderProgram dsProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "resources/shaders/downsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         dsProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-		m_DownsampleDescSets.push_back(
-                p_Core->createDescriptorSet(dsProg.getReflectedDescriptors()[0]));
-    }
-    m_DownsamplePipe = p_Core->createComputePipeline(
-            dsProg, { p_Core->getDescriptorSet(m_DownsampleDescSets[0]).layout });
-
-    // UPSAMPLE
-    vkcv::ShaderProgram usProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "resources/shaders/upsample.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         usProg.addShader(shaderStage, path);
-                     });
-    for(uint32_t mipLevel = 0; mipLevel < m_Blur.getMipCount(); mipLevel++)
-    {
-        m_UpsampleDescSets.push_back(
-                p_Core->createDescriptorSet(usProg.getReflectedDescriptors()[0]));
-    }
-    m_UpsamplePipe = p_Core->createComputePipeline(
-            usProg, { p_Core->getDescriptorSet(m_UpsampleDescSets[0]).layout });
-
-    // LENS FEATURES
-    vkcv::ShaderProgram lensProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "resources/shaders/lensFlares.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         lensProg.addShader(shaderStage, path);
-                     });
-    m_LensFlareDescSet = p_Core->createDescriptorSet(lensProg.getReflectedDescriptors()[0]);
-    m_LensFlarePipe = p_Core->createComputePipeline(
-            lensProg, { p_Core->getDescriptorSet(m_LensFlareDescSet).layout });
-
-    // COMPOSITE
-    vkcv::ShaderProgram compProg;
-    compiler.compile(vkcv::ShaderStage::COMPUTE,
-                     "resources/shaders/composite.comp",
-                     [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path)
-                     {
-                         compProg.addShader(shaderStage, path);
-                     });
-    m_CompositeDescSet = p_Core->createDescriptorSet(compProg.getReflectedDescriptors()[0]);
-    m_CompositePipe = p_Core->createComputePipeline(
-            compProg, { p_Core->getDescriptorSet(m_CompositeDescSet).layout });
-}
-
-void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream,
-                                        const vkcv::ImageHandle &colorAttachment)
-{
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // blur dispatch
-    uint32_t initialDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    // downsample dispatch of original color attachment
-    p_Core->prepareImageForSampling(cmdStream, colorAttachment);
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    vkcv::DescriptorWrites initialDownsampleWrites;
-    initialDownsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, colorAttachment)};
-    initialDownsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    initialDownsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), 0) };
-    p_Core->writeDescriptorSet(m_DownsampleDescSets[0], initialDownsampleWrites);
-
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_DownsamplePipe,
-            initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
-            vkcv::PushConstantData(nullptr, 0));
-
-    // downsample dispatches of blur buffer's mip maps
-    float mipDispatchCountX = dispatchCountX;
-    float mipDispatchCountY = dispatchCountY;
-    for(uint32_t mipLevel = 1; mipLevel < m_DownsampleDescSets.size(); mipLevel++)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipDescriptorWrites;
-        mipDescriptorWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel - 1, true)};
-        mipDescriptorWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipDescriptorWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel) };
-        p_Core->writeDescriptorSet(m_DownsampleDescSets[mipLevel], mipDescriptorWrites);
-
-        // mip dispatch calculation
-        mipDispatchCountX  /= 2.0f;
-        mipDispatchCountY /= 2.0f;
-
-        uint32_t mipDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountX)),
-                static_cast<uint32_t>(glm::ceil(mipDispatchCountY)),
-                1
-        };
-
-        if(mipDispatchCount[0] == 0)
-            mipDispatchCount[0] = 1;
-        if(mipDispatchCount[1] == 0)
-            mipDispatchCount[1] = 1;
-
-        // mip blur dispatch
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_DownsamplePipe,
-                mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
-                vkcv::PushConstantData(nullptr, 0));
-
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // upsample dispatch
-    p_Core->prepareImageForStorage(cmdStream, m_Blur.getHandle());
-
-    const uint32_t upsampleMipLevels = std::min(
-    		static_cast<uint32_t>(m_UpsampleDescSets.size() - 1),
-    		static_cast<uint32_t>(5)
-	);
-
-    // upsample dispatch for each mip map
-    for(uint32_t mipLevel = upsampleMipLevels; mipLevel > 0; mipLevel--)
-    {
-        // mip descriptor writes
-        vkcv::DescriptorWrites mipUpsampleWrites;
-        mipUpsampleWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), mipLevel, true)};
-        mipUpsampleWrites.samplerWrites      = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-        mipUpsampleWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_Blur.getHandle(), mipLevel - 1) };
-        p_Core->writeDescriptorSet(m_UpsampleDescSets[mipLevel], mipUpsampleWrites);
-
-        auto mipDivisor = glm::pow(2.0f, static_cast<float>(mipLevel) - 1.0f);
-
-        auto upsampleDispatchX  = static_cast<float>(m_Width) / mipDivisor;
-        auto upsampleDispatchY = static_cast<float>(m_Height) / mipDivisor;
-        upsampleDispatchX /= 8.0f;
-        upsampleDispatchY /= 8.0f;
-
-        const uint32_t upsampleDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchX)),
-                static_cast<uint32_t>(glm::ceil(upsampleDispatchY)),
-                1
-        };
-
-        p_Core->recordComputeDispatchToCmdStream(
-                cmdStream,
-                m_UpsamplePipe,
-                upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
-                vkcv::PushConstantData(nullptr, 0)
-        );
-        // image barrier between mips
-        p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
-    }
-}
-
-void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream)
-{
-    // lens feature generation descriptor writes
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
-
-    vkcv::DescriptorWrites lensFeatureWrites;
-    lensFeatureWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle(), 0)};
-    lensFeatureWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(1, m_LinearSampler)};
-    lensFeatureWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(2, m_LensFeatures.getHandle(), 0)};
-    p_Core->writeDescriptorSet(m_LensFlareDescSet, lensFeatureWrites);
-
-    auto dispatchCountX  = static_cast<float>(m_Width)  / 8.0f;
-    auto dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-    // lens feature generation dispatch
-    uint32_t lensFeatureDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_LensFlarePipe,
-            lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
-            vkcv::PushConstantData(nullptr, 0));
-}
-
-void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    p_Core->prepareImageForSampling(cmdStream, m_Blur.getHandle());
-    p_Core->prepareImageForSampling(cmdStream, m_LensFeatures.getHandle());
-    p_Core->prepareImageForStorage(cmdStream, colorAttachment);
-
-    // bloom composite descriptor write
-    vkcv::DescriptorWrites compositeWrites;
-    compositeWrites.sampledImageWrites = {vkcv::SampledImageDescriptorWrite(0, m_Blur.getHandle()),
-                                          vkcv::SampledImageDescriptorWrite(1, m_LensFeatures.getHandle())};
-    compositeWrites.samplerWrites = {vkcv::SamplerDescriptorWrite(2, m_LinearSampler)};
-    compositeWrites.storageImageWrites = {vkcv::StorageImageDescriptorWrite(3, colorAttachment)};
-    p_Core->writeDescriptorSet(m_CompositeDescSet, compositeWrites);
-
-    float dispatchCountX = static_cast<float>(m_Width)  / 8.0f;
-    float dispatchCountY = static_cast<float>(m_Height) / 8.0f;
-
-    uint32_t compositeDispatchCount[3] = {
-            static_cast<uint32_t>(glm::ceil(dispatchCountX)),
-            static_cast<uint32_t>(glm::ceil(dispatchCountY)),
-            1
-    };
-
-    // bloom composite dispatch
-    p_Core->recordComputeDispatchToCmdStream(
-            cmdStream,
-            m_CompositePipe,
-            compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
-            vkcv::PushConstantData(nullptr, 0));
-}
-
-void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream,
-                                       const vkcv::ImageHandle &colorAttachment)
-{
-    execDownsamplePipe(cmdStream, colorAttachment);
-    execUpsamplePipe(cmdStream);
-    execLensFeaturePipe(cmdStream);
-    execCompositePipe(cmdStream, colorAttachment);
-}
-
-void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
-{
-    m_Width  = width;
-    m_Height = height;
-
-    p_Core->getContext().getDevice().waitIdle();
-    m_Blur = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, true, true, false);
-    m_LensFeatures = p_Core->createImage(m_ColorBufferFormat, m_Width, m_Height, 1, false, true, false);
-}
-
-
diff --git a/projects/bloom/src/BloomAndFlares.hpp b/projects/bloom/src/BloomAndFlares.hpp
deleted file mode 100644
index 756b1ca154ea5232df04eb09a88bb743c5bd28aa..0000000000000000000000000000000000000000
--- a/projects/bloom/src/BloomAndFlares.hpp
+++ /dev/null
@@ -1,47 +0,0 @@
-#pragma once
-#include <vkcv/Core.hpp>
-#include <glm/glm.hpp>
-
-class BloomAndFlares{
-public:
-    BloomAndFlares(vkcv::Core *p_Core,
-                   vk::Format colorBufferFormat,
-                   uint32_t width,
-                   uint32_t height);
-
-    void execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-
-    void updateImageDimensions(uint32_t width, uint32_t height);
-
-private:
-    vkcv::Core *p_Core;
-
-    vk::Format m_ColorBufferFormat;
-    uint32_t m_Width;
-    uint32_t m_Height;
-
-    vkcv::SamplerHandle m_LinearSampler;
-    vkcv::Image m_Blur;
-    vkcv::Image m_LensFeatures;
-
-
-    vkcv::PipelineHandle                     m_DownsamplePipe;
-    std::vector<vkcv::DescriptorSetHandle>   m_DownsampleDescSets; // per mip desc set
-
-    vkcv::PipelineHandle                     m_UpsamplePipe;
-    std::vector<vkcv::DescriptorSetHandle>   m_UpsampleDescSets;   // per mip desc set
-
-    vkcv::PipelineHandle                     m_LensFlarePipe;
-    vkcv::DescriptorSetHandle                m_LensFlareDescSet;
-
-    vkcv::PipelineHandle                     m_CompositePipe;
-    vkcv::DescriptorSetHandle                m_CompositeDescSet;
-
-    void execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-    void execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStream);
-    void execCompositePipe(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment);
-};
-
-
-
diff --git a/projects/bloom/src/main.cpp b/projects/bloom/src/main.cpp
deleted file mode 100644
index 7a17a51f1c7d638575c0b5aafcdca49b589533ef..0000000000000000000000000000000000000000
--- a/projects/bloom/src/main.cpp
+++ /dev/null
@@ -1,419 +0,0 @@
-#include <iostream>
-#include <vkcv/Core.hpp>
-#include <GLFW/glfw3.h>
-#include <vkcv/camera/CameraManager.hpp>
-#include <chrono>
-#include <vkcv/asset/asset_loader.hpp>
-#include <vkcv/shader/GLSLCompiler.hpp>
-#include <vkcv/Logger.hpp>
-#include "BloomAndFlares.hpp"
-#include <glm/glm.hpp>
-
-int main(int argc, const char** argv) {
-	const char* applicationName = "Bloom";
-
-	uint32_t windowWidth = 1920;
-	uint32_t windowHeight = 1080;
-	
-	vkcv::Window window = vkcv::Window::create(
-		applicationName,
-		windowWidth,
-		windowHeight,
-		true
-	);
-
-    vkcv::camera::CameraManager cameraManager(window);
-    uint32_t camIndex = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
-    uint32_t camIndex2 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
-    
-    cameraManager.getCamera(camIndex).setPosition(glm::vec3(0.f, 0.f, 3.f));
-    cameraManager.getCamera(camIndex).setNearFar(0.1f, 30.0f);
-	cameraManager.getCamera(camIndex).setYaw(180.0f);
-	
-	cameraManager.getCamera(camIndex2).setNearFar(0.1f, 30.0f);
-
-	vkcv::Core core = vkcv::Core::create(
-		window,
-		applicationName,
-		VK_MAKE_VERSION(0, 0, 1),
-		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
-		{},
-		{ "VK_KHR_swapchain" }
-	);
-
-	const char* path = argc > 1 ? argv[1] : "resources/Sponza/Sponza.gltf";
-	vkcv::asset::Scene scene;
-	int result = vkcv::asset::loadScene(path, scene);
-
-	if (result == 1) {
-		std::cout << "Scene loading successful!" << std::endl;
-	}
-	else {
-		std::cout << "Scene loading failed: " << result << std::endl;
-		return 1;
-	}
-
-	// build index and vertex buffers
-	assert(!scene.vertexGroups.empty());
-	std::vector<std::vector<uint8_t>> vBuffers;
-	std::vector<std::vector<uint8_t>> iBuffers;
-
-	std::vector<vkcv::VertexBufferBinding> vBufferBindings;
-	std::vector<std::vector<vkcv::VertexBufferBinding>> vertexBufferBindings;
-	std::vector<vkcv::asset::VertexAttribute> vAttributes;
-
-	for (int i = 0; i < scene.vertexGroups.size(); i++) {
-
-		vBuffers.push_back(scene.vertexGroups[i].vertexBuffer.data);
-		iBuffers.push_back(scene.vertexGroups[i].indexBuffer.data);
-
-		auto& attributes = scene.vertexGroups[i].vertexBuffer.attributes;
-
-		std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
-			return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
-		});
-	}
-
-	std::vector<vkcv::Buffer<uint8_t>> vertexBuffers;
-	for (const vkcv::asset::VertexGroup& group : scene.vertexGroups) {
-		vertexBuffers.push_back(core.createBuffer<uint8_t>(
-			vkcv::BufferType::VERTEX,
-			group.vertexBuffer.data.size()));
-		vertexBuffers.back().fill(group.vertexBuffer.data);
-	}
-
-	std::vector<vkcv::Buffer<uint8_t>> indexBuffers;
-	for (const auto& dataBuffer : iBuffers) {
-		indexBuffers.push_back(core.createBuffer<uint8_t>(
-			vkcv::BufferType::INDEX,
-			dataBuffer.size()));
-		indexBuffers.back().fill(dataBuffer);
-	}
-
-	int vertexBufferIndex = 0;
-	for (const auto& vertexGroup : scene.vertexGroups) {
-		for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
-			vAttributes.push_back(attribute);
-			vBufferBindings.push_back(vkcv::VertexBufferBinding(attribute.offset, vertexBuffers[vertexBufferIndex].getVulkanHandle()));
-		}
-		vertexBufferBindings.push_back(vBufferBindings);
-		vBufferBindings.clear();
-		vertexBufferIndex++;
-	}
-
-	const vk::Format colorBufferFormat = vk::Format::eB10G11R11UfloatPack32;
-	const vkcv::AttachmentDescription color_attachment(
-		vkcv::AttachmentOperation::STORE,
-		vkcv::AttachmentOperation::CLEAR,
-		colorBufferFormat
-	);
-	
-	const vk::Format depthBufferFormat = vk::Format::eD32Sfloat;
-	const vkcv::AttachmentDescription depth_attachment(
-		vkcv::AttachmentOperation::STORE,
-		vkcv::AttachmentOperation::CLEAR,
-		depthBufferFormat
-	);
-
-	vkcv::PassConfig forwardPassDefinition({ color_attachment, depth_attachment });
-	vkcv::PassHandle forwardPass = core.createPass(forwardPassDefinition);
-
-	vkcv::shader::GLSLCompiler compiler;
-
-	vkcv::ShaderProgram forwardProgram;
-	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"), 
-		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		forwardProgram.addShader(shaderStage, path);
-	});
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
-		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		forwardProgram.addShader(shaderStage, path);
-	});
-
-	const std::vector<vkcv::VertexAttachment> vertexAttachments = forwardProgram.getVertexAttachments();
-
-	std::vector<vkcv::VertexBinding> vertexBindings;
-	for (size_t i = 0; i < vertexAttachments.size(); i++) {
-		vertexBindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
-	}
-	const vkcv::VertexLayout vertexLayout (vertexBindings);
-
-	// shadow map
-	vkcv::SamplerHandle shadowSampler = core.createSampler(
-		vkcv::SamplerFilterType::NEAREST,
-		vkcv::SamplerFilterType::NEAREST,
-		vkcv::SamplerMipmapMode::NEAREST,
-		vkcv::SamplerAddressMode::CLAMP_TO_EDGE
-	);
-	const vk::Format shadowMapFormat = vk::Format::eD16Unorm;
-	const uint32_t shadowMapResolution = 1024;
-	const vkcv::Image shadowMap = core.createImage(shadowMapFormat, shadowMapResolution, shadowMapResolution, 1);
-
-	// light info buffer
-	struct LightInfo {
-		glm::vec3 direction;
-		float padding;
-		glm::mat4 lightMatrix;
-	};
-	LightInfo lightInfo;
-	vkcv::Buffer lightBuffer = core.createBuffer<LightInfo>(vkcv::BufferType::UNIFORM, sizeof(glm::vec3));
-
-	vkcv::DescriptorSetHandle forwardShadingDescriptorSet = 
-		core.createDescriptorSet({ forwardProgram.getReflectedDescriptors()[0] });
-
-	vkcv::DescriptorWrites forwardDescriptorWrites;
-	forwardDescriptorWrites.uniformBufferWrites = { vkcv::UniformBufferDescriptorWrite(0, lightBuffer.getHandle()) };
-	forwardDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(1, shadowMap.getHandle()) };
-	forwardDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(2, shadowSampler) };
-	core.writeDescriptorSet(forwardShadingDescriptorSet, forwardDescriptorWrites);
-
-	vkcv::SamplerHandle colorSampler = core.createSampler(
-		vkcv::SamplerFilterType::LINEAR,
-		vkcv::SamplerFilterType::LINEAR,
-		vkcv::SamplerMipmapMode::LINEAR,
-		vkcv::SamplerAddressMode::REPEAT
-	);
-
-	// prepare per mesh descriptor sets
-	std::vector<vkcv::DescriptorSetHandle> perMeshDescriptorSets;
-	std::vector<vkcv::Image> sceneImages;
-	for (const auto& vertexGroup : scene.vertexGroups) {
-		perMeshDescriptorSets.push_back(core.createDescriptorSet(forwardProgram.getReflectedDescriptors()[1]));
-
-		const auto& material = scene.materials[vertexGroup.materialIndex];
-
-		int baseColorIndex = material.baseColor;
-		if (baseColorIndex < 0) {
-			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks base color");
-			baseColorIndex = 0;
-		}
-
-		vkcv::asset::Texture& sceneTexture = scene.textures[baseColorIndex];
-
-		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, sceneTexture.w, sceneTexture.h));
-		sceneImages.back().fill(sceneTexture.data.data());
-
-		vkcv::DescriptorWrites setWrites;
-		setWrites.sampledImageWrites = {
-			vkcv::SampledImageDescriptorWrite(0, sceneImages.back().getHandle())
-		};
-		setWrites.samplerWrites = {
-			vkcv::SamplerDescriptorWrite(1, colorSampler),
-		};
-		core.writeDescriptorSet(perMeshDescriptorSets.back(), setWrites);
-	}
-
-	const vkcv::PipelineConfig forwardPipelineConfig {
-		forwardProgram,
-		windowWidth,
-		windowHeight,
-		forwardPass,
-		vertexLayout,
-		{	core.getDescriptorSet(forwardShadingDescriptorSet).layout, 
-			core.getDescriptorSet(perMeshDescriptorSets[0]).layout },
-		true
-	};
-	
-	vkcv::PipelineHandle forwardPipeline = core.createGraphicsPipeline(forwardPipelineConfig);
-	
-	if (!forwardPipeline) {
-		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
-		return EXIT_FAILURE;
-	}
-
-	vkcv::ImageHandle depthBuffer       = core.createImage(depthBufferFormat, windowWidth, windowHeight).getHandle();
-	vkcv::ImageHandle colorBuffer       = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true, true).getHandle();
-
-	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
-
-	vkcv::ShaderProgram shadowShader;
-	compiler.compile(vkcv::ShaderStage::VERTEX, "resources/shaders/shadow.vert",
-		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		shadowShader.addShader(shaderStage, path);
-	});
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, "resources/shaders/shadow.frag",
-		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		shadowShader.addShader(shaderStage, path);
-	});
-
-	const std::vector<vkcv::AttachmentDescription> shadowAttachments = {
-		vkcv::AttachmentDescription(vkcv::AttachmentOperation::STORE, vkcv::AttachmentOperation::CLEAR, shadowMapFormat)
-	};
-	const vkcv::PassConfig shadowPassConfig(shadowAttachments);
-	const vkcv::PassHandle shadowPass = core.createPass(shadowPassConfig);
-	const vkcv::PipelineConfig shadowPipeConfig{
-		shadowShader,
-		shadowMapResolution,
-		shadowMapResolution,
-		shadowPass,
-		vertexLayout,
-		{},
-		false
-	};
-	const vkcv::PipelineHandle shadowPipe = core.createGraphicsPipeline(shadowPipeConfig);
-
-	std::vector<std::array<glm::mat4, 2>> mainPassMatrices;
-	std::vector<glm::mat4> mvpLight;
-
-	// gamma correction compute shader
-	vkcv::ShaderProgram gammaCorrectionProgram;
-	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/gammaCorrection.comp",
-		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		gammaCorrectionProgram.addShader(shaderStage, path);
-	});
-	vkcv::DescriptorSetHandle gammaCorrectionDescriptorSet = core.createDescriptorSet(gammaCorrectionProgram.getReflectedDescriptors()[0]);
-	vkcv::PipelineHandle gammaCorrectionPipeline = core.createComputePipeline(gammaCorrectionProgram,
-		{ core.getDescriptorSet(gammaCorrectionDescriptorSet).layout });
-
-    BloomAndFlares baf(&core, colorBufferFormat, windowWidth, windowHeight);
-
-
-	// model matrices per mesh
-	std::vector<glm::mat4> modelMatrices;
-	modelMatrices.resize(scene.vertexGroups.size(), glm::mat4(1.f));
-	for (const auto& mesh : scene.meshes) {
-		const glm::mat4 m = *reinterpret_cast<const glm::mat4*>(&mesh.modelMatrix[0]);
-		for (const auto& vertexGroupIndex : mesh.vertexGroups) {
-			modelMatrices[vertexGroupIndex] = m;
-		}
-	}
-
-	// prepare drawcalls
-	std::vector<vkcv::Mesh> meshes;
-	for (int i = 0; i < scene.vertexGroups.size(); i++) {
-		vkcv::Mesh mesh(
-			vertexBufferBindings[i], 
-			indexBuffers[i].getVulkanHandle(), 
-			scene.vertexGroups[i].numIndices);
-		meshes.push_back(mesh);
-	}
-
-	std::vector<vkcv::DrawcallInfo> drawcalls;
-	std::vector<vkcv::DrawcallInfo> shadowDrawcalls;
-	for (int i = 0; i < meshes.size(); i++) {
-		drawcalls.push_back(vkcv::DrawcallInfo(meshes[i], { 
-			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(forwardShadingDescriptorSet).vulkanHandle),
-			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
-		shadowDrawcalls.push_back(vkcv::DrawcallInfo(meshes[i], {}));
-	}
-
-	auto start = std::chrono::system_clock::now();
-	const auto appStartTime = start;
-	while (window.isWindowOpen()) {
-		vkcv::Window::pollEvents();
-
-		uint32_t swapchainWidth, swapchainHeight;
-		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
-			continue;
-		}
-
-		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
-			depthBuffer = core.createImage(depthBufferFormat, swapchainWidth, swapchainHeight).getHandle();
-			colorBuffer = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, true, true).getHandle();
-
-			baf.updateImageDimensions(swapchainWidth, swapchainHeight);
-
-			windowWidth = swapchainWidth;
-			windowHeight = swapchainHeight;
-		}
-
-		auto end = std::chrono::system_clock::now();
-		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
-
-		start = end;
-		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
-
-		auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - appStartTime);
-		
-		const float sunTheta = 0.0001f * static_cast<float>(duration.count());
-		lightInfo.direction = glm::normalize(glm::vec3(std::cos(sunTheta), 1, std::sin(sunTheta)));
-
-		const float shadowProjectionSize = 20.f;
-		glm::mat4 projectionLight = glm::ortho(
-			-shadowProjectionSize,
-			shadowProjectionSize,
-			-shadowProjectionSize,
-			shadowProjectionSize,
-			-shadowProjectionSize,
-			shadowProjectionSize);
-
-		glm::mat4 vulkanCorrectionMatrix(1.f);
-		vulkanCorrectionMatrix[2][2] = 0.5;
-		vulkanCorrectionMatrix[3][2] = 0.5;
-		projectionLight = vulkanCorrectionMatrix * projectionLight;
-
-		const glm::mat4 viewLight = glm::lookAt(glm::vec3(0), -lightInfo.direction, glm::vec3(0, -1, 0));
-
-		lightInfo.lightMatrix = projectionLight * viewLight;
-		lightBuffer.fill({ lightInfo });
-
-		const glm::mat4 viewProjectionCamera = cameraManager.getActiveCamera().getMVP();
-
-		mainPassMatrices.clear();
-		mvpLight.clear();
-		for (const auto& m : modelMatrices) {
-			mainPassMatrices.push_back({ viewProjectionCamera * m, m });
-			mvpLight.push_back(lightInfo.lightMatrix * m);
-		}
-
-		vkcv::PushConstantData pushConstantData((void*)mainPassMatrices.data(), 2 * sizeof(glm::mat4));
-		const std::vector<vkcv::ImageHandle> renderTargets = { colorBuffer, depthBuffer };
-
-		const vkcv::PushConstantData shadowPushConstantData((void*)mvpLight.data(), sizeof(glm::mat4));
-
-		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
-
-		// shadow map
-		core.recordDrawcallsToCmdStream(
-			cmdStream,
-			shadowPass,
-			shadowPipe,
-			shadowPushConstantData,
-			shadowDrawcalls,
-			{ shadowMap.getHandle() });
-		core.prepareImageForSampling(cmdStream, shadowMap.getHandle());
-
-		// main pass
-		core.recordDrawcallsToCmdStream(
-			cmdStream,
-            forwardPass,
-            forwardPipeline,
-			pushConstantData,
-			drawcalls,
-			renderTargets);
-
-        const uint32_t gammaCorrectionLocalGroupSize = 8;
-        const uint32_t gammaCorrectionDispatchCount[3] = {
-                static_cast<uint32_t>(glm::ceil(static_cast<float>(windowWidth) / static_cast<float>(gammaCorrectionLocalGroupSize))),
-                static_cast<uint32_t>(glm::ceil(static_cast<float>(windowHeight) / static_cast<float>(gammaCorrectionLocalGroupSize))),
-                1
-        };
-
-        baf.execWholePipeline(cmdStream, colorBuffer);
-
-        core.prepareImageForStorage(cmdStream, swapchainInput);
-        
-        // gamma correction descriptor write
-        vkcv::DescriptorWrites gammaCorrectionDescriptorWrites;
-        gammaCorrectionDescriptorWrites.storageImageWrites = {
-                vkcv::StorageImageDescriptorWrite(0, colorBuffer),
-                vkcv::StorageImageDescriptorWrite(1, swapchainInput) };
-        core.writeDescriptorSet(gammaCorrectionDescriptorSet, gammaCorrectionDescriptorWrites);
-
-        // gamma correction dispatch
-        core.recordComputeDispatchToCmdStream(
-			cmdStream, 
-			gammaCorrectionPipeline, 
-			gammaCorrectionDispatchCount,
-			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(gammaCorrectionDescriptorSet).vulkanHandle) },
-			vkcv::PushConstantData(nullptr, 0));
-
-		// present and end
-		core.prepareSwapchainImageForPresent(cmdStream);
-		core.submitCommandStream(cmdStream);
-
-		core.endFrame();
-	}
-	
-	return 0;
-}
diff --git a/projects/cmd_sync_test/src/main.cpp b/projects/cmd_sync_test/src/main.cpp
deleted file mode 100644
index 6e53eb8c5ec1825135778dc91b11dd6e45f44276..0000000000000000000000000000000000000000
--- a/projects/cmd_sync_test/src/main.cpp
+++ /dev/null
@@ -1,317 +0,0 @@
-#include <iostream>
-#include <vkcv/Core.hpp>
-#include <GLFW/glfw3.h>
-#include <vkcv/camera/CameraManager.hpp>
-#include <chrono>
-#include <vkcv/asset/asset_loader.hpp>
-
-int main(int argc, const char** argv) {
-	const char* applicationName = "First Mesh";
-
-	uint32_t windowWidth = 800;
-	uint32_t windowHeight = 600;
-	
-	vkcv::Window window = vkcv::Window::create(
-		applicationName,
-		windowWidth,
-		windowHeight,
-		true
-	);
-
-    vkcv::camera::CameraManager cameraManager(window);
-    uint32_t camIndex = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
-    uint32_t camIndex2 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
-    
-    cameraManager.getCamera(camIndex).setPosition(glm::vec3(0.f, 0.f, 3.f));
-    cameraManager.getCamera(camIndex).setNearFar(0.1f, 30.0f);
-	cameraManager.getCamera(camIndex).setYaw(180.0f);
-	
-	cameraManager.getCamera(camIndex2).setNearFar(0.1f, 30.0f);
-
-	window.initEvents();
-
-	vkcv::Core core = vkcv::Core::create(
-		window,
-		applicationName,
-		VK_MAKE_VERSION(0, 0, 1),
-		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
-		{},
-		{ "VK_KHR_swapchain" }
-	);
-
-	vkcv::asset::Scene mesh;
-
-	const char* path = argc > 1 ? argv[1] : "resources/cube/cube.gltf";
-	int result = vkcv::asset::loadScene(path, mesh);
-
-	if (result == 1) {
-		std::cout << "Mesh loading successful!" << std::endl;
-	}
-	else {
-		std::cout << "Mesh loading failed: " << result << std::endl;
-		return 1;
-	}
-
-	assert(mesh.vertexGroups.size() > 0);
-	auto vertexBuffer = core.createBuffer<uint8_t>(
-			vkcv::BufferType::VERTEX,
-			mesh.vertexGroups[0].vertexBuffer.data.size(),
-			vkcv::BufferMemoryType::DEVICE_LOCAL
-	);
-	
-	vertexBuffer.fill(mesh.vertexGroups[0].vertexBuffer.data);
-
-	auto indexBuffer = core.createBuffer<uint8_t>(
-			vkcv::BufferType::INDEX,
-			mesh.vertexGroups[0].indexBuffer.data.size(),
-			vkcv::BufferMemoryType::DEVICE_LOCAL
-	);
-	
-	indexBuffer.fill(mesh.vertexGroups[0].indexBuffer.data);
-	
-	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
-	
-	std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
-		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
-	});
-
-	const std::vector<vkcv::VertexBufferBinding> vertexBufferBindings = {
-		vkcv::VertexBufferBinding(attributes[0].offset, vertexBuffer.getVulkanHandle()),
-		vkcv::VertexBufferBinding(attributes[1].offset, vertexBuffer.getVulkanHandle()),
-		vkcv::VertexBufferBinding(attributes[2].offset, vertexBuffer.getVulkanHandle()) };
-
-	const vkcv::Mesh loadedMesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), mesh.vertexGroups[0].numIndices);
-
-	// an example attachment for passes that output to the window
-	const vkcv::AttachmentDescription present_color_attachment(
-		vkcv::AttachmentOperation::STORE,
-		vkcv::AttachmentOperation::CLEAR,
-		core.getSwapchain().getFormat()
-	);
-	
-	const vkcv::AttachmentDescription depth_attachment(
-			vkcv::AttachmentOperation::STORE,
-			vkcv::AttachmentOperation::CLEAR,
-			vk::Format::eD32Sfloat
-	);
-
-	vkcv::PassConfig firstMeshPassDefinition({ present_color_attachment, depth_attachment });
-	vkcv::PassHandle firstMeshPass = core.createPass(firstMeshPassDefinition);
-
-	if (!firstMeshPass) {
-		std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
-		return EXIT_FAILURE;
-	}
-
-	vkcv::ShaderProgram firstMeshProgram{};
-    firstMeshProgram.addShader(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/vert.spv"));
-    firstMeshProgram.addShader(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/frag.spv"));
-
-    const std::vector<vkcv::VertexAttachment> vertexAttachments = firstMeshProgram.getVertexAttachments();
-    
-    std::vector<vkcv::VertexBinding> bindings;
-    for (size_t i = 0; i < vertexAttachments.size(); i++) {
-		bindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
-    }
-    
-    const vkcv::VertexLayout firstMeshLayout (bindings);
-
-	std::vector<vkcv::DescriptorBinding> descriptorBindings = { firstMeshProgram.getReflectedDescriptors()[0] };
-	vkcv::DescriptorSetHandle descriptorSet = core.createDescriptorSet(descriptorBindings);
-
-	const vkcv::PipelineConfig firstMeshPipelineConfig {
-        firstMeshProgram,
-		windowWidth,
-		windowHeight,
-        firstMeshPass,
-        firstMeshLayout,
-		{ core.getDescriptorSet(descriptorSet).layout },
-		true
-	};
-	
-	vkcv::PipelineHandle firstMeshPipeline = core.createGraphicsPipeline(firstMeshPipelineConfig);
-	
-	if (!firstMeshPipeline) {
-		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
-		return EXIT_FAILURE;
-	}
-	
-	//vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, mesh.texture_hack.w, mesh.texture_hack.h);
-	//texture.fill(mesh.texture_hack.img);
-    vkcv::asset::Texture &tex = mesh.textures[0];
-    vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, tex.w, tex.h);
-    texture.fill(tex.data.data());
-
-	vkcv::SamplerHandle sampler = core.createSampler(
-		vkcv::SamplerFilterType::LINEAR,
-		vkcv::SamplerFilterType::LINEAR,
-		vkcv::SamplerMipmapMode::LINEAR,
-		vkcv::SamplerAddressMode::REPEAT
-	);
-
-    vkcv::SamplerHandle shadowSampler = core.createSampler(
-        vkcv::SamplerFilterType::NEAREST,
-        vkcv::SamplerFilterType::NEAREST,
-        vkcv::SamplerMipmapMode::NEAREST,
-        vkcv::SamplerAddressMode::CLAMP_TO_EDGE
-    );
-
-	vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight).getHandle();
-
-	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
-
-	const vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
-
-	const std::vector<glm::vec3> instancePositions = {
-		glm::vec3( 0.f, -2.f, 0.f),
-		glm::vec3( 3.f,  0.f, 0.f),
-		glm::vec3(-3.f,  0.f, 0.f),
-		glm::vec3( 0.f,  2.f, 0.f),
-		glm::vec3( 0.f, -5.f, 0.f)
-	};
-
-	std::vector<glm::mat4> modelMatrices;
-	std::vector<vkcv::DrawcallInfo> drawcalls;
-	std::vector<vkcv::DrawcallInfo> shadowDrawcalls;
-	for (const auto& position : instancePositions) {
-		modelMatrices.push_back(glm::translate(glm::mat4(1.f), position));
-		drawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, { descriptorUsage },1));
-		shadowDrawcalls.push_back(vkcv::DrawcallInfo(loadedMesh, {},1));
-	}
-
-	modelMatrices.back() *= glm::scale(glm::mat4(1.f), glm::vec3(10.f, 1.f, 10.f));
-
-	std::vector<std::array<glm::mat4, 2>> mainPassMatrices;
-	std::vector<glm::mat4> mvpLight;
-
-	vkcv::ShaderProgram shadowShader;
-	shadowShader.addShader(vkcv::ShaderStage::VERTEX, "resources/shaders/shadow_vert.spv");
-	shadowShader.addShader(vkcv::ShaderStage::FRAGMENT, "resources/shaders/shadow_frag.spv");
-
-	const vk::Format shadowMapFormat = vk::Format::eD16Unorm;
-	const std::vector<vkcv::AttachmentDescription> shadowAttachments = {
-		vkcv::AttachmentDescription(vkcv::AttachmentOperation::STORE, vkcv::AttachmentOperation::CLEAR, shadowMapFormat)
-	};
-	const vkcv::PassConfig shadowPassConfig(shadowAttachments);
-	const vkcv::PassHandle shadowPass = core.createPass(shadowPassConfig);
-
-	const uint32_t shadowMapResolution = 1024;
-	const vkcv::Image shadowMap = core.createImage(shadowMapFormat, shadowMapResolution, shadowMapResolution, 1);
-	const vkcv::PipelineConfig shadowPipeConfig {
-		shadowShader, 
-		shadowMapResolution, 
-		shadowMapResolution, 
-		shadowPass,
-        firstMeshLayout,
-		{},
-		false
-	};
-	
-	const vkcv::PipelineHandle shadowPipe = core.createGraphicsPipeline(shadowPipeConfig);
-
-	struct LightInfo {
-		glm::vec3 direction;
-		float padding;
-		glm::mat4 lightMatrix;
-	};
-	LightInfo lightInfo;
-	vkcv::Buffer lightBuffer = core.createBuffer<LightInfo>(vkcv::BufferType::UNIFORM, sizeof(glm::vec3));
-
-	vkcv::DescriptorWrites setWrites;
-	setWrites.sampledImageWrites    = { 
-        vkcv::SampledImageDescriptorWrite(0, texture.getHandle()),
-        vkcv::SampledImageDescriptorWrite(3, shadowMap.getHandle()) };
-	setWrites.samplerWrites         = { 
-        vkcv::SamplerDescriptorWrite(1, sampler), 
-        vkcv::SamplerDescriptorWrite(4, shadowSampler) };
-    setWrites.uniformBufferWrites   = { vkcv::UniformBufferDescriptorWrite(2, lightBuffer.getHandle()) };
-	core.writeDescriptorSet(descriptorSet, setWrites);
-
-	auto start = std::chrono::system_clock::now();
-	const auto appStartTime = start;
-	while (window.isWindowOpen()) {
-		window.pollEvents();
-		
-		uint32_t swapchainWidth, swapchainHeight;
-		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
-			continue;
-		}
-		
-		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
-			depthBuffer = core.createImage(vk::Format::eD32Sfloat, swapchainWidth, swapchainHeight).getHandle();
-			
-			windowWidth = swapchainWidth;
-			windowHeight = swapchainHeight;
-		}
-		
-		auto end = std::chrono::system_clock::now();
-		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
-		
-		start = end;
-		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
-
-		auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - appStartTime);
-		
-		const float sunTheta = 0.001f * static_cast<float>(duration.count());
-		lightInfo.direction = glm::normalize(glm::vec3(std::cos(sunTheta), 1, std::sin(sunTheta)));
-
-		const float shadowProjectionSize = 5.f;
-		glm::mat4 projectionLight = glm::ortho(
-			-shadowProjectionSize,
-			shadowProjectionSize,
-			-shadowProjectionSize,
-			shadowProjectionSize,
-			-shadowProjectionSize,
-			shadowProjectionSize);
-		
-		glm::mat4 vulkanCorrectionMatrix(1.f);
-		vulkanCorrectionMatrix[2][2] = 0.5;
-		vulkanCorrectionMatrix[3][2] = 0.5;
-		projectionLight = vulkanCorrectionMatrix * projectionLight;
-
-		const glm::mat4 viewLight = glm::lookAt(glm::vec3(0), -lightInfo.direction, glm::vec3(0, -1, 0));
-
-		lightInfo.lightMatrix = projectionLight * viewLight;
-		lightBuffer.fill({ lightInfo });
-
-		const glm::mat4 viewProjectionCamera = cameraManager.getActiveCamera().getMVP();
-
-		mainPassMatrices.clear();
-		mvpLight.clear();
-		for (const auto& m : modelMatrices) {
-			mainPassMatrices.push_back({ viewProjectionCamera * m, m });
-			mvpLight.push_back(lightInfo.lightMatrix* m);
-		}
-
-		vkcv::PushConstantData pushConstantData((void*)mainPassMatrices.data(), 2 * sizeof(glm::mat4));
-		const std::vector<vkcv::ImageHandle> renderTargets = { swapchainInput, depthBuffer };
-
-		vkcv::PushConstantData shadowPushConstantData((void*)mvpLight.data(), sizeof(glm::mat4));
-
-		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
-
-		core.recordDrawcallsToCmdStream(
-			cmdStream,
-			shadowPass,
-			shadowPipe,
-			shadowPushConstantData,
-			shadowDrawcalls,
-			{ shadowMap.getHandle() });
-
-		core.prepareImageForSampling(cmdStream, shadowMap.getHandle());
-
-		core.recordDrawcallsToCmdStream(
-			cmdStream,
-            firstMeshPass,
-            firstMeshPipeline,
-			pushConstantData,
-			drawcalls,
-			renderTargets);
-		core.prepareSwapchainImageForPresent(cmdStream);
-		core.submitCommandStream(cmdStream);
-
-		core.endFrame();
-	}
-	
-	return 0;
-}
diff --git a/projects/first_mesh/CMakeLists.txt b/projects/first_mesh/CMakeLists.txt
index eb0f028db38707272f9fbcf61662633f2868eedc..6455e75d88eee276fb89b9f7a1b3462fcbc54da2 100644
--- a/projects/first_mesh/CMakeLists.txt
+++ b/projects/first_mesh/CMakeLists.txt
@@ -22,7 +22,7 @@ if(MSVC)
 endif()
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(first_mesh SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include})
+target_include_directories(first_mesh SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(first_mesh vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera)
+target_link_libraries(first_mesh vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler)
diff --git a/projects/first_mesh/resources/shaders/compile.bat b/projects/first_mesh/resources/shaders/compile.bat
deleted file mode 100644
index b4521235c40fe5fb163bab874560c2f219b7517f..0000000000000000000000000000000000000000
--- a/projects/first_mesh/resources/shaders/compile.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-%VULKAN_SDK%\Bin32\glslc.exe shader.vert -o vert.spv
-%VULKAN_SDK%\Bin32\glslc.exe shader.frag -o frag.spv
-pause
\ No newline at end of file
diff --git a/projects/first_mesh/resources/shaders/frag.spv b/projects/first_mesh/resources/shaders/frag.spv
deleted file mode 100644
index 087e4e22fb2fcec27d99b3ff2aa1a705fe755796..0000000000000000000000000000000000000000
Binary files a/projects/first_mesh/resources/shaders/frag.spv and /dev/null differ
diff --git a/projects/first_mesh/resources/shaders/vert.spv b/projects/first_mesh/resources/shaders/vert.spv
deleted file mode 100644
index 374c023e14b351eb43cbcda5951cbb8b3d6f96a1..0000000000000000000000000000000000000000
Binary files a/projects/first_mesh/resources/shaders/vert.spv and /dev/null differ
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index e7546fc3a143b3638cceb36869c519336ebec751..fc682ae1f8b3d1a174ff230c274b89093bc3325c 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -4,6 +4,7 @@
 #include <vkcv/camera/CameraManager.hpp>
 #include <chrono>
 #include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "First Mesh";
@@ -34,9 +35,8 @@ int main(int argc, const char** argv) {
 
 	if (result == 1) {
 		std::cout << "Mesh loading successful!" << std::endl;
-	}
-	else {
-		std::cout << "Mesh loading failed: " << result << std::endl;
+	} else {
+		std::cerr << "Mesh loading failed: " << result << std::endl;
 		return 1;
 	}
 
@@ -74,14 +74,23 @@ int main(int argc, const char** argv) {
 	vkcv::PassHandle firstMeshPass = core.createPass(firstMeshPassDefinition);
 
 	if (!firstMeshPass) {
-		std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+		std::cerr << "Error. Could not create renderpass. Exiting." << std::endl;
 		return EXIT_FAILURE;
 	}
 
-	vkcv::ShaderProgram firstMeshProgram{};
-    firstMeshProgram.addShader(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/vert.spv"));
-    firstMeshProgram.addShader(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/frag.spv"));
+	vkcv::ShaderProgram firstMeshProgram;
+	vkcv::shader::GLSLCompiler compiler;
+	
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"),
+					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		firstMeshProgram.addShader(shaderStage, path);
+	});
 	
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		firstMeshProgram.addShader(shaderStage, path);
+	});
+ 
 	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
 
 	
@@ -113,12 +122,15 @@ int main(int argc, const char** argv) {
 	vkcv::PipelineHandle firstMeshPipeline = core.createGraphicsPipeline(firstMeshPipelineConfig);
 	
 	if (!firstMeshPipeline) {
-		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		std::cerr << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+	
+	if (mesh.textures.empty()) {
+		std::cerr << "Error. No textures found. Exiting." << std::endl;
 		return EXIT_FAILURE;
 	}
 	
-	// FIXME There should be a test here to make sure there is at least 1
-	// texture in the mesh.
 	vkcv::asset::Texture &tex = mesh.textures[0];
 	vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, tex.w, tex.h);
 	texture.fill(tex.data.data());
@@ -154,14 +166,13 @@ int main(int argc, const char** argv) {
 
     vkcv::camera::CameraManager cameraManager(window);
     uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
-	uint32_t camIndex1 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
 	
 	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -3));
 
     auto start = std::chrono::system_clock::now();
     
 	while (window.isWindowOpen()) {
-        window.pollEvents();
+        vkcv::Window::pollEvents();
 		
 		if(window.getHeight() == 0 || window.getWidth() == 0)
 			continue;
@@ -185,7 +196,8 @@ int main(int argc, const char** argv) {
 		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
         glm::mat4 mvp = cameraManager.getActiveCamera().getMVP();
 
-		vkcv::PushConstantData pushConstantData((void*)&mvp, sizeof(glm::mat4));
+		vkcv::PushConstants pushConstants (sizeof(glm::mat4));
+		pushConstants.appendDrawcall(mvp);
 
 		const std::vector<vkcv::ImageHandle> renderTargets = { swapchainInput, depthBuffer };
 		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
@@ -194,7 +206,7 @@ int main(int argc, const char** argv) {
 			cmdStream,
 			firstMeshPass,
 			firstMeshPipeline,
-			pushConstantData,
+			pushConstants,
 			{ drawcall },
 			renderTargets);
 		core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/projects/first_scene/CMakeLists.txt b/projects/first_scene/CMakeLists.txt
index 8b90739750011a36b4c1d9e0bff7cba986074228..ba2f7b1a7ae4845a12b9701269361a0a3f8affb7 100644
--- a/projects/first_scene/CMakeLists.txt
+++ b/projects/first_scene/CMakeLists.txt
@@ -22,7 +22,7 @@ if(MSVC)
 endif()
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(first_scene SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include})
+target_include_directories(first_scene SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_scene_include} ${vkcv_shader_compiler_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(first_scene vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera)
+target_link_libraries(first_scene vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_scene vkcv_shader_compiler)
diff --git a/projects/first_scene/resources/Sponza/SponzaFloor.bin b/projects/first_scene/resources/Sponza/SponzaFloor.bin
new file mode 100644
index 0000000000000000000000000000000000000000..684251288f35070d2e7d244877fd844cc00ca632
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/SponzaFloor.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:678455aca641cb1f449aa1a5054a7cae132be81c2b333aac283053967da66df0
+size 512
diff --git a/projects/first_scene/resources/Sponza/SponzaFloor.gltf b/projects/first_scene/resources/Sponza/SponzaFloor.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..b45f1c55ef85f2aa1d4bff01df3d9625aa38c809
--- /dev/null
+++ b/projects/first_scene/resources/Sponza/SponzaFloor.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a6deb75441b1138b50a6b0eec05e60df276fe8fb6d58118fdfce2090b6fbe734
+size 3139
diff --git a/projects/first_scene/resources/shaders/compile.bat b/projects/first_scene/resources/shaders/compile.bat
deleted file mode 100644
index b4521235c40fe5fb163bab874560c2f219b7517f..0000000000000000000000000000000000000000
--- a/projects/first_scene/resources/shaders/compile.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-%VULKAN_SDK%\Bin32\glslc.exe shader.vert -o vert.spv
-%VULKAN_SDK%\Bin32\glslc.exe shader.frag -o frag.spv
-pause
\ No newline at end of file
diff --git a/projects/first_scene/resources/shaders/frag.spv b/projects/first_scene/resources/shaders/frag.spv
deleted file mode 100644
index 087e4e22fb2fcec27d99b3ff2aa1a705fe755796..0000000000000000000000000000000000000000
Binary files a/projects/first_scene/resources/shaders/frag.spv and /dev/null differ
diff --git a/projects/first_scene/resources/shaders/vert.spv b/projects/first_scene/resources/shaders/vert.spv
deleted file mode 100644
index 374c023e14b351eb43cbcda5951cbb8b3d6f96a1..0000000000000000000000000000000000000000
Binary files a/projects/first_scene/resources/shaders/vert.spv and /dev/null differ
diff --git a/projects/first_scene/src/main.cpp b/projects/first_scene/src/main.cpp
index 521818732f7a60eabe9f0c2c080c6d343a71b1d8..527eba8c3a1e020e14d92f5d305e2ddced936333 100644
--- a/projects/first_scene/src/main.cpp
+++ b/projects/first_scene/src/main.cpp
@@ -4,17 +4,8 @@
 #include <vkcv/camera/CameraManager.hpp>
 #include <chrono>
 #include <vkcv/asset/asset_loader.hpp>
-#include <vkcv/Logger.hpp>
-
-glm::mat4 arrayTo4x4Matrix(std::array<float,16> array){
-    glm::mat4 matrix;
-    for (int i = 0; i < 4; i++){
-        for (int j = 0; j < 4; j++){
-            matrix[i][j] = array[j * 4 + i];
-        }
-    }
-    return matrix;
-}
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/scene/Scene.hpp>
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "First Scene";
@@ -32,8 +23,8 @@ int main(int argc, const char** argv) {
 	vkcv::camera::CameraManager cameraManager(window);
 	uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
 	uint32_t camIndex1 = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
-
-	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -3));
+	
+	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(-8, 1, -0.5));
 	cameraManager.getCamera(camIndex0).setNearFar(0.1f, 30.0f);
 	
 	cameraManager.getCamera(camIndex1).setNearFar(0.1f, 30.0f);
@@ -46,66 +37,10 @@ int main(int argc, const char** argv) {
 		{},
 		{ "VK_KHR_swapchain" }
 	);
-
-	vkcv::asset::Scene scene;
-
-	const char* path = argc > 1 ? argv[1] : "resources/Sponza/Sponza.gltf";
-	int result = vkcv::asset::loadScene(path, scene);
-
-	if (result == 1) {
-		std::cout << "Mesh loading successful!" << std::endl;
-	}
-	else {
-		std::cout << "Mesh loading failed: " << result << std::endl;
-		return 1;
-	}
-
-	assert(!scene.vertexGroups.empty());
-	std::vector<std::vector<uint8_t>> vBuffers;
-	std::vector<std::vector<uint8_t>> iBuffers;
-
-	std::vector<vkcv::VertexBufferBinding> vBufferBindings;
-	std::vector<std::vector<vkcv::VertexBufferBinding>> vertexBufferBindings;
-	std::vector<vkcv::asset::VertexAttribute> vAttributes;
-
-	for (int i = 0; i < scene.vertexGroups.size(); i++) {
-
-		vBuffers.push_back(scene.vertexGroups[i].vertexBuffer.data);
-		iBuffers.push_back(scene.vertexGroups[i].indexBuffer.data);
-
-		auto& attributes = scene.vertexGroups[i].vertexBuffer.attributes;
-
-		std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
-			return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
-			});
-	}
-
-	std::vector<vkcv::Buffer<uint8_t>> vertexBuffers;
-	for (const vkcv::asset::VertexGroup& group : scene.vertexGroups) {
-		vertexBuffers.push_back(core.createBuffer<uint8_t>(
-			vkcv::BufferType::VERTEX,
-			group.vertexBuffer.data.size()));
-		vertexBuffers.back().fill(group.vertexBuffer.data);
-	}
-
-	std::vector<vkcv::Buffer<uint8_t>> indexBuffers;
-	for (const auto& dataBuffer : iBuffers) {
-		indexBuffers.push_back(core.createBuffer<uint8_t>(
-			vkcv::BufferType::INDEX,
-			dataBuffer.size()));
-		indexBuffers.back().fill(dataBuffer);
-	}
-
-	int vertexBufferIndex = 0;
-	for (const auto& vertexGroup : scene.vertexGroups) {
-		for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
-			vAttributes.push_back(attribute);
-			vBufferBindings.push_back(vkcv::VertexBufferBinding(attribute.offset, vertexBuffers[vertexBufferIndex].getVulkanHandle()));
-		}
-		vertexBufferBindings.push_back(vBufferBindings);
-		vBufferBindings.clear();
-		vertexBufferIndex++;
-	}
+	
+	vkcv::scene::Scene scene = vkcv::scene::Scene::load(core, std::filesystem::path(
+			argc > 1 ? argv[1] : "resources/Sponza/Sponza.gltf"
+	));
 
 	const vkcv::AttachmentDescription present_color_attachment(
 		vkcv::AttachmentOperation::STORE,
@@ -127,9 +62,18 @@ int main(int argc, const char** argv) {
 		return EXIT_FAILURE;
 	}
 
-	vkcv::ShaderProgram sceneShaderProgram{};
-	sceneShaderProgram.addShader(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/vert.spv"));
-	sceneShaderProgram.addShader(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/frag.spv"));
+	vkcv::ShaderProgram sceneShaderProgram;
+	vkcv::shader::GLSLCompiler compiler;
+	
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"),
+					 [&sceneShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		sceneShaderProgram.addShader(shaderStage, path);
+	});
+	
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+					 [&sceneShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		sceneShaderProgram.addShader(shaderStage, path);
+	});
 
 	const std::vector<vkcv::VertexAttachment> vertexAttachments = sceneShaderProgram.getVertexAttachments();
 	std::vector<vkcv::VertexBinding> bindings;
@@ -138,41 +82,8 @@ int main(int argc, const char** argv) {
 	}
 
 	const vkcv::VertexLayout sceneLayout(bindings);
-
-	uint32_t setID = 0;
-
-	std::vector<vkcv::DescriptorBinding> descriptorBindings = { sceneShaderProgram.getReflectedDescriptors()[setID] };
-
-	vkcv::SamplerHandle sampler = core.createSampler(
-		vkcv::SamplerFilterType::LINEAR,
-		vkcv::SamplerFilterType::LINEAR,
-		vkcv::SamplerMipmapMode::LINEAR,
-		vkcv::SamplerAddressMode::REPEAT
-	);
-
-	std::vector<vkcv::Image> sceneImages;
-	std::vector<vkcv::DescriptorSetHandle> descriptorSets;
-	for (const auto& vertexGroup : scene.vertexGroups) {
-		descriptorSets.push_back(core.createDescriptorSet(descriptorBindings));
-
-		const auto& material = scene.materials[vertexGroup.materialIndex];
-
-		int baseColorIndex = material.baseColor;
-		if (baseColorIndex < 0) {
-			vkcv_log(vkcv::LogLevel::WARNING, "Material lacks base color");
-			baseColorIndex = 0;
-		}
-
-		vkcv::asset::Texture& sceneTexture = scene.textures[baseColorIndex];
-
-		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, sceneTexture.w, sceneTexture.h));
-		sceneImages.back().fill(sceneTexture.data.data());
-
-		vkcv::DescriptorWrites setWrites;
-		setWrites.sampledImageWrites = { vkcv::SampledImageDescriptorWrite(0, sceneImages.back().getHandle()) };
-		setWrites.samplerWrites = { vkcv::SamplerDescriptorWrite(1, sampler) };
-		core.writeDescriptorSet(descriptorSets.back(), setWrites);
-	}
+	
+	const auto& material0 = scene.getMaterial(0);
 
 	const vkcv::PipelineConfig scenePipelineDefsinition{
 		sceneShaderProgram,
@@ -180,7 +91,7 @@ int main(int argc, const char** argv) {
 		UINT32_MAX,
 		scenePass,
 		{sceneLayout},
-		{ core.getDescriptorSet(descriptorSets[0]).layout },
+		{ core.getDescriptorSet(material0.getDescriptorSet()).layout },
 		true };
 	vkcv::PipelineHandle scenePipeline = core.createGraphicsPipeline(scenePipelineDefsinition);
 	
@@ -192,26 +103,7 @@ int main(int argc, const char** argv) {
 	vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight).getHandle();
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
-
-    std::vector<vkcv::DrawcallInfo> drawcalls;
-	for(int i = 0; i < scene.vertexGroups.size(); i++){
-        vkcv::Mesh renderMesh(vertexBufferBindings[i], indexBuffers[i].getVulkanHandle(), scene.vertexGroups[i].numIndices);
-
-        vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSets[i]).vulkanHandle);
-
-	    drawcalls.push_back(vkcv::DrawcallInfo(renderMesh, {descriptorUsage},1));
-	}
-
-	std::vector<glm::mat4> modelMatrices;
-	modelMatrices.resize(scene.vertexGroups.size(), glm::mat4(1.f));
-	for (const auto &mesh : scene.meshes) {
-		const glm::mat4 m = arrayTo4x4Matrix(mesh.modelMatrix);
-		for (const auto &vertexGroupIndex : mesh.vertexGroups) {
-			modelMatrices[vertexGroupIndex] = m;
-		}
-	}
-	std::vector<glm::mat4> mvp;
-
+	
 	auto start = std::chrono::system_clock::now();
 	while (window.isWindowOpen()) {
         vkcv::Window::pollEvents();
@@ -236,25 +128,24 @@ int main(int argc, const char** argv) {
 		
 		start = end;
 		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
-		glm::mat4 vp = cameraManager.getActiveCamera().getMVP();
-
-		mvp.clear();
-        for (const auto& m : modelMatrices) {
-            mvp.push_back(vp * m);
-        }
-
-		vkcv::PushConstantData pushConstantData((void*)mvp.data(), sizeof(glm::mat4));
 
 		const std::vector<vkcv::ImageHandle> renderTargets = { swapchainInput, depthBuffer };
 		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
 
-		core.recordDrawcallsToCmdStream(
-			cmdStream,
-			scenePass,
-			scenePipeline,
-			pushConstantData,
-			drawcalls,
-			renderTargets);
+		auto recordMesh = [](const glm::mat4& MVP, const glm::mat4& M,
+							 vkcv::PushConstants &pushConstants,
+							 vkcv::DrawcallInfo& drawcallInfo) {
+			pushConstants.appendDrawcall(MVP);
+		};
+		
+		scene.recordDrawcalls(cmdStream,
+							  cameraManager.getActiveCamera(),
+							  scenePass,
+							  scenePipeline,
+							  sizeof(glm::mat4),
+							  recordMesh,
+							  renderTargets);
+		
 		core.prepareSwapchainImageForPresent(cmdStream);
 		core.submitCommandStream(cmdStream);
 		core.endFrame();
diff --git a/projects/first_triangle/shaders/comp.spv b/projects/first_triangle/shaders/comp.spv
deleted file mode 100644
index b414e36b2bea66dab00746298e536d029091e0fd..0000000000000000000000000000000000000000
Binary files a/projects/first_triangle/shaders/comp.spv and /dev/null differ
diff --git a/projects/first_triangle/shaders/compile.bat b/projects/first_triangle/shaders/compile.bat
deleted file mode 100644
index 17743a7c49cdfc6e091c43a42a0adb755a731682..0000000000000000000000000000000000000000
--- a/projects/first_triangle/shaders/compile.bat
+++ /dev/null
@@ -1,4 +0,0 @@
-%VULKAN_SDK%\Bin32\glslc.exe shader.vert -o vert.spv
-%VULKAN_SDK%\Bin32\glslc.exe shader.frag -o frag.spv
-%VULKAN_SDK%\Bin32\glslc.exe shader.comp -o comp.spv
-pause
\ No newline at end of file
diff --git a/projects/first_triangle/shaders/frag.spv b/projects/first_triangle/shaders/frag.spv
deleted file mode 100644
index cb13e606fc0041e24ff6a63c0ec7dcca466732aa..0000000000000000000000000000000000000000
Binary files a/projects/first_triangle/shaders/frag.spv and /dev/null differ
diff --git a/projects/first_triangle/shaders/shader.comp b/projects/first_triangle/shaders/shader.comp
deleted file mode 100644
index fad6cd0815f2f09bf92dcc3171e2e3723f5466df..0000000000000000000000000000000000000000
--- a/projects/first_triangle/shaders/shader.comp
+++ /dev/null
@@ -1,25 +0,0 @@
-#version 440
-
-layout(std430, binding = 0) buffer testBuffer
-{ 
-    float test1[10];
-    float test2[10];
-    float test3[10];
-};
-
-layout( push_constant ) uniform constants{
-    float pushConstant;
-};
-
-layout(local_size_x = 5) in;
-
-void main(){
-
-    if(gl_GlobalInvocationID.x >= 10){
-        return;
-    }
-
-    test1[gl_GlobalInvocationID.x] = gl_GlobalInvocationID.x;
-    test2[gl_GlobalInvocationID.x] = 69;  // nice!
-    test3[gl_GlobalInvocationID.x] = pushConstant;    
-}
\ No newline at end of file
diff --git a/projects/first_triangle/shaders/vert.spv b/projects/first_triangle/shaders/vert.spv
deleted file mode 100644
index 03af5758ffff1b5b6505fe98b02044849026832d..0000000000000000000000000000000000000000
Binary files a/projects/first_triangle/shaders/vert.spv and /dev/null differ
diff --git a/projects/first_triangle/src/main.cpp b/projects/first_triangle/src/main.cpp
index 5bdd55a263f4d81d8f424c056d7d6c0b54ccb1ca..3598da5f579b608d2c29f1f6fea0b0e25a560336 100644
--- a/projects/first_triangle/src/main.cpp
+++ b/projects/first_triangle/src/main.cpp
@@ -2,10 +2,8 @@
 #include <vkcv/Core.hpp>
 #include <GLFW/glfw3.h>
 #include <vkcv/camera/CameraManager.hpp>
-#include <chrono>
-
 #include <vkcv/shader/GLSLCompiler.hpp>
-#include <vkcv/gui/GUI.hpp>
+#include <chrono>
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "First Triangle";
@@ -27,57 +25,11 @@ int main(int argc, const char** argv) {
 		{},
 		{ "VK_KHR_swapchain" }
 	);
-	
-	vkcv::gui::GUI gui (core, window);
-
-	const auto& context = core.getContext();
-	const vk::Instance& instance = context.getInstance();
-	const vk::PhysicalDevice& physicalDevice = context.getPhysicalDevice();
-	const vk::Device& device = context.getDevice();
-
-	struct vec3 {
-		float x, y, z;
-	};
-
-	const size_t n = 5027;
 
-	auto testBuffer = core.createBuffer<vec3>(vkcv::BufferType::VERTEX, n, vkcv::BufferMemoryType::DEVICE_LOCAL);
-	vec3 vec_data[n];
-
-	for (size_t i = 0; i < n; i++) {
-		vec_data[i] = { 42, static_cast<float>(i), 7 };
-	}
-
-	testBuffer.fill(vec_data);
-
-	auto triangleIndexBuffer = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, n, vkcv::BufferMemoryType::DEVICE_LOCAL);
+	auto triangleIndexBuffer = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 3, vkcv::BufferMemoryType::DEVICE_LOCAL);
 	uint16_t indices[3] = { 0, 1, 2 };
 	triangleIndexBuffer.fill(&indices[0], sizeof(indices));
 
-	/*vec3* m = buffer.map();
-	m[0] = { 0, 0, 0 };
-	m[1] = { 0, 0, 0 };
-	m[2] = { 0, 0, 0 };
-	buffer.unmap();*/
-
-	vkcv::SamplerHandle sampler = core.createSampler(
-		vkcv::SamplerFilterType::NEAREST,
-		vkcv::SamplerFilterType::NEAREST,
-		vkcv::SamplerMipmapMode::NEAREST,
-		vkcv::SamplerAddressMode::REPEAT
-	);
-
-	std::cout << "Physical device: " << physicalDevice.getProperties().deviceName << std::endl;
-
-	switch (physicalDevice.getProperties().vendorID) {
-	case 0x1002: std::cout << "Running AMD huh? You like underdogs, are you a Linux user?" << std::endl; break;
-	case 0x10DE: std::cout << "An NVidia GPU, how predictable..." << std::endl; break;
-	case 0x8086: std::cout << "Poor child, running on an Intel GPU, probably integrated..."
-		"or perhaps you are from the future with a dedicated one?" << std::endl; break;
-	case 0x13B5: std::cout << "ARM? What the hell are you running on, next thing I know you're trying to run Vulkan on a leg..." << std::endl; break;
-	default: std::cout << "Unknown GPU vendor?! Either you're on an exotic system or your driver is broken..." << std::endl;
-	}
-
 	// an example attachment for passes that output to the window
 	const vkcv::AttachmentDescription present_color_attachment(
 		vkcv::AttachmentOperation::STORE,
@@ -93,7 +45,7 @@ int main(int argc, const char** argv) {
 		return EXIT_FAILURE;
 	}
 
-	vkcv::ShaderProgram triangleShaderProgram{};
+	vkcv::ShaderProgram triangleShaderProgram;
 	vkcv::shader::GLSLCompiler compiler;
 	
 	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("shaders/shader.vert"),
@@ -123,49 +75,9 @@ int main(int argc, const char** argv) {
 		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
 		return EXIT_FAILURE;
 	}
-
-	// Compute Pipeline
-	vkcv::ShaderProgram computeShaderProgram{};
-	computeShaderProgram.addShader(vkcv::ShaderStage::COMPUTE, std::filesystem::path("shaders/comp.spv"));
-
-	// take care, assuming shader has exactly one descriptor set
-	vkcv::DescriptorSetHandle computeDescriptorSet = core.createDescriptorSet(computeShaderProgram.getReflectedDescriptors()[0]);
-
-	vkcv::PipelineHandle computePipeline = core.createComputePipeline(
-		computeShaderProgram, 
-		{ core.getDescriptorSet(computeDescriptorSet).layout });
-
-	struct ComputeTestBuffer {
-		float test1[10];
-		float test2[10];
-		float test3[10];
-	};
-
-	vkcv::Buffer computeTestBuffer = core.createBuffer<ComputeTestBuffer>(vkcv::BufferType::STORAGE, 1);
-
-	vkcv::DescriptorWrites computeDescriptorWrites;
-	computeDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, computeTestBuffer.getHandle()) };
-	core.writeDescriptorSet(computeDescriptorSet, computeDescriptorWrites);
-
-	/*
-	 * BufferHandle triangleVertices = core.createBuffer(vertices);
-	 * BufferHandle triangleIndices = core.createBuffer(indices);
-	 *
-	 * // triangle Model creation goes here
-	 *
-	 *
-	 * // attachment creation goes here
-	 * PassHandle trianglePass = core.CreatePass(presentationPass);
-	 *
-	 * // shader creation goes here
-	 * // material creation goes here
-	 *
-	 * PipelineHandle trianglePipeline = core.CreatePipeline(trianglePipeline);
-	 */
+	
 	auto start = std::chrono::system_clock::now();
 
-	vkcv::ImageHandle swapchainImageHandle = vkcv::ImageHandle::createSwapchainImageHandle();
-
 	const vkcv::Mesh renderMesh({}, triangleIndexBuffer.getVulkanHandle(), 3);
 	vkcv::DrawcallInfo drawcall(renderMesh, {},1);
 
@@ -181,7 +93,7 @@ int main(int argc, const char** argv) {
 
 	while (window.isWindowOpen())
 	{
-        window.pollEvents();
+        vkcv::Window::pollEvents();
 
 		uint32_t swapchainWidth, swapchainHeight; // No resizing = No problem
 		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
@@ -195,37 +107,21 @@ int main(int argc, const char** argv) {
 		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
         glm::mat4 mvp = cameraManager.getActiveCamera().getMVP();
 
-		vkcv::PushConstantData pushConstantData((void*)&mvp, sizeof(glm::mat4));
+		vkcv::PushConstants pushConstants (sizeof(glm::mat4));
+		pushConstants.appendDrawcall(mvp);
+		
 		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
 
 		core.recordDrawcallsToCmdStream(
 			cmdStream,
 			trianglePass,
 			trianglePipeline,
-			pushConstantData,
+			pushConstants,
 			{ drawcall },
 			{ swapchainInput });
 
-		const uint32_t dispatchSize[3] = { 2, 1, 1 };
-		const float theMeaningOfLife = 42;
-
-		core.recordComputeDispatchToCmdStream(
-			cmdStream,
-			computePipeline,
-			dispatchSize,
-			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(computeDescriptorSet).vulkanHandle) },
-			vkcv::PushConstantData((void*)&theMeaningOfLife, sizeof(theMeaningOfLife)));
-
 		core.prepareSwapchainImageForPresent(cmdStream);
 		core.submitCommandStream(cmdStream);
-		
-		gui.beginGUI();
-		
-		ImGui::Begin("Hello world");
-		ImGui::Text("This is a test!");
-		ImGui::End();
-		
-		gui.endGUI();
 	    
 	    core.endFrame();
 	}
diff --git a/projects/indirect_dispatch/.gitignore b/projects/indirect_dispatch/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..5f18d9c205e538dabeb0282640bede0359edc33d
--- /dev/null
+++ b/projects/indirect_dispatch/.gitignore
@@ -0,0 +1 @@
+indirect_dispatch
diff --git a/projects/indirect_dispatch/CMakeLists.txt b/projects/indirect_dispatch/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7bc86cbc8470352f13bbfcc62f793b0a99d92884
--- /dev/null
+++ b/projects/indirect_dispatch/CMakeLists.txt
@@ -0,0 +1,44 @@
+cmake_minimum_required(VERSION 3.16)
+project(indirect_dispatch)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# this should fix the execution path to load local files from the project
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+# adding source files to the project
+add_executable(indirect_dispatch src/main.cpp)
+
+target_sources(indirect_dispatch PRIVATE
+    src/App.hpp
+    src/App.cpp
+
+    src/AppConfig.hpp
+    src/MotionBlurConfig.hpp
+    
+    src/AppSetup.hpp
+    src/AppSetup.cpp
+    
+    src/MotionBlur.hpp
+    src/MotionBlur.cpp
+    
+    src/MotionBlurSetup.hpp
+    src/MotionBlurSetup.cpp)
+    
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(indirect_dispatch PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(indirect_dispatch PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+
+	# in addition to setting the output directory, the working directory has to be set
+	# by default visual studio sets the working directory to the build directory, when using the debugger
+	set_target_properties(indirect_dispatch PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(indirect_dispatch SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(indirect_dispatch vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_testing vkcv_camera vkcv_shader_compiler vkcv_gui)
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/models/cube.bin b/projects/indirect_dispatch/resources/models/cube.bin
new file mode 100644
index 0000000000000000000000000000000000000000..728d38cd39cd10c30a93c15eef021cb0cf7dda74
--- /dev/null
+++ b/projects/indirect_dispatch/resources/models/cube.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ccc59e0be3552b4347457bc935d8e548b52f12ca91716a0f0dc37d5bac65f123
+size 840
diff --git a/projects/indirect_dispatch/resources/models/cube.gltf b/projects/indirect_dispatch/resources/models/cube.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..ef975c326c71ec1a2fa650a422989534f1c32191
--- /dev/null
+++ b/projects/indirect_dispatch/resources/models/cube.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0072448af64bdebffe8eec5a7f32f110579b1a256cd97438bf227e4cc4a87328
+size 2571
diff --git a/projects/indirect_dispatch/resources/models/grid.png b/projects/indirect_dispatch/resources/models/grid.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f40eee62f7f9dba3dc156ff6a3653ea2e7f5391
--- /dev/null
+++ b/projects/indirect_dispatch/resources/models/grid.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a11c33e4935d93723ab11f597f2aca1ca1ff84af66f2e2d10a01580eb0b7831a
+size 40135
diff --git a/projects/indirect_dispatch/resources/models/ground.bin b/projects/indirect_dispatch/resources/models/ground.bin
new file mode 100644
index 0000000000000000000000000000000000000000..e29e4f18552def1ac64c167d994be959f82e35c7
--- /dev/null
+++ b/projects/indirect_dispatch/resources/models/ground.bin
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f8e20cd1c62da3111536283517b63a149f258ea82b1dff8ddafdb79020065b7c
+size 140
diff --git a/projects/indirect_dispatch/resources/models/ground.gltf b/projects/indirect_dispatch/resources/models/ground.gltf
new file mode 100644
index 0000000000000000000000000000000000000000..6935d3e21a06da1629087c9b0b7f957c57feaf6e
--- /dev/null
+++ b/projects/indirect_dispatch/resources/models/ground.gltf
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0a12b8d7cca8110d4ffa9bc4a2223286d1ccfd9c087739a75294e0a3fbfb65c5
+size 2840
diff --git a/projects/indirect_dispatch/resources/shaders/gammaCorrection.comp b/projects/indirect_dispatch/resources/shaders/gammaCorrection.comp
new file mode 100644
index 0000000000000000000000000000000000000000..7a6e129d7f8658d3ea424d35b809a3384d12bccc
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/gammaCorrection.comp
@@ -0,0 +1,24 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+layout(set=0, binding=0)        uniform texture2D   inTexture;
+layout(set=0, binding=1)        uniform sampler     textureSampler;
+layout(set=0, binding=2, rgba8) uniform image2D     outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+
+    ivec2 outImageRes = imageSize(outImage);
+    ivec2 coord       = ivec2(gl_GlobalInvocationID.xy);
+
+    if(any(greaterThanEqual(coord, outImageRes)))
+        return;
+   
+    vec2 uv             = vec2(coord) / outImageRes;
+    vec3 linearColor    = texture(sampler2D(inTexture, textureSampler), uv).rgb;
+    
+    vec3 gammaCorrected = pow(linearColor, vec3(1 / 2.2));
+
+    imageStore(outImage, coord, vec4(gammaCorrected, 0.f));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/mesh.frag b/projects/indirect_dispatch/resources/shaders/mesh.frag
new file mode 100644
index 0000000000000000000000000000000000000000..531c9cbf8b5e097af618d2ca639821a62a30611d
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/mesh.frag
@@ -0,0 +1,17 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 passNormal;
+layout(location = 1) in vec2 passUV;
+
+layout(location = 0) out vec3 outColor;
+
+layout(set=0, binding=0)    uniform texture2D   albedoTexture;
+layout(set=0, binding=1)    uniform sampler     textureSampler;
+
+void main()	{
+    vec3    albedo  = texture(sampler2D(albedoTexture, textureSampler), passUV).rgb;
+    vec3    N       = normalize(passNormal);
+    float   light   = max(N.y * 0.5 + 0.5, 0);
+    outColor        = light * albedo;
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/shader.vert b/projects/indirect_dispatch/resources/shaders/mesh.vert
similarity index 68%
rename from projects/bloom/resources/shaders/shader.vert
rename to projects/indirect_dispatch/resources/shaders/mesh.vert
index 926f86af2860cb57c44d2d5ee78712b6ae155e5c..734fd63cdee66e5fbf61cc427ca21fae18a31d82 100644
--- a/projects/bloom/resources/shaders/shader.vert
+++ b/projects/indirect_dispatch/resources/shaders/mesh.vert
@@ -7,7 +7,6 @@ layout(location = 2) in vec2 inUV;
 
 layout(location = 0) out vec3 passNormal;
 layout(location = 1) out vec2 passUV;
-layout(location = 2) out vec3 passPos;
 
 layout( push_constant ) uniform constants{
     mat4 mvp;
@@ -16,7 +15,6 @@ layout( push_constant ) uniform constants{
 
 void main()	{
 	gl_Position = mvp * vec4(inPosition, 1.0);
-	passNormal  = mat3(model) * inNormal;    // assuming no weird stuff like shearing or non-uniform scaling
+	passNormal  = (model * vec4(inNormal, 0)).xyz;
     passUV      = inUV;
-    passPos     = (model * vec4(inPosition, 1)).xyz;
 }
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlur.comp b/projects/indirect_dispatch/resources/shaders/motionBlur.comp
new file mode 100644
index 0000000000000000000000000000000000000000..091c21aa7ddfe9db1780aa64adc77fd5457a3843
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlur.comp
@@ -0,0 +1,194 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlur.inc"
+#include "motionBlurConfig.inc"
+#include "motionBlurWorkTile.inc"
+
+layout(set=0, binding=0)                    uniform texture2D   inColor;
+layout(set=0, binding=1)                    uniform texture2D   inDepth;
+layout(set=0, binding=2)                    uniform texture2D   inMotionFullRes;
+layout(set=0, binding=3)                    uniform texture2D   inMotionNeighbourhoodMax;  
+layout(set=0, binding=4)                    uniform sampler     nearestSampler;
+layout(set=0, binding=5, r11f_g11f_b10f)    uniform image2D     outImage;
+
+layout(set=0, binding=6) buffer WorkTileBuffer {
+    WorkTiles workTiles;
+};
+
+layout(local_size_x = motionTileSize, local_size_y = motionTileSize, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    // computed from delta time and shutter speed
+    float motionScaleFactor;
+    // camera planes are needed to linearize depth
+    float cameraNearPlane;
+    float cameraFarPlane;
+    float motionTileOffsetLength;
+};
+
+float linearizeDepth(float depth, float near, float far){
+    return near * far / (far + depth * (near - far));
+}
+
+struct SampleData{
+    vec3    color;
+    float   depthLinear;
+    vec2    coordinate;
+    vec2    motion;
+    float   velocityPixels;
+};
+
+struct PointSpreadCompare{
+    float foreground;
+    float background;
+};
+
+// results in range [0, 1]
+// computes if the sample pixel in the foreground would blur over the main pixel and if the sample pixel in the background would be part of the main pixel background
+// contribution depends on if the distance between pixels is smaller than it's velocity
+// note that compared to the constant falloff used in McGuire's papers this function from Jimenez is constant until the last pixel
+// this is important for the later gradient computation
+PointSpreadCompare samplePointSpreadCompare(SampleData mainPixel, SampleData samplePixel){
+    
+    float sampleOffset = distance(mainPixel.coordinate, samplePixel.coordinate);
+    
+    PointSpreadCompare pointSpread;
+    pointSpread.foreground = clamp(1 - sampleOffset + samplePixel.velocityPixels, 0, 1);
+    pointSpread.background = clamp(1 - sampleOffset +   mainPixel.velocityPixels, 0, 1);
+    
+    return pointSpread;
+}
+
+struct DepthClassification{
+    float foreground;
+    float background;
+};
+
+// classifies depthSample compared to depthMain in regards to being in the fore- or background
+// the range is [0, 1] and sums to 1
+DepthClassification sampleDepthClassification(SampleData mainPixel, SampleData samplePixel){
+    
+    const float softDepthExtent = 0.1;
+    
+    DepthClassification classification;
+    // only the sign is different, so the latter term will cancel out on addition, so only two times 0.5 remains which sums to one
+    classification.foreground = clamp(0.5 + (mainPixel.depthLinear - samplePixel.depthLinear) / softDepthExtent, 0, 1);
+    classification.background = clamp(0.5 - (mainPixel.depthLinear - samplePixel.depthLinear) / softDepthExtent, 0, 1);
+    return classification;
+}
+
+// reconstruction filter and helper functions from "Next Generation Post Processing in Call of Duty Advanced Warfare", Jimenez
+// returns value in range [0, 1]
+float computeSampleWeigth(SampleData mainPixel, SampleData samplePixel){
+    
+    PointSpreadCompare  pointSpread         = samplePointSpreadCompare( mainPixel, samplePixel);
+    DepthClassification depthClassification = sampleDepthClassification(mainPixel, samplePixel);
+    
+    return 
+        depthClassification.foreground * pointSpread.foreground + 
+        depthClassification.background * pointSpread.background;
+}
+
+SampleData loadSampleData(vec2 uv){
+    
+    SampleData data;
+    data.color          = texture(sampler2D(inColor, nearestSampler), uv).rgb;
+    data.coordinate     = ivec2(uv * imageSize(outImage)); 
+    data.motion         = processMotionVector(texture(sampler2D(inMotionFullRes, nearestSampler), uv).rg, motionScaleFactor, imageSize(outImage));
+    data.velocityPixels = length(data.motion * imageSize(outImage));
+    data.depthLinear    = texture(sampler2D(inDepth, nearestSampler), uv).r;
+    data.depthLinear    = linearizeDepth(data.depthLinear, cameraNearPlane, cameraFarPlane);
+    
+    return data;
+}
+
+void main(){
+
+    uint    tileIndex       = gl_WorkGroupID.x;
+    ivec2   tileCoordinates = workTiles.tileXY[tileIndex];
+    ivec2   coord           = ivec2(tileCoordinates * motionTileSize + gl_LocalInvocationID.xy);
+
+    if(any(greaterThanEqual(coord, imageSize(outImage))))
+        return;
+   
+    ivec2   textureRes  = textureSize(sampler2D(inColor, nearestSampler), 0);
+    vec2    uv          = vec2(coord + 0.5) / textureRes;   // + 0.5 to shift uv into pixel center
+    
+    // the motion tile lookup is jittered, so the hard edges in the blur are replaced by noise
+    // dither is shifted, so it does not line up with motion tiles
+    float   motionOffset            = motionTileOffsetLength * (dither(coord + ivec2(ditherSize / 2)) * 2 - 1);
+    vec2    motionNeighbourhoodMax  = processMotionVector(texelFetch(sampler2D(inMotionNeighbourhoodMax, nearestSampler), ivec2(coord + motionOffset) / motionTileSize, 0).rg, motionScaleFactor, imageSize(outImage));
+    
+    SampleData mainPixel = loadSampleData(uv);
+    
+    // early out on movement less than half a pixel
+    if(length(motionNeighbourhoodMax * imageSize(outImage)) <= 0.5){
+        imageStore(outImage, coord, vec4(mainPixel.color, 0.f));
+        return;
+    }
+    
+    vec3    color           = vec3(0);
+    float   weightSum       = 0;      
+    
+    // clamping start and end points avoids artifacts at image borders
+    // the sampler clamps the sample uvs anyways, but without clamping here, many samples can be stuck at the border
+    vec2 uvStart    = clamp(uv - motionNeighbourhoodMax, 0, 1);
+    vec2 uvEnd      = clamp(uv + motionNeighbourhoodMax, 0, 1);
+    
+    // samples are placed evenly, but the entire filter is jittered
+    // dither returns either 0 or 1
+    // the sampleUV code expects an offset in range [-0.5, 0.5], so the dither is rescaled to a binary -0.25/0.25
+    float random = dither(coord) * 0.5 - 0.25;
+    
+    const int sampleCountHalf = 8; 
+    
+    // two samples are processed at a time to allow for mirrored background reconstruction
+    for(int i = 0; i < sampleCountHalf; i++){
+        
+        vec2 sampleUV1 = mix(uv, uvEnd,     (i + random + 1) / float(sampleCountHalf + 1));
+        vec2 sampleUV2 = mix(uv, uvStart,   (i + random + 1) / float(sampleCountHalf + 1));
+        
+        SampleData sample1 = loadSampleData(sampleUV1);
+        SampleData sample2 = loadSampleData(sampleUV2);
+        
+        float weight1 = computeSampleWeigth(mainPixel, sample1);
+        float weight2 = computeSampleWeigth(mainPixel, sample2);
+
+        bool mirroredBackgroundReconstruction = true;
+        if(mirroredBackgroundReconstruction){
+            // see Jimenez paper for details and comparison
+            // problem is that in the foreground the background is reconstructed, which is blurry
+            // in the background the background is obviously known, so it is sharper
+            // at the border between fore- and background this causes a discontinuity
+            // to fix this the weights are mirrored on this border, effectively reconstructing the background, even though it is known
+            
+            // these bools check if sample1 is an affected background pixel (further away and slower moving than sample2)
+            bool inBackground = sample1.depthLinear     > sample2.depthLinear;
+            bool blurredOver  = sample1.velocityPixels  < sample2.velocityPixels;
+            
+            // this mirrors the weights depending on the results:
+            // if both conditions are true,   then weight2 is mirrored to weight1
+            // if both conditions are false,  then weight1 is mirrored to weight2, as sample2 is an affected background pixel
+            // if only one condition is true, then the weights are kept as is
+            weight1 = inBackground && blurredOver ? weight2 : weight1;
+            weight2 = inBackground || blurredOver ? weight2 : weight1;
+        }
+        
+        weightSum   += weight1;
+        weightSum   += weight2;
+        
+        color       += sample1.color * weight1;
+        color       += sample2.color * weight2;
+    }
+    
+    // normalize color and weight
+    weightSum   /= sampleCountHalf * 2;
+    color       /= sampleCountHalf * 2;
+    
+    // the main color is considered the background
+    // the weight sum can be interpreted as the alpha of the combined samples, see Jimenez paper
+    color += (1 - weightSum) * mainPixel.color;
+
+    imageStore(outImage, coord, vec4(color, 0.f));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlur.inc b/projects/indirect_dispatch/resources/shaders/motionBlur.inc
new file mode 100644
index 0000000000000000000000000000000000000000..6fdaf4c5f5e4b07a3111946b0732137f42f295ef
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlur.inc
@@ -0,0 +1,35 @@
+#ifndef MOTION_BLUR
+#define MOTION_BLUR
+
+#include "motionBlurConfig.inc"
+
+// see "A Reconstruction Filter for Plausible Motion Blur", section 2.2
+vec2 processMotionVector(vec2 motion, float motionScaleFactor, ivec2 imageResolution){
+    // every frame a pixel should blur over the distance it moves
+    // as we blur in two directions (where it was and where it will be) we must half the motion 
+    vec2 motionHalf     = motion * 0.5;
+    vec2 motionScaled   = motionHalf * motionScaleFactor; // scale factor contains shutter speed and delta time
+    
+    // pixels are anisotropic, so the ratio for clamping the velocity is computed in pixels instead of uv coordinates
+    vec2    motionPixel     = motionScaled * imageResolution;
+    float   velocityPixels  = length(motionPixel);
+    
+    float   epsilon         = 0.0001;
+    
+    // this clamps the motion to not exceed the radius given by the motion tile size
+    return motionScaled * max(0.5, min(velocityPixels, motionTileSize)) / (velocityPixels + epsilon);
+}
+
+const int ditherSize = 4;
+
+// simple binary dither pattern
+// could be optimized to avoid modulo and branch
+float dither(ivec2 coord){
+    
+    bool x = coord.x % ditherSize < (ditherSize / 2);
+    bool y = coord.y % ditherSize < (ditherSize / 2);
+    
+    return x ^^ y ? 1 : 0;
+}
+
+#endif // #ifndef MOTION_BLUR
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurColorCopy.comp b/projects/indirect_dispatch/resources/shaders/motionBlurColorCopy.comp
new file mode 100644
index 0000000000000000000000000000000000000000..1d8f210c86c2670241fa1d011835b120a39eddc0
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurColorCopy.comp
@@ -0,0 +1,29 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlurConfig.inc"
+#include "motionBlurWorkTile.inc"
+
+layout(set=0, binding=0)                    uniform texture2D   inColor;
+layout(set=0, binding=1)                    uniform sampler     nearestSampler;
+layout(set=0, binding=2, r11f_g11f_b10f)    uniform image2D     outImage;
+
+layout(set=0, binding=3) buffer WorkTileBuffer {
+    WorkTiles workTiles;
+};
+
+layout(local_size_x = motionTileSize, local_size_y = motionTileSize, local_size_z = 1) in;
+
+void main(){
+
+    uint    tileIndex       = gl_WorkGroupID.x;
+    ivec2   tileCoordinates = workTiles.tileXY[tileIndex];
+    ivec2   coordinate      = ivec2(tileCoordinates * motionTileSize + gl_LocalInvocationID.xy);
+    
+    if(any(greaterThanEqual(coordinate, imageSize(outImage))))
+        return;
+    
+    vec3 color = texelFetch(sampler2D(inColor, nearestSampler), coordinate, 0).rgb;
+    
+    imageStore(outImage, coordinate, vec4(color, 0.f));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurConfig.inc b/projects/indirect_dispatch/resources/shaders/motionBlurConfig.inc
new file mode 100644
index 0000000000000000000000000000000000000000..5b8679da119d84242c55d7d89de80ed8b64e5cc9
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurConfig.inc
@@ -0,0 +1,8 @@
+#ifndef MOTION_BLUR_CONFIG
+#define MOTION_BLUR_CONFIG
+
+const int motionTileSize        = 16;
+const int maxMotionBlurWidth    = 3840;
+const int maxMotionBlurHeight   = 2160;
+
+#endif // #ifndef MOTION_BLUR_CONFIG
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurFastPath.comp b/projects/indirect_dispatch/resources/shaders/motionBlurFastPath.comp
new file mode 100644
index 0000000000000000000000000000000000000000..2e27ebedcc4be1da93ff89a187fe1d3e992e8d22
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurFastPath.comp
@@ -0,0 +1,68 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlur.inc"
+#include "motionBlurConfig.inc"
+#include "motionBlurWorkTile.inc"
+
+layout(set=0, binding=0)                    uniform texture2D   inColor;
+layout(set=0, binding=1)                    uniform texture2D   inMotionNeighbourhoodMax;  
+layout(set=0, binding=2)                    uniform sampler     nearestSampler;
+layout(set=0, binding=3, r11f_g11f_b10f)    uniform image2D     outImage;
+
+layout(set=0, binding=4) buffer WorkTileBuffer {
+    WorkTiles workTiles;
+};
+
+layout(local_size_x = motionTileSize, local_size_y = motionTileSize, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    // computed from delta time and shutter speed
+    float motionScaleFactor;
+};
+
+void main(){
+
+    uint    tileIndex       = gl_WorkGroupID.x;
+    ivec2   tileCoordinates = workTiles.tileXY[tileIndex];
+    ivec2   coord           = ivec2(tileCoordinates * motionTileSize + gl_LocalInvocationID.xy);
+
+    if(any(greaterThanEqual(coord, imageSize(outImage))))
+        return;
+   
+    ivec2   textureRes  = textureSize(sampler2D(inColor, nearestSampler), 0);
+    vec2    uv          = vec2(coord + 0.5) / textureRes;   // + 0.5 to shift uv into pixel center
+    
+    vec2    motionNeighbourhoodMax  = processMotionVector(texelFetch(sampler2D(inMotionNeighbourhoodMax, nearestSampler), coord / motionTileSize, 0).rg, motionScaleFactor, imageSize(outImage));
+    
+    // early out on movement less than half a pixel
+    if(length(motionNeighbourhoodMax * imageSize(outImage)) <= 0.5){
+        vec3 color = texture(sampler2D(inColor, nearestSampler), uv).rgb;
+        imageStore(outImage, coord, vec4(color, 0.f));
+        return;
+    }
+    
+    vec3    color = vec3(0);   
+    
+    // clamping start and end points avoids artifacts at image borders
+    // the sampler clamps the sample uvs anyways, but without clamping here, many samples can be stuck at the border
+    vec2 uvStart    = clamp(uv - motionNeighbourhoodMax, 0, 1);
+    vec2 uvEnd      = clamp(uv + motionNeighbourhoodMax, 0, 1);
+    
+    // samples are placed evenly, but the entire filter is jittered
+    // dither returns either 0 or 1
+    // the sampleUV code expects an offset in range [-0.5, 0.5], so the dither is rescaled to a binary -0.25/0.25
+    float random = dither(coord) * 0.5 - 0.25;
+    
+    const int sampleCount = 16; 
+    
+    for(int i = 0; i < sampleCount; i++){
+        
+        vec2 sampleUV   = mix(uvStart, uvEnd,     (i + random + 1) / float(sampleCount + 1));
+        color           += texture(sampler2D(inColor, nearestSampler), sampleUV).rgb;
+    }
+    
+    color /= sampleCount;
+
+    imageStore(outImage, coord, vec4(color, 0.f));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurTileClassification.comp b/projects/indirect_dispatch/resources/shaders/motionBlurTileClassification.comp
new file mode 100644
index 0000000000000000000000000000000000000000..3c6f9e3715951ac4fe6770725c3314590cbbff47
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurTileClassification.comp
@@ -0,0 +1,58 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlurWorkTile.inc"
+
+layout(set=0, binding=0) uniform texture2D  inMotionMax;
+layout(set=0, binding=1) uniform texture2D  inMotionMin;
+layout(set=0, binding=2) uniform sampler    nearestSampler;
+
+layout(set=0, binding=3) buffer FullPathTileBuffer {
+    WorkTiles fullPathTiles;
+};
+
+layout(set=0, binding=4) buffer CopyPathTileBuffer {
+    WorkTiles copyPathTiles;
+};
+
+layout(set=0, binding=5) buffer FastPathTileBuffer {
+    WorkTiles fastPathTiles;
+};
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    uint    width;
+    uint    height;
+    float   fastPathThreshold;
+};
+
+void main(){
+    
+    ivec2 tileCoord = ivec2(gl_GlobalInvocationID.xy);
+    
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, textureSize(sampler2D(inMotionMax, nearestSampler), 0))))
+        return;
+    
+    vec2    motionMax           = texelFetch(sampler2D(inMotionMax, nearestSampler), tileCoord, 0).rg;
+    vec2    motionMin           = texelFetch(sampler2D(inMotionMin, nearestSampler), tileCoord, 0).rg;
+    
+    vec2    motionPixelMax      = motionMax * vec2(width, height);
+    vec2    motionPixelMin      = motionMin * vec2(width, height);
+    
+    float   velocityPixelMax    = length(motionPixelMax);
+    float   minMaxDistance      = distance(motionPixelMin, motionPixelMax);
+    
+    if(velocityPixelMax <= 0.5){
+        uint index                  = atomicAdd(copyPathTiles.tileCount, 1);
+        copyPathTiles.tileXY[index] = tileCoord;
+    }
+    else if(minMaxDistance <= fastPathThreshold){
+        uint index                  = atomicAdd(fastPathTiles.tileCount, 1);
+        fastPathTiles.tileXY[index] = tileCoord;
+    }
+    else{
+        uint index                  = atomicAdd(fullPathTiles.tileCount, 1);
+        fullPathTiles.tileXY[index] = tileCoord;
+    }
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurTileClassificationVis.comp b/projects/indirect_dispatch/resources/shaders/motionBlurTileClassificationVis.comp
new file mode 100644
index 0000000000000000000000000000000000000000..3382ff5ef0b407b9a3a7785eda0d19efe5a8f96e
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurTileClassificationVis.comp
@@ -0,0 +1,56 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlurConfig.inc"
+#include "motionBlurWorkTile.inc"
+
+layout(set=0, binding=0)                    uniform texture2D   inColor;
+layout(set=0, binding=1)                    uniform sampler     nearestSampler;
+layout(set=0, binding=2, r11f_g11f_b10f)    uniform image2D     outImage;
+
+layout(set=0, binding=3) buffer FullPathTileBuffer {
+    WorkTiles fullPathTiles;
+};
+
+layout(set=0, binding=4) buffer CopyPathTileBuffer {
+    WorkTiles copyPathTiles;
+};
+
+layout(set=0, binding=5) buffer FastPathTileBuffer {
+    WorkTiles fastPathTiles;
+};
+
+layout(local_size_x = motionTileSize, local_size_y = motionTileSize, local_size_z = 1) in;
+
+void main(){
+    
+    uint tileIndexFullPath = gl_WorkGroupID.x;
+    uint tileIndexCopyPath = gl_WorkGroupID.x - fullPathTiles.tileCount;
+    uint tileIndexFastPath = gl_WorkGroupID.x - fullPathTiles.tileCount - copyPathTiles.tileCount;
+    
+    vec3    debugColor;
+    ivec2   tileCoordinates;
+    
+    if(tileIndexFullPath < fullPathTiles.tileCount){
+        debugColor      = vec3(1, 0, 0);
+        tileCoordinates = fullPathTiles.tileXY[tileIndexFullPath];
+    }
+    else if(tileIndexCopyPath < copyPathTiles.tileCount){
+        debugColor      = vec3(0, 1, 0);
+        tileCoordinates = copyPathTiles.tileXY[tileIndexCopyPath];
+    }
+    else if(tileIndexFastPath < fastPathTiles.tileCount){
+        debugColor      = vec3(0, 0, 1);
+        tileCoordinates = fastPathTiles.tileXY[tileIndexFastPath];
+    }
+    else{
+        return;
+    }
+    
+    ivec2   coordinate  = ivec2(tileCoordinates * motionTileSize + gl_LocalInvocationID.xy);
+    vec3    color       = texelFetch(sampler2D(inColor, nearestSampler), coordinate, 0).rgb;
+    
+    color = mix(color, debugColor, 0.5);
+    
+    imageStore(outImage, coordinate, vec4(color, 0));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurWorkTile.inc b/projects/indirect_dispatch/resources/shaders/motionBlurWorkTile.inc
new file mode 100644
index 0000000000000000000000000000000000000000..8577f100aac524b93eecac406606a962bc52d222
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurWorkTile.inc
@@ -0,0 +1,19 @@
+#ifndef MOTION_BLUR_WORK_TILE
+#define MOTION_BLUR_WORK_TILE
+
+#include "motionBlurConfig.inc"
+
+const int maxTileCount = 
+    (maxMotionBlurWidth  + motionTileSize - 1) / motionTileSize * 
+    (maxMotionBlurHeight + motionTileSize - 1) / motionTileSize;
+
+struct WorkTiles{
+    uint    tileCount;
+    // dispatch Y/Z are here so the buffer can be used directly as an indirect dispatch argument buffer
+    uint    dispatchY;
+    uint    dispatchZ;
+    
+    ivec2   tileXY[maxTileCount];
+};
+
+#endif // #ifndef MOTION_BLUR_WORK_TILE
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurWorkTileReset.comp b/projects/indirect_dispatch/resources/shaders/motionBlurWorkTileReset.comp
new file mode 100644
index 0000000000000000000000000000000000000000..d4b55582a0a18c0c6a3fecf1dd6ce69ed49ca2c1
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurWorkTileReset.comp
@@ -0,0 +1,32 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlurWorkTile.inc"
+
+layout(set=0, binding=0) buffer FullPathTileBuffer {
+    WorkTiles fullPathTiles;
+};
+
+layout(set=0, binding=1) buffer CopyPathTileBuffer {
+    WorkTiles copyPathTiles;
+};
+
+layout(set=0, binding=2) buffer FastPathTileBuffer {
+    WorkTiles fastPathTiles;
+};
+
+layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
+
+void main(){
+    fullPathTiles.tileCount = 0;
+    fullPathTiles.dispatchY = 1;
+    fullPathTiles.dispatchZ = 1;
+    
+    copyPathTiles.tileCount = 0;
+    copyPathTiles.dispatchY = 1;
+    copyPathTiles.dispatchZ = 1;
+    
+    fastPathTiles.tileCount = 0;
+    fastPathTiles.dispatchY = 1;
+    fastPathTiles.dispatchZ = 1;
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionVector.inc b/projects/indirect_dispatch/resources/shaders/motionVector.inc
new file mode 100644
index 0000000000000000000000000000000000000000..498478cbc38b9666366eaa3d3e1a715dfc30236b
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionVector.inc
@@ -0,0 +1,9 @@
+vec2 computeMotionVector(vec4 NDC, vec4 NDCPrevious){
+    vec2 ndc            = NDC.xy            / NDC.w;
+    vec2 ndcPrevious    = NDCPrevious.xy    / NDCPrevious.w;
+
+    vec2 uv         = ndc           * 0.5 + 0.5;
+    vec2 uvPrevious = ndcPrevious   * 0.5 + 0.5;
+
+	return uvPrevious - uv;
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionVectorMinMax.comp b/projects/indirect_dispatch/resources/shaders/motionVectorMinMax.comp
new file mode 100644
index 0000000000000000000000000000000000000000..4ad350b0d5300aa63a66d7aceb00ea0b642d07ee
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionVectorMinMax.comp
@@ -0,0 +1,48 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+#include "motionBlurConfig.inc"
+
+layout(set=0, binding=0)        uniform texture2D   inMotion;
+layout(set=0, binding=1)        uniform sampler     textureSampler;
+layout(set=0, binding=2, rg16)  uniform image2D     outMotionMax;
+layout(set=0, binding=3, rg16)  uniform image2D     outMotionMin;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+    
+    ivec2 outImageRes       = imageSize(outMotionMax);
+    ivec2 motionTileCoord   = ivec2(gl_GlobalInvocationID.xy);
+    
+    if(any(greaterThanEqual(motionTileCoord, outImageRes)))
+        return;
+    
+    float   velocityMax = 0;
+    vec2    motionMax   = vec2(0);
+    
+    float   velocityMin = 100000;
+    vec2    motionMin   = vec2(0);
+    
+    ivec2 motionBufferBaseCoord = motionTileCoord * motionTileSize;
+    
+    for(int x = 0; x < motionTileSize; x++){
+        for(int y = 0; y < motionTileSize; y++){
+            ivec2   sampleCoord     = motionBufferBaseCoord + ivec2(x, y);
+            vec2    motionSample    = texelFetch(sampler2D(inMotion, textureSampler), sampleCoord, 0).rg;
+            float   velocitySample  = length(motionSample);
+            
+            if(velocitySample > velocityMax){
+                velocityMax = velocitySample;
+                motionMax   = motionSample;
+            }
+            
+            if(velocitySample < velocityMin){
+                velocityMin = velocitySample;
+                motionMin   = motionSample;
+            }
+        }
+    }
+
+    imageStore(outMotionMax, motionTileCoord, vec4(motionMax, 0, 0));
+    imageStore(outMotionMin, motionTileCoord, vec4(motionMin, 0, 0));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionVectorMinMaxNeighbourhood.comp b/projects/indirect_dispatch/resources/shaders/motionVectorMinMaxNeighbourhood.comp
new file mode 100644
index 0000000000000000000000000000000000000000..4d6e7c0af6115e816ba087570e5585ffde23b1e6
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionVectorMinMaxNeighbourhood.comp
@@ -0,0 +1,51 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+layout(set=0, binding=0)        uniform texture2D   inMotionMax;
+layout(set=0, binding=1)        uniform texture2D   inMotionMin;
+layout(set=0, binding=2)        uniform sampler     textureSampler;
+layout(set=0, binding=3, rg16)  uniform image2D     outMotionMaxNeighbourhood;
+layout(set=0, binding=4, rg16)  uniform image2D     outMotionMinNeighbourhood;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+void main(){
+    
+    ivec2 outImageRes       = imageSize(outMotionMaxNeighbourhood);
+    ivec2 motionTileCoord   = ivec2(gl_GlobalInvocationID.xy);
+    
+    if(any(greaterThanEqual(motionTileCoord, outImageRes)))
+        return;
+    
+    float   velocityMax = 0;
+    vec2    motionMax   = vec2(0);
+    
+    float   velocityMin = 10000;
+    vec2    motionMin   = vec2(0);
+    
+    for(int x = -1; x <= 1; x++){
+        for(int y = -1; y <= 1; y++){
+            ivec2   sampleCoord         = motionTileCoord + ivec2(x, y);
+            
+            vec2    motionSampleMax     = texelFetch(sampler2D(inMotionMax, textureSampler), sampleCoord, 0).rg;
+            float   velocitySampleMax   = length(motionSampleMax);
+            
+            if(velocitySampleMax > velocityMax){
+                velocityMax = velocitySampleMax;
+                motionMax   = motionSampleMax;
+            }
+            
+            
+            vec2    motionSampleMin     = texelFetch(sampler2D(inMotionMin, textureSampler), sampleCoord, 0).rg;
+            float   velocitySampleMin   = length(motionSampleMin);
+            
+            if(velocitySampleMin < velocityMin){
+                velocityMin = velocitySampleMin;
+                motionMin   = motionSampleMin;
+            }
+        }
+    }
+
+    imageStore(outMotionMaxNeighbourhood, motionTileCoord, vec4(motionMax, 0, 0));
+    imageStore(outMotionMinNeighbourhood, motionTileCoord, vec4(motionMin, 0, 0));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionVectorVisualisation.comp b/projects/indirect_dispatch/resources/shaders/motionVectorVisualisation.comp
new file mode 100644
index 0000000000000000000000000000000000000000..1cfb09c87e8288b8ea80c6ddfbe5f0d4918b7f2e
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionVectorVisualisation.comp
@@ -0,0 +1,30 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlurConfig.inc"
+
+layout(set=0, binding=0)                    uniform texture2D   inMotion;
+layout(set=0, binding=1)                    uniform sampler     textureSampler;
+layout(set=0, binding=2, r11f_g11f_b10f)    uniform image2D     outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    float range;
+};
+
+void main(){
+
+    ivec2 outImageRes = imageSize(outImage);
+    ivec2 coord       = ivec2(gl_GlobalInvocationID.xy);
+
+    if(any(greaterThanEqual(coord, outImageRes)))
+        return;
+
+    vec2 motionVector           = texelFetch(sampler2D(inMotion, textureSampler), coord / motionTileSize, 0).rg;
+    vec2 motionVectorNormalized = clamp(motionVector / range, -1, 1);
+    
+    vec2 color  = motionVectorNormalized * 0.5 + 0.5;
+
+    imageStore(outImage, coord, vec4(color, 0.5, 0));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/prepass.frag b/projects/indirect_dispatch/resources/shaders/prepass.frag
new file mode 100644
index 0000000000000000000000000000000000000000..ccfc84d982253f7b89551c099a92b5686a811163
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/prepass.frag
@@ -0,0 +1,14 @@
+#version 450
+#extension GL_ARB_separate_shader_objects   : enable
+#extension GL_GOOGLE_include_directive      : enable
+
+#include "motionVector.inc"
+
+layout(location = 0) in vec4 passNDC;
+layout(location = 1) in vec4 passNDCPrevious;
+
+layout(location = 0) out vec2 outMotion;
+
+void main()	{
+	outMotion = computeMotionVector(passNDC, passNDCPrevious);
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/prepass.vert b/projects/indirect_dispatch/resources/shaders/prepass.vert
new file mode 100644
index 0000000000000000000000000000000000000000..230346208007fae0bb7724b5b6d05f62726c4ded
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/prepass.vert
@@ -0,0 +1,18 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+
+layout(location = 0) out vec4 passNDC;
+layout(location = 1) out vec4 passNDCPrevious;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+    mat4 mvpPrevious;
+};
+
+void main()	{
+	gl_Position     = mvp * vec4(inPosition, 1.0);
+	passNDC         = gl_Position;
+    passNDCPrevious = mvpPrevious * vec4(inPosition, 1.0);
+}
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/shadow.frag b/projects/indirect_dispatch/resources/shaders/sky.frag
similarity index 53%
rename from projects/bloom/resources/shaders/shadow.frag
rename to projects/indirect_dispatch/resources/shaders/sky.frag
index 848f853f556660b4900b5db7fb6fc98d57c1cd5b..efc0e03b2d6ee1c71930c866293da66857bd56c7 100644
--- a/projects/bloom/resources/shaders/shadow.frag
+++ b/projects/indirect_dispatch/resources/shaders/sky.frag
@@ -1,6 +1,8 @@
 #version 450
 #extension GL_ARB_separate_shader_objects : enable
 
-void main()	{
+layout(location = 0) out vec3 outColor;
 
+void main()	{
+	outColor = vec3(0, 0.2, 0.9);
 }
\ No newline at end of file
diff --git a/projects/bloom/resources/shaders/shadow.vert b/projects/indirect_dispatch/resources/shaders/sky.vert
similarity index 58%
rename from projects/bloom/resources/shaders/shadow.vert
rename to projects/indirect_dispatch/resources/shaders/sky.vert
index e0f41d42d575fa64fedbfa04adf89ac0f4aeebe8..44b48cd7f3bfc44e2e43edef0d474581d50608de 100644
--- a/projects/bloom/resources/shaders/shadow.vert
+++ b/projects/indirect_dispatch/resources/shaders/sky.vert
@@ -4,9 +4,10 @@
 layout(location = 0) in vec3 inPosition;
 
 layout( push_constant ) uniform constants{
-    mat4 mvp;
+    mat4 viewProjection;
 };
 
 void main()	{
-	gl_Position = mvp * vec4(inPosition, 1.0);
+	gl_Position     = viewProjection * vec4(inPosition, 0.0);
+    gl_Position.w   = gl_Position.z;
 }
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/skyPrepass.frag b/projects/indirect_dispatch/resources/shaders/skyPrepass.frag
new file mode 100644
index 0000000000000000000000000000000000000000..64ec4f18bbcf89153d70019ace570da53d44a505
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/skyPrepass.frag
@@ -0,0 +1,14 @@
+#version 450
+#extension GL_ARB_separate_shader_objects   : enable
+#extension GL_GOOGLE_include_directive      : enable
+
+#include "motionVector.inc"
+
+layout(location = 0) out vec2 outMotion;
+
+layout(location = 0) in vec4 passNDC;
+layout(location = 1) in vec4 passNDCPrevious;
+
+void main()	{
+	outMotion = computeMotionVector(passNDC, passNDCPrevious);
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/skyPrepass.vert b/projects/indirect_dispatch/resources/shaders/skyPrepass.vert
new file mode 100644
index 0000000000000000000000000000000000000000..31b9016a592d097825a09e1daa888cb7b72b2cbc
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/skyPrepass.vert
@@ -0,0 +1,22 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+
+layout( push_constant ) uniform constants{
+    mat4 viewProjection;
+    mat4 viewProjectionPrevious;
+};
+
+layout(location = 0) out vec4 passNDC;
+layout(location = 1) out vec4 passNDCPrevious;
+
+void main()	{
+	gl_Position     = viewProjection * vec4(inPosition, 0.0);
+    gl_Position.w   = gl_Position.z;
+    
+    passNDC         = gl_Position;
+    
+    passNDCPrevious     = viewProjectionPrevious * vec4(inPosition, 0.0);
+    passNDCPrevious.w   = passNDCPrevious.z;
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/App.cpp b/projects/indirect_dispatch/src/App.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..92d548acde9c5a27e69c6daf4d92ca1da9d50a2c
--- /dev/null
+++ b/projects/indirect_dispatch/src/App.cpp
@@ -0,0 +1,369 @@
+#include "App.hpp"
+#include "AppConfig.hpp"
+#include <chrono>
+#include <vkcv/gui/GUI.hpp>
+#include <functional>
+
+App::App() : 
+	m_applicationName("Indirect Dispatch"),
+	m_windowWidth(AppConfig::defaultWindowWidth),
+	m_windowHeight(AppConfig::defaultWindowHeight),
+	m_window(vkcv::Window::create(
+		m_applicationName,
+		m_windowWidth,
+		m_windowHeight,
+		true)),
+	m_core(vkcv::Core::create(
+		m_window,
+		m_applicationName,
+		VK_MAKE_VERSION(0, 0, 1),
+		{ vk::QueueFlagBits::eGraphics ,vk::QueueFlagBits::eCompute , vk::QueueFlagBits::eTransfer },
+		{},
+		{ "VK_KHR_swapchain" })),
+	m_cameraManager(m_window){}
+
+bool App::initialize() {
+
+	if (!loadMeshPass(m_core, &m_meshPass))
+		return false;
+
+	if (!loadSkyPass(m_core, &m_skyPass))
+		return false;
+
+	if (!loadPrePass(m_core, &m_prePass))
+		return false;
+
+	if (!loadSkyPrePass(m_core, &m_skyPrePass))
+		return false;
+
+	if (!loadComputePass(m_core, "resources/shaders/gammaCorrection.comp", &m_gammaCorrectionPass))
+		return false;
+
+	if (!loadMesh(m_core, "resources/models/cube.gltf", &m_cubeMesh))
+		return false;
+
+	if (!loadMesh(m_core, "resources/models/ground.gltf", &m_groundMesh))
+		return false;
+
+	if(!loadImage(m_core, "resources/models/grid.png", &m_gridTexture))
+		return false;
+
+	if (!m_motionBlur.initialize(&m_core, m_windowWidth, m_windowHeight))
+		return false;
+
+	m_linearSampler = m_core.createSampler(
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerFilterType::LINEAR,
+		vkcv::SamplerMipmapMode::LINEAR,
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
+
+	m_renderTargets = createRenderTargets(m_core, m_windowWidth, m_windowHeight);
+
+	const int cameraIndex = m_cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	m_cameraManager.getCamera(cameraIndex).setPosition(glm::vec3(0, 1, -3));
+	m_cameraManager.getCamera(cameraIndex).setNearFar(0.1f, 30.f);
+	
+	vkcv::DescriptorWrites meshPassDescriptorWrites;
+	meshPassDescriptorWrites.sampledImageWrites = { vkcv::SampledImageDescriptorWrite(0, m_gridTexture) };
+	meshPassDescriptorWrites.samplerWrites = { vkcv::SamplerDescriptorWrite(1, m_linearSampler) };
+	m_core.writeDescriptorSet(m_meshPass.descriptorSet, meshPassDescriptorWrites);
+
+	return true;
+}
+
+void App::run() {
+
+	auto                        frameStartTime = std::chrono::system_clock::now();
+	const auto                  appStartTime   = std::chrono::system_clock::now();
+	const vkcv::ImageHandle     swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+	const vkcv::DrawcallInfo    skyDrawcall(m_cubeMesh.mesh, {}, 1);
+
+	vkcv::gui::GUI gui(m_core, m_window);
+
+	eMotionVectorVisualisationMode  motionVectorVisualisationMode   = eMotionVectorVisualisationMode::None;
+	eMotionBlurMode                 motionBlurMode                  = eMotionBlurMode::Default;
+
+	bool    freezeFrame                     = false;
+	float   motionBlurTileOffsetLength      = 3;
+	float   objectVerticalSpeed             = 5;
+	float   objectAmplitude                 = 0;
+	float   objectMeanHeight                = 1;
+	float   objectRotationSpeedX            = 5;
+	float   objectRotationSpeedY            = 5;
+	int     cameraShutterSpeedInverse       = 24;
+	float   motionVectorVisualisationRange  = 0.008;
+	float   motionBlurFastPathThreshold     = 1;
+
+	glm::mat4 viewProjection            = m_cameraManager.getActiveCamera().getMVP();
+	glm::mat4 viewProjectionPrevious    = m_cameraManager.getActiveCamera().getMVP();
+
+	struct Object {
+		MeshResources meshResources;
+		glm::mat4 modelMatrix   = glm::mat4(1.f);
+		glm::mat4 mvp           = glm::mat4(1.f);
+		glm::mat4 mvpPrevious   = glm::mat4(1.f);
+		std::function<void(float, Object&)> modelMatrixUpdate;
+	};
+	std::vector<Object> sceneObjects;
+
+	Object ground;
+	ground.meshResources = m_groundMesh;
+	sceneObjects.push_back(ground);
+
+	Object sphere;
+	sphere.meshResources = m_cubeMesh;
+	sphere.modelMatrixUpdate = [&](float time, Object& obj) {
+		const float currentHeight   = objectMeanHeight + objectAmplitude * glm::sin(time * objectVerticalSpeed);
+		const glm::mat4 translation = glm::translate(glm::mat4(1), glm::vec3(0, currentHeight, 0));
+		const glm::mat4 rotationX   = glm::rotate(glm::mat4(1), objectRotationSpeedX * time, glm::vec3(1, 0, 0));
+		const glm::mat4 rotationY   = glm::rotate(glm::mat4(1), objectRotationSpeedY * time, glm::vec3(0, 1, 0));
+		obj.modelMatrix             = translation * rotationX * rotationY;
+	};
+	sceneObjects.push_back(sphere);
+
+	bool spaceWasPressed = false;
+
+	m_window.e_key.add([&](int key, int scancode, int action, int mods) {
+		if (key == GLFW_KEY_SPACE) {
+			if (action == GLFW_PRESS) {
+				if (!spaceWasPressed) {
+					freezeFrame = !freezeFrame;
+				}
+				spaceWasPressed = true;
+			}
+			else if (action == GLFW_RELEASE) {
+				spaceWasPressed = false;
+			}
+		}
+	});
+
+	auto frameEndTime = std::chrono::system_clock::now();
+
+	while (m_window.isWindowOpen()) {
+
+		vkcv::Window::pollEvents();
+
+		if (!freezeFrame) {
+
+			frameStartTime          = frameEndTime;
+			viewProjectionPrevious  = viewProjection;
+
+			for (Object& obj : sceneObjects) {
+				obj.mvpPrevious = obj.mvp;
+			}
+		}
+
+		if (m_window.getHeight() == 0 || m_window.getWidth() == 0)
+			continue;
+
+		uint32_t swapchainWidth, swapchainHeight;
+		if (!m_core.beginFrame(swapchainWidth, swapchainHeight))
+			continue;
+
+		const bool hasResolutionChanged = (swapchainWidth != m_windowWidth) || (swapchainHeight != m_windowHeight);
+		if (hasResolutionChanged) {
+			m_windowWidth  = swapchainWidth;
+			m_windowHeight = swapchainHeight;
+
+			m_renderTargets = createRenderTargets(m_core, m_windowWidth, m_windowHeight);
+			m_motionBlur.setResolution(m_windowWidth, m_windowHeight);
+		}
+
+		if(!freezeFrame)
+			frameEndTime = std::chrono::system_clock::now();
+
+		const float microsecondToSecond = 0.000001;
+		const float fDeltaTimeSeconds = microsecondToSecond * std::chrono::duration_cast<std::chrono::microseconds>(frameEndTime - frameStartTime).count();
+
+		m_cameraManager.update(fDeltaTimeSeconds);
+
+		const auto      time                = frameEndTime - appStartTime;
+		const float     fCurrentTime        = std::chrono::duration_cast<std::chrono::milliseconds>(time).count() * 0.001f;
+
+		// update matrices
+		if (!freezeFrame) {
+
+			viewProjection = m_cameraManager.getActiveCamera().getMVP();
+
+			for (Object& obj : sceneObjects) {
+				if (obj.modelMatrixUpdate) {
+					obj.modelMatrixUpdate(fCurrentTime, obj);
+				}
+				obj.mvp = viewProjection * obj.modelMatrix;
+			}
+		}
+
+		const vkcv::CommandStreamHandle cmdStream = m_core.createCommandStream(vkcv::QueueType::Graphics);
+
+		// prepass
+		vkcv::PushConstants prepassPushConstants(sizeof(glm::mat4) * 2);
+
+		for (const Object& obj : sceneObjects) {
+			glm::mat4 prepassMatrices[2] = { obj.mvp, obj.mvpPrevious };
+			prepassPushConstants.appendDrawcall(prepassMatrices);
+		}
+
+		const std::vector<vkcv::ImageHandle> prepassRenderTargets = {
+			m_renderTargets.motionBuffer,
+			m_renderTargets.depthBuffer };
+
+		std::vector<vkcv::DrawcallInfo> prepassSceneDrawcalls;
+		for (const Object& obj : sceneObjects) {
+			prepassSceneDrawcalls.push_back(vkcv::DrawcallInfo(obj.meshResources.mesh, {}));
+		}
+
+		m_core.recordDrawcallsToCmdStream(
+			cmdStream,
+			m_prePass.renderPass,
+			m_prePass.pipeline,
+			prepassPushConstants,
+			prepassSceneDrawcalls,
+			prepassRenderTargets);
+
+		// sky prepass
+		glm::mat4 skyPrepassMatrices[2] = {
+			viewProjection,
+			viewProjectionPrevious };
+		vkcv::PushConstants skyPrepassPushConstants(sizeof(glm::mat4) * 2);
+		skyPrepassPushConstants.appendDrawcall(skyPrepassMatrices);
+
+		m_core.recordDrawcallsToCmdStream(
+			cmdStream,
+			m_skyPrePass.renderPass,
+			m_skyPrePass.pipeline,
+			skyPrepassPushConstants,
+			{ skyDrawcall },
+			prepassRenderTargets);
+
+		// main pass
+		const std::vector<vkcv::ImageHandle> renderTargets   = { 
+			m_renderTargets.colorBuffer, 
+			m_renderTargets.depthBuffer };
+
+		vkcv::PushConstants meshPushConstants(2 * sizeof(glm::mat4));
+		for (const Object& obj : sceneObjects) {
+			glm::mat4 matrices[2] = { obj.mvp, obj.modelMatrix };
+			meshPushConstants.appendDrawcall(matrices);
+		}
+
+		std::vector<vkcv::DrawcallInfo> forwardSceneDrawcalls;
+		for (const Object& obj : sceneObjects) {
+			forwardSceneDrawcalls.push_back(vkcv::DrawcallInfo(
+				obj.meshResources.mesh, 
+				{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_meshPass.descriptorSet).vulkanHandle) }));
+		}
+
+		m_core.recordDrawcallsToCmdStream(
+			cmdStream,
+			m_meshPass.renderPass,
+			m_meshPass.pipeline,
+			meshPushConstants,
+			forwardSceneDrawcalls,
+			renderTargets);
+
+		// sky
+		vkcv::PushConstants skyPushConstants(sizeof(glm::mat4));
+		skyPushConstants.appendDrawcall(viewProjection);
+
+		m_core.recordDrawcallsToCmdStream(
+			cmdStream,
+			m_skyPass.renderPass,
+			m_skyPass.pipeline,
+			skyPushConstants,
+			{ skyDrawcall },
+			renderTargets);
+
+		// motion blur
+		vkcv::ImageHandle motionBlurOutput;
+
+		if (motionVectorVisualisationMode == eMotionVectorVisualisationMode::None) {
+			float cameraNear;
+			float cameraFar;
+			m_cameraManager.getActiveCamera().getNearFar(cameraNear, cameraFar);
+
+			motionBlurOutput = m_motionBlur.render(
+				cmdStream,
+				m_renderTargets.motionBuffer,
+				m_renderTargets.colorBuffer,
+				m_renderTargets.depthBuffer,
+				motionBlurMode,
+				cameraNear,
+				cameraFar,
+				fDeltaTimeSeconds,
+				cameraShutterSpeedInverse,
+				motionBlurTileOffsetLength,
+				motionBlurFastPathThreshold);
+		}
+		else {
+			motionBlurOutput = m_motionBlur.renderMotionVectorVisualisation(
+				cmdStream,
+				m_renderTargets.motionBuffer,
+				motionVectorVisualisationMode,
+				motionVectorVisualisationRange);
+		}
+
+		// gamma correction
+		vkcv::DescriptorWrites gammaCorrectionDescriptorWrites;
+		gammaCorrectionDescriptorWrites.sampledImageWrites = {
+			vkcv::SampledImageDescriptorWrite(0, motionBlurOutput) };
+		gammaCorrectionDescriptorWrites.samplerWrites = {
+			vkcv::SamplerDescriptorWrite(1, m_linearSampler) };
+		gammaCorrectionDescriptorWrites.storageImageWrites = {
+			vkcv::StorageImageDescriptorWrite(2, swapchainInput) };
+
+		m_core.writeDescriptorSet(m_gammaCorrectionPass.descriptorSet, gammaCorrectionDescriptorWrites);
+
+		m_core.prepareImageForSampling(cmdStream, motionBlurOutput);
+		m_core.prepareImageForStorage (cmdStream, swapchainInput);
+
+		const uint32_t fullScreenImageDispatch[3] = {
+			static_cast<uint32_t>((m_windowWidth  + 7) / 8),
+			static_cast<uint32_t>((m_windowHeight + 7) / 8),
+			static_cast<uint32_t>(1) };
+
+		m_core.recordComputeDispatchToCmdStream(
+			cmdStream,
+			m_gammaCorrectionPass.pipeline,
+			fullScreenImageDispatch,
+			{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_gammaCorrectionPass.descriptorSet).vulkanHandle) },
+			vkcv::PushConstants(0));
+
+		m_core.prepareSwapchainImageForPresent(cmdStream);
+		m_core.submitCommandStream(cmdStream);
+
+		gui.beginGUI();
+		ImGui::Begin("Settings");
+
+		ImGui::Checkbox("Freeze frame", &freezeFrame);
+		ImGui::InputFloat("Motion tile offset length", &motionBlurTileOffsetLength);
+		ImGui::InputFloat("Motion blur fast path threshold", &motionBlurFastPathThreshold);
+
+		ImGui::Combo(
+			"Motion blur mode",
+			reinterpret_cast<int*>(&motionBlurMode),
+			MotionBlurModeLabels,
+			static_cast<int>(eMotionBlurMode::OptionCount));
+
+		ImGui::Combo(
+			"Debug view",
+			reinterpret_cast<int*>(&motionVectorVisualisationMode),
+			MotionVectorVisualisationModeLabels,
+			static_cast<int>(eMotionVectorVisualisationMode::OptionCount));
+
+		if (motionVectorVisualisationMode != eMotionVectorVisualisationMode::None)
+			ImGui::InputFloat("Motion vector visualisation range", &motionVectorVisualisationRange);
+
+		ImGui::InputInt("Camera shutter speed inverse", &cameraShutterSpeedInverse);
+
+		ImGui::InputFloat("Object movement speed",      &objectVerticalSpeed);
+		ImGui::InputFloat("Object movement amplitude",  &objectAmplitude);
+		ImGui::InputFloat("Object mean height",         &objectMeanHeight);
+		ImGui::InputFloat("Object rotation speed X",    &objectRotationSpeedX);
+		ImGui::InputFloat("Object rotation speed Y",    &objectRotationSpeedY);
+
+		ImGui::End();
+		gui.endGUI();
+
+		m_core.endFrame();
+	}
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/App.hpp b/projects/indirect_dispatch/src/App.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d580793b0fdc4e7dc8c8654d29a75f04e14ea422
--- /dev/null
+++ b/projects/indirect_dispatch/src/App.hpp
@@ -0,0 +1,38 @@
+#pragma once
+#include <vkcv/Core.hpp>
+#include <vkcv/camera/CameraManager.hpp>
+#include "AppSetup.hpp"
+#include "MotionBlur.hpp"
+
+class App {
+public:
+	App();
+	bool initialize();
+	void run();
+private:
+	const char* m_applicationName;
+
+	int m_windowWidth;
+	int m_windowHeight;
+
+	vkcv::Window                m_window;
+	vkcv::Core                  m_core;
+	vkcv::camera::CameraManager m_cameraManager;
+
+	MotionBlur m_motionBlur;
+
+	vkcv::ImageHandle m_gridTexture;
+
+	MeshResources m_cubeMesh;
+	MeshResources m_groundMesh;
+
+	GraphicPassHandles m_meshPass;
+	GraphicPassHandles m_skyPass;
+	GraphicPassHandles m_prePass;
+	GraphicPassHandles m_skyPrePass;
+
+	ComputePassHandles m_gammaCorrectionPass;
+
+	AppRenderTargets    m_renderTargets;
+	vkcv::SamplerHandle m_linearSampler;
+};
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/AppConfig.hpp b/projects/indirect_dispatch/src/AppConfig.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c89c34ea8e3c0c45708ca998a642faffb31403d3
--- /dev/null
+++ b/projects/indirect_dispatch/src/AppConfig.hpp
@@ -0,0 +1,10 @@
+#pragma once
+#include "vulkan/vulkan.hpp"
+
+namespace AppConfig{
+	const int           defaultWindowWidth  = 1280;
+	const int           defaultWindowHeight = 720;
+	const vk::Format    depthBufferFormat   = vk::Format::eD32Sfloat;
+	const vk::Format    colorBufferFormat   = vk::Format::eB10G11R11UfloatPack32;
+	const vk::Format    motionBufferFormat  = vk::Format::eR16G16Sfloat;
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/AppSetup.cpp b/projects/indirect_dispatch/src/AppSetup.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..267ac6bd8ef44dcee9b3a05d7204e8d33fbe86a7
--- /dev/null
+++ b/projects/indirect_dispatch/src/AppSetup.cpp
@@ -0,0 +1,301 @@
+#include "AppSetup.hpp"
+#include "AppConfig.hpp"
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+bool loadMesh(vkcv::Core& core, const std::filesystem::path& path, MeshResources* outMesh) {
+	assert(outMesh);
+
+	vkcv::asset::Scene scene;
+	const int meshLoadResult = vkcv::asset::loadScene(path.string(), scene);
+
+	if (meshLoadResult != 1) {
+		vkcv_log(vkcv::LogLevel::ERROR, "Mesh loading failed");
+		return false;
+	}
+
+	if (scene.meshes.size() < 1) {
+		vkcv_log(vkcv::LogLevel::ERROR, "Cube mesh scene does not contain any vertex groups");
+		return false;
+	}
+	assert(!scene.vertexGroups.empty());
+
+	auto& vertexData = scene.vertexGroups[0].vertexBuffer;
+	auto& indexData  = scene.vertexGroups[0].indexBuffer;
+
+	vkcv::Buffer vertexBuffer = core.createBuffer<uint8_t>(
+		vkcv::BufferType::VERTEX,
+		vertexData.data.size(),
+		vkcv::BufferMemoryType::DEVICE_LOCAL);
+
+	vkcv::Buffer indexBuffer = core.createBuffer<uint8_t>(
+		vkcv::BufferType::INDEX,
+		indexData.data.size(),
+		vkcv::BufferMemoryType::DEVICE_LOCAL);
+
+	vertexBuffer.fill(vertexData.data);
+	indexBuffer.fill(indexData.data);
+
+	outMesh->vertexBuffer = vertexBuffer.getHandle();
+	outMesh->indexBuffer  = indexBuffer.getHandle();
+
+	auto& attributes = vertexData.attributes;
+
+	std::sort(attributes.begin(), attributes.end(),
+		[](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
+		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
+	});
+
+	const std::vector<vkcv::VertexBufferBinding> vertexBufferBindings = {
+		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[0].offset), vertexBuffer.getVulkanHandle()),
+		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[1].offset), vertexBuffer.getVulkanHandle()),
+		vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[2].offset), vertexBuffer.getVulkanHandle()) };
+
+	outMesh->mesh = vkcv::Mesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), scene.vertexGroups[0].numIndices);
+
+	return true;
+}
+
+bool loadImage(vkcv::Core& core, const std::filesystem::path& path, vkcv::ImageHandle* outImage) {
+
+	assert(outImage);
+
+	const vkcv::asset::Texture textureData = vkcv::asset::loadTexture(path);
+
+	if (textureData.channels != 4) {
+		vkcv_log(vkcv::LogLevel::ERROR, "Expecting image with four components");
+		return false;
+	}
+
+	vkcv::Image image = core.createImage(
+		vk::Format::eR8G8B8A8Srgb, 
+		textureData.width, 
+		textureData.height, 
+		1, 
+		true);
+
+	image.fill(textureData.data.data(), textureData.data.size());
+	image.generateMipChainImmediate();
+	image.switchLayout(vk::ImageLayout::eReadOnlyOptimalKHR);
+
+	*outImage = image.getHandle();
+	return true;
+}
+
+bool loadGraphicPass(
+	vkcv::Core& core,
+	const std::filesystem::path vertexPath,
+	const std::filesystem::path fragmentPath,
+	const vkcv::PassConfig&     passConfig,
+	const vkcv::DepthTest       depthTest,
+	GraphicPassHandles*         outPassHandles) {
+
+	assert(outPassHandles);
+
+	outPassHandles->renderPass = core.createPass(passConfig);
+
+	if (!outPassHandles->renderPass) {
+		vkcv_log(vkcv::LogLevel::ERROR, "Error: Could not create renderpass");
+		return false;
+	}
+
+	vkcv::ShaderProgram         shaderProgram;
+	vkcv::shader::GLSLCompiler  compiler;
+
+	compiler.compile(vkcv::ShaderStage::VERTEX, vertexPath,
+		[&shaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shaderProgram.addShader(shaderStage, path);
+	});
+
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, fragmentPath,
+		[&shaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shaderProgram.addShader(shaderStage, path);
+	});
+
+	const std::vector<vkcv::VertexAttachment> vertexAttachments = shaderProgram.getVertexAttachments();
+	std::vector<vkcv::VertexBinding> bindings;
+	for (size_t i = 0; i < vertexAttachments.size(); i++) {
+		bindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
+	}
+
+	const vkcv::VertexLayout vertexLayout(bindings);
+
+	const auto descriptorBindings = shaderProgram.getReflectedDescriptors();
+	const bool hasDescriptor = descriptorBindings.size() > 0;
+	if (hasDescriptor)
+		outPassHandles->descriptorSet = core.createDescriptorSet(descriptorBindings[0]);
+
+	std::vector<vk::DescriptorSetLayout> descriptorLayouts;
+	if (hasDescriptor)
+		descriptorLayouts.push_back(core.getDescriptorSet(outPassHandles->descriptorSet).layout);
+
+	vkcv::PipelineConfig pipelineConfig{
+		shaderProgram,
+		UINT32_MAX,
+		UINT32_MAX,
+		outPassHandles->renderPass,
+		{ vertexLayout },
+		descriptorLayouts,
+		true };
+	pipelineConfig.m_depthTest  = depthTest;
+	outPassHandles->pipeline    = core.createGraphicsPipeline(pipelineConfig);
+
+	if (!outPassHandles->pipeline) {
+		vkcv_log(vkcv::LogLevel::ERROR, "Error: Could not create graphics pipeline");
+		return false;
+	}
+
+	return true;
+}
+
+bool loadMeshPass(vkcv::Core& core, GraphicPassHandles* outHandles) {
+
+	assert(outHandles);
+
+	vkcv::AttachmentDescription colorAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::DONT_CARE,
+		AppConfig::colorBufferFormat);
+
+	vkcv::AttachmentDescription depthAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		AppConfig::depthBufferFormat);
+
+	return loadGraphicPass(
+		core,
+		"resources/shaders/mesh.vert",
+		"resources/shaders/mesh.frag",
+		vkcv::PassConfig({ colorAttachment, depthAttachment }),
+		vkcv::DepthTest::Equal,
+		outHandles);
+}
+
+bool loadSkyPass(vkcv::Core& core, GraphicPassHandles* outHandles) {
+
+	assert(outHandles);
+
+	vkcv::AttachmentDescription colorAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		AppConfig::colorBufferFormat);
+
+	vkcv::AttachmentDescription depthAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		AppConfig::depthBufferFormat);
+
+	return loadGraphicPass(
+		core,
+		"resources/shaders/sky.vert",
+		"resources/shaders/sky.frag",
+		vkcv::PassConfig({ colorAttachment, depthAttachment }),
+		vkcv::DepthTest::Equal,
+		outHandles);
+}
+
+bool loadPrePass(vkcv::Core& core, GraphicPassHandles* outHandles) {
+	assert(outHandles);
+
+	vkcv::AttachmentDescription motionAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		AppConfig::motionBufferFormat);
+
+	vkcv::AttachmentDescription depthAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		AppConfig::depthBufferFormat);
+
+	return loadGraphicPass(
+		core,
+		"resources/shaders/prepass.vert",
+		"resources/shaders/prepass.frag",
+		vkcv::PassConfig({ motionAttachment, depthAttachment }),
+		vkcv::DepthTest::LessEqual,
+		outHandles);
+}
+
+bool loadSkyPrePass(vkcv::Core& core, GraphicPassHandles* outHandles) {
+	assert(outHandles);
+
+	vkcv::AttachmentDescription motionAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		AppConfig::motionBufferFormat);
+
+	vkcv::AttachmentDescription depthAttachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::LOAD,
+		AppConfig::depthBufferFormat);
+
+	return loadGraphicPass(
+		core,
+		"resources/shaders/skyPrepass.vert",
+		"resources/shaders/skyPrepass.frag",
+		vkcv::PassConfig({ motionAttachment, depthAttachment }),
+		vkcv::DepthTest::LessEqual,
+		outHandles);
+}
+
+bool loadComputePass(vkcv::Core& core, const std::filesystem::path& path, ComputePassHandles* outComputePass) {
+
+	assert(outComputePass);
+	vkcv::ShaderProgram shaderProgram;
+	vkcv::shader::GLSLCompiler compiler;
+
+	compiler.compile(vkcv::ShaderStage::COMPUTE, path,
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		shaderProgram.addShader(shaderStage, path);
+	});
+
+	if (shaderProgram.getReflectedDescriptors().size() < 1) {
+		vkcv_log(vkcv::LogLevel::ERROR, "Compute shader has no descriptor set");
+		return false;
+	}
+
+	outComputePass->descriptorSet = core.createDescriptorSet(shaderProgram.getReflectedDescriptors()[0]);
+
+	outComputePass->pipeline = core.createComputePipeline(
+		shaderProgram,
+		{ core.getDescriptorSet(outComputePass->descriptorSet).layout });
+
+	if (!outComputePass->pipeline) {
+		vkcv_log(vkcv::LogLevel::ERROR, "Compute shader pipeline creation failed");
+		return false;
+	}
+
+	return true;
+}
+
+AppRenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const uint32_t height) {
+
+	AppRenderTargets targets;
+
+	targets.depthBuffer = core.createImage(
+		AppConfig::depthBufferFormat,
+		width,
+		height,
+		1,
+		false).getHandle();
+
+	targets.colorBuffer = core.createImage(
+		AppConfig::colorBufferFormat,
+		width,
+		height,
+		1,
+		false,
+		false,
+		true).getHandle();
+
+	targets.motionBuffer = core.createImage(
+		AppConfig::motionBufferFormat,
+		width,
+		height,
+		1,
+		false,
+		false,
+		true).getHandle();
+
+	return targets;
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/AppSetup.hpp b/projects/indirect_dispatch/src/AppSetup.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3125bc516b553de715d6e51bbda259e3e16f758f
--- /dev/null
+++ b/projects/indirect_dispatch/src/AppSetup.hpp
@@ -0,0 +1,47 @@
+#pragma once
+#include <vkcv/Core.hpp>
+
+struct AppRenderTargets {
+	vkcv::ImageHandle depthBuffer;
+	vkcv::ImageHandle colorBuffer;
+	vkcv::ImageHandle motionBuffer;
+};
+
+struct GraphicPassHandles {
+	vkcv::PipelineHandle        pipeline;
+	vkcv::PassHandle            renderPass;
+	vkcv::DescriptorSetHandle   descriptorSet;
+};
+
+struct ComputePassHandles {
+	vkcv::PipelineHandle        pipeline;
+	vkcv::DescriptorSetHandle   descriptorSet;
+};
+
+struct MeshResources {
+	vkcv::Mesh          mesh;
+	vkcv::BufferHandle  vertexBuffer;
+	vkcv::BufferHandle  indexBuffer;
+};
+
+// loads position, uv and normal of the first mesh in a scene
+bool loadMesh(vkcv::Core& core, const std::filesystem::path& path, MeshResources* outMesh);
+
+bool loadImage(vkcv::Core& core, const std::filesystem::path& path, vkcv::ImageHandle* outImage);
+
+bool loadGraphicPass(
+	vkcv::Core& core,
+	const std::filesystem::path vertexPath,
+	const std::filesystem::path fragmentPath,
+	const vkcv::PassConfig&     passConfig,
+	const vkcv::DepthTest       depthTest,
+	GraphicPassHandles*         outPassHandles);
+
+bool loadMeshPass  (vkcv::Core& core, GraphicPassHandles* outHandles);
+bool loadSkyPass   (vkcv::Core& core, GraphicPassHandles* outHandles);
+bool loadPrePass   (vkcv::Core& core, GraphicPassHandles* outHandles);
+bool loadSkyPrePass(vkcv::Core& core, GraphicPassHandles* outHandles);
+
+bool loadComputePass(vkcv::Core& core, const std::filesystem::path& path, ComputePassHandles* outComputePass);
+
+AppRenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const uint32_t height);
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlur.cpp b/projects/indirect_dispatch/src/MotionBlur.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..49f650a97e2fea5821959ae53f468e6fe7de6ffe
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlur.cpp
@@ -0,0 +1,437 @@
+#include "MotionBlur.hpp"
+#include "MotionBlurConfig.hpp"
+#include "MotionBlurSetup.hpp"
+#include <array>
+
+std::array<uint32_t, 3> computeFullscreenDispatchSize(
+	const uint32_t imageWidth,
+	const uint32_t imageHeight,
+	const uint32_t localGroupSize) {
+
+	// optimized divide and ceil
+	return std::array<uint32_t, 3>{
+		static_cast<uint32_t>(imageWidth  + (localGroupSize - 1)) / localGroupSize,
+		static_cast<uint32_t>(imageHeight + (localGroupSize - 1)) / localGroupSize,
+		static_cast<uint32_t>(1) };
+}
+
+bool MotionBlur::initialize(vkcv::Core* corePtr, const uint32_t targetWidth, const uint32_t targetHeight) {
+
+	if (!corePtr) {
+		vkcv_log(vkcv::LogLevel::ERROR, "MotionBlur got invalid corePtr")
+		return false;
+	}
+
+	m_core = corePtr;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlur.comp", &m_motionBlurPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionVectorMinMax.comp", &m_motionVectorMinMaxPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionVectorMinMaxNeighbourhood.comp", &m_motionVectorMinMaxNeighbourhoodPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionVectorVisualisation.comp", &m_motionVectorVisualisationPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlurColorCopy.comp", &m_colorCopyPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlurTileClassification.comp", &m_tileClassificationPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlurWorkTileReset.comp", &m_tileResetPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlurTileClassificationVis.comp", &m_tileVisualisationPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlurFastPath.comp", &m_motionBlurFastPathPass))
+		return false;
+
+	// work tile buffers and descriptors
+	const uint32_t workTileBufferSize = static_cast<uint32_t>(2 * sizeof(uint32_t)) * (3 +
+		((MotionBlurConfig::maxWidth + MotionBlurConfig::maxMotionTileSize - 1) / MotionBlurConfig::maxMotionTileSize) *
+		((MotionBlurConfig::maxHeight + MotionBlurConfig::maxMotionTileSize - 1) / MotionBlurConfig::maxMotionTileSize));
+
+	m_copyPathWorkTileBuffer = m_core->createBuffer<uint32_t>(
+		vkcv::BufferType::STORAGE, 
+		workTileBufferSize, 
+		vkcv::BufferMemoryType::DEVICE_LOCAL, 
+		true).getHandle();
+
+	m_fullPathWorkTileBuffer = m_core->createBuffer<uint32_t>(
+		vkcv::BufferType::STORAGE, 
+		workTileBufferSize, 
+		vkcv::BufferMemoryType::DEVICE_LOCAL, 
+		true).getHandle();
+
+	m_fastPathWorkTileBuffer = m_core->createBuffer<uint32_t>(
+		vkcv::BufferType::STORAGE,
+		workTileBufferSize,
+		vkcv::BufferMemoryType::DEVICE_LOCAL,
+		true).getHandle();
+
+	vkcv::DescriptorWrites tileResetDescriptorWrites;
+	tileResetDescriptorWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(0, m_fullPathWorkTileBuffer),
+		vkcv::BufferDescriptorWrite(1, m_copyPathWorkTileBuffer),
+		vkcv::BufferDescriptorWrite(2, m_fastPathWorkTileBuffer) };
+
+	m_core->writeDescriptorSet(m_tileResetPass.descriptorSet, tileResetDescriptorWrites);
+
+
+	m_renderTargets = MotionBlurSetup::createRenderTargets(targetWidth, targetHeight, *m_core);
+
+	m_nearestSampler = m_core->createSampler(
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerFilterType::NEAREST,
+		vkcv::SamplerMipmapMode::NEAREST,
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
+	
+	return true;
+}
+
+void MotionBlur::setResolution(const uint32_t targetWidth, const uint32_t targetHeight) {
+	m_renderTargets = MotionBlurSetup::createRenderTargets(targetWidth, targetHeight, *m_core);
+}
+
+vkcv::ImageHandle MotionBlur::render(
+	const vkcv::CommandStreamHandle cmdStream,
+	const vkcv::ImageHandle         motionBufferFullRes,
+	const vkcv::ImageHandle         colorBuffer,
+	const vkcv::ImageHandle         depthBuffer,
+	const eMotionBlurMode           mode,
+	const float                     cameraNear,
+	const float                     cameraFar,
+	const float                     deltaTimeSeconds,
+	const float                     cameraShutterSpeedInverse,
+	const float                     motionTileOffsetLength,
+	const float                     fastPathThreshold) {
+
+	computeMotionTiles(cmdStream, motionBufferFullRes);
+
+	// work tile reset
+	const uint32_t dispatchSizeOne[3] = { 1, 1, 1 };
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_tileResetPass.pipeline,
+		dispatchSizeOne,
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileResetPass.descriptorSet).vulkanHandle) },
+		vkcv::PushConstants(0));
+
+	m_core->recordBufferMemoryBarrier(cmdStream, m_fullPathWorkTileBuffer);
+	m_core->recordBufferMemoryBarrier(cmdStream, m_copyPathWorkTileBuffer);
+	m_core->recordBufferMemoryBarrier(cmdStream, m_fastPathWorkTileBuffer);
+
+	// work tile classification
+	vkcv::DescriptorWrites tileClassificationDescriptorWrites;
+	tileClassificationDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, m_renderTargets.motionMaxNeighbourhood),
+		vkcv::SampledImageDescriptorWrite(1, m_renderTargets.motionMinNeighbourhood) };
+	tileClassificationDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(2, m_nearestSampler) };
+	tileClassificationDescriptorWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(3, m_fullPathWorkTileBuffer),
+		vkcv::BufferDescriptorWrite(4, m_copyPathWorkTileBuffer),
+		vkcv::BufferDescriptorWrite(5, m_fastPathWorkTileBuffer) };
+
+	m_core->writeDescriptorSet(m_tileClassificationPass.descriptorSet, tileClassificationDescriptorWrites);
+
+	const auto tileClassificationDispatch = computeFullscreenDispatchSize(
+		m_core->getImageWidth(m_renderTargets.motionMaxNeighbourhood), 
+		m_core->getImageHeight(m_renderTargets.motionMaxNeighbourhood),
+		8);
+
+	struct ClassificationConstants {
+		uint32_t    width;
+		uint32_t    height;
+		float       fastPathThreshold;
+	};
+	ClassificationConstants classificationConstants;
+	classificationConstants.width               = m_core->getImageWidth(m_renderTargets.outputColor);
+	classificationConstants.height              = m_core->getImageHeight(m_renderTargets.outputColor);
+	classificationConstants.fastPathThreshold   = fastPathThreshold;
+
+	vkcv::PushConstants classificationPushConstants(sizeof(ClassificationConstants));
+    classificationPushConstants.appendDrawcall(classificationConstants);
+
+	m_core->prepareImageForSampling(cmdStream, m_renderTargets.motionMaxNeighbourhood);
+	m_core->prepareImageForSampling(cmdStream, m_renderTargets.motionMinNeighbourhood);
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_tileClassificationPass.pipeline,
+		tileClassificationDispatch.data(),
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileClassificationPass.descriptorSet).vulkanHandle) },
+		classificationPushConstants);
+
+	m_core->recordBufferMemoryBarrier(cmdStream, m_fullPathWorkTileBuffer);
+	m_core->recordBufferMemoryBarrier(cmdStream, m_copyPathWorkTileBuffer);
+	m_core->recordBufferMemoryBarrier(cmdStream, m_fastPathWorkTileBuffer);
+
+	vkcv::DescriptorWrites motionBlurDescriptorWrites;
+	motionBlurDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, colorBuffer),
+		vkcv::SampledImageDescriptorWrite(1, depthBuffer),
+		vkcv::SampledImageDescriptorWrite(2, motionBufferFullRes),
+		vkcv::SampledImageDescriptorWrite(3, m_renderTargets.motionMaxNeighbourhood) };
+	motionBlurDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(4, m_nearestSampler) };
+	motionBlurDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(5, m_renderTargets.outputColor) };
+	motionBlurDescriptorWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(6, m_fullPathWorkTileBuffer)};
+
+	m_core->writeDescriptorSet(m_motionBlurPass.descriptorSet, motionBlurDescriptorWrites);
+
+
+	vkcv::DescriptorWrites colorCopyDescriptorWrites;
+	colorCopyDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, colorBuffer) };
+	colorCopyDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
+	colorCopyDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(2, m_renderTargets.outputColor) };
+	colorCopyDescriptorWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(3, m_copyPathWorkTileBuffer) };
+
+	m_core->writeDescriptorSet(m_colorCopyPass.descriptorSet, colorCopyDescriptorWrites);
+
+
+	vkcv::DescriptorWrites fastPathDescriptorWrites;
+	fastPathDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, colorBuffer),
+		vkcv::SampledImageDescriptorWrite(1, m_renderTargets.motionMaxNeighbourhood) };
+	fastPathDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(2, m_nearestSampler) };
+	fastPathDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(3, m_renderTargets.outputColor) };
+	fastPathDescriptorWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(4, m_fastPathWorkTileBuffer) };
+
+	m_core->writeDescriptorSet(m_motionBlurFastPathPass.descriptorSet, fastPathDescriptorWrites);
+
+	// must match layout in "motionBlur.comp"
+	struct MotionBlurConstantData {
+		float motionFactor;
+		float cameraNearPlane;
+		float cameraFarPlane;
+		float motionTileOffsetLength;
+	};
+	MotionBlurConstantData motionBlurConstantData;
+
+	const float deltaTimeMotionBlur = deltaTimeSeconds;
+
+	motionBlurConstantData.motionFactor             = 1 / (deltaTimeMotionBlur * cameraShutterSpeedInverse);
+	motionBlurConstantData.cameraNearPlane          = cameraNear;
+	motionBlurConstantData.cameraFarPlane           = cameraFar;
+	motionBlurConstantData.motionTileOffsetLength   = motionTileOffsetLength;
+
+	vkcv::PushConstants motionBlurPushConstants(sizeof(motionBlurConstantData));
+	motionBlurPushConstants.appendDrawcall(motionBlurConstantData);
+
+	struct FastPathConstants {
+		float motionFactor;
+	};
+	FastPathConstants fastPathConstants;
+	fastPathConstants.motionFactor = motionBlurConstantData.motionFactor;
+
+	vkcv::PushConstants fastPathPushConstants(sizeof(FastPathConstants));
+	fastPathPushConstants.appendDrawcall(fastPathConstants);
+
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.outputColor);
+	m_core->prepareImageForSampling(cmdStream, colorBuffer);
+	m_core->prepareImageForSampling(cmdStream, depthBuffer);
+	m_core->prepareImageForSampling(cmdStream, m_renderTargets.motionMaxNeighbourhood);
+
+	if (mode == eMotionBlurMode::Default) {
+		m_core->recordComputeIndirectDispatchToCmdStream(
+			cmdStream,
+			m_motionBlurPass.pipeline,
+			m_fullPathWorkTileBuffer,
+			0,
+			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionBlurPass.descriptorSet).vulkanHandle) },
+			motionBlurPushConstants);
+
+		m_core->recordComputeIndirectDispatchToCmdStream(
+			cmdStream,
+			m_colorCopyPass.pipeline,
+			m_copyPathWorkTileBuffer,
+			0,
+			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_colorCopyPass.descriptorSet).vulkanHandle) },
+			vkcv::PushConstants(0));
+
+		m_core->recordComputeIndirectDispatchToCmdStream(
+			cmdStream,
+			m_motionBlurFastPathPass.pipeline,
+			m_fastPathWorkTileBuffer,
+			0,
+			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionBlurFastPathPass.descriptorSet).vulkanHandle) },
+			fastPathPushConstants);
+	}
+	else if(mode == eMotionBlurMode::Disabled) {
+		return colorBuffer;
+	}
+	else if (mode == eMotionBlurMode::TileVisualisation) {
+
+		vkcv::DescriptorWrites visualisationDescriptorWrites;
+		visualisationDescriptorWrites.sampledImageWrites = { 
+			vkcv::SampledImageDescriptorWrite(0, colorBuffer) };
+		visualisationDescriptorWrites.samplerWrites = {
+			vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
+		visualisationDescriptorWrites.storageImageWrites = {
+			vkcv::StorageImageDescriptorWrite(2, m_renderTargets.outputColor)};
+		visualisationDescriptorWrites.storageBufferWrites = {
+			vkcv::BufferDescriptorWrite(3, m_fullPathWorkTileBuffer),
+			vkcv::BufferDescriptorWrite(4, m_copyPathWorkTileBuffer),
+			vkcv::BufferDescriptorWrite(5, m_fastPathWorkTileBuffer) };
+
+		m_core->writeDescriptorSet(m_tileVisualisationPass.descriptorSet, visualisationDescriptorWrites);
+
+		const uint32_t tileCount = 
+			(m_core->getImageWidth(m_renderTargets.outputColor)  + MotionBlurConfig::maxMotionTileSize - 1) / MotionBlurConfig::maxMotionTileSize * 
+			(m_core->getImageHeight(m_renderTargets.outputColor) + MotionBlurConfig::maxMotionTileSize - 1) / MotionBlurConfig::maxMotionTileSize;
+
+		const uint32_t dispatchCounts[3] = {
+			tileCount,
+			1,
+			1 };
+
+		m_core->recordComputeDispatchToCmdStream(
+			cmdStream,
+			m_tileVisualisationPass.pipeline,
+			dispatchCounts,
+			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileVisualisationPass.descriptorSet).vulkanHandle) },
+			vkcv::PushConstants(0));
+	}
+	else {
+		vkcv_log(vkcv::LogLevel::ERROR, "Unknown eMotionBlurMode enum option");
+		return colorBuffer;
+	}
+
+	return m_renderTargets.outputColor;
+}
+
+vkcv::ImageHandle MotionBlur::renderMotionVectorVisualisation(
+	const vkcv::CommandStreamHandle         cmdStream,
+	const vkcv::ImageHandle                 motionBuffer,
+	const eMotionVectorVisualisationMode    mode,
+	const float                             velocityRange) {
+
+	computeMotionTiles(cmdStream, motionBuffer);
+
+	vkcv::ImageHandle visualisationInput;
+	if (     mode == eMotionVectorVisualisationMode::FullResolution)
+		visualisationInput = motionBuffer;
+	else if (mode == eMotionVectorVisualisationMode::MaxTile)
+		visualisationInput = m_renderTargets.motionMax;
+	else if (mode == eMotionVectorVisualisationMode::MaxTileNeighbourhood)
+		visualisationInput = m_renderTargets.motionMaxNeighbourhood;
+	else if (mode == eMotionVectorVisualisationMode::MinTile)
+		visualisationInput = m_renderTargets.motionMin;
+	else if (mode == eMotionVectorVisualisationMode::MinTileNeighbourhood)
+		visualisationInput = m_renderTargets.motionMinNeighbourhood;
+	else if (mode == eMotionVectorVisualisationMode::None) {
+		vkcv_log(vkcv::LogLevel::ERROR, "renderMotionVectorVisualisation called with visualisation mode 'None'");
+		return motionBuffer;
+	}
+	else {
+		vkcv_log(vkcv::LogLevel::ERROR, "Unknown eDebugView enum value");
+		return motionBuffer;
+	}
+
+	vkcv::DescriptorWrites motionVectorVisualisationDescriptorWrites;
+	motionVectorVisualisationDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, visualisationInput) };
+	motionVectorVisualisationDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
+	motionVectorVisualisationDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(2, m_renderTargets.outputColor) };
+
+	m_core->writeDescriptorSet(
+		m_motionVectorVisualisationPass.descriptorSet,
+		motionVectorVisualisationDescriptorWrites);
+
+	m_core->prepareImageForSampling(cmdStream, visualisationInput);
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.outputColor);
+
+	vkcv::PushConstants motionVectorVisualisationPushConstants(sizeof(float));
+	motionVectorVisualisationPushConstants.appendDrawcall(velocityRange);
+
+	const auto dispatchSizes = computeFullscreenDispatchSize(
+		m_core->getImageWidth(m_renderTargets.outputColor), 
+		m_core->getImageHeight(m_renderTargets.outputColor), 
+		8);
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_motionVectorVisualisationPass.pipeline,
+		dispatchSizes.data(),
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorVisualisationPass.descriptorSet).vulkanHandle) },
+		motionVectorVisualisationPushConstants);
+
+	return m_renderTargets.outputColor;
+}
+
+void MotionBlur::computeMotionTiles(
+	const vkcv::CommandStreamHandle cmdStream,
+	const vkcv::ImageHandle         motionBufferFullRes) {
+
+	// motion vector min max tiles
+	vkcv::DescriptorWrites motionVectorMaxTilesDescriptorWrites;
+	motionVectorMaxTilesDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, motionBufferFullRes) };
+	motionVectorMaxTilesDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
+	motionVectorMaxTilesDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(2, m_renderTargets.motionMax),
+		vkcv::StorageImageDescriptorWrite(3, m_renderTargets.motionMin) };
+
+	m_core->writeDescriptorSet(m_motionVectorMinMaxPass.descriptorSet, motionVectorMaxTilesDescriptorWrites);
+
+	m_core->prepareImageForSampling(cmdStream, motionBufferFullRes);
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.motionMax);
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.motionMin);
+
+	const std::array<uint32_t, 3> motionTileDispatchCounts = computeFullscreenDispatchSize(
+		m_core->getImageWidth( m_renderTargets.motionMax),
+		m_core->getImageHeight(m_renderTargets.motionMax),
+		8);
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_motionVectorMinMaxPass.pipeline,
+		motionTileDispatchCounts.data(),
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorMinMaxPass.descriptorSet).vulkanHandle) },
+		vkcv::PushConstants(0));
+
+	// motion vector min max neighbourhood
+	vkcv::DescriptorWrites motionVectorMaxNeighbourhoodDescriptorWrites;
+	motionVectorMaxNeighbourhoodDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, m_renderTargets.motionMax),
+		vkcv::SampledImageDescriptorWrite(1, m_renderTargets.motionMin) };
+	motionVectorMaxNeighbourhoodDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(2, m_nearestSampler) };
+	motionVectorMaxNeighbourhoodDescriptorWrites.storageImageWrites = {
+		vkcv::StorageImageDescriptorWrite(3, m_renderTargets.motionMaxNeighbourhood),
+		vkcv::StorageImageDescriptorWrite(4, m_renderTargets.motionMinNeighbourhood) };
+
+	m_core->writeDescriptorSet(m_motionVectorMinMaxNeighbourhoodPass.descriptorSet, motionVectorMaxNeighbourhoodDescriptorWrites);
+
+	m_core->prepareImageForSampling(cmdStream, m_renderTargets.motionMax);
+	m_core->prepareImageForSampling(cmdStream, m_renderTargets.motionMin);
+
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.motionMaxNeighbourhood);
+	m_core->prepareImageForStorage(cmdStream, m_renderTargets.motionMinNeighbourhood);
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_motionVectorMinMaxNeighbourhoodPass.pipeline,
+		motionTileDispatchCounts.data(),
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorMinMaxNeighbourhoodPass.descriptorSet).vulkanHandle) },
+		vkcv::PushConstants(0));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlur.hpp b/projects/indirect_dispatch/src/MotionBlur.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b50f0af60d566dc0e4fc00c31b7b834e11679bf5
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlur.hpp
@@ -0,0 +1,84 @@
+#pragma once
+#include <vkcv/Core.hpp>
+#include "AppSetup.hpp"
+#include "MotionBlurSetup.hpp"
+
+// selection for motion blur input and visualisation
+enum class eMotionVectorVisualisationMode : int {
+	None                    = 0,
+	FullResolution          = 1,
+	MaxTile                 = 2,
+	MaxTileNeighbourhood    = 3,
+	MinTile                 = 4,
+	MinTileNeighbourhood    = 5,
+	OptionCount             = 6 };
+
+static const char* MotionVectorVisualisationModeLabels[6] = {
+	"None",
+	"Full resolution",
+	"Max tile",
+	"Tile neighbourhood max",
+	"Min Tile",
+	"Tile neighbourhood min"};
+
+enum class eMotionBlurMode : int {
+	Default             = 0,
+	Disabled            = 1,
+	TileVisualisation   = 2,
+	OptionCount         = 3 };
+
+static const char* MotionBlurModeLabels[3] = {
+	"Default",
+	"Disabled",
+	"Tile visualisation" };
+
+class MotionBlur {
+public:
+
+	bool initialize(vkcv::Core* corePtr, const uint32_t targetWidth, const uint32_t targetHeight);
+	void setResolution(const uint32_t targetWidth, const uint32_t targetHeight);
+
+	vkcv::ImageHandle render(
+		const vkcv::CommandStreamHandle cmdStream,
+		const vkcv::ImageHandle         motionBufferFullRes,
+		const vkcv::ImageHandle         colorBuffer,
+		const vkcv::ImageHandle         depthBuffer,
+		const eMotionBlurMode           mode,
+		const float                     cameraNear,
+		const float                     cameraFar,
+		const float                     deltaTimeSeconds,
+		const float                     cameraShutterSpeedInverse,
+		const float                     motionTileOffsetLength,
+		const float                     fastPathThreshold);
+
+	vkcv::ImageHandle renderMotionVectorVisualisation(
+		const vkcv::CommandStreamHandle         cmdStream,
+		const vkcv::ImageHandle                 motionBuffer,
+		const eMotionVectorVisualisationMode    mode,
+		const float                             velocityRange);
+
+private:
+	// computes max per tile and neighbourhood tile max
+	void computeMotionTiles(
+		const vkcv::CommandStreamHandle cmdStream,
+		const vkcv::ImageHandle         motionBufferFullRes);
+
+	vkcv::Core* m_core;
+
+	MotionBlurRenderTargets m_renderTargets;
+	vkcv::SamplerHandle     m_nearestSampler;
+
+	ComputePassHandles m_motionBlurPass;
+	ComputePassHandles m_motionVectorMinMaxPass;
+	ComputePassHandles m_motionVectorMinMaxNeighbourhoodPass;
+	ComputePassHandles m_motionVectorVisualisationPass;
+	ComputePassHandles m_colorCopyPass;
+	ComputePassHandles m_tileClassificationPass;
+	ComputePassHandles m_tileResetPass;
+	ComputePassHandles m_tileVisualisationPass;
+	ComputePassHandles m_motionBlurFastPathPass;
+
+	vkcv::BufferHandle m_fullPathWorkTileBuffer;
+	vkcv::BufferHandle m_copyPathWorkTileBuffer;
+	vkcv::BufferHandle m_fastPathWorkTileBuffer;
+};
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlurConfig.hpp b/projects/indirect_dispatch/src/MotionBlurConfig.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7552abd246ca8d2e7489c5065f43ef8b48af7cd2
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlurConfig.hpp
@@ -0,0 +1,10 @@
+#pragma once
+#include "vulkan/vulkan.hpp"
+
+namespace MotionBlurConfig {
+	const vk::Format    motionVectorTileFormat  = vk::Format::eR16G16Sfloat;
+	const vk::Format    outputColorFormat       = vk::Format::eB10G11R11UfloatPack32;
+	const uint32_t      maxMotionTileSize       = 16;	// must match "motionTileSize" in motionBlurConfig.inc
+	const uint32_t      maxWidth                = 3840;
+	const uint32_t      maxHeight               = 2160;
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlurSetup.cpp b/projects/indirect_dispatch/src/MotionBlurSetup.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..82d2593a5b976f9389b58dddac43e3a45d1db303
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlurSetup.cpp
@@ -0,0 +1,57 @@
+#include "MotionBlurSetup.hpp"
+#include "MotionBlurConfig.hpp"
+
+namespace MotionBlurSetup {
+
+MotionBlurRenderTargets createRenderTargets(const uint32_t width, const uint32_t height, vkcv::Core& core) {
+
+	MotionBlurRenderTargets targets;
+
+	// divide and ceil to int
+	const uint32_t motionMaxWidth  = (width  + (MotionBlurConfig::maxMotionTileSize - 1)) / MotionBlurConfig::maxMotionTileSize;
+	const uint32_t motionMaxheight = (height + (MotionBlurConfig::maxMotionTileSize - 1)) / MotionBlurConfig::maxMotionTileSize;
+
+	targets.motionMax = core.createImage(
+		MotionBlurConfig::motionVectorTileFormat,
+		motionMaxWidth,
+		motionMaxheight,
+		1,
+		false,
+		true).getHandle();
+
+	targets.motionMaxNeighbourhood = core.createImage(
+		MotionBlurConfig::motionVectorTileFormat,
+		motionMaxWidth,
+		motionMaxheight,
+		1,
+		false,
+		true).getHandle();
+
+	targets.motionMin = core.createImage(
+		MotionBlurConfig::motionVectorTileFormat,
+		motionMaxWidth,
+		motionMaxheight,
+		1,
+		false,
+		true).getHandle();
+
+	targets.motionMinNeighbourhood = core.createImage(
+		MotionBlurConfig::motionVectorTileFormat,
+		motionMaxWidth,
+		motionMaxheight,
+		1,
+		false,
+		true).getHandle();
+
+	targets.outputColor = core.createImage(
+		MotionBlurConfig::outputColorFormat,
+		width,
+		height,
+		1,
+		false,
+		true).getHandle();
+
+	return targets;
+}
+
+}	// namespace MotionBlurSetup
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlurSetup.hpp b/projects/indirect_dispatch/src/MotionBlurSetup.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..ca169d7c6b04aa152d42ba36c3d2e02e563bbd91
--- /dev/null
+++ b/projects/indirect_dispatch/src/MotionBlurSetup.hpp
@@ -0,0 +1,14 @@
+#pragma once
+#include <vkcv/Core.hpp>
+
+struct MotionBlurRenderTargets {
+	vkcv::ImageHandle outputColor;
+	vkcv::ImageHandle motionMax;
+	vkcv::ImageHandle motionMaxNeighbourhood;
+	vkcv::ImageHandle motionMin;
+	vkcv::ImageHandle motionMinNeighbourhood;
+};
+
+namespace MotionBlurSetup {
+	MotionBlurRenderTargets createRenderTargets(const uint32_t width, const uint32_t height, vkcv::Core& core);
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/main.cpp b/projects/indirect_dispatch/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b27e0bcb8f1991d76b570b79da9cc4734cf52950
--- /dev/null
+++ b/projects/indirect_dispatch/src/main.cpp
@@ -0,0 +1,13 @@
+#include "App.hpp"
+
+int main(int argc, const char** argv) {
+
+	App app;
+	if (!app.initialize()) {
+		std::cerr << "Application initialization failed, exiting" << std::endl;
+		return 1;
+	}
+	app.run();
+
+	return 0;
+}
diff --git a/projects/mesh_shader/.gitignore b/projects/mesh_shader/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..54601c357bf3fb97b914a6e657c042a5c6a985d7
--- /dev/null
+++ b/projects/mesh_shader/.gitignore
@@ -0,0 +1 @@
+mesh_shader
diff --git a/projects/mesh_shader/CMakeLists.txt b/projects/mesh_shader/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1aa5d5ff3977a47dce75a38329216d550b1b9311
--- /dev/null
+++ b/projects/mesh_shader/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 3.16)
+project(mesh_shader)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# this should fix the execution path to load local files from the project
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+# adding source files to the project
+add_executable(mesh_shader src/main.cpp)
+
+target_sources(mesh_shader PRIVATE)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(mesh_shader PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(mesh_shader PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+
+	# in addition to setting the output directory, the working directory has to be set
+	# by default visual studio sets the working directory to the build directory, when using the debugger
+	set_target_properties(mesh_shader PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(mesh_shader SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_meshlet_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(mesh_shader vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_testing vkcv_camera vkcv_meshlet vkcv_shader_compiler vkcv_gui)
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/Bunny/Bunny.glb b/projects/mesh_shader/resources/Bunny/Bunny.glb
new file mode 100644
index 0000000000000000000000000000000000000000..181f1f92f1906e1e1ba900768580203efe19e9be
--- /dev/null
+++ b/projects/mesh_shader/resources/Bunny/Bunny.glb
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b8bc6fab11929ca11bdf4e892ffb03b621b10307f705cdea17d82d3dee3b9aae
+size 4045836
diff --git a/projects/mesh_shader/resources/monke.glb b/projects/mesh_shader/resources/monke.glb
new file mode 100644
index 0000000000000000000000000000000000000000..47d0b9131f15a8f0697318d0a47302c71cad1db8
--- /dev/null
+++ b/projects/mesh_shader/resources/monke.glb
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:597584db90a3f51088beea6652d8320e82cb025f9d3d036b89e54ad72c732a06
+size 98612
diff --git a/projects/mesh_shader/resources/shaders/common.inc b/projects/mesh_shader/resources/shaders/common.inc
new file mode 100644
index 0000000000000000000000000000000000000000..280ffee215a8b8342b78d1f5558d63a05e16859b
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/common.inc
@@ -0,0 +1,4 @@
+struct ObjectMatrices{
+    mat4 model;
+    mat4 mvp;
+};
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/meshlet.inc b/projects/mesh_shader/resources/shaders/meshlet.inc
new file mode 100644
index 0000000000000000000000000000000000000000..0594f62ceead8ffca09b585305075eb6046f3c46
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/meshlet.inc
@@ -0,0 +1,8 @@
+struct Meshlet{
+    uint    vertexOffset;
+    uint    vertexCount;
+    uint    indexOffset;
+    uint    indexCount;
+    vec3    meanPosition;
+    float   boundingSphereRadius;
+};
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/shader.frag b/projects/mesh_shader/resources/shaders/shader.frag
new file mode 100644
index 0000000000000000000000000000000000000000..f4f6982f2089e6c8e102027f3b8763bb38f8e59c
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/shader.frag
@@ -0,0 +1,32 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in  vec3 passNormal;
+layout(location = 1) in  flat uint passTaskIndex;
+layout(location = 0) out vec3 outColor;
+
+uint lowbias32(uint x)
+{
+    x ^= x >> 16;
+    x *= 0x7feb352dU;
+    x ^= x >> 15;
+    x *= 0x846ca68bU;
+    x ^= x >> 16;
+    return x;
+}
+
+float hashToFloat(uint hash){
+    return (hash % 255) / 255.f;
+}
+
+vec3 colorFromIndex(uint i){
+    return vec3(
+        hashToFloat(lowbias32(i+0)),
+        hashToFloat(lowbias32(i+1)),
+        hashToFloat(lowbias32(i+2)));
+}
+
+void main() {
+	outColor = normalize(passNormal) * 0.5 + 0.5;
+    outColor = colorFromIndex(passTaskIndex);
+}
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/shader.mesh b/projects/mesh_shader/resources/shaders/shader.mesh
new file mode 100644
index 0000000000000000000000000000000000000000..30c98610f4776204ff526c57c1f793e371194629
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/shader.mesh
@@ -0,0 +1,78 @@
+#version 460
+#extension GL_ARB_separate_shader_objects   : enable
+#extension GL_GOOGLE_include_directive      : enable
+#extension GL_NV_mesh_shader                : require
+
+#include "meshlet.inc"
+
+layout(local_size_x=32) in;
+
+layout(triangles) out;
+layout(max_vertices=64, max_primitives=126) out;
+
+layout(location = 0) out vec3 passNormal[];
+layout(location = 1) out uint passTaskIndex[];
+
+struct Vertex
+{
+    vec3 position;  float padding0;
+    vec3 normal;    float padding1;
+};
+
+layout(std430, binding = 0) readonly buffer vertexBuffer
+{
+    Vertex vertices[];
+};
+
+layout(std430, binding = 1) readonly buffer indexBuffer
+{
+    uint localIndices[]; // breaks for 16 bit indices
+};
+
+layout(std430, binding = 2) readonly buffer meshletBuffer
+{
+    Meshlet meshlets[];
+};
+
+taskNV in Task {
+  uint meshletIndices[32];
+  mat4 mvp;
+} IN;
+
+void main()	{
+    
+    uint meshletIndex = IN.meshletIndices[gl_WorkGroupID.x];
+    Meshlet meshlet = meshlets[meshletIndex];
+    
+    // set vertices
+    for(uint i = 0; i < 2; i++){
+    
+        uint workIndex = gl_LocalInvocationID.x + 32 * i;
+        if(workIndex >= meshlet.vertexCount){
+            break;
+        }
+    
+        uint vertexIndex    = meshlet.vertexOffset + workIndex;
+        Vertex vertex       = vertices[vertexIndex];
+    
+        gl_MeshVerticesNV[workIndex].gl_Position    = IN.mvp * vec4(vertex.position, 1);
+        passNormal[workIndex]                       = vertex.normal;
+        passTaskIndex[workIndex]                    = meshletIndex;
+    }
+    
+    // set local indices
+    for(uint i = 0; i < 12; i++){
+    
+        uint workIndex = gl_LocalInvocationID.x + i * 32;
+        if(workIndex >= meshlet.indexCount){
+            break;
+        }    
+        
+        uint indexBufferIndex               = meshlet.indexOffset + workIndex;
+        gl_PrimitiveIndicesNV[workIndex]    = localIndices[indexBufferIndex];
+    }
+    
+    if(gl_LocalInvocationID.x == 0){
+        gl_PrimitiveCountNV = meshlet.indexCount / 3;
+    }
+}
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/shader.task b/projects/mesh_shader/resources/shaders/shader.task
new file mode 100644
index 0000000000000000000000000000000000000000..7a692e98e6384767191d76cef940e295ca127d62
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/shader.task
@@ -0,0 +1,78 @@
+#version 460
+#extension GL_ARB_separate_shader_objects   : enable
+#extension GL_NV_mesh_shader                : require
+#extension GL_GOOGLE_include_directive      : enable
+
+#include "meshlet.inc"
+#include "common.inc"
+
+layout(local_size_x=32) in;
+
+taskNV out Task {
+  uint meshletIndices[32];
+  mat4 mvp;
+} OUT;
+
+layout( push_constant ) uniform constants{
+    uint matrixIndex;
+    uint meshletCount;
+};
+
+// TODO: reuse mesh stage binding at location 2 after required fix in framework
+layout(std430, binding = 5) readonly buffer meshletBuffer
+{
+    Meshlet meshlets[];
+};
+
+struct Plane{
+    vec3    pointOnPlane;
+    float   padding0;
+    vec3    normal;
+    float   padding1;
+};
+
+layout(set=0, binding=3, std140) uniform cameraPlaneBuffer{
+    Plane cameraPlanes[6];
+};
+
+layout(std430, binding = 4) readonly buffer matrixBuffer
+{
+    ObjectMatrices objectMatrices[];
+};
+
+shared uint taskCount;
+
+bool isSphereInsideFrustum(vec3 spherePos, float sphereRadius, Plane cameraPlanes[6]){
+    bool isInside = true;
+    for(int i = 0; i < 6; i++){
+        Plane p     = cameraPlanes[i];
+        isInside    = isInside && dot(p.normal, spherePos - p.pointOnPlane) - sphereRadius < 0;
+    }
+    return isInside;
+}
+
+void main() {
+
+    if(gl_LocalInvocationID.x >= meshletCount){
+        return;
+    }
+    
+    uint meshletIndex   = gl_GlobalInvocationID.x;
+    Meshlet meshlet     = meshlets[meshletIndex]; 
+    
+    if(gl_LocalInvocationID.x == 0){
+        taskCount = 0;
+    }
+    
+    // TODO: scaling support
+    vec3 meshletPositionWorld = (vec4(meshlet.meanPosition, 1) * objectMatrices[matrixIndex].model).xyz;
+    if(isSphereInsideFrustum(meshletPositionWorld, meshlet.boundingSphereRadius, cameraPlanes)){
+        uint outIndex = atomicAdd(taskCount, 1);
+        OUT.meshletIndices[outIndex] = gl_GlobalInvocationID.x;
+    }
+
+    if(gl_LocalInvocationID.x == 0){
+        gl_TaskCountNV              = taskCount;
+        OUT.mvp = objectMatrices[matrixIndex].mvp;
+    }
+}
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/shader.vert b/projects/mesh_shader/resources/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..fca5057976f995183c040195bdbd592c63f1074e
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/shader.vert
@@ -0,0 +1,29 @@
+#version 450
+#extension GL_ARB_separate_shader_objects   : enable
+#extension GL_GOOGLE_include_directive      : enable
+
+#include "common.inc"
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 1) in vec3 inNormal;
+
+layout(location = 0) out vec3 passNormal;
+layout(location = 1) out uint dummyOutput;
+
+layout(std430, binding = 0) readonly buffer matrixBuffer
+{
+    ObjectMatrices objectMatrices[];
+};
+
+layout( push_constant ) uniform constants{
+    uint matrixIndex;
+    uint padding; // pad to same size as mesh shader constants
+};
+
+
+void main()	{
+	gl_Position = objectMatrices[matrixIndex].mvp * vec4(inPosition, 1.0);
+	passNormal  = inNormal;
+    
+    dummyOutput = padding * 0;  // padding must be used, else compiler shrinks constant size
+}
\ No newline at end of file
diff --git a/projects/mesh_shader/src/main.cpp b/projects/mesh_shader/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..611a324f875f5726ebd674e3ee51d27ad2d8e849
--- /dev/null
+++ b/projects/mesh_shader/src/main.cpp
@@ -0,0 +1,387 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <chrono>
+
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/gui/GUI.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/meshlet/Meshlet.hpp>
+#include <vkcv/meshlet/Tipsify.hpp>
+#include <vkcv/meshlet/Forsyth.hpp>
+
+struct Plane {
+	glm::vec3 pointOnPlane;
+	float padding0;
+	glm::vec3 normal;
+	float padding1;
+};
+
+struct CameraPlanes {
+	Plane planes[6];
+};
+
+CameraPlanes computeCameraPlanes(const vkcv::camera::Camera& camera) {
+	const float     fov     = camera.getFov();
+	const glm::vec3 pos     = camera.getPosition();
+	const float     ratio   = camera.getRatio();
+	const glm::vec3 forward = glm::normalize(camera.getFront());
+	float near;
+	float far;
+	camera.getNearFar(near, far);
+
+	glm::vec3 up    = glm::vec3(0, -1, 0);
+	glm::vec3 right = glm::normalize(glm::cross(forward, up));
+	up              = glm::cross(forward, right);
+
+	const glm::vec3 nearCenter      = pos + forward * near;
+	const glm::vec3 farCenter       = pos + forward * far;
+
+	const float tanFovHalf          = glm::tan(fov / 2);
+
+	const glm::vec3 nearUpCenter    = nearCenter + up    * tanFovHalf * near;
+	const glm::vec3 nearDownCenter  = nearCenter - up    * tanFovHalf * near;
+	const glm::vec3 nearRightCenter = nearCenter + right * tanFovHalf * near * ratio;
+	const glm::vec3 nearLeftCenter  = nearCenter - right * tanFovHalf * near * ratio;
+
+	const glm::vec3 farUpCenter     = farCenter + up    * tanFovHalf * far;
+	const glm::vec3 farDownCenter   = farCenter - up    * tanFovHalf * far;
+	const glm::vec3 farRightCenter  = farCenter + right * tanFovHalf * far * ratio;
+	const glm::vec3 farLeftCenter   = farCenter - right * tanFovHalf * far * ratio;
+
+	CameraPlanes cameraPlanes;
+	// near
+	cameraPlanes.planes[0].pointOnPlane = nearCenter;
+	cameraPlanes.planes[0].normal       = -forward;
+	// far
+	cameraPlanes.planes[1].pointOnPlane = farCenter;
+	cameraPlanes.planes[1].normal       = forward;
+
+	// top
+	cameraPlanes.planes[2].pointOnPlane = nearUpCenter;
+	cameraPlanes.planes[2].normal       = glm::normalize(glm::cross(farUpCenter - nearUpCenter, right));
+	// bot
+	cameraPlanes.planes[3].pointOnPlane = nearDownCenter;
+	cameraPlanes.planes[3].normal       = glm::normalize(glm::cross(right, farDownCenter - nearDownCenter));
+
+	// right
+	cameraPlanes.planes[4].pointOnPlane = nearRightCenter;
+	cameraPlanes.planes[4].normal       = glm::normalize(glm::cross(up, farRightCenter - nearRightCenter));
+	// left
+	cameraPlanes.planes[5].pointOnPlane = nearLeftCenter;
+	cameraPlanes.planes[5].normal       = glm::normalize(glm::cross(farLeftCenter - nearLeftCenter, up));
+
+	return cameraPlanes;
+}
+
+int main(int argc, const char** argv) {
+	const char* applicationName = "Mesh shader";
+
+	const int windowWidth = 1280;
+	const int windowHeight = 720;
+	vkcv::Window window = vkcv::Window::create(
+		applicationName,
+		windowWidth,
+		windowHeight,
+		false
+	);
+
+	vkcv::Core core = vkcv::Core::create(
+		window,
+		applicationName,
+		VK_MAKE_VERSION(0, 0, 1),
+		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
+		{},
+		{ "VK_KHR_swapchain", VK_NV_MESH_SHADER_EXTENSION_NAME }
+	);
+
+    vkcv::gui::GUI gui (core, window);
+
+    vkcv::asset::Scene mesh;
+    const char* path = argc > 1 ? argv[1] : "resources/Bunny/Bunny.glb";
+    vkcv::asset::loadScene(path, mesh);
+
+    assert(!mesh.vertexGroups.empty());
+
+    auto vertexBuffer = core.createBuffer<uint8_t>(
+            vkcv::BufferType::VERTEX,
+            mesh.vertexGroups[0].vertexBuffer.data.size(),
+            vkcv::BufferMemoryType::DEVICE_LOCAL
+    );
+    vertexBuffer.fill(mesh.vertexGroups[0].vertexBuffer.data);
+
+    auto indexBuffer = core.createBuffer<uint8_t>(
+            vkcv::BufferType::INDEX,
+            mesh.vertexGroups[0].indexBuffer.data.size(),
+            vkcv::BufferMemoryType::DEVICE_LOCAL
+    );
+    indexBuffer.fill(mesh.vertexGroups[0].indexBuffer.data);
+
+	// format data for mesh shader
+	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
+
+	std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
+		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
+	});
+
+	const std::vector<vkcv::VertexBufferBinding> vertexBufferBindings = {
+			vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[0].offset), vertexBuffer.getVulkanHandle()),
+			vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[1].offset), vertexBuffer.getVulkanHandle()),
+			vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[2].offset), vertexBuffer.getVulkanHandle()) };
+
+	const auto& bunny = mesh.vertexGroups[0];
+	std::vector<vkcv::meshlet::Vertex> interleavedVertices = vkcv::meshlet::convertToVertices(bunny.vertexBuffer.data, bunny.numVertices, attributes[0], attributes[1]);
+	// mesh shader buffers
+	const auto& assetLoaderIndexBuffer                    = mesh.vertexGroups[0].indexBuffer;
+	std::vector<uint32_t> indexBuffer32Bit                = vkcv::meshlet::assetLoaderIndicesTo32BitIndices(assetLoaderIndexBuffer.data, assetLoaderIndexBuffer.type);
+    vkcv::meshlet::VertexCacheReorderResult tipsifyResult = vkcv::meshlet::tipsifyMesh(indexBuffer32Bit, interleavedVertices.size());
+    vkcv::meshlet::VertexCacheReorderResult forsythResult = vkcv::meshlet::forsythReorder(indexBuffer32Bit, interleavedVertices.size());
+
+    const auto meshShaderModelData = createMeshShaderModelData(interleavedVertices, forsythResult.indexBuffer, forsythResult.skippedIndices);
+
+	auto meshShaderVertexBuffer = core.createBuffer<vkcv::meshlet::Vertex>(
+		vkcv::BufferType::STORAGE,
+		meshShaderModelData.vertices.size());
+	meshShaderVertexBuffer.fill(meshShaderModelData.vertices);
+
+	auto meshShaderIndexBuffer = core.createBuffer<uint32_t>(
+		vkcv::BufferType::STORAGE,
+		meshShaderModelData.localIndices.size());
+	meshShaderIndexBuffer.fill(meshShaderModelData.localIndices);
+
+	auto meshletBuffer = core.createBuffer<vkcv::meshlet::Meshlet>(
+		vkcv::BufferType::STORAGE,
+		meshShaderModelData.meshlets.size(),
+		vkcv::BufferMemoryType::DEVICE_LOCAL
+		);
+	meshletBuffer.fill(meshShaderModelData.meshlets);
+
+	// attachments
+	const vkcv::AttachmentDescription present_color_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		core.getSwapchain().getFormat());
+
+    const vkcv::AttachmentDescription depth_attachment(
+            vkcv::AttachmentOperation::STORE,
+            vkcv::AttachmentOperation::CLEAR,
+            vk::Format::eD32Sfloat
+    );
+
+	vkcv::PassConfig bunnyPassDefinition({ present_color_attachment, depth_attachment });
+	vkcv::PassHandle renderPass = core.createPass(bunnyPassDefinition);
+
+	if (!renderPass)
+	{
+		std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	vkcv::ShaderProgram bunnyShaderProgram{};
+	vkcv::shader::GLSLCompiler compiler;
+	
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"),
+					 [&bunnyShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		 bunnyShaderProgram.addShader(shaderStage, path);
+	});
+	
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+					 [&bunnyShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		bunnyShaderProgram.addShader(shaderStage, path);
+	});
+
+    const std::vector<vkcv::VertexAttachment> vertexAttachments = bunnyShaderProgram.getVertexAttachments();
+    std::vector<vkcv::VertexBinding> bindings;
+    for (size_t i = 0; i < vertexAttachments.size(); i++) {
+        bindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
+    }
+    const vkcv::VertexLayout bunnyLayout (bindings);
+
+	vkcv::DescriptorSetHandle vertexShaderDescriptorSet = core.createDescriptorSet(bunnyShaderProgram.getReflectedDescriptors()[0]);
+
+	const vkcv::PipelineConfig bunnyPipelineDefinition {
+			bunnyShaderProgram,
+			(uint32_t)windowWidth,
+			(uint32_t)windowHeight,
+			renderPass,
+			{ bunnyLayout },
+			{ core.getDescriptorSet(vertexShaderDescriptorSet).layout },
+			false
+	};
+
+	struct ObjectMatrices {
+		glm::mat4 model;
+		glm::mat4 mvp;
+	};
+	const size_t objectCount = 1;
+	vkcv::Buffer<ObjectMatrices> matrixBuffer = core.createBuffer<ObjectMatrices>(vkcv::BufferType::STORAGE, objectCount);
+
+	vkcv::DescriptorWrites vertexShaderDescriptorWrites;
+	vertexShaderDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, matrixBuffer.getHandle()) };
+	core.writeDescriptorSet(vertexShaderDescriptorSet, vertexShaderDescriptorWrites);
+
+	vkcv::PipelineHandle bunnyPipeline = core.createGraphicsPipeline(bunnyPipelineDefinition);
+
+	if (!bunnyPipeline)
+	{
+		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	// mesh shader
+	vkcv::ShaderProgram meshShaderProgram;
+	compiler.compile(vkcv::ShaderStage::TASK, std::filesystem::path("resources/shaders/shader.task"),
+		[&meshShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		meshShaderProgram.addShader(shaderStage, path);
+	});
+
+	compiler.compile(vkcv::ShaderStage::MESH, std::filesystem::path("resources/shaders/shader.mesh"),
+		[&meshShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		meshShaderProgram.addShader(shaderStage, path);
+	});
+
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+		[&meshShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		meshShaderProgram.addShader(shaderStage, path);
+	});
+
+	uint32_t setID = 0;
+	vkcv::DescriptorSetHandle meshShaderDescriptorSet = core.createDescriptorSet( meshShaderProgram.getReflectedDescriptors()[setID]);
+	const vkcv::VertexLayout meshShaderLayout(bindings);
+
+	const vkcv::PipelineConfig meshShaderPipelineDefinition{
+		meshShaderProgram,
+		(uint32_t)windowWidth,
+		(uint32_t)windowHeight,
+		renderPass,
+		{meshShaderLayout},
+		{core.getDescriptorSet(meshShaderDescriptorSet).layout},
+		false
+	};
+
+	vkcv::PipelineHandle meshShaderPipeline = core.createGraphicsPipeline(meshShaderPipelineDefinition);
+
+	if (!meshShaderPipeline)
+	{
+		std::cout << "Error. Could not create mesh shader pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	vkcv::Buffer<CameraPlanes> cameraPlaneBuffer = core.createBuffer<CameraPlanes>(vkcv::BufferType::UNIFORM, 1);
+
+	vkcv::DescriptorWrites meshShaderWrites;
+	meshShaderWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(0, meshShaderVertexBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(1, meshShaderIndexBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(2, meshletBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(4, matrixBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(5, meshletBuffer.getHandle()),
+	};
+	meshShaderWrites.uniformBufferWrites = {
+		vkcv::BufferDescriptorWrite(3, cameraPlaneBuffer.getHandle()),
+	};
+
+    core.writeDescriptorSet( meshShaderDescriptorSet, meshShaderWrites);
+
+    vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight, 1, false).getHandle();
+
+    auto start = std::chrono::system_clock::now();
+
+	vkcv::ImageHandle swapchainImageHandle = vkcv::ImageHandle::createSwapchainImageHandle();
+
+    const vkcv::Mesh renderMesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), mesh.vertexGroups[0].numIndices, vkcv::IndexBitCount::Bit32);
+
+	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+	vkcv::camera::CameraManager cameraManager(window);
+	uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	
+	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -2));
+
+	bool useMeshShader          = true;
+	bool updateFrustumPlanes    = true;
+
+	while (window.isWindowOpen())
+	{
+		vkcv::Window::pollEvents();
+
+		uint32_t swapchainWidth, swapchainHeight; // No resizing = No problem
+		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
+			continue;
+		}
+		
+		auto end = std::chrono::system_clock::now();
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
+		start = end;
+		
+		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
+
+		const vkcv::camera::Camera& camera = cameraManager.getActiveCamera();
+
+		ObjectMatrices objectMatrices;
+		objectMatrices.model    = *reinterpret_cast<glm::mat4*>(&mesh.meshes.front().modelMatrix);
+		objectMatrices.mvp      = camera.getMVP() * objectMatrices.model;
+
+		matrixBuffer.fill({ objectMatrices });
+
+		struct PushConstants {
+			uint32_t matrixIndex;
+			uint32_t meshletCount;
+		};
+		PushConstants pushConstants{ 0, static_cast<uint32_t>(meshShaderModelData.meshlets.size()) };
+
+		if (updateFrustumPlanes) {
+			const CameraPlanes cameraPlanes = computeCameraPlanes(camera);
+			cameraPlaneBuffer.fill({ cameraPlanes });
+		}
+
+		const std::vector<vkcv::ImageHandle> renderTargets = { swapchainInput, depthBuffer };
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+
+		vkcv::PushConstants pushConstantData(sizeof(pushConstants));
+		pushConstantData.appendDrawcall(pushConstants);
+
+		if (useMeshShader) {
+
+			vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(meshShaderDescriptorSet).vulkanHandle);
+			const uint32_t taskCount = (meshShaderModelData.meshlets.size() + 31) / 32;
+
+			core.recordMeshShaderDrawcalls(
+				cmdStream,
+				renderPass,
+				meshShaderPipeline,
+				pushConstantData,
+				{ vkcv::MeshShaderDrawcall({descriptorUsage}, taskCount)},
+				{ renderTargets });
+		}
+		else {
+
+			vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(vertexShaderDescriptorSet).vulkanHandle);
+
+			core.recordDrawcallsToCmdStream(
+				cmdStream,
+				renderPass,
+				bunnyPipeline,
+				pushConstantData,
+				{ vkcv::DrawcallInfo(renderMesh, { descriptorUsage }) },
+				{ renderTargets });
+		}
+
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+		
+		gui.beginGUI();
+		
+		ImGui::Begin("Settings");
+		ImGui::Checkbox("Use mesh shader", &useMeshShader);
+		ImGui::Checkbox("Update frustum culling", &updateFrustumPlanes);
+
+		ImGui::End();
+		
+		gui.endGUI();
+
+		core.endFrame();
+	}
+	return 0;
+}
diff --git a/projects/particle_simulation/src/BloomAndFlares.cpp b/projects/particle_simulation/src/BloomAndFlares.cpp
index 23ace2bc35a2e421613718c62380f9161a408f70..5961aae664a39dfb9bd597ffa7648c9b67999af4 100644
--- a/projects/particle_simulation/src/BloomAndFlares.cpp
+++ b/projects/particle_simulation/src/BloomAndFlares.cpp
@@ -104,7 +104,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
             m_DownsamplePipe,
             initialDispatchCount,
             {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
-            vkcv::PushConstantData(nullptr, 0));
+            vkcv::PushConstants(0));
 
     // downsample dispatches of blur buffer's mip maps
     float mipDispatchCountX = dispatchCountX;
@@ -139,7 +139,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
                 m_DownsamplePipe,
                 mipDispatchCount,
                 {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
-                vkcv::PushConstantData(nullptr, 0));
+                vkcv::PushConstants(0));
 
         // image barrier between mips
         p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
@@ -184,7 +184,7 @@ void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream
                 m_UpsamplePipe,
                 upsampleDispatchCount,
                 {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
-                vkcv::PushConstantData(nullptr, 0)
+                vkcv::PushConstants(0)
         );
         // image barrier between mips
         p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
@@ -216,7 +216,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             m_LensFlarePipe,
             lensFeatureDispatchCount,
             {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
-            vkcv::PushConstantData(nullptr, 0));
+            vkcv::PushConstants(0));
 }
 
 void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStream,
@@ -249,7 +249,7 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
             m_CompositePipe,
             compositeDispatchCount,
             {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
-            vkcv::PushConstantData(nullptr, 0));
+            vkcv::PushConstants(0));
 }
 
 void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream,
@@ -263,6 +263,10 @@ void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStrea
 
 void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
 {
+    if ((width == m_Width) && (height == m_Height)) {
+        return;
+    }
+    
     m_Width  = width;
     m_Height = height;
 
diff --git a/projects/particle_simulation/src/Particle.cpp b/projects/particle_simulation/src/Particle.cpp
index 387728eb366430e4373282da785bbff47de17e7a..b80d063d382c9ae1cb63887388cce065b8289b63 100644
--- a/projects/particle_simulation/src/Particle.cpp
+++ b/projects/particle_simulation/src/Particle.cpp
@@ -3,16 +3,17 @@
 
 Particle::Particle(glm::vec3 position, glm::vec3 velocity, float lifeTime)
 : m_position(position),
-m_velocity(velocity),
-m_lifeTime(lifeTime),
-m_reset_velocity(velocity)
+  m_lifeTime(lifeTime),
+  m_velocity(velocity),
+  m_mass(1.0f),
+  m_reset_velocity(velocity)
 {}
 
 const glm::vec3& Particle::getPosition()const{
     return m_position;
 }
 
-const bool Particle::isAlive()const{
+bool Particle::isAlive()const{
     return m_lifeTime > 0.f;
 }
 
diff --git a/projects/particle_simulation/src/Particle.hpp b/projects/particle_simulation/src/Particle.hpp
index f374218fd8a08f1e1bf367bdc899a71c55ea1b78..73e7cbf517709ee03274cfd199081ade3f756545 100644
--- a/projects/particle_simulation/src/Particle.hpp
+++ b/projects/particle_simulation/src/Particle.hpp
@@ -17,7 +17,7 @@ public:
 
     void update( const float delta );
 
-    const bool isAlive()const;
+    bool isAlive()const;
 
     void setLifeTime( const float lifeTime );
 
@@ -28,7 +28,7 @@ private:
     glm::vec3 m_position;
     float m_lifeTime;
     glm::vec3 m_velocity;
-    float mass = 1.f;
+    float m_mass;
     glm::vec3 m_reset_velocity;
-    float padding_3 = 0.f;
+    float padding_3;
 };
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
index a22044f0d2588a43a5e7a0f6cba25d9c7460be9f..07ba6b194ce72dbad15a921ca13a4814c6d4f5df 100644
--- a/projects/particle_simulation/src/main.cpp
+++ b/projects/particle_simulation/src/main.cpp
@@ -58,12 +58,28 @@ int main(int argc, const char **argv) {
         return EXIT_FAILURE;
     }
 
-    // use space or use water
-    bool useSpace = true;
+    // use space or use water or gravity
+    std::string shaderPathCompute = "shaders/shader_space.comp";
+	std::string shaderPathFragment = "shaders/shader_space.frag";
+    
+    for (int i = 1; i < argc; i++) {
+    	if (strcmp(argv[i], "--space") == 0) {
+    		shaderPathCompute = "shaders/shader_space.comp";
+			shaderPathFragment = "shaders/shader_space.frag";
+    	} else
+		if (strcmp(argv[i], "--water") == 0) {
+			shaderPathCompute = "shaders/shader_water.comp";
+			shaderPathFragment = "shaders/shader_water.frag";
+		} else
+		if (strcmp(argv[i], "--gravity") == 0) {
+			shaderPathCompute = "shaders/shader_gravity.comp";
+			shaderPathFragment = "shaders/shader_space.frag";
+		}
+    }
 
     vkcv::shader::GLSLCompiler compiler;
     vkcv::ShaderProgram computeShaderProgram{};
-    compiler.compile(vkcv::ShaderStage::COMPUTE, useSpace ? "shaders/shader_space.comp" : "shaders/shader_water.comp", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+    compiler.compile(vkcv::ShaderStage::COMPUTE, shaderPathCompute, [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
         computeShaderProgram.addShader(shaderStage, path);
     });
 
@@ -81,7 +97,7 @@ int main(int argc, const char **argv) {
     compiler.compile(vkcv::ShaderStage::VERTEX, "shaders/shader.vert", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
         particleShaderProgram.addShader(shaderStage, path);
     });
-    compiler.compile(vkcv::ShaderStage::FRAGMENT, useSpace ? "shaders/shader_space.frag" : "shaders/shader_water.frag", [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+    compiler.compile(vkcv::ShaderStage::FRAGMENT, shaderPathFragment, [&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
         particleShaderProgram.addShader(shaderStage, path);
     });
 
@@ -147,13 +163,13 @@ int main(int argc, const char **argv) {
     particleBuffer.fill(particleSystem.getParticles());
 
     vkcv::DescriptorWrites setWrites;
-    setWrites.uniformBufferWrites = {vkcv::UniformBufferDescriptorWrite(0,color.getHandle()),
-                                     vkcv::UniformBufferDescriptorWrite(1,position.getHandle())};
-    setWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(2,particleBuffer.getHandle())};
+    setWrites.uniformBufferWrites = {vkcv::BufferDescriptorWrite(0,color.getHandle()),
+                                     vkcv::BufferDescriptorWrite(1,position.getHandle())};
+    setWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(2,particleBuffer.getHandle())};
     core.writeDescriptorSet(descriptorSet, setWrites);
 
     vkcv::DescriptorWrites computeWrites;
-    computeWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0,particleBuffer.getHandle())};
+    computeWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0,particleBuffer.getHandle())};
     core.writeDescriptorSet(computeDescriptorSet, computeWrites);
 
     if (!particlePipeline || !computePipeline)
@@ -167,38 +183,16 @@ int main(int argc, const char **argv) {
     const vkcv::Mesh renderMesh({vertexBufferBindings}, particleIndexBuffer.getVulkanHandle(),
                                 particleIndexBuffer.getCount());
     vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
-    //vkcv::DrawcallInfo drawcalls(renderMesh, {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle)});
 
-    glm::vec2 pos = glm::vec2(0.f);
-    glm::vec3 spawnPosition = glm::vec3(0.f);
-    glm::vec4 tempPosition = glm::vec4(0.f);
+    auto pos = glm::vec2(0.f);
+    auto spawnPosition = glm::vec3(0.f);
 
     window.e_mouseMove.add([&](double offsetX, double offsetY) {
         pos = glm::vec2(static_cast<float>(offsetX), static_cast<float>(offsetY));
-//        std::cout << offsetX << " , " << offsetY << std::endl;
-        // borders are assumed to be 0.5
-        //pos = glm::vec2((pos.x -0.5f * static_cast<float>(window.getWidth()))/static_cast<float>(window.getWidth()), (pos.y -0.5f * static_cast<float>(window.getHeight()))/static_cast<float>(window.getHeight()));
-        //borders are assumed to be 1
         pos.x = (-2 * pos.x + static_cast<float>(window.getWidth())) / static_cast<float>(window.getWidth());
         pos.y = (-2 * pos.y + static_cast<float>(window.getHeight())) / static_cast<float>(window.getHeight());
-        glm::vec4 row1 = glm::row(cameraManager.getCamera(0).getView(), 0);
-        glm::vec4 row2 = glm::row(cameraManager.getCamera(0).getView(), 1);
-        glm::vec4 row3 = glm::row(cameraManager.getCamera(0).getView(), 2);
-        glm::vec4 camera_pos = glm::column(cameraManager.getCamera(0).getView(), 3);
-//        std::cout << "row1: " << row1.x << ", " << row1.y << ", " << row1.z << std::endl;
-//        std::cout << "row2: " << row2.x << ", " << row2.y << ", " << row2.z << std::endl;
-//        std::cout << "row3: " << row3.x << ", " << row3.y << ", " << row3.z << std::endl;
-//        std::cout << "camerapos: " << camera_pos.x << ", " << camera_pos.y << ", " << camera_pos.z << std::endl;
-//        std::cout << "camerapos: " << camera_pos.x << ", " << camera_pos.y << ", " << camera_pos.z << std::endl;
-        //glm::vec4 view_axis = glm::row(cameraManager.getCamera().getView(), 2);
-        // std::cout << "view_axis: " << view_axis.x << ", " << view_axis.y << ", " << view_axis.z << std::endl;
-        //std::cout << "Front: " << cameraManager.getCamera().getFront().x << ", " << cameraManager.getCamera().getFront().z << ", " << cameraManager.getCamera().getFront().z << std::endl;
-        glm::mat4 viewmat = cameraManager.getCamera(0).getView();
         spawnPosition = glm::vec3(pos.x, pos.y, 0.f);
-        tempPosition = glm::vec4(spawnPosition, 1.0f);
-        spawnPosition = glm::vec3(tempPosition.x, tempPosition.y, tempPosition.z);
         particleSystem.setRespawnPos(glm::vec3(-spawnPosition.x, spawnPosition.y, spawnPosition.z));
-//        std::cout << "respawn pos: " << spawnPosition.x << ", " << spawnPosition.y << ", " << spawnPosition.z << std::endl;
     });
 
     std::vector<glm::mat4> modelMatrices;
@@ -242,7 +236,7 @@ int main(int argc, const char **argv) {
     std::uniform_real_distribution<float> rdm = std::uniform_real_distribution<float>(0.95f, 1.05f);
     std::default_random_engine rdmEngine;
     while (window.isWindowOpen()) {
-        window.pollEvents();
+        vkcv::Window::pollEvents();
 
         uint32_t swapchainWidth, swapchainHeight;
         if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
@@ -255,35 +249,42 @@ int main(int argc, const char **argv) {
         auto end = std::chrono::system_clock::now();
         float deltatime = 0.000001 * static_cast<float>( std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() );
         start = end;
-//        particleSystem.updateParticles(deltatime);
 
         cameraManager.update(deltatime);
 
         // split view and projection to allow for easy billboarding in shader
-        glm::mat4 renderingMatrices[2];
-        renderingMatrices[0] = cameraManager.getActiveCamera().getView();
-        renderingMatrices[1] = cameraManager.getActiveCamera().getProjection();
+        struct {
+			glm::mat4 view;
+			glm::mat4 projection;
+        } renderingMatrices;
+        
+        renderingMatrices.view = cameraManager.getActiveCamera().getView();
+        renderingMatrices.projection = cameraManager.getActiveCamera().getProjection();
 
         auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
         float random = rdm(rdmEngine);
         glm::vec2 pushData = glm::vec2(deltatime, random);
 
-        vkcv::PushConstantData pushConstantDataCompute( &pushData, sizeof(glm::vec2));
+        vkcv::PushConstants pushConstantsCompute (sizeof(glm::vec2));
+        pushConstantsCompute.appendDrawcall(pushData);
+        
         uint32_t computeDispatchCount[3] = {static_cast<uint32_t> (std::ceil(particleSystem.getParticles().size()/256.f)),1,1};
         core.recordComputeDispatchToCmdStream(cmdStream,
                                               computePipeline,
                                               computeDispatchCount,
                                               {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet).vulkanHandle)},
-                                              pushConstantDataCompute);
+											  pushConstantsCompute);
 
         core.recordBufferMemoryBarrier(cmdStream, particleBuffer.getHandle());
 
-        vkcv::PushConstantData pushConstantDataDraw((void *) &renderingMatrices[0], 2 * sizeof(glm::mat4));
+        vkcv::PushConstants pushConstantsDraw (sizeof(renderingMatrices));
+        pushConstantsDraw.appendDrawcall(renderingMatrices);
+        
         core.recordDrawcallsToCmdStream(
                 cmdStream,
                 particlePass,
                 particlePipeline,
-                pushConstantDataDraw,
+				pushConstantsDraw,
                 {drawcalls},
                 { colorBuffer });
 
@@ -309,7 +310,7 @@ int main(int argc, const char **argv) {
             tonemappingPipe, 
             tonemappingDispatchCount, 
             {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptor).vulkanHandle) },
-            vkcv::PushConstantData(nullptr, 0));
+            vkcv::PushConstants(0));
 
         core.prepareSwapchainImageForPresent(cmdStream);
         core.submitCommandStream(cmdStream);
diff --git a/projects/voxelization/CMakeLists.txt b/projects/voxelization/CMakeLists.txt
index c962409f2e14994f0c38b923de7b9b1a4d198cab..d2f533b0f9c7313ddcc6046fb29378c3a507d1fe 100644
--- a/projects/voxelization/CMakeLists.txt
+++ b/projects/voxelization/CMakeLists.txt
@@ -30,7 +30,7 @@ if(MSVC)
 endif()
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(voxelization SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
+target_include_directories(voxelization SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include} ${vkcv_upscaling_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(voxelization vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler vkcv_gui)
+target_link_libraries(voxelization vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler vkcv_gui vkcv_upscaling)
diff --git a/projects/voxelization/resources/shaders/postEffects.comp b/projects/voxelization/resources/shaders/postEffects.comp
new file mode 100644
index 0000000000000000000000000000000000000000..c0f9fe1a764bcdabac5501e2f82692c6f476e9e6
--- /dev/null
+++ b/projects/voxelization/resources/shaders/postEffects.comp
@@ -0,0 +1,149 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "luma.inc"
+
+layout(set=0, binding=0)        uniform texture2D   inTexture;
+layout(set=0, binding=1)        uniform sampler     textureSampler;
+layout(set=0, binding=2, rgba8) uniform image2D     outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    float time;
+};
+
+// from: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
+vec3 ACESFilm(vec3 x)
+{
+    float a = 2.51f;
+    float b = 0.03f;
+    float c = 2.43f;
+    float d = 0.59f;
+    float e = 0.14f;
+    return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0, 1);
+}
+
+// From Dave Hoskins: https://www.shadertoy.com/view/4djSRW.
+float hash(vec3 p3){
+    p3 = fract(p3 * 0.1031);
+    p3 += dot(p3,p3.yzx + 19.19);
+    return fract((p3.x + p3.y) * p3.z);
+}
+
+// From iq: https://www.shadertoy.com/view/4sfGzS.
+float noise(vec3 x){
+    vec3 i = floor(x);
+    vec3 f = fract(x);
+    f = f*f*(3.0-2.0*f);
+    return mix(mix(mix(hash(i+vec3(0, 0, 0)),
+    hash(i+vec3(1, 0, 0)),f.x),
+    mix(hash(i+vec3(0, 1, 0)),
+    hash(i+vec3(1, 1, 0)),f.x),f.y),
+    mix(mix(hash(i+vec3(0, 0, 1)),
+    hash(i+vec3(1, 0, 1)),f.x),
+    mix(hash(i+vec3(0, 1, 1)),
+    hash(i+vec3(1, 1, 1)),f.x),f.y),f.z);
+}
+
+// From: https://www.shadertoy.com/view/3sGSWVF
+// Slightly high-passed continuous value-noise.
+float grainSource(vec3 x, float strength, float pitch){
+    float center = noise(x);
+    float v1 = center - noise(vec3( 1, 0, 0)/pitch + x) + 0.5;
+    float v2 = center - noise(vec3( 0, 1, 0)/pitch + x) + 0.5;
+    float v3 = center - noise(vec3(-1, 0, 0)/pitch + x) + 0.5;
+    float v4 = center - noise(vec3( 0,-1, 0)/pitch + x) + 0.5;
+
+    float total = (v1 + v2 + v3 + v4) / 4.0;
+    return mix(1, 0.5 + total, strength);
+}
+
+vec3 applyGrain(ivec2 uv, vec3 c){
+    float grainLift     = 0.6;
+    float grainStrength = 0.4;
+    float grainTimeFactor = 0.1;
+
+    float timeColorOffset = 1.2;
+    vec3 grain = vec3(
+    grainSource(vec3(uv, floor(grainTimeFactor*time)),                   grainStrength, grainLift),
+    grainSource(vec3(uv, floor(grainTimeFactor*time + timeColorOffset)), grainStrength, grainLift),
+    grainSource(vec3(uv, floor(grainTimeFactor*time - timeColorOffset)), grainStrength, grainLift));
+
+    return c * grain;
+}
+
+vec2 computeDistortedUV(vec2 uv, float aspectRatio){
+    uv          = uv * 2 - 1;
+    float   r2  = dot(uv, uv);
+    float   k1  = 0.02f;
+
+    float maxR2     = dot(vec2(1), vec2(1));
+    float maxFactor = maxR2 * k1;
+
+    // correction only needed for pincushion distortion
+    maxFactor       = min(maxFactor, 0);
+
+    uv /= 1 + r2*k1;
+
+    // correction to avoid going out of [-1, 1] range when using barrel distortion
+    uv *= 1 + maxFactor;
+
+    return uv * 0.5 + 0.5;
+}
+
+float computeLocalContrast(vec2 uv){
+    float lumaMin = 100;
+    float lumaMax = 0;
+
+    vec2 pixelSize = vec2(1) / textureSize(sampler2D(inTexture, textureSampler), 0);
+
+    for(int x = -1; x <= 1; x++){
+        for(int y = -1; y <= 1; y++){
+            vec3 c = texture(sampler2D(inTexture, textureSampler), uv + vec2(x, y) * pixelSize).rgb;
+            float luma  = computeLuma(c);
+            lumaMin     = min(lumaMin, luma);
+            lumaMax     = max(lumaMax, luma);
+        }
+    }
+
+    return lumaMax - lumaMin;
+}
+
+vec3 computeChromaticAberrationScale(vec2 uv){
+    float   localContrast   = computeLocalContrast(uv);
+    vec3    colorScales     = vec3(-1, 0, 1);
+    float   aberrationScale = 0.004;
+    vec3    maxScaleFactors = colorScales * aberrationScale;
+    float   factor          = clamp(localContrast, 0, 1);
+    return mix(vec3(0), maxScaleFactors, factor);
+}
+
+vec3 sampleColorChromaticAberration(vec2 uv){
+    vec2 toCenter       = (vec2(0.5) - uv);
+
+    vec3 scaleFactors = computeChromaticAberrationScale(uv);
+
+    float r = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.r).r;
+    float g = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.g).g;
+    float b = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.b).b;
+    return vec3(r, g, b);
+}
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
+        return;
+    }
+    ivec2   textureRes  = textureSize(sampler2D(inTexture, textureSampler), 0);
+    ivec2   coord       = ivec2(gl_GlobalInvocationID.xy);
+    vec2    uv          = vec2(coord) / textureRes;
+    float   aspectRatio = float(textureRes.x) / textureRes.y;
+    uv                  = computeDistortedUV(uv, aspectRatio);
+
+    vec3 tonemapped    = sampleColorChromaticAberration(uv);
+    tonemapped          = applyGrain(coord, tonemapped);
+
+    vec3 gammaCorrected = pow(tonemapped, vec3(1.f / 2.2f));
+    imageStore(outImage, coord, vec4(gammaCorrected, 0.f));
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/tonemapping.comp b/projects/voxelization/resources/shaders/tonemapping.comp
index 8fa07d39ebb56eab857cdccb755a6558f5ae1ec3..ffadc9a71e207f97fec9a8815aa1c61bc709c369 100644
--- a/projects/voxelization/resources/shaders/tonemapping.comp
+++ b/projects/voxelization/resources/shaders/tonemapping.comp
@@ -1,18 +1,12 @@
 #version 440
 #extension GL_GOOGLE_include_directive : enable
 
-#include "luma.inc"
-
 layout(set=0, binding=0)        uniform texture2D   inTexture;
 layout(set=0, binding=1)        uniform sampler     textureSampler;
 layout(set=0, binding=2, rgba8) uniform image2D     outImage;
 
 layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
 
-layout( push_constant ) uniform constants{
-    float time;
-};
-
 // from: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
 vec3 ACESFilm(vec3 x)
 {
@@ -24,112 +18,6 @@ vec3 ACESFilm(vec3 x)
     return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0, 1);
 }
 
-// From Dave Hoskins: https://www.shadertoy.com/view/4djSRW.
-float hash(vec3 p3){
-    p3 = fract(p3 * 0.1031);
-    p3 += dot(p3,p3.yzx + 19.19);
-    return fract((p3.x + p3.y) * p3.z);
-}
-
-// From iq: https://www.shadertoy.com/view/4sfGzS.
-float noise(vec3 x){
-    vec3 i = floor(x);
-    vec3 f = fract(x);
-    f = f*f*(3.0-2.0*f);
-    return mix(mix(mix(hash(i+vec3(0, 0, 0)), 
-                       hash(i+vec3(1, 0, 0)),f.x),
-                   mix(hash(i+vec3(0, 1, 0)), 
-                       hash(i+vec3(1, 1, 0)),f.x),f.y),
-               mix(mix(hash(i+vec3(0, 0, 1)), 
-                       hash(i+vec3(1, 0, 1)),f.x),
-                   mix(hash(i+vec3(0, 1, 1)), 
-                       hash(i+vec3(1, 1, 1)),f.x),f.y),f.z);
-}
-
-// From: https://www.shadertoy.com/view/3sGSWVF
-// Slightly high-passed continuous value-noise.
-float grainSource(vec3 x, float strength, float pitch){
-    float center = noise(x);
-	float v1 = center - noise(vec3( 1, 0, 0)/pitch + x) + 0.5;
-	float v2 = center - noise(vec3( 0, 1, 0)/pitch + x) + 0.5;
-	float v3 = center - noise(vec3(-1, 0, 0)/pitch + x) + 0.5;
-	float v4 = center - noise(vec3( 0,-1, 0)/pitch + x) + 0.5;
-    
-	float total = (v1 + v2 + v3 + v4) / 4.0;
-	return mix(1, 0.5 + total, strength);
-}
-
-vec3 applyGrain(ivec2 uv, vec3 c){
-    float grainLift     = 0.6;
-    float grainStrength = 0.4;
-    float grainTimeFactor = 0.1;
-    
-    float timeColorOffset = 1.2;
-    vec3 grain = vec3(
-        grainSource(vec3(uv, floor(grainTimeFactor*time)),                   grainStrength, grainLift),
-        grainSource(vec3(uv, floor(grainTimeFactor*time + timeColorOffset)), grainStrength, grainLift),
-        grainSource(vec3(uv, floor(grainTimeFactor*time - timeColorOffset)), grainStrength, grainLift));
-    
-    return c * grain;
-}
-
-vec2 computeDistortedUV(vec2 uv, float aspectRatio){
-    uv          = uv * 2 - 1;
-    float   r2  = dot(uv, uv);
-    float   k1  = 0.02f;
-    
-    float maxR2     = dot(vec2(1), vec2(1));
-    float maxFactor = maxR2 * k1;
-    
-    // correction only needed for pincushion distortion
-    maxFactor       = min(maxFactor, 0);
-    
-    uv /= 1 + r2*k1;
-    
-    // correction to avoid going out of [-1, 1] range when using barrel distortion 
-    uv *= 1 + maxFactor;
-    
-    return uv * 0.5 + 0.5;
-}
-
-float computeLocalContrast(vec2 uv){
-    float lumaMin = 100;
-    float lumaMax = 0;
-    
-    vec2 pixelSize = vec2(1) / textureSize(sampler2D(inTexture, textureSampler), 0);
-    
-    for(int x = -1; x <= 1; x++){
-        for(int y = -1; y <= 1; y++){
-            vec3 c = texture(sampler2D(inTexture, textureSampler), uv + vec2(x, y) * pixelSize).rgb;
-            float luma  = computeLuma(c);
-            lumaMin     = min(lumaMin, luma);
-            lumaMax     = max(lumaMax, luma);
-        }
-    }
-    
-    return lumaMax - lumaMin;
-}
-
-vec3 computeChromaticAberrationScale(vec2 uv){
-    float   localContrast   = computeLocalContrast(uv);
-    vec3    colorScales     = vec3(-1, 0, 1);
-    float   aberrationScale = 0.004;
-    vec3    maxScaleFactors = colorScales * aberrationScale;
-    float   factor          = clamp(localContrast, 0, 1);
-    return mix(vec3(0), maxScaleFactors, factor);
-}
-
-vec3 sampleColorChromaticAberration(vec2 uv){
-    vec2 toCenter       = (vec2(0.5) - uv);
-    
-    vec3 scaleFactors = computeChromaticAberrationScale(uv);
-    
-    float r = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.r).r;
-    float g = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.g).g;
-    float b = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.b).b;
-    return vec3(r, g, b);
-}
-
 void main(){
 
     if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
@@ -138,12 +26,9 @@ void main(){
     ivec2   textureRes  = textureSize(sampler2D(inTexture, textureSampler), 0);
     ivec2   coord       = ivec2(gl_GlobalInvocationID.xy);
     vec2    uv          = vec2(coord) / textureRes;
-    float   aspectRatio = float(textureRes.x) / textureRes.y;
-    uv                  = computeDistortedUV(uv, aspectRatio);
-    vec3 linearColor    = sampleColorChromaticAberration(uv);
+
+    vec3 linearColor    = texture(sampler2D(inTexture, textureSampler), uv).rgb;
     vec3 tonemapped     = ACESFilm(linearColor);
-    tonemapped          = applyGrain(coord, tonemapped);
-    
-    vec3 gammaCorrected = pow(tonemapped, vec3(1.f / 2.2f));
-    imageStore(outImage, coord, vec4(gammaCorrected, 0.f));
+
+    imageStore(outImage, coord, vec4(tonemapped, 0.f));
 }
\ No newline at end of file
diff --git a/projects/voxelization/src/BloomAndFlares.cpp b/projects/voxelization/src/BloomAndFlares.cpp
index fac57735a6544c197f880f78e1f512382607d048..6cb02e9035daf7abebc047d26137d0ba973bb4f1 100644
--- a/projects/voxelization/src/BloomAndFlares.cpp
+++ b/projects/voxelization/src/BloomAndFlares.cpp
@@ -128,7 +128,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
             m_DownsamplePipe,
             initialDispatchCount,
             {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
-            vkcv::PushConstantData(nullptr, 0));
+            vkcv::PushConstants(0));
 
     // downsample dispatches of blur buffer's mip maps
     float mipDispatchCountX = dispatchCountX;
@@ -163,7 +163,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
                 m_DownsamplePipe,
                 mipDispatchCount,
                 {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
-                vkcv::PushConstantData(nullptr, 0));
+                vkcv::PushConstants(0));
 
         // image barrier between mips
         p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
@@ -208,7 +208,7 @@ void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream
                 m_UpsamplePipe,
                 upsampleDispatchCount,
                 {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
-                vkcv::PushConstantData(nullptr, 0)
+                vkcv::PushConstants(0)
         );
         // image barrier between mips
         p_Core->recordImageMemoryBarrier(cmdStream, m_Blur.getHandle());
@@ -243,7 +243,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             m_LensFlarePipe,
             lensFeatureDispatchCount,
             {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
-            vkcv::PushConstantData(nullptr, 0));
+            vkcv::PushConstants(0));
 
     // upsample dispatch
     p_Core->prepareImageForStorage(cmdStream, m_LensFeatures.getHandle());
@@ -276,7 +276,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             m_UpsamplePipe,
             upsampleDispatchCount,
             { vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleLensFlareDescSets[i]).vulkanHandle) },
-            vkcv::PushConstantData(nullptr, 0)
+            vkcv::PushConstants(0)
         );
         // image barrier between mips
         p_Core->recordImageMemoryBarrier(cmdStream, m_LensFeatures.getHandle());
@@ -309,6 +309,9 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
             static_cast<uint32_t>(glm::ceil(dispatchCountY)),
             1
     };
+	
+	vkcv::PushConstants pushConstants (sizeof(cameraForward));
+	pushConstants.appendDrawcall(cameraForward);
 
     // bloom composite dispatch
     p_Core->recordComputeDispatchToCmdStream(
@@ -316,7 +319,7 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
             m_CompositePipe,
             compositeDispatchCount,
             {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
-            vkcv::PushConstantData((void*)&cameraForward, sizeof(cameraForward)));
+			pushConstants);
 }
 
 void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStream, const vkcv::ImageHandle &colorAttachment, 
diff --git a/projects/voxelization/src/ShadowMapping.cpp b/projects/voxelization/src/ShadowMapping.cpp
index a330394b7bd7ff2a4b8c347bd79e676dbc70f846..32dd5457541f8f09f4d2711ea831e3c78de2303a 100644
--- a/projects/voxelization/src/ShadowMapping.cpp
+++ b/projects/voxelization/src/ShadowMapping.cpp
@@ -248,12 +248,13 @@ void ShadowMapping::recordShadowMapRendering(
 		voxelVolumeOffset,
 		voxelVolumeExtent);
 	m_lightInfoBuffer.fill({ lightInfo });
-
-	std::vector<glm::mat4> mvpLight;
+	
+	vkcv::PushConstants shadowPushConstants (sizeof(glm::mat4));
+	
 	for (const auto& m : modelMatrices) {
-		mvpLight.push_back(lightInfo.lightMatrix * m);
+		shadowPushConstants.appendDrawcall(lightInfo.lightMatrix * m);
 	}
-	const vkcv::PushConstantData shadowPushConstantData((void*)mvpLight.data(), sizeof(glm::mat4));
+	
 
 	std::vector<vkcv::DrawcallInfo> drawcalls;
 	for (const auto& mesh : meshes) {
@@ -264,7 +265,7 @@ void ShadowMapping::recordShadowMapRendering(
 		cmdStream,
 		m_shadowMapPass,
 		m_shadowMapPipe,
-		shadowPushConstantData,
+		shadowPushConstants,
 		drawcalls,
 		{ m_shadowMapDepth.getHandle() });
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMapDepth.getHandle());
@@ -276,6 +277,9 @@ void ShadowMapping::recordShadowMapRendering(
 	dispatchCount[2] = 1;
 
 	const uint32_t msaaSampleCount = msaaToSampleCount(msaa);
+	
+	vkcv::PushConstants msaaPushConstants (sizeof(msaaSampleCount));
+	msaaPushConstants.appendDrawcall(msaaSampleCount);
 
 	m_corePtr->prepareImageForStorage(cmdStream, m_shadowMap.getHandle());
 	m_corePtr->recordComputeDispatchToCmdStream(
@@ -283,7 +287,7 @@ void ShadowMapping::recordShadowMapRendering(
 		m_depthToMomentsPipe,
 		dispatchCount,
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_depthToMomentsDescriptorSet).vulkanHandle) },
-		vkcv::PushConstantData((void*)&msaaSampleCount, sizeof(msaaSampleCount)));
+		msaaPushConstants);
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
 
 	// blur X
@@ -293,7 +297,7 @@ void ShadowMapping::recordShadowMapRendering(
 		m_shadowBlurXPipe,
 		dispatchCount,
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_shadowBlurXDescriptorSet).vulkanHandle) },
-		vkcv::PushConstantData(nullptr, 0));
+		vkcv::PushConstants(0));
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMapIntermediate.getHandle());
 
 	// blur Y
@@ -303,7 +307,7 @@ void ShadowMapping::recordShadowMapRendering(
 		m_shadowBlurYPipe,
 		dispatchCount,
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_shadowBlurYDescriptorSet).vulkanHandle) },
-		vkcv::PushConstantData(nullptr, 0));
+		vkcv::PushConstants(0));
 	m_shadowMap.recordMipChainGeneration(cmdStream);
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
 }
diff --git a/projects/voxelization/src/Voxelization.cpp b/projects/voxelization/src/Voxelization.cpp
index c117b4b9e6b896fbf51aae83343f30281061be9f..f7e03709c6423ef0e3c43251afb28e887b9be61f 100644
--- a/projects/voxelization/src/Voxelization.cpp
+++ b/projects/voxelization/src/Voxelization.cpp
@@ -119,10 +119,10 @@ Voxelization::Voxelization(
 	m_voxelizationPipe = m_corePtr->createGraphicsPipeline(voxelizationPipeConfig);
 
 	vkcv::DescriptorWrites voxelizationDescriptorWrites;
-	voxelizationDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	voxelizationDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
 	voxelizationDescriptorWrites.uniformBufferWrites = { 
-		vkcv::UniformBufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()),
-		vkcv::UniformBufferDescriptorWrite(3, lightInfoBuffer)
+		vkcv::BufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(3, lightInfoBuffer)
 	};
 	voxelizationDescriptorWrites.sampledImageWrites = { vkcv::SampledImageDescriptorWrite(4, shadowMap) };
 	voxelizationDescriptorWrites.samplerWrites      = { vkcv::SamplerDescriptorWrite(5, shadowSampler) };
@@ -180,7 +180,7 @@ Voxelization::Voxelization(
 		{ m_corePtr->getDescriptorSet(m_voxelResetDescriptorSet).layout });
 
 	vkcv::DescriptorWrites resetVoxelWrites;
-	resetVoxelWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	resetVoxelWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
 	m_corePtr->writeDescriptorSet(m_voxelResetDescriptorSet, resetVoxelWrites);
 
 	// buffer to image
@@ -192,7 +192,7 @@ Voxelization::Voxelization(
 		{ m_corePtr->getDescriptorSet(m_bufferToImageDescriptorSet).layout });
 
 	vkcv::DescriptorWrites bufferToImageDescriptorWrites;
-	bufferToImageDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	bufferToImageDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
 	bufferToImageDescriptorWrites.storageImageWrites = { vkcv::StorageImageDescriptorWrite(1, m_voxelImageIntermediate.getHandle()) };
 	m_corePtr->writeDescriptorSet(m_bufferToImageDescriptorSet, bufferToImageDescriptorWrites);
 
@@ -205,11 +205,11 @@ Voxelization::Voxelization(
 		{ m_corePtr->getDescriptorSet(m_secondaryBounceDescriptorSet).layout });
 
 	vkcv::DescriptorWrites secondaryBounceDescriptorWrites;
-	secondaryBounceDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	secondaryBounceDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
 	secondaryBounceDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(1, m_voxelImageIntermediate.getHandle()) };
 	secondaryBounceDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(2, voxelSampler) };
 	secondaryBounceDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(3, m_voxelImage.getHandle()) };
-	secondaryBounceDescriptorWrites.uniformBufferWrites = { vkcv::UniformBufferDescriptorWrite(4, m_voxelInfoBuffer.getHandle()) };
+	secondaryBounceDescriptorWrites.uniformBufferWrites = { vkcv::BufferDescriptorWrite(4, m_voxelInfoBuffer.getHandle()) };
 	m_corePtr->writeDescriptorSet(m_secondaryBounceDescriptorSet, secondaryBounceDescriptorWrites);
 }
 
@@ -232,34 +232,36 @@ void Voxelization::voxelizeMeshes(
 
 	const glm::mat4 voxelizationView = glm::translate(glm::mat4(1.f), -m_voxelInfo.offset);
 	const glm::mat4 voxelizationViewProjection = voxelizationProjection * voxelizationView;
-
-	std::vector<std::array<glm::mat4, 2>> voxelizationMatrices;
+	
+	vkcv::PushConstants voxelizationPushConstants (2 * sizeof(glm::mat4));
+	
 	for (const auto& m : modelMatrices) {
-		voxelizationMatrices.push_back({ voxelizationViewProjection * m, m });
+		voxelizationPushConstants.appendDrawcall(std::array<glm::mat4, 2>{ voxelizationViewProjection * m, m });
 	}
 
-	const vkcv::PushConstantData voxelizationPushConstantData((void*)voxelizationMatrices.data(), 2 * sizeof(glm::mat4));
-
 	// reset voxels
 	const uint32_t resetVoxelGroupSize = 64;
 	uint32_t resetVoxelDispatchCount[3];
 	resetVoxelDispatchCount[0] = glm::ceil(voxelCount / float(resetVoxelGroupSize));
 	resetVoxelDispatchCount[1] = 1;
 	resetVoxelDispatchCount[2] = 1;
+	
+	vkcv::PushConstants voxelCountPushConstants (sizeof(voxelCount));
+	voxelCountPushConstants.appendDrawcall(voxelCount);
 
 	m_corePtr->recordComputeDispatchToCmdStream(
 		cmdStream,
 		m_voxelResetPipe,
 		resetVoxelDispatchCount,
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelResetDescriptorSet).vulkanHandle) },
-		vkcv::PushConstantData(&voxelCount, sizeof(voxelCount)));
+		voxelCountPushConstants);
 	m_corePtr->recordBufferMemoryBarrier(cmdStream, m_voxelBuffer.getHandle());
 
 	// voxelization
 	std::vector<vkcv::DrawcallInfo> drawcalls;
-	for (int i = 0; i < meshes.size(); i++) {
+	for (size_t i = 0; i < meshes.size(); i++) {
 		drawcalls.push_back(vkcv::DrawcallInfo(
-			meshes[i], 
+			meshes[i],
 			{ 
 				vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelizationDescriptorSet).vulkanHandle),
 				vkcv::DescriptorSetUsage(1, m_corePtr->getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) 
@@ -271,7 +273,7 @@ void Voxelization::voxelizeMeshes(
 		cmdStream,
 		m_voxelizationPass,
 		m_voxelizationPipe,
-		voxelizationPushConstantData,
+		voxelizationPushConstants,
 		drawcalls,
 		{ m_dummyRenderTarget.getHandle() });
 
@@ -287,7 +289,7 @@ void Voxelization::voxelizeMeshes(
 		m_bufferToImagePipe,
 		bufferToImageDispatchCount,
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_bufferToImageDescriptorSet).vulkanHandle) },
-		vkcv::PushConstantData(nullptr, 0));
+		vkcv::PushConstants(0));
 
 	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImageIntermediate.getHandle());
 
@@ -303,7 +305,7 @@ void Voxelization::voxelizeMeshes(
 		m_secondaryBouncePipe,
 		bufferToImageDispatchCount,
 		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_secondaryBounceDescriptorSet).vulkanHandle) },
-		vkcv::PushConstantData(nullptr, 0));
+		vkcv::PushConstants(0));
 	m_voxelImage.recordMipChainGeneration(cmdStream);
 
 	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImage.getHandle());
@@ -319,7 +321,8 @@ void Voxelization::renderVoxelVisualisation(
 	const std::vector<vkcv::ImageHandle>&   renderTargets,
 	uint32_t                                mipLevel) {
 
-	const vkcv::PushConstantData voxelVisualisationPushConstantData((void*)&viewProjectin, sizeof(glm::mat4));
+	vkcv::PushConstants voxelVisualisationPushConstants (sizeof(glm::mat4));
+	voxelVisualisationPushConstants.appendDrawcall(viewProjectin);
 
 	mipLevel = std::clamp(mipLevel, (uint32_t)0, m_voxelImage.getMipCount()-1);
 
@@ -328,7 +331,7 @@ void Voxelization::renderVoxelVisualisation(
 	voxelVisualisationDescriptorWrite.storageImageWrites =
 	{ vkcv::StorageImageDescriptorWrite(0, m_voxelImage.getHandle(), mipLevel) };
 	voxelVisualisationDescriptorWrite.uniformBufferWrites =
-	{ vkcv::UniformBufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()) };
+	{ vkcv::BufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()) };
 	m_corePtr->writeDescriptorSet(m_visualisationDescriptorSet, voxelVisualisationDescriptorWrite);
 
 	uint32_t drawVoxelCount = voxelCount / exp2(mipLevel);
@@ -342,7 +345,7 @@ void Voxelization::renderVoxelVisualisation(
 		cmdStream,
 		m_visualisationPass,
 		m_visualisationPipe,
-		voxelVisualisationPushConstantData,
+		voxelVisualisationPushConstants,
 		{ drawcall },
 		renderTargets);
 }
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index edc50c554b6c73bd2f06914eba6dd7adf9e43483..e7f9caa493714d30f13f64c292f1b6e51e5170b1 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -11,6 +11,8 @@
 #include "vkcv/gui/GUI.hpp"
 #include "ShadowMapping.hpp"
 #include "BloomAndFlares.hpp"
+#include <vkcv/upscaling/FSRUpscaling.hpp>
+#include <vkcv/upscaling/BilinearUpscaling.hpp>
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "Voxelization";
@@ -27,11 +29,11 @@ int main(int argc, const char** argv) {
 		true
 	);
 
-	bool    isFullscreen            = false;
-	int     windowedWidthBackup     = windowWidth;
-	int     windowedHeightBackup    = windowHeight;
-	int     windowedPosXBackup;
-	int     windowedPosYBackup;
+	bool     isFullscreen            = false;
+	uint32_t windowedWidthBackup     = windowWidth;
+	uint32_t windowedHeightBackup    = windowHeight;
+	int      windowedPosXBackup;
+	int      windowedPosYBackup;
     glfwGetWindowPos(window.getWindow(), &windowedPosXBackup, &windowedPosYBackup);
 
 	window.e_key.add([&](int key, int scancode, int action, int mods) {
@@ -85,7 +87,7 @@ int main(int argc, const char** argv) {
 		VK_MAKE_VERSION(0, 0, 1),
 		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
 		{},
-		{ "VK_KHR_swapchain" }
+		{ "VK_KHR_swapchain", "VK_KHR_shader_float16_int8", "VK_KHR_16bit_storage" }
 	);
 
 	vkcv::asset::Scene mesh;
@@ -111,7 +113,7 @@ int main(int argc, const char** argv) {
 	std::vector<std::vector<vkcv::VertexBufferBinding>> vertexBufferBindings;
 	std::vector<vkcv::asset::VertexAttribute> vAttributes;
 
-	for (int i = 0; i < scene.vertexGroups.size(); i++) {
+	for (size_t i = 0; i < scene.vertexGroups.size(); i++) {
 
 		vBuffers.push_back(scene.vertexGroups[i].vertexBuffer.data);
 		iBuffers.push_back(scene.vertexGroups[i].indexBuffer.data);
@@ -393,6 +395,9 @@ int main(int argc, const char** argv) {
 	else {
 		resolvedColorBuffer = colorBuffer;
 	}
+	
+	vkcv::ImageHandle swapBuffer = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true).getHandle();
+	vkcv::ImageHandle swapBuffer2 = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true).getHandle();
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 
@@ -421,6 +426,18 @@ int main(int argc, const char** argv) {
 	vkcv::PipelineHandle tonemappingPipeline = core.createComputePipeline(
 		tonemappingProgram,
 		{ core.getDescriptorSet(tonemappingDescriptorSet).layout });
+	
+	// tonemapping compute shader
+	vkcv::ShaderProgram postEffectsProgram;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/postEffects.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		postEffectsProgram.addShader(shaderStage, path);
+	});
+	vkcv::DescriptorSetHandle postEffectsDescriptorSet = core.createDescriptorSet(
+			postEffectsProgram.getReflectedDescriptors()[0]);
+	vkcv::PipelineHandle postEffectsPipeline = core.createComputePipeline(
+			postEffectsProgram,
+			{ core.getDescriptorSet(postEffectsDescriptorSet).layout });
 
 	// resolve compute shader
 	vkcv::ShaderProgram resolveProgram;
@@ -438,7 +455,8 @@ int main(int argc, const char** argv) {
 		vkcv::SamplerFilterType::NEAREST,
 		vkcv::SamplerFilterType::NEAREST,
 		vkcv::SamplerMipmapMode::NEAREST,
-		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	);
 
 	// model matrices per mesh
 	std::vector<glm::mat4> modelMatrices;
@@ -452,14 +470,14 @@ int main(int argc, const char** argv) {
 
 	// prepare meshes
 	std::vector<vkcv::Mesh> meshes;
-	for (int i = 0; i < scene.vertexGroups.size(); i++) {
+	for (size_t i = 0; i < scene.vertexGroups.size(); i++) {
 		vkcv::Mesh mesh(vertexBufferBindings[i], indexBuffers[i].getVulkanHandle(), scene.vertexGroups[i].numIndices);
 		meshes.push_back(mesh);
 	}
 
 	std::vector<vkcv::DrawcallInfo> drawcalls;
 	std::vector<vkcv::DrawcallInfo> prepassDrawcalls;
-	for (int i = 0; i < meshes.size(); i++) {
+	for (size_t i = 0; i < meshes.size(); i++) {
 
 		drawcalls.push_back(vkcv::DrawcallInfo(meshes[i], { 
 			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(forwardShadingDescriptorSet).vulkanHandle),
@@ -473,7 +491,8 @@ int main(int argc, const char** argv) {
 		vkcv::SamplerFilterType::LINEAR,
 		vkcv::SamplerFilterType::LINEAR,
 		vkcv::SamplerMipmapMode::LINEAR,
-		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	);
 
 	ShadowMapping shadowMapping(&core, vertexLayout);
 
@@ -511,10 +530,10 @@ int main(int argc, const char** argv) {
 	// write forward pass descriptor set
 	vkcv::DescriptorWrites forwardDescriptorWrites;
 	forwardDescriptorWrites.uniformBufferWrites = {
-		vkcv::UniformBufferDescriptorWrite(0, shadowMapping.getLightInfoBuffer()),
-		vkcv::UniformBufferDescriptorWrite(3, cameraPosBuffer.getHandle()),
-		vkcv::UniformBufferDescriptorWrite(6, voxelization.getVoxelInfoBufferHandle()),
-		vkcv::UniformBufferDescriptorWrite(7, volumetricSettingsBuffer.getHandle())};
+		vkcv::BufferDescriptorWrite(0, shadowMapping.getLightInfoBuffer()),
+		vkcv::BufferDescriptorWrite(3, cameraPosBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(6, voxelization.getVoxelInfoBufferHandle()),
+		vkcv::BufferDescriptorWrite(7, volumetricSettingsBuffer.getHandle())};
 	forwardDescriptorWrites.sampledImageWrites = {
 		vkcv::SampledImageDescriptorWrite(1, shadowMapping.getShadowMap()),
 		vkcv::SampledImageDescriptorWrite(4, voxelization.getVoxelImageHandle()) };
@@ -523,6 +542,27 @@ int main(int argc, const char** argv) {
 		vkcv::SamplerDescriptorWrite(5, voxelSampler) };
 	core.writeDescriptorSet(forwardShadingDescriptorSet, forwardDescriptorWrites);
 
+	vkcv::upscaling::FSRUpscaling upscaling (core);
+	uint32_t fsrWidth = windowWidth, fsrHeight = windowHeight;
+	
+	vkcv::upscaling::FSRQualityMode fsrMode = vkcv::upscaling::FSRQualityMode::NONE;
+	int fsrModeIndex = static_cast<int>(fsrMode);
+	
+	const std::vector<const char*> fsrModeNames = {
+			"None",
+			"Ultra Quality",
+			"Quality",
+			"Balanced",
+			"Performance"
+	};
+	
+	bool fsrMipLoadBiasFlag = true;
+	bool fsrMipLoadBiasFlagBackup = fsrMipLoadBiasFlag;
+	
+	vkcv::upscaling::BilinearUpscaling upscaling1 (core);
+	
+	bool bilinearUpscaling = false;
+	
 	vkcv::gui::GUI gui(core, window);
 
 	glm::vec2   lightAnglesDegree               = glm::vec2(90.f, 0.f);
@@ -550,22 +590,72 @@ int main(int argc, const char** argv) {
 		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
 			continue;
 		}
-
-		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
-			depthBuffer         = core.createImage(depthBufferFormat, swapchainWidth, swapchainHeight, 1, false, false, false, msaa).getHandle();
-			colorBuffer         = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, colorBufferRequiresStorage, true, msaa).getHandle();
+		
+		uint32_t width, height;
+		vkcv::upscaling::getFSRResolution(
+				fsrMode,
+				swapchainWidth, swapchainHeight,
+				width, height
+		);
+
+		if ((width != fsrWidth) || ((height != fsrHeight)) || (fsrMipLoadBiasFlagBackup != fsrMipLoadBiasFlag)) {
+			fsrWidth = width;
+			fsrHeight = height;
+			fsrMipLoadBiasFlagBackup = fsrMipLoadBiasFlag;
+			
+			colorSampler = core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT,
+					fsrMipLoadBiasFlag? vkcv::upscaling::getFSRLodBias(fsrMode) : 0.0f
+			);
+			
+			for (size_t i = 0; i < scene.materials.size(); i++) {
+				vkcv::DescriptorWrites setWrites;
+				setWrites.samplerWrites = {
+						vkcv::SamplerDescriptorWrite(1, colorSampler),
+				};
+				core.writeDescriptorSet(materialDescriptorSets[i], setWrites);
+			}
+			
+			depthBuffer = core.createImage(
+					depthBufferFormat,
+					fsrWidth, fsrHeight, 1,
+					false, false, false,
+					msaa
+			).getHandle();
+			
+			colorBuffer = core.createImage(
+					colorBufferFormat,
+					fsrWidth, fsrHeight, 1,
+					false, colorBufferRequiresStorage, true,
+					msaa
+			).getHandle();
 
 			if (usingMsaa) {
-				resolvedColorBuffer = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, true, true).getHandle();
-			}
-			else {
+				resolvedColorBuffer = core.createImage(
+						colorBufferFormat,
+						fsrWidth, fsrHeight, 1,
+						false, true, true
+				).getHandle();
+			} else {
 				resolvedColorBuffer = colorBuffer;
 			}
-
-			windowWidth = swapchainWidth;
-			windowHeight = swapchainHeight;
-
-			bloomFlares.updateImageDimensions(windowWidth, windowHeight);
+			
+			swapBuffer = core.createImage(
+					colorBufferFormat,
+					fsrWidth, fsrHeight, 1,
+					false, true
+			).getHandle();
+			
+			swapBuffer2 = core.createImage(
+					colorBufferFormat,
+					swapchainWidth, swapchainHeight, 1,
+					false, true
+			).getHandle();
+			
+			bloomFlares.updateImageDimensions(swapchainWidth, swapchainHeight);
 		}
 
 		auto end = std::chrono::system_clock::now();
@@ -575,9 +665,17 @@ int main(int argc, const char** argv) {
 		vkcv::DescriptorWrites tonemappingDescriptorWrites;
 		tonemappingDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, resolvedColorBuffer) };
 		tonemappingDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(1, colorSampler) };
-		tonemappingDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, swapchainInput) };
+		tonemappingDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, swapBuffer) };
 
 		core.writeDescriptorSet(tonemappingDescriptorSet, tonemappingDescriptorWrites);
+		
+		// update descriptor sets which use swapchain image
+		vkcv::DescriptorWrites postEffectsDescriptorWrites;
+		postEffectsDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, swapBuffer2) };
+		postEffectsDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(1, colorSampler) };
+		postEffectsDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, swapchainInput) };
+		
+		core.writeDescriptorSet(postEffectsDescriptorSet, postEffectsDescriptorWrites);
 
 		// update resolve descriptor, color images could be changed
 		vkcv::DescriptorWrites resolveDescriptorWrites;
@@ -618,29 +716,32 @@ int main(int argc, const char** argv) {
 
 		// depth prepass
 		const glm::mat4 viewProjectionCamera = cameraManager.getActiveCamera().getMVP();
-
+		
+		vkcv::PushConstants prepassPushConstants (sizeof(glm::mat4));
+		
 		std::vector<glm::mat4> prepassMatrices;
 		for (const auto& m : modelMatrices) {
-			prepassMatrices.push_back(viewProjectionCamera * m);
+			prepassPushConstants.appendDrawcall(viewProjectionCamera * m);
 		}
 
-		const vkcv::PushConstantData            prepassPushConstantData((void*)prepassMatrices.data(), sizeof(glm::mat4));
+		
 		const std::vector<vkcv::ImageHandle>    prepassRenderTargets = { depthBuffer };
 
 		core.recordDrawcallsToCmdStream(
 			cmdStream,
 			prepassPass,
 			prepassPipeline,
-			prepassPushConstantData,
+			prepassPushConstants,
 			prepassDrawcalls,
 			prepassRenderTargets);
 
 		core.recordImageMemoryBarrier(cmdStream, depthBuffer);
-
+		
+		vkcv::PushConstants pushConstants (2 * sizeof(glm::mat4));
+		
 		// main pass
-		std::vector<std::array<glm::mat4, 2>> mainPassMatrices;
 		for (const auto& m : modelMatrices) {
-			mainPassMatrices.push_back({ viewProjectionCamera * m, m });
+			pushConstants.appendDrawcall(std::array<glm::mat4, 2>{ viewProjectionCamera * m, m });
 		}
 
 		VolumetricSettings volumeSettings;
@@ -648,37 +749,46 @@ int main(int argc, const char** argv) {
 		volumeSettings.absorptionCoefficient    = absorptionColor * absorptionDensity;
 		volumeSettings.ambientLight             = volumetricAmbient;
 		volumetricSettingsBuffer.fill({ volumeSettings });
-
-		const vkcv::PushConstantData            pushConstantData((void*)mainPassMatrices.data(), 2 * sizeof(glm::mat4));
+		
 		const std::vector<vkcv::ImageHandle>    renderTargets = { colorBuffer, depthBuffer };
 
 		core.recordDrawcallsToCmdStream(
 			cmdStream,
 			forwardPass,
 			forwardPipeline,
-			pushConstantData,
+			pushConstants,
 			drawcalls,
 			renderTargets);
 
 		if (renderVoxelVis) {
 			voxelization.renderVoxelVisualisation(cmdStream, viewProjectionCamera, renderTargets, voxelVisualisationMip);
 		}
+		
+		vkcv::PushConstants skySettingsPushConstants (sizeof(skySettings));
+		skySettingsPushConstants.appendDrawcall(skySettings);
 
 		// sky
 		core.recordDrawcallsToCmdStream(
 			cmdStream,
 			skyPass,
 			skyPipe,
-			vkcv::PushConstantData((void*)&skySettings, sizeof(skySettings)),
+			skySettingsPushConstants,
 			{ vkcv::DrawcallInfo(vkcv::Mesh({}, nullptr, 3), {}) },
 			renderTargets);
 
 		const uint32_t fullscreenLocalGroupSize = 8;
-		const uint32_t fulsscreenDispatchCount[3] = {
-			static_cast<uint32_t>(glm::ceil(windowWidth  / static_cast<float>(fullscreenLocalGroupSize))),
-			static_cast<uint32_t>(glm::ceil(windowHeight / static_cast<float>(fullscreenLocalGroupSize))),
-			1
-		};
+		
+		uint32_t fulsscreenDispatchCount [3];
+		
+		fulsscreenDispatchCount[0] = static_cast<uint32_t>(
+				glm::ceil(fsrWidth  / static_cast<float>(fullscreenLocalGroupSize))
+		);
+		
+		fulsscreenDispatchCount[1] = static_cast<uint32_t>(
+				glm::ceil(fsrHeight / static_cast<float>(fullscreenLocalGroupSize))
+		);
+		
+		fulsscreenDispatchCount[2] = 1;
 
 		if (usingMsaa) {
 			if (msaaCustomResolve) {
@@ -692,7 +802,7 @@ int main(int argc, const char** argv) {
 					resolvePipeline,
 					fulsscreenDispatchCount,
 					{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(resolveDescriptorSet).vulkanHandle) },
-					vkcv::PushConstantData(nullptr, 0));
+					vkcv::PushConstants(0));
 
 				core.recordImageMemoryBarrier(cmdStream, resolvedColorBuffer);
 			}
@@ -701,21 +811,58 @@ int main(int argc, const char** argv) {
 			}
 		}
 
-		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer, windowWidth, windowHeight, 
-			glm::normalize(cameraManager.getActiveCamera().getFront()));
+		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer, fsrWidth, fsrHeight,
+			glm::normalize(cameraManager.getActiveCamera().getFront())
+		);
 
-		core.prepareImageForStorage(cmdStream, swapchainInput);
+		core.prepareImageForStorage(cmdStream, swapBuffer);
 		core.prepareImageForSampling(cmdStream, resolvedColorBuffer);
-
-		auto timeSinceStart = std::chrono::duration_cast<std::chrono::microseconds>(end - appStartTime);
-		float timeF         = static_cast<float>(timeSinceStart.count()) * 0.01;
-
+		
 		core.recordComputeDispatchToCmdStream(
 			cmdStream, 
 			tonemappingPipeline, 
 			fulsscreenDispatchCount,
-			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptorSet).vulkanHandle) },
-			vkcv::PushConstantData(&timeF, sizeof(timeF)));
+			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(
+					tonemappingDescriptorSet
+			).vulkanHandle) },
+			vkcv::PushConstants(0)
+		);
+		
+		core.prepareImageForStorage(cmdStream, swapBuffer2);
+		core.prepareImageForSampling(cmdStream, swapBuffer);
+		
+		if (bilinearUpscaling) {
+			upscaling1.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
+		} else {
+			upscaling.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
+		}
+		
+		core.prepareImageForStorage(cmdStream, swapchainInput);
+		core.prepareImageForSampling(cmdStream, swapBuffer2);
+		
+		auto timeSinceStart = std::chrono::duration_cast<std::chrono::microseconds>(end - appStartTime);
+		float timeF         = static_cast<float>(timeSinceStart.count()) * 0.01f;
+		
+		vkcv::PushConstants timePushConstants (sizeof(timeF));
+		timePushConstants.appendDrawcall(timeF);
+		
+		fulsscreenDispatchCount[0] = static_cast<uint32_t>(
+				glm::ceil(swapchainWidth  / static_cast<float>(fullscreenLocalGroupSize))
+		);
+		
+		fulsscreenDispatchCount[1] = static_cast<uint32_t>(
+				glm::ceil(swapchainHeight / static_cast<float>(fullscreenLocalGroupSize))
+		);
+		
+		core.recordComputeDispatchToCmdStream(
+				cmdStream,
+				postEffectsPipeline,
+				fulsscreenDispatchCount,
+				{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(
+						postEffectsDescriptorSet
+				).vulkanHandle) },
+				timePushConstants
+		);
 
 		// present and end
 		core.prepareSwapchainImageForPresent(cmdStream);
@@ -743,12 +890,25 @@ int main(int argc, const char** argv) {
 			ImGui::DragFloat("Voxelization extent", &voxelizationExtent, 1.f, 0.f);
 			voxelizationExtent = std::max(voxelizationExtent, 1.f);
 			voxelVisualisationMip = std::max(voxelVisualisationMip, 0);
-
+			
 			ImGui::ColorEdit3("Scattering color", &scatteringColor.x);
 			ImGui::DragFloat("Scattering density", &scatteringDensity, 0.0001);
 			ImGui::ColorEdit3("Absorption color", &absorptionColor.x);
 			ImGui::DragFloat("Absorption density", &absorptionDensity, 0.0001);
 			ImGui::DragFloat("Volumetric ambient", &volumetricAmbient, 0.002);
+			
+			float fsrSharpness = upscaling.getSharpness();
+			
+			ImGui::Combo("FSR Quality Mode", &fsrModeIndex, fsrModeNames.data(), fsrModeNames.size());
+			ImGui::DragFloat("FSR Sharpness", &fsrSharpness, 0.001, 0.0f, 1.0f);
+			ImGui::Checkbox("FSR Mip Lod Bias", &fsrMipLoadBiasFlag);
+			ImGui::Checkbox("Bilinear Upscaling", &bilinearUpscaling);
+			
+			if ((fsrModeIndex >= 0) && (fsrModeIndex <= 4)) {
+				fsrMode = static_cast<vkcv::upscaling::FSRQualityMode>(fsrModeIndex);
+			}
+			
+			upscaling.setSharpness(fsrSharpness);
 
 			if (ImGui::Button("Reload forward pass")) {
 
diff --git a/src/vkcv/BufferManager.cpp b/src/vkcv/BufferManager.cpp
index aec96411c5d9e07f200b24fbdcf9fa69e2af53d5..f22d56650654f66dd1fea4141a449004dcad88cc 100644
--- a/src/vkcv/BufferManager.cpp
+++ b/src/vkcv/BufferManager.cpp
@@ -19,7 +19,7 @@ namespace vkcv {
 			return;
 		}
 		
-		m_stagingBuffer = createBuffer(BufferType::STAGING, 1024 * 1024, BufferMemoryType::HOST_VISIBLE);
+		m_stagingBuffer = createBuffer(BufferType::STAGING, 1024 * 1024, BufferMemoryType::HOST_VISIBLE, false);
 	}
 	
 	BufferManager::~BufferManager() noexcept {
@@ -28,33 +28,7 @@ namespace vkcv {
 		}
 	}
 	
-	/**
-	 * @brief searches memory type index for buffer allocation, combines requirements of buffer and application
-	 * @param physicalMemoryProperties Memory Properties of physical device
-	 * @param typeBits Bit field for suitable memory types
-	 * @param requirements Property flags that are required
-	 * @return memory type index for Buffer
-	 */
-	uint32_t searchBufferMemoryType(const vk::PhysicalDeviceMemoryProperties& physicalMemoryProperties, uint32_t typeBits, vk::MemoryPropertyFlags requirements) {
-		const uint32_t memoryCount = physicalMemoryProperties.memoryTypeCount;
-		for (uint32_t memoryIndex = 0; memoryIndex < memoryCount; ++memoryIndex) {
-			const uint32_t memoryTypeBits = (1 << memoryIndex);
-			const bool isRequiredMemoryType = typeBits & memoryTypeBits;
-
-			const vk::MemoryPropertyFlags properties =
-				physicalMemoryProperties.memoryTypes[memoryIndex].propertyFlags;
-			const bool hasRequiredProperties =
-				(properties & requirements) == requirements;
-
-			if (isRequiredMemoryType && hasRequiredProperties)
-				return static_cast<int32_t>(memoryIndex);
-		}
-
-		// failed to find memory type
-		return -1;
-	}
-	
-	BufferHandle BufferManager::createBuffer(BufferType type, size_t size, BufferMemoryType memoryType) {
+	BufferHandle BufferManager::createBuffer(BufferType type, size_t size, BufferMemoryType memoryType, bool supportIndirect) {
 		vk::BufferCreateFlags createFlags;
 		vk::BufferUsageFlags usageFlags;
 		
@@ -75,51 +49,62 @@ namespace vkcv {
 				usageFlags = vk::BufferUsageFlagBits::eIndexBuffer;
 				break;
 			default:
-				// TODO: maybe an issue
+				vkcv_log(LogLevel::WARNING, "Unknown buffer type");
 				break;
 		}
 		
 		if (memoryType == BufferMemoryType::DEVICE_LOCAL) {
 			usageFlags |= vk::BufferUsageFlagBits::eTransferDst;
 		}
+		if (supportIndirect)
+			usageFlags |= vk::BufferUsageFlagBits::eIndirectBuffer;
 		
-		const vk::Device& device = m_core->getContext().getDevice();
-		
-		vk::Buffer buffer = device.createBuffer(
-				vk::BufferCreateInfo(createFlags, size, usageFlags)
-		);
-		
-		const vk::MemoryRequirements requirements = device.getBufferMemoryRequirements(buffer);
-		const vk::PhysicalDevice& physicalDevice = m_core->getContext().getPhysicalDevice();
+		const vma::Allocator& allocator = m_core->getContext().getAllocator();
 		
 		vk::MemoryPropertyFlags memoryTypeFlags;
+		vma::MemoryUsage memoryUsage;
 		bool mappable = false;
 		
 		switch (memoryType) {
 			case BufferMemoryType::DEVICE_LOCAL:
 				memoryTypeFlags = vk::MemoryPropertyFlagBits::eDeviceLocal;
+				memoryUsage = vma::MemoryUsage::eGpuOnly;
+				mappable = false;
 				break;
 			case BufferMemoryType::HOST_VISIBLE:
 				memoryTypeFlags = vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent;
+				memoryUsage = vma::MemoryUsage::eCpuOnly;
 				mappable = true;
 				break;
 			default:
-				// TODO: maybe an issue
+				vkcv_log(LogLevel::WARNING, "Unknown buffer memory type");
+				memoryUsage = vma::MemoryUsage::eUnknown;
+				mappable = false;
 				break;
 		}
 		
-		const uint32_t memoryTypeIndex = searchBufferMemoryType(
-				physicalDevice.getMemoryProperties(),
-				requirements.memoryTypeBits,
-				memoryTypeFlags
-		);
+		if (type == BufferType::STAGING) {
+			memoryUsage = vma::MemoryUsage::eCpuToGpu;
+		}
 		
-		vk::DeviceMemory memory = device.allocateMemory(vk::MemoryAllocateInfo(requirements.size, memoryTypeIndex));
+		auto bufferAllocation = allocator.createBuffer(
+				vk::BufferCreateInfo(createFlags, size, usageFlags),
+				vma::AllocationCreateInfo(
+						vma::AllocationCreateFlags(),
+						memoryUsage,
+						memoryTypeFlags,
+						memoryTypeFlags,
+						0,
+						vma::Pool(),
+						nullptr
+				)
+		);
 		
-		device.bindBufferMemory(buffer, memory, 0);
+		vk::Buffer buffer = bufferAllocation.first;
+		vma::Allocation allocation = bufferAllocation.second;
 		
 		const uint64_t id = m_buffers.size();
-		m_buffers.push_back({ buffer, memory, size, nullptr, mappable });
+		m_buffers.push_back({ buffer, allocation, size, mappable });
 		return BufferHandle(id, [&](uint64_t id) { destroyBufferById(id); });
 	}
 	
@@ -130,7 +115,7 @@ namespace vkcv {
 		
 		vk::Buffer buffer;
 		vk::Buffer stagingBuffer;
-		vk::DeviceMemory stagingMemory;
+		vma::Allocation stagingAllocation;
 		
 		size_t stagingLimit;
 		size_t stagingPosition;
@@ -150,11 +135,11 @@ namespace vkcv {
 		const size_t remaining = info.size - info.stagingPosition;
 		const size_t mapped_size = std::min(remaining, info.stagingLimit);
 		
-		const vk::Device& device = core->getContext().getDevice();
+		const vma::Allocator& allocator = core->getContext().getAllocator();
 		
-		void* mapped = device.mapMemory(info.stagingMemory, 0, mapped_size);
+		void* mapped = allocator.mapMemory(info.stagingAllocation);
 		memcpy(mapped, reinterpret_cast<const char*>(info.data) + info.stagingPosition, mapped_size);
-		device.unmapMemory(info.stagingMemory);
+		allocator.unmapMemory(info.stagingAllocation);
 		
 		SubmitInfo submitInfo;
 		submitInfo.queueType = QueueType::Transfer;
@@ -216,7 +201,13 @@ namespace vkcv {
 		
 		auto& buffer = m_buffers[id];
 		
-		return buffer.m_memory;
+		const vma::Allocator& allocator = m_core->getContext().getAllocator();
+		
+		auto info = allocator.getAllocationInfo(
+				buffer.m_allocation
+		);
+		
+		return info.deviceMemory;
 	}
 	
 	void BufferManager::fillBuffer(const BufferHandle& handle, const void *data, size_t size, size_t offset) {
@@ -232,11 +223,7 @@ namespace vkcv {
 		
 		auto& buffer = m_buffers[id];
 		
-		if (buffer.m_mapped) {
-			return;
-		}
-		
-		const vk::Device& device = m_core->getContext().getDevice();
+		const vma::Allocator& allocator = m_core->getContext().getAllocator();
 		
 		if (offset > buffer.m_size) {
 			return;
@@ -245,9 +232,9 @@ namespace vkcv {
 		const size_t max_size = std::min(size, buffer.m_size - offset);
 		
 		if (buffer.m_mappable) {
-			void* mapped = device.mapMemory(buffer.m_memory, offset, max_size);
-			memcpy(mapped, data, max_size);
-			device.unmapMemory(buffer.m_memory);
+			void* mapped = allocator.mapMemory(buffer.m_allocation);
+			memcpy(reinterpret_cast<char*>(mapped) + offset, data, max_size);
+			allocator.unmapMemory(buffer.m_allocation);
 		} else {
 			auto& stagingBuffer = m_buffers[ m_stagingBuffer.getId() ];
 			
@@ -258,11 +245,9 @@ namespace vkcv {
 			
 			info.buffer = buffer.m_handle;
 			info.stagingBuffer = stagingBuffer.m_handle;
-			info.stagingMemory = stagingBuffer.m_memory;
+			info.stagingAllocation = stagingBuffer.m_allocation;
 			
-			const vk::MemoryRequirements stagingRequirements = device.getBufferMemoryRequirements(stagingBuffer.m_handle);
-			
-			info.stagingLimit = stagingRequirements.size;
+			info.stagingLimit = stagingBuffer.m_size;
 			info.stagingPosition = 0;
 			
 			copyFromStagingBuffer(m_core, info);
@@ -282,19 +267,13 @@ namespace vkcv {
 		
 		auto& buffer = m_buffers[id];
 		
-		if (buffer.m_mapped) {
-			return nullptr;
-		}
-		
-		const vk::Device& device = m_core->getContext().getDevice();
+		const vma::Allocator& allocator = m_core->getContext().getAllocator();
 		
 		if (offset > buffer.m_size) {
 			return nullptr;
 		}
 		
-		const size_t max_size = std::min(size, buffer.m_size - offset);
-		buffer.m_mapped = device.mapMemory(buffer.m_memory, offset, max_size);
-		return buffer.m_mapped;
+		return reinterpret_cast<char*>(allocator.mapMemory(buffer.m_allocation)) + offset;
 	}
 	
 	void BufferManager::unmapBuffer(const BufferHandle& handle) {
@@ -306,14 +285,9 @@ namespace vkcv {
 		
 		auto& buffer = m_buffers[id];
 		
-		if (buffer.m_mapped == nullptr) {
-			return;
-		}
-		
-		const vk::Device& device = m_core->getContext().getDevice();
+		const vma::Allocator& allocator = m_core->getContext().getAllocator();
 		
-		device.unmapMemory(buffer.m_memory);
-		buffer.m_mapped = nullptr;
+		allocator.unmapMemory(buffer.m_allocation);
 	}
 	
 	void BufferManager::destroyBufferById(uint64_t id) {
@@ -323,16 +297,13 @@ namespace vkcv {
 		
 		auto& buffer = m_buffers[id];
 		
-		const vk::Device& device = m_core->getContext().getDevice();
-		
-		if (buffer.m_memory) {
-			device.freeMemory(buffer.m_memory);
-			buffer.m_memory = nullptr;
-		}
+		const vma::Allocator& allocator = m_core->getContext().getAllocator();
 		
 		if (buffer.m_handle) {
-			device.destroyBuffer(buffer.m_handle);
+			allocator.destroyBuffer(buffer.m_handle, buffer.m_allocation);
+			
 			buffer.m_handle = nullptr;
+			buffer.m_allocation = nullptr;
 		}
 	}
 
diff --git a/src/vkcv/CommandStreamManager.cpp b/src/vkcv/CommandStreamManager.cpp
index 5a5b359b912d9cef36e0b03379d7f0f6f0951381..52b73213dbc5837f6be4a2aa25c28615dccf5969 100644
--- a/src/vkcv/CommandStreamManager.cpp
+++ b/src/vkcv/CommandStreamManager.cpp
@@ -32,11 +32,10 @@ namespace vkcv {
 
 		// find unused stream
 		int unusedStreamIndex = -1;
-		for (int i = 0; i < m_commandStreams.size(); i++) {
+		for (size_t i = 0; i < m_commandStreams.size(); i++) {
 			if (m_commandStreams[i].cmdBuffer) {
 				// still in use
-			}
-			else {
+			} else {
 				unusedStreamIndex = i;
 				break;
 			}
diff --git a/src/vkcv/Context.cpp b/src/vkcv/Context.cpp
index e23213b41a3c9a289b679652b66bbe2e75cf0340..2e30fb961d0b0931e4ff8796dd92b2cbd0b5f734 100644
--- a/src/vkcv/Context.cpp
+++ b/src/vkcv/Context.cpp
@@ -9,11 +9,13 @@ namespace vkcv
             m_Instance(other.m_Instance),
             m_PhysicalDevice(other.m_PhysicalDevice),
             m_Device(other.m_Device),
-            m_QueueManager(other.m_QueueManager)
+            m_QueueManager(other.m_QueueManager),
+            m_Allocator(other.m_Allocator)
     {
         other.m_Instance        = nullptr;
         other.m_PhysicalDevice  = nullptr;
         other.m_Device          = nullptr;
+		other.m_Allocator		= nullptr;
     }
 
     Context & Context::operator=(Context &&other) noexcept
@@ -22,10 +24,12 @@ namespace vkcv
         m_PhysicalDevice    = other.m_PhysicalDevice;
         m_Device            = other.m_Device;
         m_QueueManager		= other.m_QueueManager;
+        m_Allocator			= other.m_Allocator;
 
         other.m_Instance        = nullptr;
         other.m_PhysicalDevice  = nullptr;
         other.m_Device          = nullptr;
+        other.m_Allocator		= nullptr;
 
         return *this;
     }
@@ -33,15 +37,18 @@ namespace vkcv
     Context::Context(vk::Instance instance,
                      vk::PhysicalDevice physicalDevice,
                      vk::Device device,
-					 QueueManager&& queueManager) noexcept :
-    m_Instance{instance},
-    m_PhysicalDevice{physicalDevice},
-    m_Device{device},
-    m_QueueManager{queueManager}
+					 QueueManager&& queueManager,
+					 vma::Allocator&& allocator) noexcept :
+    m_Instance(instance),
+    m_PhysicalDevice(physicalDevice),
+    m_Device(device),
+    m_QueueManager(queueManager),
+    m_Allocator(allocator)
     {}
 
     Context::~Context() noexcept
     {
+    	m_Allocator.destroy();
         m_Device.destroy();
         m_Instance.destroy();
     }
@@ -64,6 +71,10 @@ namespace vkcv
     const QueueManager& Context::getQueueManager() const {
     	return m_QueueManager;
     }
+    
+    const vma::Allocator& Context::getAllocator() const {
+    	return m_Allocator;
+    }
 	
 	/**
 	 * @brief The physical device is evaluated by three categories:
@@ -140,7 +151,7 @@ namespace vkcv
 	 * @param check The elements to be checked
 	 * @return True, if all elements in "check" are supported
 	*/
-	bool checkSupport(std::vector<const char*>& supported, std::vector<const char*>& check)
+	bool checkSupport(const std::vector<const char*>& supported, const std::vector<const char*>& check)
 	{
 		for (auto checkElem : check) {
 			bool found = false;
@@ -169,11 +180,20 @@ namespace vkcv
 		return extensions;
 	}
 	
+	bool isPresentInCharPtrVector(const std::vector<const char*>& v, const char* term){
+		for (const auto& entry : v) {
+			if (strcmp(entry, term) != 0) {
+				return true;
+			}
+		}
+		return false;
+	}
+	
 	Context Context::create(const char *applicationName,
 							uint32_t applicationVersion,
-							std::vector<vk::QueueFlagBits> queueFlags,
-							std::vector<const char *> instanceExtensions,
-							std::vector<const char *> deviceExtensions) {
+							const std::vector<vk::QueueFlagBits>& queueFlags,
+							const std::vector<const char *>& instanceExtensions,
+							const std::vector<const char *>& deviceExtensions) {
 		// check for layer support
 		
 		const std::vector<vk::LayerProperties>& layerProperties = vk::enumerateInstanceLayerProperties();
@@ -212,7 +232,7 @@ namespace vkcv
 		
 		// for GLFW: get all required extensions
 		std::vector<const char*> requiredExtensions = getRequiredExtensions();
-		instanceExtensions.insert(instanceExtensions.end(), requiredExtensions.begin(), requiredExtensions.end());
+		requiredExtensions.insert(requiredExtensions.end(), instanceExtensions.begin(), instanceExtensions.end());
 		
 		const vk::ApplicationInfo applicationInfo(
 				applicationName,
@@ -227,8 +247,8 @@ namespace vkcv
 				&applicationInfo,
 				0,
 				nullptr,
-				static_cast<uint32_t>(instanceExtensions.size()),
-				instanceExtensions.data()
+				static_cast<uint32_t>(requiredExtensions.size()),
+				requiredExtensions.data()
 		);
 
 #ifndef NDEBUG
@@ -275,13 +295,39 @@ namespace vkcv
 		deviceCreateInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
 		deviceCreateInfo.ppEnabledLayerNames = validationLayers.data();
 #endif
-
+		const bool shaderFloat16 = checkSupport(deviceExtensions, { "VK_KHR_shader_float16_int8" });
+		const bool storage16bit  = checkSupport(deviceExtensions, { "VK_KHR_16bit_storage" });
+		
 		// FIXME: check if device feature is supported
-		vk::PhysicalDeviceFeatures deviceFeatures;
-		deviceFeatures.fragmentStoresAndAtomics = true;
-		deviceFeatures.geometryShader = true;
-		deviceFeatures.depthClamp = true;
-		deviceCreateInfo.pEnabledFeatures = &deviceFeatures;
+		vk::PhysicalDeviceShaderFloat16Int8Features deviceShaderFloat16Int8Features;
+		deviceShaderFloat16Int8Features.shaderFloat16 = shaderFloat16;
+		
+		vk::PhysicalDevice16BitStorageFeatures device16BitStorageFeatures;
+		device16BitStorageFeatures.storageBuffer16BitAccess = storage16bit;
+		
+		vk::PhysicalDeviceFeatures2 deviceFeatures2;
+		deviceFeatures2.features.fragmentStoresAndAtomics = true;
+		deviceFeatures2.features.geometryShader = true;
+		deviceFeatures2.features.depthClamp = true;
+		deviceFeatures2.features.shaderInt16 = true;
+		
+		const bool usingMeshShaders = isPresentInCharPtrVector(deviceExtensions, VK_NV_MESH_SHADER_EXTENSION_NAME);
+		vk::PhysicalDeviceMeshShaderFeaturesNV meshShadingFeatures;
+		if (usingMeshShaders) {
+			meshShadingFeatures.taskShader = true;
+			meshShadingFeatures.meshShader = true;
+            deviceFeatures2.setPNext(&meshShadingFeatures);
+		}
+		
+		if (shaderFloat16) {
+			deviceFeatures2.setPNext(&deviceShaderFloat16Int8Features);
+		}
+		
+		if (storage16bit) {
+			deviceShaderFloat16Int8Features.setPNext(&device16BitStorageFeatures);
+		}
+		
+		deviceCreateInfo.setPNext(&deviceFeatures2);
 
 		// Ablauf
 		// qCreateInfos erstellen --> braucht das Device
@@ -289,10 +335,50 @@ namespace vkcv
 		// jetzt koennen wir mit dem device die queues erstellen
 		
 		vk::Device device = physicalDevice.createDevice(deviceCreateInfo);
+
+		if (usingMeshShaders)
+		{
+			InitMeshShaderDrawFunctions(device);
+		}
 		
-		QueueManager queueManager = QueueManager::create(device, queuePairsGraphics, queuePairsCompute, queuePairsTransfer);
+		QueueManager queueManager = QueueManager::create(
+				device,
+				queuePairsGraphics,
+				queuePairsCompute,
+				queuePairsTransfer
+		);
 		
-		return Context(instance, physicalDevice, device, std::move(queueManager));
+		const vma::AllocatorCreateInfo allocatorCreateInfo (
+				vma::AllocatorCreateFlags(),
+				physicalDevice,
+				device,
+				0,
+				nullptr,
+				nullptr,
+				0,
+				nullptr,
+				nullptr,
+				nullptr,
+				instance,
+				
+				/* Uses default version when set to 0 (currently VK_VERSION_1_0):
+				 *
+				 * The reason for this is that the allocator restricts the allowed version
+				 * to be at maximum VK_VERSION_1_1 which is already less than
+				 * VK_HEADER_VERSION_COMPLETE at most platforms.
+				 * */
+				0
+		);
+		
+		vma::Allocator allocator = vma::createAllocator(allocatorCreateInfo);
+		
+		return Context(
+				instance,
+				physicalDevice,
+				device,
+				std::move(queueManager),
+				std::move(allocator)
+		);
 	}
  
 }
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index 352a1cf62eabe55ce1bbf2f53a6b5a4bd6e91753..92e2df4f18f59355868e9dcce7a78c4e1a9c5cb7 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -53,9 +53,9 @@ namespace vkcv
     Core Core::create(Window &window,
                       const char *applicationName,
                       uint32_t applicationVersion,
-                      std::vector<vk::QueueFlagBits> queueFlags,
-                      std::vector<const char *> instanceExtensions,
-                      std::vector<const char *> deviceExtensions)
+                      const std::vector<vk::QueueFlagBits>& queueFlags,
+                      const std::vector<const char *>& instanceExtensions,
+                      const std::vector<const char *>& deviceExtensions)
     {
         Context context = Context::create(
         		applicationName, applicationVersion,
@@ -71,7 +71,6 @@ namespace vkcv
 
         const auto& queueManager = context.getQueueManager();
         
-		const int						graphicQueueFamilyIndex	= queueManager.getGraphicsQueues()[0].familyIndex;
 		const std::unordered_set<int>	queueFamilySet			= generateQueueFamilyIndexSet(queueManager);
 		const auto						commandResources		= createCommandResources(context.getDevice(), queueFamilySet);
 		const auto						defaultSyncResources	= createSyncResources(context.getDevice());
@@ -91,8 +90,8 @@ namespace vkcv
     Core::Core(Context &&context, Window &window, const Swapchain& swapChain,  std::vector<vk::ImageView> swapchainImageViews,
         const CommandResources& commandResources, const SyncResources& syncResources) noexcept :
             m_Context(std::move(context)),
+			m_swapchain(swapChain),
             m_window(window),
-            m_swapchain(swapChain),
             m_PassManager{std::make_unique<PassManager>(m_Context.m_Device)},
             m_PipelineManager{std::make_unique<PipelineManager>(m_Context.m_Device)},
             m_DescriptorManager(std::make_unique<DescriptorManager>(m_Context.m_Device)),
@@ -119,7 +118,8 @@ namespace vkcv
 			swapchainImageViews, 
 			swapChain.getExtent().width,
 			swapChain.getExtent().height,
-			swapChain.getFormat());
+			swapChain.getFormat()
+		);
 	}
 
 	Core::~Core() noexcept {
@@ -163,9 +163,9 @@ namespace vkcv
 					nullptr,
 					&imageIndex, {}
 			);
-		} catch (vk::OutOfDateKHRError e) {
+		} catch (const vk::OutOfDateKHRError& e) {
 			result = vk::Result::eErrorOutOfDateKHR;
-		} catch (vk::DeviceLostError e) {
+		} catch (const vk::DeviceLostError& e) {
 			result = vk::Result::eErrorDeviceLost;
 		}
 		
@@ -228,133 +228,246 @@ namespace vkcv
 		return (m_currentSwapchainImageIndex != std::numeric_limits<uint32_t>::max());
 	}
 
-	void Core::recordDrawcallsToCmdStream(
-		const CommandStreamHandle       cmdStreamHandle,
-		const PassHandle                renderpassHandle, 
-		const PipelineHandle            pipelineHandle, 
-        const PushConstantData          &pushConstantData,
-        const std::vector<DrawcallInfo> &drawcalls,
-		const std::vector<ImageHandle>  &renderTargets) {
+	std::array<uint32_t, 2> getWidthHeightFromRenderTargets(
+		const std::vector<ImageHandle>& renderTargets,
+		const Swapchain& swapchain,
+		const ImageManager& imageManager) {
 
-		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
-			return;
-		}
+		std::array<uint32_t, 2> widthHeight;
 
-		uint32_t width;
-		uint32_t height;
 		if (renderTargets.size() > 0) {
 			const vkcv::ImageHandle firstImage = renderTargets[0];
 			if (firstImage.isSwapchainImage()) {
-				const auto& swapchainExtent = m_swapchain.getExtent();
-				width = swapchainExtent.width;
-				height = swapchainExtent.height;
+				const auto& swapchainExtent = swapchain.getExtent();
+				widthHeight[0] = swapchainExtent.width;
+				widthHeight[1] = swapchainExtent.height;
 			}
 			else {
-				width = m_ImageManager->getImageWidth(firstImage);
-				height = m_ImageManager->getImageHeight(firstImage);
+				widthHeight[0] = imageManager.getImageWidth(firstImage);
+				widthHeight[1] = imageManager.getImageHeight(firstImage);
 			}
 		}
 		else {
-			width = 1;
-			height = 1;
+			widthHeight[0] = 1;
+			widthHeight[1] = 1;
 		}
 		// TODO: validate that width/height match for all attachments
+		return widthHeight;
+	}
 
-		const vk::RenderPass renderpass = m_PassManager->getVkPass(renderpassHandle);
-		const PassConfig passConfig = m_PassManager->getPassConfig(renderpassHandle);
-
-		const vk::Pipeline pipeline		= m_PipelineManager->getVkPipeline(pipelineHandle);
-		const vk::PipelineLayout pipelineLayout = m_PipelineManager->getVkPipelineLayout(pipelineHandle);
-		const vk::Rect2D renderArea(vk::Offset2D(0, 0), vk::Extent2D(width, height));
+	vk::Framebuffer createFramebuffer(
+		const std::vector<ImageHandle>& renderTargets,
+		const ImageManager&             imageManager,
+		const Swapchain&                swapchain,
+		vk::RenderPass                  renderpass,
+		vk::Device                      device) {
 
 		std::vector<vk::ImageView> attachmentsViews;
 		for (const ImageHandle handle : renderTargets) {
-			vk::ImageView targetHandle;
-			const auto cmdBuffer = m_CommandStreamManager->getStreamCommandBuffer(cmdStreamHandle);
+			vk::ImageView targetHandle = imageManager.getVulkanImageView(handle);
+			attachmentsViews.push_back(targetHandle);
+		}
+
+		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, swapchain, imageManager);
+
+		const vk::FramebufferCreateInfo createInfo(
+			{},
+			renderpass,
+			static_cast<uint32_t>(attachmentsViews.size()),
+			attachmentsViews.data(),
+			widthHeight[0],
+			widthHeight[1],
+			1);
+
+		return device.createFramebuffer(createInfo);
+	}
 
-			targetHandle = m_ImageManager->getVulkanImageView(handle);
-			const bool isDepthImage = isDepthFormat(m_ImageManager->getImageFormat(handle));
-			const vk::ImageLayout targetLayout = 
+	void transitionRendertargetsToAttachmentLayout(
+		const std::vector<ImageHandle>& renderTargets,
+		ImageManager&                   imageManager,
+		const vk::CommandBuffer         cmdBuffer) {
+
+		for (const ImageHandle handle : renderTargets) {
+			vk::ImageView targetHandle = imageManager.getVulkanImageView(handle);
+			const bool isDepthImage = isDepthFormat(imageManager.getImageFormat(handle));
+			const vk::ImageLayout targetLayout =
 				isDepthImage ? vk::ImageLayout::eDepthStencilAttachmentOptimal : vk::ImageLayout::eColorAttachmentOptimal;
-			m_ImageManager->recordImageLayoutTransition(handle, targetLayout, cmdBuffer);
-			attachmentsViews.push_back(targetHandle);
+			imageManager.recordImageLayoutTransition(handle, targetLayout, cmdBuffer);
 		}
-		
-        const vk::FramebufferCreateInfo createInfo(
-            {},
-            renderpass,
-            static_cast<uint32_t>(attachmentsViews.size()),
-            attachmentsViews.data(),
-            width,
-            height,
-            1
+	}
+
+	std::vector<vk::ClearValue> createAttachmentClearValues(const std::vector<AttachmentDescription>& attachments) {
+		std::vector<vk::ClearValue> clearValues;
+		for (const auto& attachment : attachments) {
+			if (attachment.load_operation == AttachmentOperation::CLEAR) {
+				float clear = 0.0f;
+
+				if (isDepthFormat(attachment.format)) {
+					clear = 1.0f;
+				}
+
+				clearValues.emplace_back(std::array<float, 4>{
+					clear,
+						clear,
+						clear,
+						1.f
+				});
+			}
+		}
+		return clearValues;
+	}
+
+	void recordDynamicViewport(vk::CommandBuffer cmdBuffer, uint32_t width, uint32_t height) {
+		vk::Viewport dynamicViewport(
+			0.0f, 0.0f,
+			static_cast<float>(width), static_cast<float>(height),
+			0.0f, 1.0f
 		);
-		
-		vk::Framebuffer framebuffer = m_Context.m_Device.createFramebuffer(createInfo);
-        
-        if (!framebuffer) {
+
+		vk::Rect2D dynamicScissor({ 0, 0 }, { width, height });
+
+		cmdBuffer.setViewport(0, 1, &dynamicViewport);
+		cmdBuffer.setScissor(0, 1, &dynamicScissor);
+	}
+
+	void Core::recordDrawcallsToCmdStream(
+		const CommandStreamHandle       cmdStreamHandle,
+		const PassHandle                renderpassHandle, 
+		const PipelineHandle            pipelineHandle, 
+        const PushConstants             &pushConstantData,
+        const std::vector<DrawcallInfo> &drawcalls,
+		const std::vector<ImageHandle>  &renderTargets) {
+
+		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
+			return;
+		}
+
+		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, m_swapchain, *m_ImageManager);
+		const auto width  = widthHeight[0];
+		const auto height = widthHeight[1];
+
+		const vk::RenderPass        renderpass      = m_PassManager->getVkPass(renderpassHandle);
+		const PassConfig            passConfig      = m_PassManager->getPassConfig(renderpassHandle);
+
+		const vk::Pipeline          pipeline        = m_PipelineManager->getVkPipeline(pipelineHandle);
+		const vk::PipelineLayout    pipelineLayout  = m_PipelineManager->getVkPipelineLayout(pipelineHandle);
+		const vk::Rect2D            renderArea(vk::Offset2D(0, 0), vk::Extent2D(width, height));
+
+		vk::CommandBuffer cmdBuffer = m_CommandStreamManager->getStreamCommandBuffer(cmdStreamHandle);
+		transitionRendertargetsToAttachmentLayout(renderTargets, *m_ImageManager, cmdBuffer);
+
+		const vk::Framebuffer framebuffer = createFramebuffer(renderTargets, *m_ImageManager, m_swapchain, renderpass, m_Context.m_Device);
+
+		if (!framebuffer) {
 			vkcv_log(LogLevel::ERROR, "Failed to create temporary framebuffer");
-            return;
-        }
+			return;
+		}
 
-        vk::Viewport dynamicViewport(
-        		0.0f, 0.0f,
-            	static_cast<float>(width), static_cast<float>(height),
-            0.0f, 1.0f
-		);
+		SubmitInfo submitInfo;
+		submitInfo.queueType = QueueType::Graphics;
+		submitInfo.signalSemaphores = { m_SyncResources.renderFinished };
+
+		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
+
+			const std::vector<vk::ClearValue> clearValues = createAttachmentClearValues(passConfig.attachments);
+
+			const vk::RenderPassBeginInfo beginInfo(renderpass, framebuffer, renderArea, clearValues.size(), clearValues.data());
+			cmdBuffer.beginRenderPass(beginInfo, {}, {});
+
+			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});
+
+			const PipelineConfig &pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
+			if(pipeConfig.m_UseDynamicViewport)
+			{
+				recordDynamicViewport(cmdBuffer, width, height);
+			}
+
+			for (int i = 0; i < drawcalls.size(); i++) {
+				recordDrawcall(drawcalls[i], cmdBuffer, pipelineLayout, pushConstantData, i);
+			}
 
         vk::Rect2D dynamicScissor({0, 0}, {width, height});
+			cmdBuffer.endRenderPass();
+		};
+
+		auto finishFunction = [framebuffer, this]()
+		{
+			m_Context.m_Device.destroy(framebuffer);
+		};
 
-		auto &bufferManager = m_BufferManager;
+		recordCommandsToStream(cmdStreamHandle, submitFunction, finishFunction);
+	}
+
+	void Core::recordMeshShaderDrawcalls(
+		const CommandStreamHandle                           cmdStreamHandle,
+		const PassHandle                                    renderpassHandle,
+		const PipelineHandle                                pipelineHandle,
+		const PushConstants&                                pushConstantData,
+		const std::vector<MeshShaderDrawcall>&              drawcalls,
+		const std::vector<ImageHandle>&                     renderTargets) {
+
+		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
+			return;
+		}
+
+		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, m_swapchain, *m_ImageManager);
+		const auto width  = widthHeight[0];
+		const auto height = widthHeight[1];
+
+		const vk::RenderPass        renderpass = m_PassManager->getVkPass(renderpassHandle);
+		const PassConfig            passConfig = m_PassManager->getPassConfig(renderpassHandle);
+
+		const vk::Pipeline          pipeline = m_PipelineManager->getVkPipeline(pipelineHandle);
+		const vk::PipelineLayout    pipelineLayout = m_PipelineManager->getVkPipelineLayout(pipelineHandle);
+		const vk::Rect2D            renderArea(vk::Offset2D(0, 0), vk::Extent2D(width, height));
+
+		vk::CommandBuffer cmdBuffer = m_CommandStreamManager->getStreamCommandBuffer(cmdStreamHandle);
+		transitionRendertargetsToAttachmentLayout(renderTargets, *m_ImageManager, cmdBuffer);
+
+		const vk::Framebuffer framebuffer = createFramebuffer(renderTargets, *m_ImageManager, m_swapchain, renderpass, m_Context.m_Device);
+
+		if (!framebuffer) {
+			vkcv_log(LogLevel::ERROR, "Failed to create temporary framebuffer");
+			return;
+		}
 
 		SubmitInfo submitInfo;
 		submitInfo.queueType = QueueType::Graphics;
 		submitInfo.signalSemaphores = { m_SyncResources.renderFinished };
 
 		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
-            std::vector<vk::ClearValue> clearValues;
-
-            for (const auto& attachment : passConfig.attachments) {
-                if (attachment.load_operation == AttachmentOperation::CLEAR) {
-                    float clear = 0.0f;
-
-                    if (isDepthFormat(attachment.format)) {
-                        clear = 1.0f;
-                    }
-
-                    clearValues.emplace_back(std::array<float, 4>{
-                            clear,
-                            clear,
-                            clear,
-                            1.f
-                    });
-                }
-            }
-
-            const vk::RenderPassBeginInfo beginInfo(renderpass, framebuffer, renderArea, clearValues.size(), clearValues.data());
-            const vk::SubpassContents subpassContents = {};
-            cmdBuffer.beginRenderPass(beginInfo, subpassContents, {});
-
-            cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});
-
-            const PipelineConfig &pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
-            if(pipeConfig.m_UseDynamicViewport)
-            {
-                cmdBuffer.setViewport(0, 1, &dynamicViewport);
-                cmdBuffer.setScissor(0, 1, &dynamicScissor);
-            }
-
-            for (int i = 0; i < drawcalls.size(); i++) {
-                recordDrawcall(drawcalls[i], cmdBuffer, pipelineLayout, pushConstantData, i);
-            }
-
-            cmdBuffer.endRenderPass();
-        };
-
-        auto finishFunction = [framebuffer, this]()
-        {
-            m_Context.m_Device.destroy(framebuffer);
-        };
+
+			const std::vector<vk::ClearValue> clearValues = createAttachmentClearValues(passConfig.attachments);
+
+			const vk::RenderPassBeginInfo beginInfo(renderpass, framebuffer, renderArea, clearValues.size(), clearValues.data());
+			cmdBuffer.beginRenderPass(beginInfo, {}, {});
+
+			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});
+
+			const PipelineConfig& pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
+			if (pipeConfig.m_UseDynamicViewport)
+			{
+				recordDynamicViewport(cmdBuffer, width, height);
+			}
+
+			for (int i = 0; i < drawcalls.size(); i++) {
+                const uint32_t pushConstantOffset = i * pushConstantData.getSizePerDrawcall();
+                recordMeshShaderDrawcall(
+                    cmdBuffer,
+                    pipelineLayout,
+                    pushConstantData,
+                    pushConstantOffset,
+                    drawcalls[i],
+                    0);
+			}
+
+			cmdBuffer.endRenderPass();
+		};
+
+		auto finishFunction = [framebuffer, this]()
+		{
+			m_Context.m_Device.destroy(framebuffer);
+		};
 
 		recordCommandsToStream(cmdStreamHandle, submitFunction, finishFunction);
 	}
@@ -364,7 +477,7 @@ namespace vkcv
 		PipelineHandle computePipeline,
 		const uint32_t dispatchCount[3],
 		const std::vector<DescriptorSetUsage>& descriptorSetUsages,
-		const PushConstantData& pushConstantData) {
+		const PushConstants& pushConstants) {
 
 		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
 
@@ -377,15 +490,16 @@ namespace vkcv
 					pipelineLayout,
 					usage.setLocation,
 					{ usage.vulkanHandle },
-					{});
+					usage.dynamicOffsets
+				);
 			}
-			if (pushConstantData.sizePerDrawcall > 0) {
+			if (pushConstants.getSizePerDrawcall() > 0) {
 				cmdBuffer.pushConstants(
 					pipelineLayout,
 					vk::ShaderStageFlagBits::eCompute,
 					0,
-					pushConstantData.sizePerDrawcall,
-					pushConstantData.data);
+					pushConstants.getSizePerDrawcall(),
+					pushConstants.getData());
 			}
 			cmdBuffer.dispatch(dispatchCount[0], dispatchCount[1], dispatchCount[2]);
 		};
@@ -393,6 +507,42 @@ namespace vkcv
 		recordCommandsToStream(cmdStreamHandle, submitFunction, nullptr);
 	}
 
+	void Core::recordComputeIndirectDispatchToCmdStream(
+		const CommandStreamHandle               cmdStream,
+		const PipelineHandle                    computePipeline,
+		const vkcv::BufferHandle                buffer,
+		const size_t                            bufferArgOffset,
+		const std::vector<DescriptorSetUsage>&  descriptorSetUsages,
+		const PushConstants&                    pushConstants) {
+
+		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
+
+			const auto pipelineLayout = m_PipelineManager->getVkPipelineLayout(computePipeline);
+
+			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eCompute, m_PipelineManager->getVkPipeline(computePipeline));
+			for (const auto& usage : descriptorSetUsages) {
+				cmdBuffer.bindDescriptorSets(
+					vk::PipelineBindPoint::eCompute,
+					pipelineLayout,
+					usage.setLocation,
+					{ usage.vulkanHandle },
+					usage.dynamicOffsets
+				);
+			}
+			if (pushConstants.getSizePerDrawcall() > 0) {
+				cmdBuffer.pushConstants(
+					pipelineLayout,
+					vk::ShaderStageFlagBits::eCompute,
+					0,
+					pushConstants.getSizePerDrawcall(),
+					pushConstants.getData());
+			}
+			cmdBuffer.dispatchIndirect(m_BufferManager->getBuffer(buffer), bufferArgOffset);
+		};
+
+		recordCommandsToStream(cmdStream, submitFunction, nullptr);
+	}
+
 	void Core::endFrame() {
 		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
 			return;
@@ -417,9 +567,9 @@ namespace vkcv
 		
 		try {
 			result = queueManager.getPresentQueue().handle.presentKHR(presentInfo);
-		} catch (vk::OutOfDateKHRError e) {
+		} catch (const vk::OutOfDateKHRError& e) {
 			result = vk::Result::eErrorOutOfDateKHR;
-		} catch (vk::DeviceLostError e) {
+		} catch (const vk::DeviceLostError& e) {
 			result = vk::Result::eErrorDeviceLost;
 		}
 		
@@ -463,8 +613,6 @@ namespace vkcv
 	}
 
 	CommandStreamHandle Core::createCommandStream(QueueType queueType) {
-
-		const vk::Device&       device  = m_Context.getDevice();
 		const vkcv::Queue       queue   = getQueueForSubmit(queueType, m_Context.getQueueManager());
 		const vk::CommandPool   cmdPool = chooseCmdPool(queue, m_CommandResources);
 
@@ -482,7 +630,7 @@ namespace vkcv
 		}
 	}
 
-	void Core::submitCommandStream(const CommandStreamHandle handle) {
+	void Core::submitCommandStream(const CommandStreamHandle& handle) {
 		std::vector<vk::Semaphore> waitSemaphores;
 		// FIXME: add proper user controllable sync
 		std::vector<vk::Semaphore> signalSemaphores = { m_SyncResources.renderFinished };
@@ -490,8 +638,9 @@ namespace vkcv
 	}
 
 	SamplerHandle Core::createSampler(SamplerFilterType magFilter, SamplerFilterType minFilter,
-									  SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode) {
-		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode);
+									  SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode,
+									  float mipLodBias) {
+		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode, mipLodBias);
 	}
 
 	Image Core::createImage(
@@ -522,14 +671,18 @@ namespace vkcv
 			multisampling);
 	}
 
-	const uint32_t Core::getImageWidth(ImageHandle imageHandle)
+	uint32_t Core::getImageWidth(const ImageHandle& image)
 	{
-		return m_ImageManager->getImageWidth(imageHandle);
+		return m_ImageManager->getImageWidth(image);
 	}
 
-	const uint32_t Core::getImageHeight(ImageHandle imageHandle)
+	uint32_t Core::getImageHeight(const ImageHandle& image)
 	{
-		return m_ImageManager->getImageHeight(imageHandle);
+		return m_ImageManager->getImageHeight(image);
+	}
+	
+	vk::Format Core::getImageFormat(const ImageHandle& image) {
+		return m_ImageManager->getImageFormat(image);
 	}
 
     DescriptorSetHandle Core::createDescriptorSet(const std::vector<DescriptorBinding>& bindings)
@@ -550,38 +703,38 @@ namespace vkcv
 		return m_DescriptorManager->getDescriptorSet(handle);
 	}
 
-	void Core::prepareSwapchainImageForPresent(const CommandStreamHandle cmdStream) {
+	void Core::prepareSwapchainImageForPresent(const CommandStreamHandle& cmdStream) {
 		auto swapchainHandle = ImageHandle::createSwapchainImageHandle();
 		recordCommandsToStream(cmdStream, [swapchainHandle, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(swapchainHandle, vk::ImageLayout::ePresentSrcKHR, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::prepareImageForSampling(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::prepareImageForSampling(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eShaderReadOnlyOptimal, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::prepareImageForStorage(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::prepareImageForStorage(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eGeneral, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::recordImageMemoryBarrier(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::recordImageMemoryBarrier(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageMemoryBarrier(image, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::recordBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer) {
+	void Core::recordBufferMemoryBarrier(const CommandStreamHandle& cmdStream, const BufferHandle& buffer) {
 		recordCommandsToStream(cmdStream, [buffer, this](const vk::CommandBuffer cmdBuffer) {
 			m_BufferManager->recordBufferMemoryBarrier(buffer, cmdBuffer);
 		}, nullptr);
 	}
 	
-	void Core::resolveMSAAImage(CommandStreamHandle cmdStream, ImageHandle src, ImageHandle dst) {
+	void Core::resolveMSAAImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst) {
 		recordCommandsToStream(cmdStream, [src, dst, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordMSAAResolve(cmdBuffer, src, dst);
 		}, nullptr);
@@ -590,5 +743,86 @@ namespace vkcv
 	vk::ImageView Core::getSwapchainImageView() const {
     	return m_ImageManager->getVulkanImageView(vkcv::ImageHandle::createSwapchainImageHandle());
     }
+    
+    void Core::recordMemoryBarrier(const CommandStreamHandle& cmdStream) {
+		recordCommandsToStream(cmdStream, [](const vk::CommandBuffer cmdBuffer) {
+			vk::MemoryBarrier barrier (
+					vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead,
+					vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead
+			);
+			
+			cmdBuffer.pipelineBarrier(
+					vk::PipelineStageFlagBits::eAllCommands,
+					vk::PipelineStageFlagBits::eAllCommands,
+					vk::DependencyFlags(),
+					1, &barrier,
+					0, nullptr,
+					0, nullptr
+			);
+		}, nullptr);
+	}
+	
+	void Core::recordBlitImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst,
+							   SamplerFilterType filterType) {
+		recordCommandsToStream(cmdStream, [&](const vk::CommandBuffer cmdBuffer) {
+			m_ImageManager->recordImageLayoutTransition(
+					src, vk::ImageLayout::eTransferSrcOptimal, cmdBuffer
+			);
+			
+			m_ImageManager->recordImageLayoutTransition(
+					dst, vk::ImageLayout::eTransferDstOptimal, cmdBuffer
+			);
+			
+			const std::array<vk::Offset3D, 2> srcOffsets = {
+					vk::Offset3D(0, 0, 0),
+					vk::Offset3D(
+							m_ImageManager->getImageWidth(src),
+							m_ImageManager->getImageHeight(src),
+							1
+					)
+			};
+			
+			const std::array<vk::Offset3D, 2> dstOffsets = {
+					vk::Offset3D(0, 0, 0),
+					vk::Offset3D(
+							m_ImageManager->getImageWidth(dst),
+							m_ImageManager->getImageHeight(dst),
+							1
+					)
+			};
+			
+			const bool srcDepth = isDepthFormat(m_ImageManager->getImageFormat(src));
+			const bool dstDepth = isDepthFormat(m_ImageManager->getImageFormat(dst));
+			
+			const vk::ImageBlit blit = vk::ImageBlit(
+					vk::ImageSubresourceLayers(
+							srcDepth?
+							vk::ImageAspectFlagBits::eDepth :
+							vk::ImageAspectFlagBits::eColor,
+							0, 0, 1
+					),
+					srcOffsets,
+					vk::ImageSubresourceLayers(
+							dstDepth?
+							vk::ImageAspectFlagBits::eDepth :
+							vk::ImageAspectFlagBits::eColor,
+							0, 0, 1
+					),
+					dstOffsets
+			);
+			
+			cmdBuffer.blitImage(
+					m_ImageManager->getVulkanImage(src),
+					vk::ImageLayout::eTransferSrcOptimal,
+					m_ImageManager->getVulkanImage(dst),
+					vk::ImageLayout::eTransferDstOptimal,
+					1,
+					&blit,
+					filterType == SamplerFilterType::LINEAR?
+					vk::Filter::eLinear :
+					vk::Filter::eNearest
+			);
+		}, nullptr);
+	}
 	
 }
diff --git a/src/vkcv/DescriptorManager.cpp b/src/vkcv/DescriptorManager.cpp
index d28dd9d137240ba923b55c9be9da9059d3a9ab31..76f2dae74420804c9ce168bea76e7c1bdef0fbc0 100644
--- a/src/vkcv/DescriptorManager.cpp
+++ b/src/vkcv/DescriptorManager.cpp
@@ -11,12 +11,17 @@ namespace vkcv
          * Allocate the set size for the descriptor pools, namely 1000 units of each descriptor type below.
 		 * Finally, create an initial pool.
          */
-		m_PoolSizes = { vk::DescriptorPoolSize(vk::DescriptorType::eSampler, 1000),
-													vk::DescriptorPoolSize(vk::DescriptorType::eSampledImage, 1000),
-													vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 1000),
-													vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 1000) };
+		m_PoolSizes = {
+				vk::DescriptorPoolSize(vk::DescriptorType::eSampler, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eSampledImage, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eUniformBufferDynamic, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eStorageBufferDynamic, 1000)
+		};
 
-		m_PoolInfo = vk::DescriptorPoolCreateInfo({},
+		m_PoolInfo = vk::DescriptorPoolCreateInfo(
+				vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet,
 			1000,
 			static_cast<uint32_t>(m_PoolSizes.size()),
 			m_PoolSizes.data());
@@ -29,9 +34,13 @@ namespace vkcv
         for (uint64_t id = 0; id < m_DescriptorSets.size(); id++) {
 			destroyDescriptorSetById(id);
         }
+        
 		m_DescriptorSets.clear();
+  
 		for (const auto &pool : m_Pools) {
-			m_Device.destroy(pool);
+			if (pool) {
+				m_Device.destroy(pool);
+			}
 		}
     }
 
@@ -40,12 +49,12 @@ namespace vkcv
         std::vector<vk::DescriptorSetLayoutBinding> setBindings = {};
 
         //create each set's binding
-        for (uint32_t i = 0; i < bindings.size(); i++) {
+        for (auto binding : bindings) {
             vk::DescriptorSetLayoutBinding descriptorSetLayoutBinding(
-                bindings[i].bindingID,
-                convertDescriptorTypeFlag(bindings[i].descriptorType),
-                bindings[i].descriptorCount,
-				getShaderStageFlags(bindings[i].shaderStages));
+                binding.bindingID,
+                convertDescriptorTypeFlag(binding.descriptorType),
+                binding.descriptorCount,
+                getShaderStageFlags(binding.shaderStages));
             setBindings.push_back(descriptorSetLayoutBinding);
         }
 
@@ -53,8 +62,7 @@ namespace vkcv
 
         //create the descriptor set's layout from the bindings gathered above
         vk::DescriptorSetLayoutCreateInfo layoutInfo({}, setBindings);
-        if(m_Device.createDescriptorSetLayout(&layoutInfo, nullptr, &set.layout) != vk::Result::eSuccess)
-        {
+        if (m_Device.createDescriptorSetLayout(&layoutInfo, nullptr, &set.layout) != vk::Result::eSuccess) {
 			vkcv_log(LogLevel::ERROR, "Failed to create descriptor set layout");
             return DescriptorSetHandle();
         };
@@ -70,6 +78,7 @@ namespace vkcv
 				allocInfo.setDescriptorPool(m_Pools.back());
 				result = m_Device.allocateDescriptorSets(&allocInfo, &set.vulkanHandle);
 			}
+			
 			if (result != vk::Result::eSuccess) {
 				vkcv_log(LogLevel::ERROR, "Failed to create descriptor set (%s)",
 						 vk::to_string(result).c_str());
@@ -78,6 +87,8 @@ namespace vkcv
 				return DescriptorSetHandle();
 			}
         };
+	
+		set.poolIndex = (m_Pools.size() - 1);
 
         const uint64_t id = m_DescriptorSets.size();
 
@@ -98,7 +109,6 @@ namespace vkcv
 		const ImageManager		&imageManager, 
 		const BufferManager		&bufferManager,
 		const SamplerManager	&samplerManager) {
-
 		vk::DescriptorSet set = m_DescriptorSets[handle.getId()].vulkanHandle;
 
 		std::vector<vk::DescriptorImageInfo> imageInfos;
@@ -146,10 +156,15 @@ namespace vkcv
 		}
 
 		for (const auto& write : writes.uniformBufferWrites) {
+			const size_t size = bufferManager.getBufferSize(write.buffer);
+			const uint32_t offset = std::clamp<uint32_t>(write.offset, 0, size);
+			
 			const vk::DescriptorBufferInfo bufferInfo(
 				bufferManager.getBuffer(write.buffer),
-				static_cast<uint32_t>(0),
-				bufferManager.getBufferSize(write.buffer)
+				offset,
+				write.size == 0? size : std::min<uint32_t>(
+						write.size, size - offset
+				)
 			);
 			
 			bufferInfos.push_back(bufferInfo);
@@ -158,6 +173,8 @@ namespace vkcv
 					0,
 					bufferInfos.size(),
 					write.binding,
+					write.dynamic?
+					vk::DescriptorType::eUniformBufferDynamic :
 					vk::DescriptorType::eUniformBuffer
 			};
 			
@@ -165,10 +182,15 @@ namespace vkcv
 		}
 
 		for (const auto& write : writes.storageBufferWrites) {
+			const size_t size = bufferManager.getBufferSize(write.buffer);
+			const uint32_t offset = std::clamp<uint32_t>(write.offset, 0, size);
+			
 			const vk::DescriptorBufferInfo bufferInfo(
 				bufferManager.getBuffer(write.buffer),
-				static_cast<uint32_t>(0),
-				bufferManager.getBufferSize(write.buffer)
+				offset,
+				write.size == 0? size : std::min<uint32_t>(
+						write.size, size - offset
+				)
 			);
 			
 			bufferInfos.push_back(bufferInfo);
@@ -177,6 +199,8 @@ namespace vkcv
 					0,
 					bufferInfos.size(),
 					write.binding,
+					write.dynamic?
+					vk::DescriptorType::eStorageBufferDynamic :
 					vk::DescriptorType::eStorageBuffer
 			};
 			
@@ -232,8 +256,12 @@ namespace vkcv
         {
             case DescriptorType::UNIFORM_BUFFER:
                 return vk::DescriptorType::eUniformBuffer;
+			case DescriptorType::UNIFORM_BUFFER_DYNAMIC:
+				return vk::DescriptorType::eUniformBufferDynamic;
             case DescriptorType::STORAGE_BUFFER:
                 return vk::DescriptorType::eStorageBuffer;
+			case DescriptorType::STORAGE_BUFFER_DYNAMIC:
+				return vk::DescriptorType::eStorageBufferDynamic;
             case DescriptorType::SAMPLER:
                 return vk::DescriptorType::eSampler;
             case DescriptorType::IMAGE_SAMPLED:
@@ -257,17 +285,22 @@ namespace vkcv
 			m_Device.destroyDescriptorSetLayout(set.layout);
 			set.layout = nullptr;
 		}
-		// FIXME: descriptor set itself not destroyed
+		
+		if (set.vulkanHandle) {
+			m_Device.freeDescriptorSets(m_Pools[set.poolIndex], 1, &(set.vulkanHandle));
+			set.vulkanHandle = nullptr;
+		}
 	}
 
 	vk::DescriptorPool DescriptorManager::allocateDescriptorPool() {
 		vk::DescriptorPool pool;
-		if (m_Device.createDescriptorPool(&m_PoolInfo, nullptr, &pool) != vk::Result::eSuccess)
-		{
+		if (m_Device.createDescriptorPool(&m_PoolInfo, nullptr, &pool) != vk::Result::eSuccess) {
 			vkcv_log(LogLevel::WARNING, "Failed to allocate descriptor pool");
 			pool = nullptr;
-		};
-		m_Pools.push_back(pool);
+		} else {
+			m_Pools.push_back(pool);
+		}
+		
 		return pool;
 	}
 
diff --git a/src/vkcv/DrawcallRecording.cpp b/src/vkcv/DrawcallRecording.cpp
index e6ea18588c251b5e49f454618a5ac9962cc8a264..d89ace3859717f753534402507a713a78bfb6876 100644
--- a/src/vkcv/DrawcallRecording.cpp
+++ b/src/vkcv/DrawcallRecording.cpp
@@ -1,12 +1,23 @@
 #include <vkcv/DrawcallRecording.hpp>
+#include <vkcv/Logger.hpp>
 
 namespace vkcv {
 
+    vk::IndexType getIndexType(IndexBitCount indexByteCount){
+        switch (indexByteCount) {
+            case IndexBitCount::Bit16: return vk::IndexType::eUint16;
+            case IndexBitCount::Bit32: return vk::IndexType::eUint32;
+            default:
+                vkcv_log(LogLevel::ERROR, "unknown Enum");
+                return vk::IndexType::eUint16;
+        }
+    }
+
     void recordDrawcall(
         const DrawcallInfo      &drawcall,
         vk::CommandBuffer       cmdBuffer,
         vk::PipelineLayout      pipelineLayout,
-        const PushConstantData  &pushConstantData,
+        const PushConstants     &pushConstants,
         const size_t            drawcallIndex) {
 
         for (uint32_t i = 0; i < drawcall.mesh.vertexBufferBindings.size(); i++) {
@@ -23,25 +34,69 @@ namespace vkcv {
                 nullptr);
         }
 
-        const size_t drawcallPushConstantOffset = drawcallIndex * pushConstantData.sizePerDrawcall;
-        // char* cast because void* does not support pointer arithmetic
-        const void* drawcallPushConstantData = drawcallPushConstantOffset + (char*)pushConstantData.data;
-
-        if (pushConstantData.data && pushConstantData.sizePerDrawcall > 0) {
+        if (pushConstants.getSizePerDrawcall() > 0) {
             cmdBuffer.pushConstants(
                 pipelineLayout,
                 vk::ShaderStageFlagBits::eAll,
                 0,
-                pushConstantData.sizePerDrawcall,
-                drawcallPushConstantData);
+				pushConstants.getSizePerDrawcall(),
+                pushConstants.getDrawcallData(drawcallIndex));
         }
 
         if (drawcall.mesh.indexBuffer) {
-            cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, vk::IndexType::eUint16);	//FIXME: choose proper size
+            cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, getIndexType(drawcall.mesh.indexBitCount));
             cmdBuffer.drawIndexed(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
         }
         else {
-            cmdBuffer.draw(drawcall.mesh.indexCount, 1, 0, 0, {});
+            cmdBuffer.draw(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
+        }
+    }
+
+
+
+    struct MeshShaderFunctions
+    {
+        PFN_vkCmdDrawMeshTasksNV cmdDrawMeshTasks                           = nullptr;
+        PFN_vkCmdDrawMeshTasksIndirectNV cmdDrawMeshTasksIndirect           = nullptr;
+        PFN_vkCmdDrawMeshTasksIndirectCountNV cmdDrawMeshTasksIndirectCount = nullptr;
+    } MeshShaderFunctions;
+
+    void InitMeshShaderDrawFunctions(vk::Device device)
+    {
+        MeshShaderFunctions.cmdDrawMeshTasks = PFN_vkCmdDrawMeshTasksNV(device.getProcAddr("vkCmdDrawMeshTasksNV"));
+        MeshShaderFunctions.cmdDrawMeshTasksIndirect = PFN_vkCmdDrawMeshTasksIndirectNV(device.getProcAddr("vkCmdDrawMeshTasksIndirectNV"));
+        MeshShaderFunctions.cmdDrawMeshTasksIndirectCount = PFN_vkCmdDrawMeshTasksIndirectCountNV (device.getProcAddr( "vkCmdDrawMeshTasksIndirectCountNV"));
+    }
+
+    void recordMeshShaderDrawcall(
+        vk::CommandBuffer                       cmdBuffer,
+        vk::PipelineLayout                      pipelineLayout,
+        const PushConstants&                    pushConstantData,
+        const uint32_t                          pushConstantOffset,
+        const MeshShaderDrawcall&               drawcall,
+        const uint32_t                          firstTask) {
+
+        for (const auto& descriptorUsage : drawcall.descriptorSets) {
+            cmdBuffer.bindDescriptorSets(
+                vk::PipelineBindPoint::eGraphics,
+                pipelineLayout,
+                descriptorUsage.setLocation,
+                descriptorUsage.vulkanHandle,
+                nullptr);
+        }
+
+        // char* cast because void* does not support pointer arithmetic
+        const void* drawcallPushConstantData = pushConstantOffset + (char*)pushConstantData.getData();
+
+        if (pushConstantData.getData()) {
+            cmdBuffer.pushConstants(
+                pipelineLayout,
+                vk::ShaderStageFlagBits::eAll,
+                0,
+                pushConstantData.getSizePerDrawcall(),
+                drawcallPushConstantData);
         }
+
+        MeshShaderFunctions.cmdDrawMeshTasks(VkCommandBuffer(cmdBuffer), drawcall.taskCount, firstTask);
     }
 }
diff --git a/src/vkcv/File.cpp b/src/vkcv/File.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6006b90f74e0a41f83483f2a1efbe5bda4c4e9f8
--- /dev/null
+++ b/src/vkcv/File.cpp
@@ -0,0 +1,60 @@
+
+#include "vkcv/File.hpp"
+
+#include <stdlib.h>
+
+#ifdef _WIN32
+#include <io.h>
+#else
+#include <unistd.h>
+#endif
+
+#include "vkcv/Logger.hpp"
+
+namespace vkcv {
+	
+	std::filesystem::path generateTemporaryFilePath() {
+		std::filesystem::path tmp = generateTemporaryDirectoryPath();
+		
+		if (std::filesystem::is_directory(tmp)) {
+			return std::filesystem::path(tmp.string() + "W"); // add W for Wambo
+		} else {
+			return tmp;
+		}
+	}
+	
+	std::filesystem::path generateTemporaryDirectoryPath() {
+		std::error_code code;
+		auto tmp = std::filesystem::temp_directory_path(code);
+		
+		if (tmp.empty()) {
+			tmp = std::filesystem::current_path();
+		}
+		
+		char name [16] = "vkcv_tmp_XXXXXX";
+		
+#ifdef _WIN32
+		int err = _mktemp_s(name, 16);
+		
+		if (err != 0) {
+			vkcv_log(LogLevel::ERROR, "Temporary file path could not be generated");
+			return "";
+		}
+#else
+		int fd = mkstemp(name); // creates a file locally
+		
+		if (fd == -1) {
+			vkcv_log(LogLevel::ERROR, "Temporary file path could not be generated");
+			return "";
+		}
+		
+		close(fd);
+		remove(name); // removes the local file again
+#endif
+		
+		return tmp / name;
+	}
+	
+	
+	
+}
diff --git a/src/vkcv/Handles.cpp b/src/vkcv/Handles.cpp
index 020489418c8e2db6ce2062d6fd20f06f90a05c37..65fc02dedeba39953c173103efe9b228f49e5d7f 100644
--- a/src/vkcv/Handles.cpp
+++ b/src/vkcv/Handles.cpp
@@ -1,5 +1,7 @@
 #include "vkcv/Handles.hpp"
 
+#include <iostream>
+
 namespace vkcv {
 	
 	Handle::Handle() :
@@ -11,7 +13,7 @@ namespace vkcv {
 	{}
 	
 	Handle::~Handle() {
-		if ((m_rc) && (--(*m_rc) == 0)) {
+		if ((m_rc) && (*m_rc > 0) && (--(*m_rc) == 0)) {
 			if (m_destroy) {
 				m_destroy(m_id);
 			}
@@ -82,9 +84,9 @@ namespace vkcv {
 	
 	std::ostream& operator << (std::ostream& out, const Handle& handle) {
 		if (handle) {
-			return out << "[Handle: " << handle.getId() << ":" << handle.getRC() << "]";
+			return out << "[" << typeid(handle).name() << ": " << handle.getId() << ":" << handle.getRC() << "]";
 		} else {
-			return out << "[Handle: none]";
+			return out << "[" << typeid(handle).name() << ": none]";
 		}
 	}
 	
diff --git a/src/vkcv/Image.cpp b/src/vkcv/Image.cpp
index f8d94b734599cbf1f55aad7b590ab4796501d951..15a2fc5240176742f50141407a3c72b531757ee9 100644
--- a/src/vkcv/Image.cpp
+++ b/src/vkcv/Image.cpp
@@ -56,7 +56,7 @@ namespace vkcv{
 		m_manager->switchImageLayoutImmediate(m_handle, newLayout);
 	}
 
-	vkcv::ImageHandle Image::getHandle() const {
+	const vkcv::ImageHandle& Image::getHandle() const {
 		return m_handle;
 	}
 
@@ -64,7 +64,7 @@ namespace vkcv{
 		return m_manager->getImageMipCount(m_handle);
 	}
 
-	void Image::fill(void *data, size_t size) {
+	void Image::fill(const void *data, size_t size) {
 		m_manager->fillImage(m_handle, data, size);
 	}
 
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index ae554e6babdd2b2f42c352515c02a34e45182fec..4ddd7f8c44c6023a80831bc8b4b092692e84ec86 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -12,26 +12,6 @@
 
 namespace vkcv {
 
-	ImageManager::Image::Image(
-		vk::Image                   handle,
-		vk::DeviceMemory            memory,
-		std::vector<vk::ImageView>  views,
-		uint32_t                    width,
-		uint32_t                    height,
-		uint32_t                    depth,
-		vk::Format                  format,
-		uint32_t                    layers)
-		:
-		m_handle(handle),
-		m_memory(memory),
-        m_viewPerMip(views),
-		m_width(width),
-		m_height(height),
-		m_depth(depth),
-		m_format(format),
-		m_layers(layers)
-	{}
-
 	/**
 	 * @brief searches memory type index for image allocation, combines requirements of image and application
 	 * @param physicalMemoryProperties Memory Properties of physical device
@@ -67,7 +47,8 @@ namespace vkcv {
 		for (uint64_t id = 0; id < m_images.size(); id++) {
 			destroyImageById(id);
 		}
-		for (const auto swapchainImage : m_swapchainImages) {
+		
+		for (const auto& swapchainImage : m_swapchainImages) {
 			for (const auto view : swapchainImage.m_viewPerMip) {
 				m_core->getContext().getDevice().destroy(view);
 			}
@@ -123,7 +104,7 @@ namespace vkcv {
 			imageUsageFlags |= vk::ImageUsageFlagBits::eDepthStencilAttachment;
 		}
 
-		const vk::Device& device = m_core->getContext().getDevice();
+		const vma::Allocator& allocator = m_core->getContext().getAllocator();
 
 		vk::ImageType imageType = vk::ImageType::e3D;
 		vk::ImageViewType imageViewType = vk::ImageViewType::e3D;
@@ -157,7 +138,7 @@ namespace vkcv {
 
 		vk::SampleCountFlagBits sampleCountFlag = msaaToVkSampleCountFlag(msaa);
 
-		const vk::ImageCreateInfo imageCreateInfo(
+		const vk::ImageCreateInfo imageCreateInfo (
 			createFlags,
 			imageType,
 			format,
@@ -171,21 +152,22 @@ namespace vkcv {
 			{},
 			vk::ImageLayout::eUndefined
 		);
-
-		vk::Image image = device.createImage(imageCreateInfo);
 		
-		const vk::MemoryRequirements requirements = device.getImageMemoryRequirements(image);
-
-		vk::MemoryPropertyFlags memoryTypeFlags = vk::MemoryPropertyFlagBits::eDeviceLocal;
-
-		const uint32_t memoryTypeIndex = searchImageMemoryType(
-			physicalDevice.getMemoryProperties(),
-			requirements.memoryTypeBits,
-			memoryTypeFlags
+		auto imageAllocation = allocator.createImage(
+				imageCreateInfo,
+				vma::AllocationCreateInfo(
+						vma::AllocationCreateFlags(),
+						vma::MemoryUsage::eGpuOnly,
+						vk::MemoryPropertyFlagBits::eDeviceLocal,
+						vk::MemoryPropertyFlagBits::eDeviceLocal,
+						0,
+						vma::Pool(),
+						nullptr
+				)
 		);
 
-		vk::DeviceMemory memory = device.allocateMemory(vk::MemoryAllocateInfo(requirements.size, memoryTypeIndex));
-		device.bindImageMemory(image, memory, 0);
+		vk::Image image = imageAllocation.first;
+		vma::Allocation allocation = imageAllocation.second;
 
 		vk::ImageAspectFlags aspectFlags;
 		
@@ -195,8 +177,10 @@ namespace vkcv {
 			aspectFlags = vk::ImageAspectFlagBits::eColor;
 		}
 		
+		const vk::Device& device = m_core->getContext().getDevice();
+		
 		std::vector<vk::ImageView> views;
-		for (int mip = 0; mip < mipCount; mip++) {
+		for (uint32_t mip = 0; mip < mipCount; mip++) {
 			const vk::ImageViewCreateInfo imageViewCreateInfo(
 				{},
 				image,
@@ -221,11 +205,11 @@ namespace vkcv {
 		}
 		
 		const uint64_t id = m_images.size();
-		m_images.push_back(Image(image, memory, views, width, height, depth, format, arrayLayers));
+		m_images.push_back({ image, allocation, views, width, height, depth, format, arrayLayers, vk::ImageLayout::eUndefined });
 		return ImageHandle(id, [&](uint64_t id) { destroyImageById(id); });
 	}
 	
-	ImageHandle ImageManager::createSwapchainImage() {
+	ImageHandle ImageManager::createSwapchainImage() const {
 		return ImageHandle::createSwapchainImageHandle();
 	}
 	
@@ -261,10 +245,16 @@ namespace vkcv {
 		
 		auto& image = m_images[id];
 		
-		return image.m_memory;
+		const vma::Allocator& allocator = m_core->getContext().getAllocator();
+		
+		auto info = allocator.getAllocationInfo(
+				image.m_allocation
+		);
+		
+		return info.deviceMemory;
 	}
 	
-	vk::ImageView ImageManager::getVulkanImageView(const ImageHandle &handle, const size_t mipLevel) const {
+	vk::ImageView ImageManager::getVulkanImageView(const ImageHandle &handle, size_t mipLevel) const {
 		
 		if (handle.isSwapchainImage()) {
 			return m_swapchainImages[m_currentSwapchainInputImage].m_viewPerMip[0];
@@ -369,7 +359,7 @@ namespace vkcv {
 		}
 	}
 	
-	void ImageManager::fillImage(const ImageHandle& handle, void* data, size_t size)
+	void ImageManager::fillImage(const ImageHandle& handle, const void* data, size_t size)
 	{
 		const uint64_t id = handle.getId();
 		
@@ -397,7 +387,7 @@ namespace vkcv {
 		const size_t max_size = std::min(size, image_size);
 		
 		BufferHandle bufferHandle = m_bufferManager.createBuffer(
-				BufferType::STAGING, max_size, BufferMemoryType::HOST_VISIBLE
+				BufferType::STAGING, max_size, BufferMemoryType::HOST_VISIBLE, false
 		);
 		
 		m_bufferManager.fillBuffer(bufferHandle, data, max_size, 0);
@@ -504,9 +494,6 @@ namespace vkcv {
 	}
 
 	void ImageManager::generateImageMipChainImmediate(const ImageHandle& handle) {
-
-		const auto& device = m_core->getContext().getDevice();
-
 		SubmitInfo submitInfo;
 		submitInfo.queueType = QueueType::Graphics;
 
@@ -628,15 +615,14 @@ namespace vkcv {
 				view = nullptr;
 			}
 		}
-
-		if (image.m_memory) {
-			device.freeMemory(image.m_memory);
-			image.m_memory = nullptr;
-		}
+		
+		const vma::Allocator& allocator = m_core->getContext().getAllocator();
 
 		if (image.m_handle) {
-			device.destroyImage(image.m_handle);
+			allocator.destroyImage(image.m_handle, image.m_allocation);
+			
 			image.m_handle = nullptr;
+			image.m_allocation = nullptr;
 		}
 	}
 
@@ -655,7 +641,6 @@ namespace vkcv {
 
 	uint32_t ImageManager::getImageMipCount(const ImageHandle& handle) const {
 		const uint64_t id = handle.getId();
-		const bool isSwapchainFormat = handle.isSwapchainImage();
 
 		if (handle.isSwapchainImage()) {
 			return 1;
@@ -673,11 +658,11 @@ namespace vkcv {
 		m_currentSwapchainInputImage = index;
 	}
 
-	void ImageManager::setSwapchainImages(const std::vector<vk::Image>& images, std::vector<vk::ImageView> views, 
+	void ImageManager::setSwapchainImages(const std::vector<vk::Image>& images, const std::vector<vk::ImageView>& views,
 		uint32_t width, uint32_t height, vk::Format format) {
 
 		// destroy old views
-		for (auto image : m_swapchainImages) {
+		for (const auto& image : m_swapchainImages) {
 			for (const auto& view : image.m_viewPerMip) {
 				m_core->getContext().getDevice().destroyImageView(view);
 			}
@@ -685,8 +670,18 @@ namespace vkcv {
 
 		assert(images.size() == views.size());
 		m_swapchainImages.clear();
-		for (int i = 0; i < images.size(); i++) {
-			m_swapchainImages.push_back(Image(images[i], nullptr, { views[i] }, width, height, 1, format, 1));
+		for (size_t i = 0; i < images.size(); i++) {
+			m_swapchainImages.push_back({
+				images[i],
+				nullptr,
+				{ views[i] },
+				width,
+				height,
+				1,
+				format,
+				1,
+				vk::ImageLayout::eUndefined
+			});
 		}
 	}
 
diff --git a/src/vkcv/ImageManager.hpp b/src/vkcv/ImageManager.hpp
index 1d8ce207b645e30cee291816eac3c934ed40e92a..4d99422118e8d464ea75d9f013b471f3dd40fd8c 100644
--- a/src/vkcv/ImageManager.hpp
+++ b/src/vkcv/ImageManager.hpp
@@ -6,12 +6,15 @@
  */
 #include <vector>
 #include <vulkan/vulkan.hpp>
+#include <vk_mem_alloc.hpp>
 
 #include "vkcv/BufferManager.hpp"
 #include "vkcv/Handles.hpp"
 #include "vkcv/ImageConfig.hpp"
 
 namespace vkcv {
+	
+	bool isDepthImageFormat(vk::Format format);
 
 	class ImageManager
 	{
@@ -20,28 +23,16 @@ namespace vkcv {
 		struct Image
 		{
 			vk::Image                   m_handle;
-			vk::DeviceMemory            m_memory;
+			vma::Allocation             m_allocation;
 			std::vector<vk::ImageView>  m_viewPerMip;
-			uint32_t                    m_width     = 0;
-			uint32_t                    m_height    = 0;
-			uint32_t                    m_depth     = 0;
+			uint32_t                    m_width;
+			uint32_t                    m_height;
+			uint32_t                    m_depth;
 			vk::Format                  m_format;
-			uint32_t                    m_layers    = 1;
-			vk::ImageLayout             m_layout    = vk::ImageLayout::eUndefined;
+			uint32_t                    m_layers;
+			vk::ImageLayout             m_layout;
 		private:
-			// struct is public so utility functions can access members, but only ImageManager can create Image
 			friend ImageManager;
-			Image(
-				vk::Image                   handle,
-				vk::DeviceMemory            memory,
-				std::vector<vk::ImageView>  views,
-				uint32_t                    width,
-				uint32_t                    height,
-				uint32_t                    depth,
-				vk::Format                  format,
-				uint32_t                    layers);
-
-			Image();
 		};
 	private:
 		
@@ -52,7 +43,7 @@ namespace vkcv {
 		std::vector<Image> m_swapchainImages;
 		int m_currentSwapchainInputImage;
 		
-		ImageManager(BufferManager& bufferManager) noexcept;
+		explicit ImageManager(BufferManager& bufferManager) noexcept;
 		
 		/**
 		 * Destroys and deallocates image represented by a given
@@ -82,7 +73,8 @@ namespace vkcv {
 			bool            supportColorAttachment,
 			Multisampling   msaa);
 		
-		ImageHandle createSwapchainImage();
+		[[nodiscard]]
+		ImageHandle createSwapchainImage() const;
 		
 		[[nodiscard]]
 		vk::Image getVulkanImage(const ImageHandle& handle) const;
@@ -91,7 +83,7 @@ namespace vkcv {
 		vk::DeviceMemory getVulkanDeviceMemory(const ImageHandle& handle) const;
 		
 		[[nodiscard]]
-		vk::ImageView getVulkanImageView(const ImageHandle& handle, const size_t mipLevel = 0) const;
+		vk::ImageView getVulkanImageView(const ImageHandle& handle, size_t mipLevel = 0) const;
 
 		void switchImageLayoutImmediate(const ImageHandle& handle, vk::ImageLayout newLayout);
 		void recordImageLayoutTransition(
@@ -103,7 +95,7 @@ namespace vkcv {
 			const ImageHandle& handle,
 			vk::CommandBuffer cmdBuffer);
 
-		void fillImage(const ImageHandle& handle, void* data, size_t size);
+		void fillImage(const ImageHandle& handle, const void* data, size_t size);
 		void generateImageMipChainImmediate(const ImageHandle& handle);
 		void recordImageMipChainGenerationToCmdStream(const vkcv::CommandStreamHandle& cmdStream, const ImageHandle& handle);
 		void recordMSAAResolve(vk::CommandBuffer cmdBuffer, ImageHandle src, ImageHandle dst);
@@ -124,8 +116,9 @@ namespace vkcv {
 		uint32_t getImageMipCount(const ImageHandle& handle) const;
 
 		void setCurrentSwapchainImageIndex(int index);
-		void setSwapchainImages(const std::vector<vk::Image>& images, std::vector<vk::ImageView> views,
-			uint32_t width, uint32_t height, vk::Format format);
+		
+		void setSwapchainImages(const std::vector<vk::Image>& images, const std::vector<vk::ImageView>& views,
+								uint32_t width, uint32_t height, vk::Format format);
 
 	};
 }
\ No newline at end of file
diff --git a/src/vkcv/PipelineManager.cpp b/src/vkcv/PipelineManager.cpp
index 8b1f0b68be3a72f60103ca0dd8136f2c923513a5..244f6723f70e5ea938c005b74b286e192d68443c 100644
--- a/src/vkcv/PipelineManager.cpp
+++ b/src/vkcv/PipelineManager.cpp
@@ -44,95 +44,190 @@ namespace vkcv
 
     vk::PrimitiveTopology primitiveTopologyToVulkanPrimitiveTopology(const PrimitiveTopology topology) {
         switch (topology) {
-        case(PrimitiveTopology::PointList):     return vk::PrimitiveTopology::ePointList;
-        case(PrimitiveTopology::LineList):      return vk::PrimitiveTopology::eLineList;
-        case(PrimitiveTopology::TriangleList):  return vk::PrimitiveTopology::eTriangleList;
-        default: std::cout << "Error: Unknown primitive topology type" << std::endl; return vk::PrimitiveTopology::eTriangleList;
+            case(PrimitiveTopology::PointList):     return vk::PrimitiveTopology::ePointList;
+            case(PrimitiveTopology::LineList):      return vk::PrimitiveTopology::eLineList;
+            case(PrimitiveTopology::TriangleList):  return vk::PrimitiveTopology::eTriangleList;
+            default: std::cout << "Error: Unknown primitive topology type" << std::endl; return vk::PrimitiveTopology::eTriangleList;
         }
     }
 
     vk::CompareOp depthTestToVkCompareOp(DepthTest depthTest) {
         switch (depthTest) {
-        case(DepthTest::None):          return vk::CompareOp::eAlways;
-        case(DepthTest::Less):          return vk::CompareOp::eLess;
-        case(DepthTest::LessEqual):     return vk::CompareOp::eLessOrEqual;
-        case(DepthTest::Greater):       return vk::CompareOp::eGreater;
-        case(DepthTest::GreatherEqual): return vk::CompareOp::eGreaterOrEqual;
-        case(DepthTest::Equal):         return vk::CompareOp::eEqual;
-        default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown depth test enum"); return vk::CompareOp::eAlways;
+            case(DepthTest::None):          return vk::CompareOp::eAlways;
+            case(DepthTest::Less):          return vk::CompareOp::eLess;
+            case(DepthTest::LessEqual):     return vk::CompareOp::eLessOrEqual;
+            case(DepthTest::Greater):       return vk::CompareOp::eGreater;
+            case(DepthTest::GreatherEqual): return vk::CompareOp::eGreaterOrEqual;
+            case(DepthTest::Equal):         return vk::CompareOp::eEqual;
+            default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown depth test enum"); return vk::CompareOp::eAlways;
         }
     }
+        
+	vk::ShaderStageFlagBits shaderStageToVkShaderStage(vkcv::ShaderStage stage) {
+		switch (stage) {
+            case vkcv::ShaderStage::VERTEX:         return vk::ShaderStageFlagBits::eVertex;
+            case vkcv::ShaderStage::FRAGMENT:       return vk::ShaderStageFlagBits::eFragment;
+            case vkcv::ShaderStage::GEOMETRY:       return vk::ShaderStageFlagBits::eGeometry;
+            case vkcv::ShaderStage::TESS_CONTROL:   return vk::ShaderStageFlagBits::eTessellationControl;
+            case vkcv::ShaderStage::TESS_EVAL:      return vk::ShaderStageFlagBits::eTessellationEvaluation;
+            case vkcv::ShaderStage::COMPUTE:        return vk::ShaderStageFlagBits::eCompute;
+            case vkcv::ShaderStage::TASK:           return vk::ShaderStageFlagBits::eTaskNV;
+            case vkcv::ShaderStage::MESH:           return vk::ShaderStageFlagBits::eMeshNV;
+            default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown shader stage"); return vk::ShaderStageFlagBits::eAll;
+		}
+	}
+
+    bool createPipelineShaderStageCreateInfo(
+        const vkcv::ShaderProgram&          shaderProgram, 
+        ShaderStage                         stage,
+        vk::Device                          device,
+        vk::PipelineShaderStageCreateInfo*  outCreateInfo) {
+
+        assert(outCreateInfo);
+        std::vector<char>           code = shaderProgram.getShader(stage).shaderCode;
+        vk::ShaderModuleCreateInfo  vertexModuleInfo({}, code.size(), reinterpret_cast<uint32_t*>(code.data()));
+        vk::ShaderModule            shaderModule;
+        if (device.createShaderModule(&vertexModuleInfo, nullptr, &shaderModule) != vk::Result::eSuccess)
+            return false;
+
+        const static auto entryName = "main";
+
+        *outCreateInfo = vk::PipelineShaderStageCreateInfo(
+            {},
+            shaderStageToVkShaderStage(stage),
+            shaderModule,
+            entryName,
+            nullptr);
+        return true;
+    }
 
     PipelineHandle PipelineManager::createPipeline(const PipelineConfig &config, PassManager& passManager)
     {
 		const vk::RenderPass &pass = passManager.getVkPass(config.m_PassHandle);
     	
+		const bool existsTaskShader = config.m_ShaderProgram.existsShader(ShaderStage::TASK);
+		const bool existsMeshShader = config.m_ShaderProgram.existsShader(ShaderStage::MESH);
+
         const bool existsVertexShader = config.m_ShaderProgram.existsShader(ShaderStage::VERTEX);
+
+        const bool validGeometryStages = existsVertexShader || (existsTaskShader && existsMeshShader);
+
         const bool existsFragmentShader = config.m_ShaderProgram.existsShader(ShaderStage::FRAGMENT);
-        if (!(existsVertexShader && existsFragmentShader))
+        if (!validGeometryStages)
         {
-			vkcv_log(LogLevel::ERROR, "Requires vertex and fragment shader code");
+			vkcv_log(LogLevel::ERROR, "Requires vertex or task and mesh shader");
             return PipelineHandle();
         }
-
-        // vertex shader stage
-        std::vector<char> vertexCode = config.m_ShaderProgram.getShader(ShaderStage::VERTEX).shaderCode;
-        vk::ShaderModuleCreateInfo vertexModuleInfo({}, vertexCode.size(), reinterpret_cast<uint32_t*>(vertexCode.data()));
-        vk::ShaderModule vertexModule{};
-        if (m_Device.createShaderModule(&vertexModuleInfo, nullptr, &vertexModule) != vk::Result::eSuccess)
+        if (!existsFragmentShader) {
+            vkcv_log(LogLevel::ERROR, "Requires fragment shader code");
             return PipelineHandle();
+        }
 
-        vk::PipelineShaderStageCreateInfo pipelineVertexShaderStageInfo(
-                {},
-                vk::ShaderStageFlagBits::eVertex,
-                vertexModule,
-                "main",
-                nullptr
-        );
+        std::vector<vk::PipelineShaderStageCreateInfo> shaderStages;
+        auto destroyShaderModules = [&shaderStages, this] {
+            for (auto stage : shaderStages) {
+                m_Device.destroyShaderModule(stage.module);
+            }
+            shaderStages.clear();
+        };
+
+        if (existsVertexShader) {
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram, 
+                vkcv::ShaderStage::VERTEX, 
+                m_Device, 
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
+        }
+
+        if (existsTaskShader) {
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram,
+                vkcv::ShaderStage::TASK,
+                m_Device,
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
+        }
+
+        if (existsMeshShader) {
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram,
+                vkcv::ShaderStage::MESH,
+                m_Device,
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
+        }
 
         // fragment shader stage
-        std::vector<char> fragCode = config.m_ShaderProgram.getShader(ShaderStage::FRAGMENT).shaderCode;
-        vk::ShaderModuleCreateInfo fragmentModuleInfo({}, fragCode.size(), reinterpret_cast<uint32_t*>(fragCode.data()));
-        vk::ShaderModule fragmentModule{};
-        if (m_Device.createShaderModule(&fragmentModuleInfo, nullptr, &fragmentModule) != vk::Result::eSuccess)
         {
-            m_Device.destroy(vertexModule);
-            return PipelineHandle();
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram,
+                vkcv::ShaderStage::FRAGMENT,
+                m_Device,
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
         }
 
-        vk::PipelineShaderStageCreateInfo pipelineFragmentShaderStageInfo(
-                {},
-                vk::ShaderStageFlagBits::eFragment,
-                fragmentModule,
-                "main",
-                nullptr
-        );
-
         // vertex input state
 
         // Fill up VertexInputBindingDescription and VertexInputAttributeDescription Containers
         std::vector<vk::VertexInputAttributeDescription>	vertexAttributeDescriptions;
 		std::vector<vk::VertexInputBindingDescription>		vertexBindingDescriptions;
 
-        const VertexLayout &layout = config.m_VertexLayout;
-
-        // iterate over the layout's specified, mutually exclusive buffer bindings that make up a vertex buffer
-        for (const auto &vertexBinding : layout.vertexBindings)
-        {
-            vertexBindingDescriptions.emplace_back(vertexBinding.bindingLocation,
-                                                   vertexBinding.stride,
-                                                   vk::VertexInputRate::eVertex);
-
-            // iterate over the bindings' specified, mutually exclusive vertex input attachments that make up a vertex
-            for(const auto &vertexAttachment: vertexBinding.vertexAttachments)
-            {
-                vertexAttributeDescriptions.emplace_back(vertexAttachment.inputLocation,
-                                                         vertexBinding.bindingLocation,
-                                                         vertexFormatToVulkanFormat(vertexAttachment.format),
-                                                         vertexAttachment.offset % vertexBinding.stride);
+		if (existsVertexShader) {
+			const VertexLayout& layout = config.m_VertexLayout;
+
+			// iterate over the layout's specified, mutually exclusive buffer bindings that make up a vertex buffer
+			for (const auto& vertexBinding : layout.vertexBindings)
+			{
+				vertexBindingDescriptions.emplace_back(vertexBinding.bindingLocation,
+					vertexBinding.stride,
+					vk::VertexInputRate::eVertex);
+
+				// iterate over the bindings' specified, mutually exclusive vertex input attachments that make up a vertex
+				for (const auto& vertexAttachment : vertexBinding.vertexAttachments)
+				{
+					vertexAttributeDescriptions.emplace_back(vertexAttachment.inputLocation,
+						vertexBinding.bindingLocation,
+						vertexFormatToVulkanFormat(vertexAttachment.format),
+						vertexAttachment.offset % vertexBinding.stride);
+
+				}
+			}
 
-            }
-        }
+		}
 
         // Handover Containers to PipelineVertexInputStateCreateIngo Struct
         vk::PipelineVertexInputStateCreateInfo pipelineVertexInputStateCreateInfo(
@@ -240,8 +335,7 @@ namespace vkcv
         vk::PipelineLayout vkPipelineLayout{};
         if (m_Device.createPipelineLayout(&pipelineLayoutCreateInfo, nullptr, &vkPipelineLayout) != vk::Result::eSuccess)
         {
-            m_Device.destroy(vertexModule);
-            m_Device.destroy(fragmentModule);
+            destroyShaderModules();
             return PipelineHandle();
         }
 	
@@ -276,25 +370,28 @@ namespace vkcv
 		    dynamicStates.push_back(vk::DynamicState::eScissor);
         }
 
-        vk::PipelineDynamicStateCreateInfo dynamicStateCreateInfo({},
-                                                            static_cast<uint32_t>(dynamicStates.size()),
-                                                            dynamicStates.data());
-
-        // graphics pipeline create
-        std::vector<vk::PipelineShaderStageCreateInfo> shaderStages = { pipelineVertexShaderStageInfo, pipelineFragmentShaderStageInfo };
-
-		const char *geometryShaderName = "main";	// outside of if to make sure it stays in scope
-		vk::ShaderModule geometryModule;
-		if (config.m_ShaderProgram.existsShader(ShaderStage::GEOMETRY)) {
-			const vkcv::Shader geometryShader = config.m_ShaderProgram.getShader(ShaderStage::GEOMETRY);
-			const auto& geometryCode = geometryShader.shaderCode;
-			const vk::ShaderModuleCreateInfo geometryModuleInfo({}, geometryCode.size(), reinterpret_cast<const uint32_t*>(geometryCode.data()));
-			if (m_Device.createShaderModule(&geometryModuleInfo, nullptr, &geometryModule) != vk::Result::eSuccess) {
-				return PipelineHandle();
-			}
-			vk::PipelineShaderStageCreateInfo geometryStage({}, vk::ShaderStageFlagBits::eGeometry, geometryModule, geometryShaderName);
-			shaderStages.push_back(geometryStage);
-		}
+        vk::PipelineDynamicStateCreateInfo dynamicStateCreateInfo(
+            {},
+            static_cast<uint32_t>(dynamicStates.size()),
+            dynamicStates.data());
+
+        const bool existsGeometryShader = config.m_ShaderProgram.existsShader(vkcv::ShaderStage::GEOMETRY);
+        if (existsGeometryShader) {
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram,
+                vkcv::ShaderStage::GEOMETRY,
+                m_Device,
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
+        }
 
         const vk::GraphicsPipelineCreateInfo graphicsPipelineCreateInfo(
                 {},
@@ -319,20 +416,11 @@ namespace vkcv
         vk::Pipeline vkPipeline{};
         if (m_Device.createGraphicsPipelines(nullptr, 1, &graphicsPipelineCreateInfo, nullptr, &vkPipeline) != vk::Result::eSuccess)
         {
-            m_Device.destroy(vertexModule);
-            m_Device.destroy(fragmentModule);
-            if (geometryModule) {
-                m_Device.destroy(geometryModule);
-            }
-            m_Device.destroy();
+            destroyShaderModules();
             return PipelineHandle();
         }
 
-        m_Device.destroy(vertexModule);
-        m_Device.destroy(fragmentModule);
-        if (geometryModule) {
-            m_Device.destroy(geometryModule);
-        }
+        destroyShaderModules();
         
         const uint64_t id = m_Pipelines.size();
         m_Pipelines.push_back({ vkPipeline, vkPipelineLayout, config });
@@ -457,4 +545,4 @@ namespace vkcv
         vk::ShaderModuleCreateInfo moduleInfo({}, code.size(), reinterpret_cast<uint32_t*>(code.data()));
         return m_Device.createShaderModule(&moduleInfo, nullptr, &module);
     }
-}
\ No newline at end of file
+}
diff --git a/src/vkcv/QueueManager.cpp b/src/vkcv/QueueManager.cpp
index df6c74cccf6c4652adc6a4c78802f282ea6ae293..15e958b0de929e53170324ade27a9b3663a15d6a 100644
--- a/src/vkcv/QueueManager.cpp
+++ b/src/vkcv/QueueManager.cpp
@@ -27,8 +27,8 @@ namespace vkcv {
      * @throws std::runtime_error If the requested queues from @p queueFlags are not creatable due to insufficient availability.
      */
     void QueueManager::queueCreateInfosQueueHandles(vk::PhysicalDevice &physicalDevice,
-                                      std::vector<float> &queuePriorities,
-                                      std::vector<vk::QueueFlagBits> &queueFlags,
+                                      const std::vector<float> &queuePriorities,
+                                      const std::vector<vk::QueueFlagBits> &queueFlags,
                                       std::vector<vk::DeviceQueueCreateInfo> &queueCreateInfos,
                                       std::vector<std::pair<int, int>> &queuePairsGraphics,
                                       std::vector<std::pair<int, int>> &queuePairsCompute,
@@ -51,7 +51,7 @@ namespace vkcv {
         }
         //resort flags with heighest priority before allocating the queues
         std::vector<vk::QueueFlagBits> newFlags;
-        for(int i = 0; i < prios.size(); i++) {
+        for(size_t i = 0; i < prios.size(); i++) {
             auto minElem = std::min_element(prios.begin(), prios.end());
             int index = minElem - prios.begin();
             newFlags.push_back(queueFlags[index]);
@@ -79,7 +79,7 @@ namespace vkcv {
             switch (qFlag) {
                 case vk::QueueFlagBits::eGraphics:
                     found = false;
-                    for (int i = 0; i < queueFamilyStatus.size() && !found; i++) {
+                    for (size_t i = 0; i < queueFamilyStatus.size() && !found; i++) {
                         if (queueFamilyStatus[i][0] > 0) {
                             queuePairsGraphics.push_back(std::pair(i, initialQueueFamilyStatus[i][0] - queueFamilyStatus[i][0]));
                             queueFamilyStatus[i][0]--;
@@ -89,7 +89,7 @@ namespace vkcv {
                         }
                     }
                     if (!found) {
-                        for (int i = 0; i < queueFamilyStatus.size() && !found; i++) {
+                        for (size_t i = 0; i < queueFamilyStatus.size() && !found; i++) {
                             if (initialQueueFamilyStatus[i][0] > 0) {
                                 queuePairsGraphics.push_back(std::pair(i, 0));
                                 found = true;
@@ -101,7 +101,7 @@ namespace vkcv {
                     break;
                 case vk::QueueFlagBits::eCompute:
                     found = false;
-                    for (int i = 0; i < queueFamilyStatus.size() && !found; i++) {
+                    for (size_t i = 0; i < queueFamilyStatus.size() && !found; i++) {
                         if (queueFamilyStatus[i][1] > 0) {
                             queuePairsCompute.push_back(std::pair(i, initialQueueFamilyStatus[i][1] - queueFamilyStatus[i][1]));
                             queueFamilyStatus[i][0]--;
@@ -111,7 +111,7 @@ namespace vkcv {
                         }
                     }
                     if (!found) {
-                        for (int i = 0; i < queueFamilyStatus.size() && !found; i++) {
+                        for (size_t i = 0; i < queueFamilyStatus.size() && !found; i++) {
                             if (initialQueueFamilyStatus[i][1] > 0) {
                                 queuePairsCompute.push_back(std::pair(i, 0));
                                 found = true;
@@ -123,7 +123,7 @@ namespace vkcv {
                     break;
                 case vk::QueueFlagBits::eTransfer:
                     found = false;
-                    for (int i = 0; i < queueFamilyStatus.size() && !found; i++) {
+                    for (size_t i = 0; i < queueFamilyStatus.size() && !found; i++) {
                         if (queueFamilyStatus[i][2] > 0) {
                             queuePairsTransfer.push_back(std::pair(i, initialQueueFamilyStatus[i][2] - queueFamilyStatus[i][2]));
                             queueFamilyStatus[i][0]--;
@@ -133,7 +133,7 @@ namespace vkcv {
                         }
                     }
                     if (!found) {
-                        for (int i = 0; i < queueFamilyStatus.size() && !found; i++) {
+                        for (size_t i = 0; i < queueFamilyStatus.size() && !found; i++) {
                             if (initialQueueFamilyStatus[i][2] > 0) {
                                 queuePairsTransfer.push_back(std::pair(i, 0));
                                 found = true;
@@ -149,7 +149,7 @@ namespace vkcv {
         }
 
         // create all requested queues
-        for (int i = 0; i < qFamilyProperties.size(); i++) {
+        for (size_t i = 0; i < qFamilyProperties.size(); i++) {
             uint32_t create = std::abs(initialQueueFamilyStatus[i][0] - queueFamilyStatus[i][0]);
             if (create > 0) {
                 vk::DeviceQueueCreateInfo qCreateInfo(
diff --git a/src/vkcv/SamplerManager.cpp b/src/vkcv/SamplerManager.cpp
index a6ebb95b5e237dcd06ed8041b3f16489f7339d6a..792e6f16b4a05af41a164a1eda9dd7423594857e 100644
--- a/src/vkcv/SamplerManager.cpp
+++ b/src/vkcv/SamplerManager.cpp
@@ -17,7 +17,8 @@ namespace vkcv {
 	SamplerHandle SamplerManager::createSampler(SamplerFilterType magFilter,
 												SamplerFilterType minFilter,
 												SamplerMipmapMode mipmapMode,
-												SamplerAddressMode addressMode) {
+												SamplerAddressMode addressMode,
+												float mipLodBias) {
 		vk::Filter vkMagFilter;
 		vk::Filter vkMinFilter;
 		vk::SamplerMipmapMode vkMipmapMode;
@@ -81,13 +82,13 @@ namespace vkcv {
 				vkAddressMode,
 				vkAddressMode,
 				vkAddressMode,
-				0.0f,
+				mipLodBias,
 				false,
 				16.0f,
 				false,
 				vk::CompareOp::eAlways,
-				0.0f,
-				16.0f,
+				-1000.0f,
+				1000.0f,
 				vk::BorderColor::eIntOpaqueBlack,
 				false
 		);
diff --git a/src/vkcv/SamplerManager.hpp b/src/vkcv/SamplerManager.hpp
index 511176d4f87633a8691ca730ecc383e2588d8cf0..aea47a03714b417314a09dfc0be855df31fbb557 100644
--- a/src/vkcv/SamplerManager.hpp
+++ b/src/vkcv/SamplerManager.hpp
@@ -32,7 +32,8 @@ namespace vkcv {
 		SamplerHandle createSampler(SamplerFilterType magFilter,
 							  		SamplerFilterType minFilter,
 							  		SamplerMipmapMode mipmapMode,
-							  		SamplerAddressMode addressMode);
+							  		SamplerAddressMode addressMode,
+							  		float mipLodBias);
 		
 		[[nodiscard]]
 		vk::Sampler getVulkanSampler(const SamplerHandle& handle) const;
diff --git a/src/vkcv/Swapchain.cpp b/src/vkcv/Swapchain.cpp
index 94e7301d66bfcc513434ef6d22520d1b95f98161..d0aa26db9c661ea40caf06349a72cc9188e791a9 100644
--- a/src/vkcv/Swapchain.cpp
+++ b/src/vkcv/Swapchain.cpp
@@ -100,18 +100,14 @@ namespace vkcv
      * @return available Format
      */
     vk::SurfaceFormatKHR chooseSurfaceFormat(vk::PhysicalDevice physicalDevice, vk::SurfaceKHR surface) {
-        uint32_t formatCount;
-        physicalDevice.getSurfaceFormatsKHR(surface, &formatCount, nullptr);
-        std::vector<vk::SurfaceFormatKHR> availableFormats(formatCount);
-        if (physicalDevice.getSurfaceFormatsKHR(surface, &formatCount, &availableFormats[0]) != vk::Result::eSuccess) {
-            throw std::runtime_error("Failed to get surface formats");
-        }
+        std::vector<vk::SurfaceFormatKHR> availableFormats = physicalDevice.getSurfaceFormatsKHR(surface);
 
         for (const auto& availableFormat : availableFormats) {
             if (availableFormat.format == vk::Format::eB8G8R8A8Unorm  && availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) {
                 return availableFormat;
             }
         }
+        
         return availableFormats[0];
     }
 
@@ -122,12 +118,7 @@ namespace vkcv
      * @return available PresentationMode
      */
     vk::PresentModeKHR choosePresentMode(vk::PhysicalDevice physicalDevice, vk::SurfaceKHR surface) {
-        uint32_t modeCount;
-        physicalDevice.getSurfacePresentModesKHR( surface, &modeCount, nullptr );
-        std::vector<vk::PresentModeKHR> availablePresentModes(modeCount);
-        if (physicalDevice.getSurfacePresentModesKHR(surface, &modeCount, &availablePresentModes[0]) != vk::Result::eSuccess) {
-            throw std::runtime_error("Failed to get presentation modes");
-        }
+        std::vector<vk::PresentModeKHR> availablePresentModes = physicalDevice.getSurfacePresentModesKHR(surface);
 
         for (const auto& availablePresentMode : availablePresentModes) {
             if (availablePresentMode == vk::PresentModeKHR::eMailbox) {
@@ -145,12 +136,11 @@ namespace vkcv
      * @return available ImageCount
      */
     uint32_t chooseImageCount(vk::PhysicalDevice physicalDevice, vk::SurfaceKHR surface) {
-        vk::SurfaceCapabilitiesKHR surfaceCapabilities;
-        if(physicalDevice.getSurfaceCapabilitiesKHR(surface, &surfaceCapabilities) != vk::Result::eSuccess){
-            throw std::runtime_error("cannot get surface capabilities. There is an issue with the surface.");
-        }
-
-        uint32_t imageCount = surfaceCapabilities.minImageCount + 1;    // minImageCount should always be at least 2; set to 3 for triple buffering
+        vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface);
+	
+		// minImageCount should always be at least 2; set to 3 for triple buffering
+        uint32_t imageCount = surfaceCapabilities.minImageCount + 1;
+        
         // check if requested image count is supported
         if (surfaceCapabilities.maxImageCount > 0 && imageCount > surfaceCapabilities.maxImageCount) {
             imageCount = surfaceCapabilities.maxImageCount;
@@ -215,8 +205,9 @@ namespace vkcv
     }
     
     void Swapchain::updateSwapchain(const Context &context, const Window &window) {
-    	if (!m_RecreationRequired.exchange(false))
-    		return;
+    	if (!m_RecreationRequired.exchange(false)) {
+			return;
+		}
     	
 		vk::SwapchainKHR oldSwapchain = m_Swapchain;
 		vk::Extent2D extent2D = chooseExtent(context.getPhysicalDevice(), m_Surface.handle, window);
diff --git a/src/vkcv/Window.cpp b/src/vkcv/Window.cpp
index ea72582d67d5350e5fbf3f3c0fa2aae2ba407b0e..072efcd00eb6520fa4f20379721b559668339f6e 100644
--- a/src/vkcv/Window.cpp
+++ b/src/vkcv/Window.cpp
@@ -4,7 +4,10 @@
  * @brief Window class to handle a basic rendering surface and input
  */
 
+#include <thread>
+#include <vector>
 #include <GLFW/glfw3.h>
+
 #include "vkcv/Window.hpp"
 
 namespace vkcv {
@@ -78,12 +81,17 @@ namespace vkcv {
 			window->e_key.unlock();
 			window->e_char.unlock();
 			window->e_gamepad.unlock();
-    	}
+		}
+
+		glfwPollEvents();
 
-        glfwPollEvents();
-    	
-    	for (int gamepadIndex = GLFW_JOYSTICK_1; gamepadIndex <= GLFW_JOYSTICK_LAST; gamepadIndex++) {
-    		if (glfwJoystickPresent(gamepadIndex)) {
+		// fixes subtle mouse stutter, which is made visible by motion blur
+		// FIXME: proper solution
+		// probably caused by main thread locking events before glfw callbacks are executed
+		std::this_thread::sleep_for(std::chrono::milliseconds(1));
+
+		for (int gamepadIndex = GLFW_JOYSTICK_1; gamepadIndex <= GLFW_JOYSTICK_LAST; gamepadIndex++) {
+			if (glfwJoystickPresent(gamepadIndex)) {
 				onGamepadEvent(gamepadIndex);
 			}
 		}
@@ -150,11 +158,15 @@ namespace vkcv {
     }
 
     void Window::onGamepadEvent(int gamepadIndex) {
-        int activeWindowIndex = std::find_if(s_Windows.begin(),
-                                             s_Windows.end(),
-                                             [](GLFWwindow* window){return glfwGetWindowAttrib(window, GLFW_FOCUSED);})
-                                - s_Windows.begin();
-        activeWindowIndex *= (activeWindowIndex < s_Windows.size());    // fixes index getting out of bounds (e.g. if there is no focused window)
+        size_t activeWindowIndex = std::find_if(
+        		s_Windows.begin(),
+        		s_Windows.end(),
+        		[](GLFWwindow* window){return glfwGetWindowAttrib(window, GLFW_FOCUSED);}
+		) - s_Windows.begin();
+	
+		// fixes index getting out of bounds (e.g. if there is no focused window)
+        activeWindowIndex *= (activeWindowIndex < s_Windows.size());
+        
         auto window = static_cast<Window *>(glfwGetWindowUserPointer(s_Windows[activeWindowIndex]));
 
         if (window != nullptr) {