diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 84a1e902ace668fbee40346cf71e3c4c7a519f2e..63fda17212cf97f2857ac1891dfad9dd052cbe6a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -23,7 +23,7 @@ build_ubuntu_gcc:
     - mkdir debug
     - cd debug
     - cmake -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build .
+    - cmake --build . -j 4
   artifacts:
     name: "Documentation - $CI_PIPELINE_ID"
     paths:
@@ -49,7 +49,7 @@ build_win10_msvc:
     - mkdir debug
     - cd debug
     - cmake -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build .
+    - cmake --build . -j 4
 
 build_win10_mingw:
   only:
@@ -66,7 +66,7 @@ build_win10_mingw:
     - 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 8
+    - cmake --build . -j 4
 
 build_mac_clang:
   only:
@@ -85,7 +85,7 @@ build_mac_clang:
     - 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 .
+    - cmake --build . -j 4
 
 deploy_doc_develop:
   only:
diff --git a/.gitmodules b/.gitmodules
index 2b2337f667c695a14989e220dffb1b5b7440a579..4e2dfae87f214566b12c2d92b046c94cdea6b302 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -22,9 +22,6 @@
 [submodule "modules/gui/lib/imgui"]
 	path = modules/gui/lib/imgui
 	url = https://github.com/ocornut/imgui.git
-[submodule "lib/VulkanMemoryAllocator"]
-	path = lib/VulkanMemoryAllocator
-	url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
 [submodule "lib/VulkanMemoryAllocator-Hpp"]
 	path = lib/VulkanMemoryAllocator-Hpp
 	url = https://github.com/malte-v/VulkanMemoryAllocator-Hpp.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index dfafe1cd084d4b324c233d502e301c24a5ee95e1..da150fcbeafec3be555d4bbefdab37dbdedf277f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -65,7 +65,7 @@ add_library(vkcv STATIC ${vkcv_sources})
 
 if(MSVC)
   #enable multicore compilation on visual studio
-  target_compile_options(vkcv PRIVATE "/MP" "/openmp")
+  target_compile_options(vkcv PRIVATE "/MP" "/openmp" "/Zc:offsetof-")
 
   #set source groups to create proper filters in visual studio
   source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${vkcv_sources})
diff --git a/config/Sources.cmake b/config/Sources.cmake
index 41cd0c20f2106dc02700d9b23227f3e6c34a057a..ea95d152280fde1dcfdd93e7e48d1f8b58d3bb9e 100644
--- a/config/Sources.cmake
+++ b/config/Sources.cmake
@@ -1,6 +1,12 @@
 
 # adding all source files and header files of the framework:
 set(vkcv_sources
+		${vkcv_include}/vkcv/Features.hpp
+		${vkcv_source}/vkcv/Features.cpp
+		
+		${vkcv_include}/vkcv/FeatureManager.hpp
+		${vkcv_source}/vkcv/FeatureManager.cpp
+		
 		${vkcv_include}/vkcv/Context.hpp
 		${vkcv_source}/vkcv/Context.cpp
 
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 1f10c8dc4bbdf3142ffffcd6e1061fc83d52c0f6..cdea90ecbb96c8ff591b879e51548f92514ef230 100644
--- a/include/vkcv/BufferManager.hpp
+++ b/include/vkcv/BufferManager.hpp
@@ -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 824713fd1e29cbb8b7e60b22768c0019daaa9938..1160857cb2cdbbb0815390ff1f8405dda0f83796 100644
--- a/include/vkcv/Context.hpp
+++ b/include/vkcv/Context.hpp
@@ -5,6 +5,7 @@
 
 #include "QueueManager.hpp"
 #include "DrawcallRecording.hpp"
+#include "Features.hpp"
 
 namespace vkcv
 {
@@ -32,6 +33,9 @@ namespace vkcv
         [[nodiscard]]
         const vk::Device &getDevice() const;
         
+        [[nodiscard]]
+        const FeatureManager& getFeatureManager() const;
+        
         [[nodiscard]]
         const QueueManager& getQueueManager() const;
 	
@@ -41,8 +45,8 @@ namespace vkcv
         static Context create(const char *applicationName,
 							  uint32_t applicationVersion,
 							  const std::vector<vk::QueueFlagBits>& queueFlags,
-							  const std::vector<const char *>& instanceExtensions,
-							  const std::vector<const char *>& deviceExtensions);
+							  const Features& features,
+							  const std::vector<const char*>& instanceExtensions = {});
 
     private:
         /**
@@ -53,11 +57,12 @@ namespace vkcv
          * @param device Vulkan-Device
          */
         Context(vk::Instance instance, vk::PhysicalDevice physicalDevice, vk::Device device,
-				QueueManager&& queueManager, vma::Allocator&& allocator) noexcept;
+				FeatureManager&& featureManager, QueueManager&& queueManager, vma::Allocator&& allocator) noexcept;
         
         vk::Instance        m_Instance;
         vk::PhysicalDevice  m_PhysicalDevice;
         vk::Device          m_Device;
+        FeatureManager		m_FeatureManager;
 		QueueManager		m_QueueManager;
 		vma::Allocator 		m_Allocator;
 		
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index 50a5a8a452a930a772533fe7cf7c627540442461..caeab7bc7ad37c0f5abbfea64ce691d0367f0edb 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -7,14 +7,14 @@
 #include <memory>
 #include <vulkan/vulkan.hpp>
 
-#include "vkcv/Context.hpp"
-#include "vkcv/Swapchain.hpp"
-#include "vkcv/Window.hpp"
-#include "vkcv/PassConfig.hpp"
-#include "vkcv/Handles.hpp"
-#include "vkcv/Buffer.hpp"
-#include "vkcv/Image.hpp"
-#include "vkcv/PipelineConfig.hpp"
+#include "Context.hpp"
+#include "Swapchain.hpp"
+#include "Window.hpp"
+#include "PassConfig.hpp"
+#include "Handles.hpp"
+#include "Buffer.hpp"
+#include "Image.hpp"
+#include "PipelineConfig.hpp"
 #include "CommandResources.hpp"
 #include "SyncResources.hpp"
 #include "Result.hpp"
@@ -139,8 +139,8 @@ namespace vkcv
                            const char *applicationName,
                            uint32_t applicationVersion,
                            const std::vector<vk::QueueFlagBits>& queueFlags    = {},
-                           const std::vector<const char*>& instanceExtensions  = {},
-                           const std::vector<const char*>& deviceExtensions    = {});
+						   const Features& features = {},
+						   const std::vector<const char *>& instanceExtensions = {});
 
         /**
          * Creates a basic vulkan graphics pipeline using @p config from the pipeline config class and returns it using the @p handle.
@@ -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);
         }
         
         /**
@@ -249,16 +249,16 @@ namespace vkcv
 		bool beginFrame(uint32_t& width, uint32_t& height);
 
 		void recordDrawcallsToCmdStream(
-			const CommandStreamHandle       cmdStreamHandle,
-			const PassHandle                renderpassHandle, 
+			const CommandStreamHandle&      cmdStreamHandle,
+			const PassHandle&               renderpassHandle,
 			const PipelineHandle            pipelineHandle,
 			const PushConstants             &pushConstants,
 			const std::vector<DrawcallInfo> &drawcalls,
 			const std::vector<ImageHandle>  &renderTargets);
 
 		void recordMeshShaderDrawcalls(
-			const CommandStreamHandle               cmdStreamHandle,
-			const PassHandle                        renderpassHandle,
+			const CommandStreamHandle&              cmdStreamHandle,
+			const PassHandle&                       renderpassHandle,
 			const PipelineHandle                    pipelineHandle,
 			const PushConstants&                    pushConstantData,
             const std::vector<MeshShaderDrawcall>&  drawcalls,
@@ -270,6 +270,20 @@ namespace vkcv
 			const uint32_t dispatchCount[3],
 			const std::vector<DescriptorSetUsage> &descriptorSetUsages,
 			const PushConstants& pushConstants);
+		
+		void recordBeginDebugLabel(const CommandStreamHandle &cmdStream,
+								   const std::string& label,
+								   const std::array<float, 4>& color);
+		
+		void recordEndDebugLabel(const CommandStreamHandle &cmdStream);
+
+		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
@@ -313,6 +327,14 @@ namespace vkcv
 		
 		void recordBlitImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst,
 							 SamplerFilterType filterType);
+	
+		void setDebugLabel(const BufferHandle &handle, const std::string &label);
+		void setDebugLabel(const PassHandle &handle, const std::string &label);
+		void setDebugLabel(const PipelineHandle &handle, const std::string &label);
+		void setDebugLabel(const DescriptorSetHandle &handle, const std::string &label);
+		void setDebugLabel(const SamplerHandle &handle, const std::string &label);
+		void setDebugLabel(const ImageHandle &handle, const std::string &label);
+		void setDebugLabel(const CommandStreamHandle &handle, const std::string &label);
 		
     };
 }
diff --git a/include/vkcv/DescriptorConfig.hpp b/include/vkcv/DescriptorConfig.hpp
index 767492eb2b27bd8dff56ef2aeb4769c08eed7200..f2a089fd624c02c57db4a65c4a101c4acff371b1 100644
--- a/include/vkcv/DescriptorConfig.hpp
+++ b/include/vkcv/DescriptorConfig.hpp
@@ -1,7 +1,5 @@
 #pragma once
 
-#include <vulkan/vulkan.hpp>
-
 #include "vkcv/Handles.hpp"
 #include "vkcv/ShaderStage.hpp"
 
@@ -41,12 +39,12 @@ namespace vkcv
             uint32_t bindingID,
             DescriptorType descriptorType,
             uint32_t descriptorCount,
-            ShaderStage shaderStage
+            ShaderStages shaderStages
         ) noexcept;
         
         uint32_t bindingID;
         DescriptorType descriptorType;
         uint32_t descriptorCount;
-        ShaderStage shaderStage;
+        ShaderStages shaderStages;
     };
 }
diff --git a/include/vkcv/DrawcallRecording.hpp b/include/vkcv/DrawcallRecording.hpp
index 260fbbc6a2a577d0d333656a1eff4f7f3f88cd69..37cf02d9102fcab5abd10ada711f67b721bcb52b 100644
--- a/include/vkcv/DrawcallRecording.hpp
+++ b/include/vkcv/DrawcallRecording.hpp
@@ -29,8 +29,19 @@ namespace vkcv {
     };
 
     struct 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){}
+
+        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;
diff --git a/include/vkcv/FeatureManager.hpp b/include/vkcv/FeatureManager.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..cf945d7498fec83f0b128294caa3ba267b69a6b3
--- /dev/null
+++ b/include/vkcv/FeatureManager.hpp
@@ -0,0 +1,150 @@
+#pragma once
+
+#include "Logger.hpp"
+
+#include <functional>
+#include <unordered_set>
+#include <vector>
+#include <vulkan/vulkan.hpp>
+
+namespace vkcv {
+
+	class FeatureManager {
+	private:
+		vk::PhysicalDevice& m_physicalDevice;
+		
+		std::vector<const char*> m_supportedExtensions;
+		std::vector<const char*> m_activeExtensions;
+		
+		vk::PhysicalDeviceFeatures2 m_featuresBase;
+		std::vector<vk::BaseOutStructure*> m_featuresExtensions;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceFeatures& features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDevice16BitStorageFeatures& features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDevice8BitStorageFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceBufferDeviceAddressFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceDescriptorIndexingFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceHostQueryResetFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceImagelessFramebufferFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceMultiviewFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceProtectedMemoryFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceSamplerYcbcrConversionFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceScalarBlockLayoutFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceSeparateDepthStencilLayoutsFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceShaderAtomicInt64Features &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceShaderFloat16Int8Features& features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceShaderSubgroupExtendedTypesFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceTimelineSemaphoreFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceUniformBufferStandardLayoutFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceVariablePointersFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceVulkanMemoryModelFeatures &features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceMeshShaderFeaturesNV& features, bool required) const;
+		
+		vk::BaseOutStructure* findFeatureStructure(vk::StructureType type) const;
+	
+	public:
+		explicit FeatureManager(vk::PhysicalDevice& physicalDevice);
+		
+		FeatureManager(const FeatureManager& other) = delete;
+		FeatureManager(FeatureManager&& other) noexcept;
+		
+		~FeatureManager();
+		
+		FeatureManager& operator=(const FeatureManager& other) = delete;
+		FeatureManager& operator=(FeatureManager&& other) noexcept;
+		
+		[[nodiscard]]
+		bool isExtensionSupported(const std::string& extension) const;
+		
+		bool useExtension(const std::string& extension, bool required = true);
+		
+		[[nodiscard]]
+		bool isExtensionActive(const std::string& extension) const;
+		
+		[[nodiscard]]
+		const std::vector<const char*>& getActiveExtensions() const;
+		
+		bool useFeatures(const std::function<void(vk::PhysicalDeviceFeatures&)>& featureFunction, bool required = true);
+		
+		template<typename T>
+		bool useFeatures(const std::function<void(T&)>& featureFunction, bool required = true) {
+			T features;
+			T* features_ptr = reinterpret_cast<T*>(findFeatureStructure(features.sType));
+			
+			if (features_ptr) {
+				features = *features_ptr;
+			}
+			
+			featureFunction(features);
+			
+			if (!checkSupport(features, required)) {
+				return false;
+			}
+			
+			if (features_ptr) {
+				*features_ptr = features;
+				return true;
+			}
+			
+			features_ptr = new T(features);
+			
+			if (m_featuresExtensions.empty()) {
+				m_featuresBase.setPNext(features_ptr);
+			} else {
+				m_featuresExtensions.back()->setPNext(
+						reinterpret_cast<vk::BaseOutStructure*>(features_ptr)
+				);
+			}
+			
+			m_featuresExtensions.push_back(
+					reinterpret_cast<vk::BaseOutStructure*>(features_ptr)
+			);
+			
+			return true;
+		}
+		
+		[[nodiscard]]
+		const vk::PhysicalDeviceFeatures2& getFeatures() const;
+		
+	};
+	
+}
diff --git a/include/vkcv/Features.hpp b/include/vkcv/Features.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6ef3fa28be912627b4495c66427336dfaa51beff
--- /dev/null
+++ b/include/vkcv/Features.hpp
@@ -0,0 +1,85 @@
+#pragma once
+
+#include <functional>
+#include <vector>
+#include <initializer_list>
+
+#include "FeatureManager.hpp"
+
+namespace vkcv {
+	
+	typedef std::function<bool(FeatureManager&)> Feature;
+	
+	class Features {
+	private:
+		std::vector<Feature> m_features;
+		
+	public:
+		Features() = default;
+		
+		Features(const std::initializer_list<std::string>& list);
+		
+		Features(const Features& other) = default;
+		Features(Features&& other) = default;
+		
+		~Features() = default;
+		
+		Features& operator=(const Features& other) = default;
+		Features& operator=(Features&& other) = default;
+		
+		void requireExtension(const std::string& extension);
+		
+		void requireExtensionFeature(const std::string& extension,
+									 const std::function<void(vk::PhysicalDeviceFeatures&)>& featureFunction);
+		
+		template<typename T>
+		void requireExtensionFeature(const std::string& extension, const std::function<void(T&)>& featureFunction) {
+			m_features.emplace_back([extension, featureFunction](FeatureManager& featureManager) {
+				if (featureManager.useExtension(extension, true)) {
+					return featureManager.template useFeatures<T>(featureFunction, true);
+				} else {
+					return false;
+				}
+			});
+		}
+		
+		void requireFeature(const std::function<void(vk::PhysicalDeviceFeatures&)>& featureFunction);
+		
+		template<typename T>
+		void requireFeature(const std::function<void(T&)>& featureFunction) {
+			m_features.emplace_back([featureFunction](FeatureManager& featureManager) {
+				return featureManager.template useFeatures<T>(featureFunction, true);
+			});
+		}
+		
+		void tryExtension(const std::string& extension);
+		
+		void tryExtensionFeature(const std::string& extension,
+								 const std::function<void(vk::PhysicalDeviceFeatures&)>& featureFunction);
+		
+		template<typename T>
+		void tryExtensionFeature(const std::string& extension, const std::function<void(T&)>& featureFunction) {
+			m_features.emplace_back([extension, featureFunction](FeatureManager& featureManager) {
+				if (featureManager.useExtension(extension, false)) {
+					return featureManager.template useFeatures<T>(featureFunction, false);
+				} else {
+					return false;
+				}
+			});
+		}
+		
+		void tryFeature(const std::function<void(vk::PhysicalDeviceFeatures&)>& featureFunction);
+		
+		template<typename T>
+		void tryFeature(const std::function<void(T&)>& featureFunction) {
+			m_features.emplace_back([featureFunction](FeatureManager& featureManager) {
+				return featureManager.template useFeatures<T>(featureFunction, false);
+			});
+		}
+		
+		[[nodiscard]]
+		const std::vector<Feature>& getList() const;
+		
+	};
+	
+}
diff --git a/include/vkcv/Logger.hpp b/include/vkcv/Logger.hpp
index 1ae0f211e1a3255d624cf78985b0797e9d90c634..bb60561e80baadfcac4956223d9313893547068f 100644
--- a/include/vkcv/Logger.hpp
+++ b/include/vkcv/Logger.hpp
@@ -53,9 +53,10 @@ namespace vkcv {
     VKCV_DEBUG_MESSAGE_LEN,                \
     __VA_ARGS__                            \
   );                                       \
+  auto output = getLogOutput(level);       \
   if (level != vkcv::LogLevel::RAW_INFO) { \
     fprintf(                               \
-      getLogOutput(level),                 \
+      output,                              \
       "[%s]: %s [%s, line %d: %s]\n",      \
       vkcv::getLogName(level),             \
       output_message,                      \
@@ -65,12 +66,13 @@ namespace vkcv {
     );                                     \
   } else {                                 \
     fprintf(                               \
-      getLogOutput(level),                 \
+      output,                              \
       "[%s]: %s\n",                        \
       vkcv::getLogName(level),             \
       output_message                       \
     );                                     \
   }                                        \
+  fflush(output);                          \
 }
 
 #else
diff --git a/include/vkcv/ShaderProgram.hpp b/include/vkcv/ShaderProgram.hpp
index 78b1f02169fe630427b9f66150e32078d42b7b3f..c7d67b19148b3c9ec19ce1b539f9661797d1b38f 100644
--- a/include/vkcv/ShaderProgram.hpp
+++ b/include/vkcv/ShaderProgram.hpp
@@ -13,8 +13,8 @@
 #include <vulkan/vulkan.hpp>
 #include <spirv_cross.hpp>
 #include "VertexLayout.hpp"
-#include "ShaderStage.hpp"
 #include "DescriptorConfig.hpp"
+#include "ShaderStage.hpp"
 
 namespace vkcv {
 
diff --git a/include/vkcv/ShaderStage.hpp b/include/vkcv/ShaderStage.hpp
index 3893bdf5f73408847ceb2b076abfb7d0902bb2f9..52ba5a7e56ed40efda96c27dfd92734880b88f18 100644
--- a/include/vkcv/ShaderStage.hpp
+++ b/include/vkcv/ShaderStage.hpp
@@ -1,19 +1,39 @@
 #pragma once
 
-namespace vkcv {
-	
-	enum class ShaderStage
-	{
-		VERTEX,
-		TESS_CONTROL,
-		TESS_EVAL,
-		GEOMETRY,
-		FRAGMENT,
-		COMPUTE,
-		TASK,
-		MESH
-	};
-
+#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),
+		TESS_EVAL       = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eTessellationEvaluation),
+		GEOMETRY        = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eGeometry),
+		FRAGMENT        = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eFragment),
+		COMPUTE         = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eCompute),
+		TASK            = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eTaskNV),
+		MESH            = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eMeshNV)
+	};
+	
+	using ShaderStages = vk::Flags<ShaderStage>;
+	
+	constexpr vk::ShaderStageFlags getShaderStageFlags(ShaderStages shaderStages) noexcept {
+		return vk::ShaderStageFlags(static_cast<VkShaderStageFlags>(shaderStages));
+	}
+	
+	constexpr ShaderStages operator|(ShaderStage stage0, ShaderStage stage1) noexcept {
+		return ShaderStages(stage0) | stage1;
+	}
+	
+	constexpr ShaderStages operator&(ShaderStage stage0, ShaderStage stage1) noexcept {
+		return ShaderStages(stage0) & stage1;
+	}
+	
+	constexpr ShaderStages operator^(ShaderStage stage0, ShaderStage stage1) noexcept {
+		return ShaderStages(stage0) ^ stage1;
+	}
+	
+	constexpr ShaderStages operator~(ShaderStage stage) noexcept {
+		return ~(ShaderStages(stage));
+	}
 }
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..2ca35bdab791ea18a67f33e53ab17fc485cbad02 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: %d",
+						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: %d",
+						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)",
+							 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/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/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 28ef7c6943428078589047497fc2d3b44fde5fd7..1c7bb12679e57c9221465452f2fc41a539b6b2a0 100644
--- a/modules/camera/src/vkcv/camera/PilotCameraController.cpp
+++ b/modules/camera/src/vkcv/camera/PilotCameraController.cpp
@@ -50,11 +50,11 @@ namespace vkcv::camera {
         }
 
         // handle yaw rotation
-        float yaw = camera.getYaw() + static_cast<float>(xOffset) * m_cameraSpeed;
+        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) * m_cameraSpeed;
+        float pitch = camera.getPitch() - static_cast<float>(yOffset) * 90.0f * m_cameraSpeed;
         pitch = glm::clamp(pitch, -89.0f, 89.0f);
         camera.setPitch(pitch);
     }
@@ -82,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;
@@ -109,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
@@ -162,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 b149a168f061125c08103ba63fcd7a97fa13ccc3..8de2beb87d8f29415db611bfe0d17c5efd57a2a3 100644
--- a/modules/camera/src/vkcv/camera/TrackballCameraController.cpp
+++ b/modules/camera/src/vkcv/camera/TrackballCameraController.cpp
@@ -23,10 +23,10 @@ namespace vkcv::camera {
         }
 
         // handle yaw rotation
-        m_yaw = m_yaw + static_cast<float>(xOffset) * m_cameraSpeed;
+        m_yaw = m_yaw + static_cast<float>(xOffset) * 90.0f * m_cameraSpeed;
 
         // handle pitch rotation
-        m_pitch = m_pitch + static_cast<float>(yOffset) * m_cameraSpeed;
+        m_pitch = m_pitch + static_cast<float>(yOffset) * 90.0f * m_cameraSpeed;
     }
 
     void TrackballCameraController::updateRadius(double offset, Camera &camera) {
@@ -67,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) {
@@ -91,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/scene/src/vkcv/scene/Scene.cpp b/modules/scene/src/vkcv/scene/Scene.cpp
index d6fa2a40a494ef57386e52a306e962a460c66dd6..c0065af5928d9ad2e2c9afd1a1ea44c35d94d799 100644
--- a/modules/scene/src/vkcv/scene/Scene.cpp
+++ b/modules/scene/src/vkcv/scene/Scene.cpp
@@ -116,6 +116,10 @@ namespace vkcv::scene {
 								size_t							 pushConstantsSizePerDrawcall,
 								const RecordMeshDrawcallFunction &record,
 								const std::vector<ImageHandle>   &renderTargets) {
+		m_core->recordBeginDebugLabel(cmdStream, "vkcv::scene::Scene", {
+			0.0f, 1.0f, 0.0f, 1.0f
+		});
+		
 		PushConstants pushConstants (pushConstantsSizePerDrawcall);
 		std::vector<DrawcallInfo> drawcalls;
 		size_t count = 0;
@@ -137,6 +141,8 @@ namespace vkcv::scene {
 				drawcalls,
 				renderTargets
 		);
+		
+		m_core->recordEndDebugLabel(cmdStream);
 	}
 	
 	Scene Scene::create(Core& core) {
diff --git a/modules/shader_compiler/CMakeLists.txt b/modules/shader_compiler/CMakeLists.txt
index 6fee42bfb571168cd2371e21e231ce417efa41f0..11c1e460575709dd9c9c16fdd02b6b923cc33045 100644
--- a/modules/shader_compiler/CMakeLists.txt
+++ b/modules/shader_compiler/CMakeLists.txt
@@ -31,7 +31,7 @@ include(config/GLSLANG.cmake)
 target_link_libraries(vkcv_shader_compiler ${vkcv_shader_compiler_libraries} vkcv)
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(vkcv_shader_compiler SYSTEM BEFORE PRIVATE ${vkcv_shader_compiler_includes} ${vkcv_include})
+target_include_directories(vkcv_shader_compiler SYSTEM BEFORE PRIVATE ${vkcv_shader_compiler_includes} ${vkcv_include} ${vkcv_includes})
 
 # add the own include directory for public headers
 target_include_directories(vkcv_shader_compiler BEFORE PUBLIC ${vkcv_shader_compiler_include})
diff --git a/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
index c8878513bf99054e357f1b076dfe12664be763b3..16067aebedfda8793a0096803ba5344275bcbbcd 100644
--- a/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
+++ b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
@@ -2,7 +2,7 @@
 #include "vkcv/shader/GLSLCompiler.hpp"
 
 #include <fstream>
-#include <strstream>
+#include <sstream>
 #include <glslang/SPIRV/GlslangToSpv.h>
 #include <glslang/StandAlone/DirStackFileIncluder.h>
 
@@ -219,12 +219,10 @@ namespace vkcv::shader {
 		std::string source (shaderSource);
 		
 		if (!m_defines.empty()) {
-			std::strstream defines;
+			std::ostringstream defines;
 			for (const auto& define : m_defines) {
 				defines << "#define " << define.first << " " << define.second << std::endl;
 			}
-			
-			defines << '\0';
 
 			size_t pos = source.find("#version") + 8;
 			if (pos >= source.length()) {
@@ -236,8 +234,10 @@ namespace vkcv::shader {
 				pos = epos;
 			}
 			
+			const auto defines_str = defines.str();
+			
 			pos = source.find('\n', pos) + 1;
-			source = source.insert(pos, defines.str());
+			source = source.insert(pos, defines_str);
 		}
 		
 		const char *shaderStrings [1];
diff --git a/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp
index 9c36acf5d050e3f4f19223020357b6c32534a2de..54df1829006964b30cc1831dc7115e9d5d222a51 100644
--- a/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp
+++ b/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp
@@ -7,7 +7,13 @@ namespace vkcv::upscaling {
 	
 	void BilinearUpscaling::recordUpscaling(const CommandStreamHandle &cmdStream, const ImageHandle &input,
 											const ImageHandle &output) {
+		m_core.recordBeginDebugLabel(cmdStream, "vkcv::upscaling::BilinearUpscaling", {
+			0.0f, 0.0f, 1.0f, 1.0f
+		});
+		
 		m_core.recordBlitImage(cmdStream, input, output, SamplerFilterType::LINEAR);
+		
+		m_core.recordEndDebugLabel(cmdStream);
 	}
 
 }
diff --git a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
index 460a6d0b459fe7d1d2a917a62138fea2e5a40908..b11051273ba6f9e56d3a537931f9d33fff657e43 100644
--- a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
+++ b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
@@ -177,7 +177,7 @@ namespace vkcv::upscaling {
 		vkcv::shader::GLSLCompiler easuCompiler;
 		vkcv::shader::GLSLCompiler rcasCompiler;
 		
-		const auto& features = m_core.getContext().getPhysicalDevice().getFeatures2();
+		const auto& features = m_core.getContext().getFeatureManager().getFeatures();
 		const bool float16Support = (
 				checkFeatures<vk::PhysicalDeviceFloat16Int8FeaturesKHR>(
 						reinterpret_cast<const vk::BaseInStructure*>(&features),
@@ -189,7 +189,7 @@ namespace vkcv::upscaling {
 						vk::StructureType::ePhysicalDevice16BitStorageFeaturesKHR,
 						check16Storage
 				)
-		) || (true); // check doesn't work because chain is empty
+		);
 		
 		if (!float16Support) {
 			easuCompiler.setDefine("SAMPLE_SLOW_FALLBACK", "1");
@@ -245,6 +245,10 @@ namespace vkcv::upscaling {
 	void FSRUpscaling::recordUpscaling(const CommandStreamHandle& cmdStream,
 									   const ImageHandle& input,
 									   const ImageHandle& output) {
+		m_core.recordBeginDebugLabel(cmdStream, "vkcv::upscaling::FSRUpscaling", {
+			1.0f, 0.0f, 0.0f, 1.0f
+		});
+		
 		const uint32_t inputWidth = m_core.getImageWidth(input);
 		const uint32_t inputHeight = m_core.getImageHeight(input);
 		
@@ -361,6 +365,8 @@ namespace vkcv::upscaling {
 					PushConstants(0)
 			);
 		}
+		
+		m_core.recordEndDebugLabel(cmdStream);
 	}
 	
 	bool FSRUpscaling::isHdrEnabled() const {
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index 6f179ebae0dedc1fc10060b14ce11133665a1538..bb26584a38e5b5c05ba2e883fe4b801377eaf3c8 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -7,3 +7,4 @@ add_subdirectory(particle_simulation)
 add_subdirectory(voxelization)
 add_subdirectory(mesh_shader)
 add_subdirectory(neural_network)
+add_subdirectory(indirect_dispatch)
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index 731d3e56975ff0cd2d8e6d503a19d56de1b922fe..2ad76e8f78c5870f6b582a1970caac306026166f 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -18,14 +18,13 @@ int main(int argc, const char** argv) {
 		windowHeight,
 		true
 	);
-
+	
 	vkcv::Core core = vkcv::Core::create(
 		window,
 		applicationName,
 		VK_MAKE_VERSION(0, 0, 1),
 		{ vk::QueueFlagBits::eGraphics ,vk::QueueFlagBits::eCompute , vk::QueueFlagBits::eTransfer },
-		{},
-		{ "VK_KHR_swapchain" }
+		{ VK_KHR_SWAPCHAIN_EXTENSION_NAME }
 	);
 
 	vkcv::asset::Scene mesh;
@@ -166,7 +165,6 @@ 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));
 
diff --git a/projects/first_scene/src/main.cpp b/projects/first_scene/src/main.cpp
index 527eba8c3a1e020e14d92f5d305e2ddced936333..21f8deacdb3f81fcd29c5f14cbe74485f36d13cd 100644
--- a/projects/first_scene/src/main.cpp
+++ b/projects/first_scene/src/main.cpp
@@ -28,14 +28,13 @@ int main(int argc, const char** argv) {
 	cameraManager.getCamera(camIndex0).setNearFar(0.1f, 30.0f);
 	
 	cameraManager.getCamera(camIndex1).setNearFar(0.1f, 30.0f);
-
+	
 	vkcv::Core core = vkcv::Core::create(
 		window,
 		applicationName,
 		VK_MAKE_VERSION(0, 0, 1),
 		{ vk::QueueFlagBits::eGraphics ,vk::QueueFlagBits::eCompute , vk::QueueFlagBits::eTransfer },
-		{},
-		{ "VK_KHR_swapchain" }
+		{ VK_KHR_SWAPCHAIN_EXTENSION_NAME }
 	);
 	
 	vkcv::scene::Scene scene = vkcv::scene::Scene::load(core, std::filesystem::path(
diff --git a/projects/first_triangle/src/main.cpp b/projects/first_triangle/src/main.cpp
index 253efad491e6e320ba5e5e8b270b187e2e79da82..d1d856b8863badef8a1b01f6277979cde019c4e0 100644
--- a/projects/first_triangle/src/main.cpp
+++ b/projects/first_triangle/src/main.cpp
@@ -16,22 +16,21 @@ int main(int argc, const char** argv) {
 		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_KHR_SWAPCHAIN_EXTENSION_NAME }
 	);
 
-	const auto& context = core.getContext();
-
 	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));
 
+	core.setDebugLabel(triangleIndexBuffer.getHandle(), "Triangle Index Buffer");
+	
 	// an example attachment for passes that output to the window
 	const vkcv::AttachmentDescription present_color_attachment(
 		vkcv::AttachmentOperation::STORE,
@@ -46,6 +45,8 @@ int main(int argc, const char** argv) {
 		std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
 		return EXIT_FAILURE;
 	}
+	
+	core.setDebugLabel(trianglePass, "Triangle Pass");
 
 	vkcv::ShaderProgram triangleShaderProgram;
 	vkcv::shader::GLSLCompiler compiler;
@@ -78,12 +79,15 @@ int main(int argc, const char** argv) {
 		return EXIT_FAILURE;
 	}
 	
+	core.setDebugLabel(trianglePipeline, "Triangle Pipeline");
+	
 	auto start = std::chrono::system_clock::now();
 
 	const vkcv::Mesh renderMesh({}, triangleIndexBuffer.getVulkanHandle(), 3);
 	vkcv::DrawcallInfo drawcall(renderMesh, {},1);
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+	core.setDebugLabel(swapchainInput, "Swapchain Image");
 
     vkcv::camera::CameraManager cameraManager(window);
     uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
@@ -113,6 +117,7 @@ int main(int argc, const char** argv) {
 		pushConstants.appendDrawcall(mvp);
 		
 		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+		core.setDebugLabel(cmdStream, "Render Commands");
 
 		core.recordDrawcallsToCmdStream(
 			cmdStream,
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/indirect_dispatch/resources/shaders/mesh.vert b/projects/indirect_dispatch/resources/shaders/mesh.vert
new file mode 100644
index 0000000000000000000000000000000000000000..734fd63cdee66e5fbf61cc427ca21fae18a31d82
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/mesh.vert
@@ -0,0 +1,20 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 1) in vec3 inNormal;
+layout(location = 2) in vec2 inUV;
+
+layout(location = 0) out vec3 passNormal;
+layout(location = 1) out vec2 passUV;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+    mat4 model;
+};
+
+void main()	{
+	gl_Position = mvp * vec4(inPosition, 1.0);
+	passNormal  = (model * vec4(inNormal, 0)).xyz;
+    passUV      = inUV;
+}
\ 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/indirect_dispatch/resources/shaders/sky.frag b/projects/indirect_dispatch/resources/shaders/sky.frag
new file mode 100644
index 0000000000000000000000000000000000000000..efc0e03b2d6ee1c71930c866293da66857bd56c7
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/sky.frag
@@ -0,0 +1,8 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+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/indirect_dispatch/resources/shaders/sky.vert b/projects/indirect_dispatch/resources/shaders/sky.vert
new file mode 100644
index 0000000000000000000000000000000000000000..44b48cd7f3bfc44e2e43edef0d474581d50608de
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/sky.vert
@@ -0,0 +1,13 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in vec3 inPosition;
+
+layout( push_constant ) uniform constants{
+    mat4 viewProjection;
+};
+
+void main()	{
+	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..5927970333d63d7e0c3bbbda4b7ccbf321c48a48
--- /dev/null
+++ b/projects/indirect_dispatch/src/App.cpp
@@ -0,0 +1,368 @@
+#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_EXTENSION_NAME })),
+	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
index 7e24fd7b853bfb0a29d8b30879ef1cb95ad141c0..fd009a6281f4b2b6716e193d23829907f4bb5f33 100644
--- a/projects/mesh_shader/.gitignore
+++ b/projects/mesh_shader/.gitignore
@@ -1 +1 @@
-first_triangle
\ No newline at end of file
+mesh_shader
\ No newline at end of file
diff --git a/projects/mesh_shader/src/main.cpp b/projects/mesh_shader/src/main.cpp
index 3a94de5842f3e70625729c9755b8c88048ece2ec..15aaf527619b4fb06d89bccc78cffe843cc723b7 100644
--- a/projects/mesh_shader/src/main.cpp
+++ b/projects/mesh_shader/src/main.cpp
@@ -86,23 +86,25 @@ int main(int argc, const char** argv) {
 		windowHeight,
 		false
 	);
+	
+	vkcv::Features features;
+	features.requireExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+	features.requireExtensionFeature<vk::PhysicalDeviceMeshShaderFeaturesNV>(
+			VK_NV_MESH_SHADER_EXTENSION_NAME, [](vk::PhysicalDeviceMeshShaderFeaturesNV& features) {
+		features.setTaskShader(true);
+		features.setMeshShader(true);
+	});
 
 	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 }
+		features
 	);
 
     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();
-
     vkcv::asset::Scene mesh;
     const char* path = argc > 1 ? argv[1] : "resources/Bunny/Bunny.glb";
     vkcv::asset::loadScene(path, mesh);
diff --git a/projects/neural_network/.gitignore b/projects/neural_network/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..350ab6eabed7ec3d27d175e3dc6f0f4e0ba6f15c
--- /dev/null
+++ b/projects/neural_network/.gitignore
@@ -0,0 +1 @@
+neural_network
\ No newline at end of file
diff --git a/projects/neural_network/src/main.cpp b/projects/neural_network/src/main.cpp
index 8958adbb9d6b9eea79f2b5a8e7a95298bb7ffe60..0d28cf38b029c4b4b3cc7c4b11bb589c64b98d3f 100644
--- a/projects/neural_network/src/main.cpp
+++ b/projects/neural_network/src/main.cpp
@@ -22,7 +22,6 @@ int main(int argc, const char** argv) {
         applicationName,
         VK_MAKE_VERSION(0, 0, 1),
         { vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
-        {},
         { "VK_KHR_swapchain" }
     );
 
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
index 07ba6b194ce72dbad15a921ca13a4814c6d4f5df..4de016aefd345a4231ea2dae0818b0839b163729 100644
--- a/projects/particle_simulation/src/main.cpp
+++ b/projects/particle_simulation/src/main.cpp
@@ -23,14 +23,13 @@ int main(int argc, const char **argv) {
     );
 
     vkcv::camera::CameraManager cameraManager(window);
-
+	
     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_KHR_SWAPCHAIN_EXTENSION_NAME }
     );
 
     auto particleIndexBuffer = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 3,
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index e7f9caa493714d30f13f64c292f1b6e51e5170b1..3c23c7eabbbdc5d83d37bde79b43a2730b1f28b2 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -80,14 +80,18 @@ int main(int argc, const char** argv) {
 	cameraManager.getCamera(camIndex).setFov(glm::radians(37.8));	// fov of a 35mm lens
 	
 	cameraManager.getCamera(camIndex2).setNearFar(0.1f, 30.0f);
-
+	
+	vkcv::Features features;
+	features.requireExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+	features.requireExtension(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME);
+	features.requireExtension(VK_KHR_16BIT_STORAGE_EXTENSION_NAME);
+	
 	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_KHR_shader_float16_int8", "VK_KHR_16bit_storage" }
+		features
 	);
 
 	vkcv::asset::Scene mesh;
diff --git a/src/vkcv/BufferManager.cpp b/src/vkcv/BufferManager.cpp
index ea0408094d86d72206eff7d31566e4af7c182d80..28e0a5ca8907061b7b9a0926918d0a544cf0d4b3 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,7 +28,7 @@ namespace vkcv {
 		}
 	}
 	
-	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;
 		
@@ -49,13 +49,15 @@ 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 vma::Allocator& allocator = m_core->getContext().getAllocator();
 		
@@ -81,6 +83,10 @@ namespace vkcv {
 				break;
 		}
 		
+		if (type == BufferType::STAGING) {
+			memoryUsage = vma::MemoryUsage::eCpuToGpu;
+		}
+		
 		auto bufferAllocation = allocator.createBuffer(
 				vk::BufferCreateInfo(createFlags, size, usageFlags),
 				vma::AllocationCreateInfo(
diff --git a/src/vkcv/Context.cpp b/src/vkcv/Context.cpp
index 2e30fb961d0b0931e4ff8796dd92b2cbd0b5f734..f48fad1a9022cbf99f6452c319d629060befbd98 100644
--- a/src/vkcv/Context.cpp
+++ b/src/vkcv/Context.cpp
@@ -9,8 +9,9 @@ namespace vkcv
             m_Instance(other.m_Instance),
             m_PhysicalDevice(other.m_PhysicalDevice),
             m_Device(other.m_Device),
-            m_QueueManager(other.m_QueueManager),
-            m_Allocator(other.m_Allocator)
+			m_FeatureManager(std::move(other.m_FeatureManager)),
+			m_QueueManager(std::move(other.m_QueueManager)),
+			m_Allocator(other.m_Allocator)
     {
         other.m_Instance        = nullptr;
         other.m_PhysicalDevice  = nullptr;
@@ -23,7 +24,8 @@ namespace vkcv
         m_Instance          = other.m_Instance;
         m_PhysicalDevice    = other.m_PhysicalDevice;
         m_Device            = other.m_Device;
-        m_QueueManager		= other.m_QueueManager;
+        m_FeatureManager	= std::move(other.m_FeatureManager);
+        m_QueueManager		= std::move(other.m_QueueManager);
         m_Allocator			= other.m_Allocator;
 
         other.m_Instance        = nullptr;
@@ -37,12 +39,14 @@ namespace vkcv
     Context::Context(vk::Instance instance,
                      vk::PhysicalDevice physicalDevice,
                      vk::Device device,
+                     FeatureManager&& featureManager,
 					 QueueManager&& queueManager,
 					 vma::Allocator&& allocator) noexcept :
     m_Instance(instance),
     m_PhysicalDevice(physicalDevice),
     m_Device(device),
-    m_QueueManager(queueManager),
+    m_FeatureManager(std::move(featureManager)),
+    m_QueueManager(std::move(queueManager)),
     m_Allocator(allocator)
     {}
 
@@ -68,6 +72,10 @@ namespace vkcv
         return m_Device;
     }
     
+    const FeatureManager& Context::getFeatureManager() const {
+    	return m_FeatureManager;
+    }
+    
     const QueueManager& Context::getQueueManager() const {
     	return m_QueueManager;
     }
@@ -167,7 +175,6 @@ namespace vkcv
 		return true;
 	}
 	
-	
 	std::vector<const char*> getRequiredExtensions() {
 		uint32_t glfwExtensionCount = 0;
 		const char** glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
@@ -180,20 +187,11 @@ 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,
 							const std::vector<vk::QueueFlagBits>& queueFlags,
-							const std::vector<const char *>& instanceExtensions,
-							const std::vector<const char *>& deviceExtensions) {
+							const Features& features,
+							const std::vector<const char*>& instanceExtensions) {
 		// check for layer support
 		
 		const std::vector<vk::LayerProperties>& layerProperties = vk::enumerateInstanceLayerProperties();
@@ -226,14 +224,14 @@ namespace vkcv
 			supportedExtensions.push_back(elem.extensionName);
 		}
 		
-		if (!checkSupport(supportedExtensions, instanceExtensions)) {
-			throw std::runtime_error("The requested instance extensions are not supported!");
-		}
-		
 		// for GLFW: get all required extensions
 		std::vector<const char*> requiredExtensions = getRequiredExtensions();
 		requiredExtensions.insert(requiredExtensions.end(), instanceExtensions.begin(), instanceExtensions.end());
 		
+		if (!checkSupport(supportedExtensions, requiredExtensions)) {
+			throw std::runtime_error("The requested instance extensions are not supported!");
+		}
+		
 		const vk::ApplicationInfo applicationInfo(
 				applicationName,
 				applicationVersion,
@@ -261,16 +259,35 @@ namespace vkcv
 		std::vector<vk::PhysicalDevice> physicalDevices = instance.enumeratePhysicalDevices();
 		vk::PhysicalDevice physicalDevice = pickPhysicalDevice(instance);
 		
-		// check for physical device extension support
-		std::vector<vk::ExtensionProperties> deviceExtensionProperties = physicalDevice.enumerateDeviceExtensionProperties();
-		supportedExtensions.clear();
-		for (auto& elem : deviceExtensionProperties) {
-			supportedExtensions.push_back(elem.extensionName);
+		FeatureManager featureManager (physicalDevice);
+		
+		if (featureManager.useExtension(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME, false)) {
+			featureManager.useFeatures<vk::PhysicalDeviceShaderFloat16Int8Features>(
+					[](vk::PhysicalDeviceShaderFloat16Int8Features& features) {
+				features.setShaderFloat16(true);
+			}, false);
 		}
-		if (!checkSupport(supportedExtensions, deviceExtensions)) {
-			throw std::runtime_error("The requested device extensions are not supported by the physical device!");
+		
+		if (featureManager.useExtension(VK_KHR_16BIT_STORAGE_EXTENSION_NAME, false)) {
+			featureManager.useFeatures<vk::PhysicalDevice16BitStorageFeatures>(
+					[](vk::PhysicalDevice16BitStorageFeatures& features) {
+				features.setStorageBuffer16BitAccess(true);
+			}, false);
 		}
 		
+		featureManager.useFeatures([](vk::PhysicalDeviceFeatures& features) {
+			features.setFragmentStoresAndAtomics(true);
+			features.setGeometryShader(true);
+			features.setDepthClamp(true);
+			features.setShaderInt16(true);
+		});
+		
+		for (const auto& feature : features.getList()) {
+			feature(featureManager);
+		}
+		
+		const auto& extensions = featureManager.getActiveExtensions();
+		
 		std::vector<vk::DeviceQueueCreateInfo> qCreateInfos;
 		
 		// create required queues
@@ -286,58 +303,21 @@ namespace vkcv
 				qCreateInfos.data(),
 				0,
 				nullptr,
-				deviceExtensions.size(),
-				deviceExtensions.data(),
-				nullptr		// Should our device use some features??? If yes: TODO
+				extensions.size(),
+				extensions.data(),
+				nullptr
 		);
 
 #ifndef NDEBUG
 		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::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
-		// device erstellen
-		// jetzt koennen wir mit dem device die queues erstellen
+		deviceCreateInfo.setPNext(&(featureManager.getFeatures()));
 		
 		vk::Device device = physicalDevice.createDevice(deviceCreateInfo);
 
-		if (usingMeshShaders)
-		{
+		if (featureManager.isExtensionActive(VK_NV_MESH_SHADER_EXTENSION_NAME)) {
 			InitMeshShaderDrawFunctions(device);
 		}
 		
@@ -376,6 +356,7 @@ namespace vkcv
 				instance,
 				physicalDevice,
 				device,
+				std::move(featureManager),
 				std::move(queueManager),
 				std::move(allocator)
 		);
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index 6c3a388b8c0f668b7994b7cd6c7c5106f2ab10dd..7ae330eb655e7572d5135ab3a7055651910228d7 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -54,14 +54,14 @@ namespace vkcv
                       const char *applicationName,
                       uint32_t applicationVersion,
                       const std::vector<vk::QueueFlagBits>& queueFlags,
-                      const std::vector<const char *>& instanceExtensions,
-                      const std::vector<const char *>& deviceExtensions)
+					  const Features& features,
+                      const std::vector<const char *>& instanceExtensions)
     {
         Context context = Context::create(
         		applicationName, applicationVersion,
         		queueFlags,
-        		instanceExtensions,
-        		deviceExtensions
+				features,
+        		instanceExtensions
 		);
 
         Swapchain swapChain = Swapchain::create(window, context);
@@ -263,9 +263,8 @@ namespace vkcv
 		vk::Device                      device) {
 
 		std::vector<vk::ImageView> attachmentsViews;
-		for (const ImageHandle handle : renderTargets) {
-			vk::ImageView targetHandle = imageManager.getVulkanImageView(handle);
-			attachmentsViews.push_back(targetHandle);
+		for (const ImageHandle& handle : renderTargets) {
+			attachmentsViews.push_back(imageManager.getVulkanImageView(handle));
 		}
 
 		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, swapchain, imageManager);
@@ -287,8 +286,7 @@ namespace vkcv
 		ImageManager&                   imageManager,
 		const vk::CommandBuffer         cmdBuffer) {
 
-		for (const ImageHandle handle : renderTargets) {
-			vk::ImageView targetHandle = imageManager.getVulkanImageView(handle);
+		for (const ImageHandle& handle : renderTargets) {
 			const bool isDepthImage = isDepthFormat(imageManager.getImageFormat(handle));
 			const vk::ImageLayout targetLayout =
 				isDepthImage ? vk::ImageLayout::eDepthStencilAttachmentOptimal : vk::ImageLayout::eColorAttachmentOptimal;
@@ -331,8 +329,8 @@ namespace vkcv
 	}
 
 	void Core::recordDrawcallsToCmdStream(
-		const CommandStreamHandle       cmdStreamHandle,
-		const PassHandle                renderpassHandle, 
+		const CommandStreamHandle&      cmdStreamHandle,
+		const PassHandle&               renderpassHandle,
 		const PipelineHandle            pipelineHandle, 
         const PushConstants             &pushConstantData,
         const std::vector<DrawcallInfo> &drawcalls,
@@ -368,7 +366,6 @@ namespace vkcv
 		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());
@@ -377,16 +374,14 @@ namespace vkcv
 			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});
 
 			const PipelineConfig &pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
-			if(pipeConfig.m_UseDynamicViewport)
-			{
+			if (pipeConfig.m_UseDynamicViewport) {
 				recordDynamicViewport(cmdBuffer, width, height);
 			}
 
-			for (int i = 0; i < drawcalls.size(); i++) {
+			for (size_t i = 0; i < drawcalls.size(); i++) {
 				recordDrawcall(drawcalls[i], cmdBuffer, pipelineLayout, pushConstantData, i);
 			}
 
-        vk::Rect2D dynamicScissor({0, 0}, {width, height});
 			cmdBuffer.endRenderPass();
 		};
 
@@ -399,8 +394,8 @@ namespace vkcv
 	}
 
 	void Core::recordMeshShaderDrawcalls(
-		const CommandStreamHandle                           cmdStreamHandle,
-		const PassHandle                                    renderpassHandle,
+		const CommandStreamHandle&                          cmdStreamHandle,
+		const PassHandle&                                   renderpassHandle,
 		const PipelineHandle                                pipelineHandle,
 		const PushConstants&                                pushConstantData,
 		const std::vector<MeshShaderDrawcall>&              drawcalls,
@@ -436,7 +431,6 @@ namespace vkcv
 		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());
@@ -445,12 +439,11 @@ namespace vkcv
 			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});
 
 			const PipelineConfig& pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
-			if (pipeConfig.m_UseDynamicViewport)
-			{
+			if (pipeConfig.m_UseDynamicViewport) {
 				recordDynamicViewport(cmdBuffer, width, height);
 			}
 
-			for (int i = 0; i < drawcalls.size(); i++) {
+			for (size_t i = 0; i < drawcalls.size(); i++) {
                 const uint32_t pushConstantOffset = i * pushConstantData.getSizePerDrawcall();
                 recordMeshShaderDrawcall(
                     cmdBuffer,
@@ -458,14 +451,14 @@ namespace vkcv
                     pushConstantData,
                     pushConstantOffset,
                     drawcalls[i],
-                    0);
+                    0
+				);
 			}
 
 			cmdBuffer.endRenderPass();
 		};
 
-		auto finishFunction = [framebuffer, this]()
-		{
+		auto finishFunction = [framebuffer, this]() {
 			m_Context.m_Device.destroy(framebuffer);
 		};
 
@@ -506,6 +499,85 @@ namespace vkcv
 
 		recordCommandsToStream(cmdStreamHandle, submitFunction, nullptr);
 	}
+	
+	void Core::recordBeginDebugLabel(const CommandStreamHandle &cmdStream,
+									 const std::string& label,
+									 const std::array<float, 4>& color) {
+#ifndef NDEBUG
+		static PFN_vkCmdBeginDebugUtilsLabelEXT beginDebugLabel = reinterpret_cast<PFN_vkCmdBeginDebugUtilsLabelEXT>(
+				m_Context.getDevice().getProcAddr("vkCmdBeginDebugUtilsLabelEXT")
+		);
+		
+		if (!beginDebugLabel) {
+			return;
+		}
+		
+		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
+			const vk::DebugUtilsLabelEXT debug (
+					label.c_str(),
+					color
+			);
+			
+			beginDebugLabel(cmdBuffer, &(static_cast<const VkDebugUtilsLabelEXT&>(debug)));
+		};
+
+		recordCommandsToStream(cmdStream, submitFunction, nullptr);
+#endif
+	}
+	
+	void Core::recordEndDebugLabel(const CommandStreamHandle &cmdStream) {
+#ifndef NDEBUG
+		static PFN_vkCmdEndDebugUtilsLabelEXT endDebugLabel = reinterpret_cast<PFN_vkCmdEndDebugUtilsLabelEXT>(
+				m_Context.getDevice().getProcAddr("vkCmdEndDebugUtilsLabelEXT")
+		);
+		
+		if (!endDebugLabel) {
+			return;
+		}
+		
+		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
+			endDebugLabel(cmdBuffer);
+		};
+
+		recordCommandsToStream(cmdStream, submitFunction, nullptr);
+#endif
+	}
+	
+	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()) {
@@ -795,4 +867,141 @@ namespace vkcv
 		}, nullptr);
 	}
 	
+	static void setDebugObjectLabel(const vk::Device& device, const vk::ObjectType& type,
+									uint64_t handle, const std::string& label) {
+#ifndef NDEBUG
+		static PFN_vkSetDebugUtilsObjectNameEXT setDebugLabel = reinterpret_cast<PFN_vkSetDebugUtilsObjectNameEXT>(
+				device.getProcAddr("vkSetDebugUtilsObjectNameEXT")
+		);
+		
+		if (!setDebugLabel) {
+			return;
+		}
+		
+		const vk::DebugUtilsObjectNameInfoEXT debug (
+				type,
+				handle,
+				label.c_str()
+		);
+		
+		setDebugLabel(device, &(static_cast<const VkDebugUtilsObjectNameInfoEXT&>(debug)));
+#endif
+	}
+	
+	void Core::setDebugLabel(const BufferHandle &handle, const std::string &label) {
+		if (!handle) {
+			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
+			return;
+		}
+		
+		setDebugObjectLabel(
+				m_Context.getDevice(),
+				vk::ObjectType::eBuffer,
+				reinterpret_cast<uint64_t>(static_cast<VkBuffer>(
+						m_BufferManager->getBuffer(handle)
+				)),
+				label
+		);
+	}
+	
+	void Core::setDebugLabel(const PassHandle &handle, const std::string &label) {
+		if (!handle) {
+			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
+			return;
+		}
+		
+		setDebugObjectLabel(
+				m_Context.getDevice(),
+				vk::ObjectType::eRenderPass,
+				reinterpret_cast<uint64_t>(static_cast<VkRenderPass>(
+						m_PassManager->getVkPass(handle)
+				)),
+				label
+		);
+	}
+	
+	void Core::setDebugLabel(const PipelineHandle &handle, const std::string &label) {
+		if (!handle) {
+			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
+			return;
+		}
+		
+		setDebugObjectLabel(
+				m_Context.getDevice(),
+				vk::ObjectType::ePipeline,
+				reinterpret_cast<uint64_t>(static_cast<VkPipeline>(
+						m_PipelineManager->getVkPipeline(handle)
+				)),
+				label
+		);
+	}
+	
+	void Core::setDebugLabel(const DescriptorSetHandle &handle, const std::string &label) {
+		if (!handle) {
+			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
+			return;
+		}
+		
+		setDebugObjectLabel(
+				m_Context.getDevice(),
+				vk::ObjectType::eDescriptorSet,
+				reinterpret_cast<uint64_t>(static_cast<VkDescriptorSet>(
+						m_DescriptorManager->getDescriptorSet(handle).vulkanHandle
+				)),
+				label
+		);
+	}
+	
+	void Core::setDebugLabel(const SamplerHandle &handle, const std::string &label) {
+		if (!handle) {
+			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
+			return;
+		}
+		
+		setDebugObjectLabel(
+				m_Context.getDevice(),
+				vk::ObjectType::eSampler,
+				reinterpret_cast<uint64_t>(static_cast<VkSampler>(
+						m_SamplerManager->getVulkanSampler(handle)
+				)),
+				label
+		);
+	}
+	
+	void Core::setDebugLabel(const ImageHandle &handle, const std::string &label) {
+		if (!handle) {
+			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
+			return;
+		} else
+		if (handle.isSwapchainImage()) {
+			vkcv_log(LogLevel::WARNING, "Can't set debug label to swapchain image");
+			return;
+		}
+		
+		setDebugObjectLabel(
+				m_Context.getDevice(),
+				vk::ObjectType::eImage,
+				reinterpret_cast<uint64_t>(static_cast<VkImage>(
+						m_ImageManager->getVulkanImage(handle)
+				)),
+				label
+		);
+	}
+	
+	void Core::setDebugLabel(const CommandStreamHandle &handle, const std::string &label) {
+		if (!handle) {
+			vkcv_log(LogLevel::WARNING, "Can't set debug label to invalid handle");
+			return;
+		}
+		
+		setDebugObjectLabel(
+				m_Context.getDevice(),
+				vk::ObjectType::eCommandBuffer,
+				reinterpret_cast<uint64_t>(static_cast<VkCommandBuffer>(
+						m_CommandStreamManager->getStreamCommandBuffer(handle)
+				)),
+				label
+		);
+	}
+	
 }
diff --git a/src/vkcv/DescriptorConfig.cpp b/src/vkcv/DescriptorConfig.cpp
index 54e879ac7e6ec7825a4c003899e3c264454c547f..a9a127fe608472682c1cbc8d32ca466fba860c72 100644
--- a/src/vkcv/DescriptorConfig.cpp
+++ b/src/vkcv/DescriptorConfig.cpp
@@ -5,11 +5,11 @@ namespace vkcv {
 		uint32_t bindingID,
 		DescriptorType descriptorType,
 		uint32_t descriptorCount,
-		ShaderStage shaderStage) noexcept
+		ShaderStages shaderStages) noexcept
 		:
 		bindingID(bindingID),
 		descriptorType(descriptorType),
 		descriptorCount(descriptorCount),
-		shaderStage(shaderStage) {}
+		shaderStages(shaderStages) {}
 	
 }
diff --git a/src/vkcv/DescriptorManager.cpp b/src/vkcv/DescriptorManager.cpp
index 0df359a15883847c132c429ed2945ac7624fb865..76f2dae74420804c9ce168bea76e7c1bdef0fbc0 100644
--- a/src/vkcv/DescriptorManager.cpp
+++ b/src/vkcv/DescriptorManager.cpp
@@ -54,7 +54,7 @@ namespace vkcv
                 binding.bindingID,
                 convertDescriptorTypeFlag(binding.descriptorType),
                 binding.descriptorCount,
-                convertShaderStageFlag(binding.shaderStage));
+                getShaderStageFlags(binding.shaderStages));
             setBindings.push_back(descriptorSetLayoutBinding);
         }
 
@@ -273,26 +273,6 @@ namespace vkcv
                 return vk::DescriptorType::eUniformBuffer;
         }
     }
-
-    vk::ShaderStageFlagBits DescriptorManager::convertShaderStageFlag(ShaderStage stage) {
-        switch (stage) 
-        {
-            case ShaderStage::VERTEX:
-                return vk::ShaderStageFlagBits::eVertex;
-            case ShaderStage::FRAGMENT:
-                return vk::ShaderStageFlagBits::eFragment;
-            case ShaderStage::TESS_CONTROL:
-                return vk::ShaderStageFlagBits::eTessellationControl;
-            case ShaderStage::TESS_EVAL:
-                return vk::ShaderStageFlagBits::eTessellationEvaluation;
-            case ShaderStage::GEOMETRY:
-                return vk::ShaderStageFlagBits::eGeometry;
-            case ShaderStage::COMPUTE:
-                return vk::ShaderStageFlagBits::eCompute;
-            default:
-                return vk::ShaderStageFlagBits::eAll;
-        }
-    }
     
     void DescriptorManager::destroyDescriptorSetById(uint64_t id) {
 		if (id >= m_DescriptorSets.size()) {
diff --git a/src/vkcv/DescriptorManager.hpp b/src/vkcv/DescriptorManager.hpp
index d18be64f3b069af68cecce68f6fa623c81f8dfa4..df58daa56c26a0432f365a4ed0d9df56adc4cd0e 100644
--- a/src/vkcv/DescriptorManager.hpp
+++ b/src/vkcv/DescriptorManager.hpp
@@ -51,12 +51,6 @@ namespace vkcv
 		* @return vk flag of the DescriptorType
 		*/
 		static vk::DescriptorType convertDescriptorTypeFlag(DescriptorType type);
-		/**
-		* Converts the flags of the shader stages from VulkanCV (vkcv) to Vulkan (vk).
-		* @param[in] vkcv flag of the ShaderStage (see ShaderProgram.hpp)
-		* @return vk flag of the ShaderStage
-		*/
-		static vk::ShaderStageFlagBits convertShaderStageFlag(ShaderStage stage);
 		
 		/**
 		* Destroys a specific resource description
diff --git a/src/vkcv/DrawcallRecording.cpp b/src/vkcv/DrawcallRecording.cpp
index d89ace3859717f753534402507a713a78bfb6876..ca8b248a06d06c7aed6f8d0e9760645b727a5993 100644
--- a/src/vkcv/DrawcallRecording.cpp
+++ b/src/vkcv/DrawcallRecording.cpp
@@ -52,8 +52,6 @@ namespace vkcv {
         }
     }
 
-
-
     struct MeshShaderFunctions
     {
         PFN_vkCmdDrawMeshTasksNV cmdDrawMeshTasks                           = nullptr;
diff --git a/src/vkcv/FeatureManager.cpp b/src/vkcv/FeatureManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..18307999bb28ed60791a801b952619eeeba51a53
--- /dev/null
+++ b/src/vkcv/FeatureManager.cpp
@@ -0,0 +1,437 @@
+
+#include "vkcv/FeatureManager.hpp"
+
+#include <stddef.h>
+#include <string.h>
+#include <type_traits>
+
+namespace vkcv {
+	
+#ifdef _MSVC_LANG
+#define typeof(var) std::decay<decltype((var))>::type
+#endif
+	
+#define vkcv_check_init_features2(type)\
+type supported;                        \
+vk::PhysicalDeviceFeatures2 query;     \
+query.setPNext(&supported);            \
+m_physicalDevice.getFeatures2(&query)
+
+#define vkcv_check_feature(attribute) {                                                                    \
+  const char *f = reinterpret_cast<const char*>(&(features));                                              \
+  const char *s = reinterpret_cast<const char*>(&(supported));                                             \
+  const vk::Bool32* fb = reinterpret_cast<const vk::Bool32*>(f + offsetof(typeof((features)), attribute)); \
+  const vk::Bool32* sb = reinterpret_cast<const vk::Bool32*>(s + offsetof(typeof((features)), attribute)); \
+  if ((*fb) && (!*sb)) {                                                                                   \
+    vkcv_log(((required)? LogLevel::ERROR : LogLevel::WARNING),                                            \
+    "Feature '" #attribute "' is not supported");                                                          \
+    return false;                                                                                          \
+  }                                                                                                        \
+}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceFeatures &features, bool required) const {
+		const auto& supported = m_physicalDevice.getFeatures();
+		
+		vkcv_check_feature(alphaToOne);
+		vkcv_check_feature(depthBiasClamp);
+		vkcv_check_feature(depthBounds);
+		vkcv_check_feature(depthClamp);
+		vkcv_check_feature(drawIndirectFirstInstance);
+		vkcv_check_feature(dualSrcBlend);
+		vkcv_check_feature(fillModeNonSolid);
+		vkcv_check_feature(fragmentStoresAndAtomics);
+		vkcv_check_feature(fullDrawIndexUint32);
+		vkcv_check_feature(geometryShader);
+		vkcv_check_feature(imageCubeArray);
+		vkcv_check_feature(independentBlend);
+		vkcv_check_feature(inheritedQueries);
+		vkcv_check_feature(largePoints);
+		vkcv_check_feature(logicOp);
+		vkcv_check_feature(multiDrawIndirect);
+		vkcv_check_feature(multiViewport);
+		vkcv_check_feature(occlusionQueryPrecise);
+		vkcv_check_feature(pipelineStatisticsQuery);
+		vkcv_check_feature(robustBufferAccess);
+		vkcv_check_feature(sampleRateShading);
+		vkcv_check_feature(samplerAnisotropy);
+		vkcv_check_feature(shaderClipDistance);
+		vkcv_check_feature(shaderCullDistance);
+		vkcv_check_feature(shaderFloat64);
+		vkcv_check_feature(shaderImageGatherExtended);
+		vkcv_check_feature(shaderInt16);
+		vkcv_check_feature(shaderInt64);
+		vkcv_check_feature(shaderResourceMinLod);
+		vkcv_check_feature(shaderResourceResidency);
+		vkcv_check_feature(shaderSampledImageArrayDynamicIndexing);
+		vkcv_check_feature(shaderStorageBufferArrayDynamicIndexing);
+		vkcv_check_feature(shaderStorageImageArrayDynamicIndexing);
+		vkcv_check_feature(shaderStorageImageExtendedFormats);
+		vkcv_check_feature(shaderStorageImageMultisample);
+		vkcv_check_feature(shaderStorageImageReadWithoutFormat);
+		vkcv_check_feature(shaderStorageImageWriteWithoutFormat);
+		vkcv_check_feature(shaderTessellationAndGeometryPointSize);
+		vkcv_check_feature(shaderUniformBufferArrayDynamicIndexing);
+		vkcv_check_feature(sparseBinding);
+		vkcv_check_feature(sparseResidency2Samples);
+		vkcv_check_feature(sparseResidency4Samples);
+		vkcv_check_feature(sparseResidency8Samples);
+		vkcv_check_feature(sparseResidency16Samples);
+		vkcv_check_feature(sparseResidencyAliased);
+		vkcv_check_feature(sparseResidencyBuffer);
+		vkcv_check_feature(sparseResidencyImage2D);
+		vkcv_check_feature(sparseResidencyImage3D);
+		vkcv_check_feature(tessellationShader);
+		vkcv_check_feature(textureCompressionASTC_LDR);
+		vkcv_check_feature(textureCompressionBC);
+		vkcv_check_feature(textureCompressionETC2);
+		vkcv_check_feature(variableMultisampleRate);
+		vkcv_check_feature(vertexPipelineStoresAndAtomics);
+		vkcv_check_feature(wideLines);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDevice16BitStorageFeatures &features, bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDevice16BitStorageFeatures);
+		
+		vkcv_check_feature(storageBuffer16BitAccess);
+		vkcv_check_feature(storageInputOutput16);
+		vkcv_check_feature(storagePushConstant16);
+		vkcv_check_feature(uniformAndStorageBuffer16BitAccess);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDevice8BitStorageFeatures &features, bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDevice8BitStorageFeatures);
+		
+		vkcv_check_feature(storageBuffer8BitAccess);
+		vkcv_check_feature(storagePushConstant8);
+		vkcv_check_feature(uniformAndStorageBuffer8BitAccess);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceBufferDeviceAddressFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceBufferDeviceAddressFeatures);
+		
+		vkcv_check_feature(bufferDeviceAddress);
+		vkcv_check_feature(bufferDeviceAddressCaptureReplay);
+		vkcv_check_feature(bufferDeviceAddressMultiDevice);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceDescriptorIndexingFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceDescriptorIndexingFeatures);
+		
+		vkcv_check_feature(shaderInputAttachmentArrayDynamicIndexing);
+		vkcv_check_feature(shaderInputAttachmentArrayNonUniformIndexing);
+		vkcv_check_feature(shaderSampledImageArrayNonUniformIndexing);
+		vkcv_check_feature(shaderStorageBufferArrayNonUniformIndexing);
+		vkcv_check_feature(shaderStorageImageArrayNonUniformIndexing);
+		vkcv_check_feature(shaderStorageTexelBufferArrayDynamicIndexing);
+		vkcv_check_feature(shaderStorageTexelBufferArrayNonUniformIndexing);
+		vkcv_check_feature(shaderUniformBufferArrayNonUniformIndexing);
+		vkcv_check_feature(shaderUniformTexelBufferArrayDynamicIndexing);
+		vkcv_check_feature(shaderUniformTexelBufferArrayNonUniformIndexing);
+		vkcv_check_feature(descriptorBindingPartiallyBound);
+		vkcv_check_feature(descriptorBindingSampledImageUpdateAfterBind);
+		vkcv_check_feature(descriptorBindingStorageBufferUpdateAfterBind);
+		vkcv_check_feature(descriptorBindingStorageImageUpdateAfterBind);
+		vkcv_check_feature(descriptorBindingStorageTexelBufferUpdateAfterBind);
+		vkcv_check_feature(descriptorBindingUniformBufferUpdateAfterBind);
+		vkcv_check_feature(descriptorBindingUniformTexelBufferUpdateAfterBind);
+		vkcv_check_feature(descriptorBindingUpdateUnusedWhilePending);
+		vkcv_check_feature(descriptorBindingVariableDescriptorCount);
+		vkcv_check_feature(runtimeDescriptorArray);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceHostQueryResetFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceHostQueryResetFeatures);
+		
+		vkcv_check_feature(hostQueryReset);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceImagelessFramebufferFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceImagelessFramebufferFeatures);
+		
+		vkcv_check_feature(imagelessFramebuffer);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceMultiviewFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceMultiviewFeatures);
+		
+		vkcv_check_feature(multiview);
+		vkcv_check_feature(multiviewGeometryShader);
+		vkcv_check_feature(multiviewTessellationShader);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceProtectedMemoryFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceProtectedMemoryFeatures);
+		
+		vkcv_check_feature(protectedMemory);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceSamplerYcbcrConversionFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceSamplerYcbcrConversionFeatures);
+
+		vkcv_check_feature(samplerYcbcrConversion);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceScalarBlockLayoutFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceScalarBlockLayoutFeatures);
+		
+		vkcv_check_feature(scalarBlockLayout);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceSeparateDepthStencilLayoutsFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceSeparateDepthStencilLayoutsFeatures);
+		
+		vkcv_check_feature(separateDepthStencilLayouts);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceShaderAtomicInt64Features &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceShaderAtomicInt64Features);
+		
+		vkcv_check_feature(shaderBufferInt64Atomics);
+		vkcv_check_feature(shaderSharedInt64Atomics);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceShaderFloat16Int8Features &features, bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceShaderFloat16Int8Features);
+		
+		vkcv_check_feature(shaderFloat16);
+		vkcv_check_feature(shaderInt8);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceShaderSubgroupExtendedTypesFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceShaderSubgroupExtendedTypesFeatures);
+		
+		vkcv_check_feature(shaderSubgroupExtendedTypes);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceTimelineSemaphoreFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceTimelineSemaphoreFeatures);
+		
+		vkcv_check_feature(timelineSemaphore);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceUniformBufferStandardLayoutFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceUniformBufferStandardLayoutFeatures);
+		
+		vkcv_check_feature(uniformBufferStandardLayout);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceVariablePointersFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceVariablePointersFeatures);
+		
+		vkcv_check_feature(variablePointers);
+		vkcv_check_feature(variablePointersStorageBuffer);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceVulkanMemoryModelFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceVulkanMemoryModelFeatures);
+		
+		vkcv_check_feature(vulkanMemoryModel);
+		vkcv_check_feature(vulkanMemoryModelDeviceScope);
+		vkcv_check_feature(vulkanMemoryModelAvailabilityVisibilityChains);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceMeshShaderFeaturesNV &features, bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceMeshShaderFeaturesNV);
+		
+		vkcv_check_feature(taskShader);
+		vkcv_check_feature(meshShader);
+		
+		return true;
+	}
+	
+	vk::BaseOutStructure* FeatureManager::findFeatureStructure(vk::StructureType type) const {
+		for (auto& base : m_featuresExtensions) {
+			if (base->sType == type) {
+				return base;
+			}
+		}
+		
+		return nullptr;
+	}
+	
+	const char* strclone(const char* str) {
+		if (!str) {
+			return nullptr;
+		}
+		
+		const size_t length = strlen(str) + 1;
+		
+		if (length <= 1) {
+			return nullptr;
+		}
+		
+		char* clone = new char[length];
+		strcpy(clone, str);
+		return clone;
+	}
+	
+	FeatureManager::FeatureManager(vk::PhysicalDevice &physicalDevice) :
+	m_physicalDevice(physicalDevice),
+	m_supportedExtensions(),
+	m_activeExtensions(),
+	m_featuresBase(),
+	m_featuresExtensions() {
+		for (const auto& extension : m_physicalDevice.enumerateDeviceExtensionProperties()) {
+			const char* clone = strclone(extension.extensionName);
+			
+			if (clone) {
+				m_supportedExtensions.push_back(clone);
+			}
+		}
+	}
+	
+	FeatureManager::FeatureManager(FeatureManager &&other) noexcept :
+	m_physicalDevice(other.m_physicalDevice),
+	m_supportedExtensions(std::move(other.m_supportedExtensions)),
+	m_activeExtensions(std::move(other.m_activeExtensions)),
+    m_featuresBase(other.m_featuresBase),
+    m_featuresExtensions(std::move(other.m_featuresExtensions)) {
+		other.m_featuresExtensions.clear();
+		other.m_activeExtensions.clear();
+		other.m_supportedExtensions.clear();
+	}
+	
+	FeatureManager::~FeatureManager() {
+		for (auto& features : m_featuresExtensions) {
+			delete features;
+		}
+		
+		for (auto& extension : m_activeExtensions) {
+			delete[] extension;
+		}
+		
+		for (auto& extension : m_supportedExtensions) {
+			delete[] extension;
+		}
+	}
+	
+	FeatureManager &FeatureManager::operator=(FeatureManager &&other) noexcept {
+		m_physicalDevice = other.m_physicalDevice;
+		m_supportedExtensions = std::move(other.m_supportedExtensions);
+		m_activeExtensions = std::move(other.m_activeExtensions);
+		m_featuresBase = other.m_featuresBase;
+		m_featuresExtensions = std::move(other.m_featuresExtensions);
+		
+		other.m_featuresExtensions.clear();
+		other.m_activeExtensions.clear();
+		other.m_supportedExtensions.clear();
+		
+		return *this;
+	}
+	
+	bool FeatureManager::isExtensionSupported(const std::string& extension) const {
+		for (const auto& supported : m_supportedExtensions) {
+			if (0 == strcmp(supported, extension.c_str())) {
+				return true;
+			}
+		}
+		
+		return false;
+	}
+	
+	bool FeatureManager::useExtension(const std::string& extension, bool required) {
+		const char* clone = strclone(extension.c_str());
+		
+		if (!clone) {
+			vkcv_log(LogLevel::WARNING, "Extension '%s' is not valid", extension.c_str());
+			return false;
+		}
+		
+		if (!isExtensionSupported(extension)) {
+			vkcv_log((required? LogLevel::ERROR : LogLevel::WARNING), "Extension '%s' is not supported",
+					 extension.c_str());
+			
+			delete[] clone;
+			return false;
+		}
+		
+		m_activeExtensions.push_back(clone);
+		return true;
+	}
+	
+	bool FeatureManager::isExtensionActive(const std::string& extension) const {
+		for (const auto& supported : m_activeExtensions) {
+			if (0 == strcmp(supported, extension.c_str())) {
+				return true;
+			}
+		}
+		
+		return false;
+	}
+	
+	const std::vector<const char*>& FeatureManager::getActiveExtensions() const {
+		return m_activeExtensions;
+	}
+	
+	bool FeatureManager::useFeatures(const std::function<void(vk::PhysicalDeviceFeatures &)> &featureFunction,
+									 bool required) {
+		vk::PhysicalDeviceFeatures features = m_featuresBase.features;
+		
+		featureFunction(features);
+		
+		if (!checkSupport(features, required)) {
+			return false;
+		}
+		
+		m_featuresBase.features = features;
+		return true;
+	}
+	
+	const vk::PhysicalDeviceFeatures2& FeatureManager::getFeatures() const {
+		return m_featuresBase;
+	}
+	
+}
diff --git a/src/vkcv/Features.cpp b/src/vkcv/Features.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..60616685f67084b6875ce14bf09f524a9127688f
--- /dev/null
+++ b/src/vkcv/Features.cpp
@@ -0,0 +1,62 @@
+
+#include "vkcv/Features.hpp"
+
+namespace vkcv {
+	
+	Features::Features(const std::initializer_list<std::string>& list) : m_features() {
+		for (const auto& extension : list) {
+			requireExtension(extension);
+		}
+	}
+	
+	void Features::requireExtension(const std::string& extension) {
+		m_features.emplace_back([extension](FeatureManager& featureManager) {
+			return featureManager.useExtension(extension, true);
+		});
+	}
+	
+	void Features::requireExtensionFeature(const std::string &extension,
+										   const std::function<void(vk::PhysicalDeviceFeatures &)> &featureFunction) {
+		m_features.emplace_back([extension, featureFunction](FeatureManager& featureManager) {
+			if (featureManager.useExtension(extension, true)) {
+				return featureManager.useFeatures(featureFunction, true);
+			} else {
+				return false;
+			}
+		});
+	}
+	
+	void Features::requireFeature(const std::function<void(vk::PhysicalDeviceFeatures &)> &featureFunction) {
+		m_features.emplace_back([featureFunction](FeatureManager& featureManager) {
+			return featureManager.useFeatures(featureFunction, true);
+		});
+	}
+	
+	void Features::tryExtension(const std::string& extension) {
+		m_features.emplace_back([extension](FeatureManager& featureManager) {
+			return featureManager.useExtension(extension, false);
+		});
+	}
+	
+	void Features::tryExtensionFeature(const std::string &extension,
+									   const std::function<void(vk::PhysicalDeviceFeatures &)> &featureFunction) {
+		m_features.emplace_back([extension, featureFunction](FeatureManager& featureManager) {
+			if (featureManager.useExtension(extension, false)) {
+				return featureManager.useFeatures(featureFunction, false);
+			} else {
+				return false;
+			}
+		});
+	}
+	
+	void Features::tryFeature(const std::function<void(vk::PhysicalDeviceFeatures &)> &featureFunction) {
+		m_features.emplace_back([featureFunction](FeatureManager& featureManager) {
+			return featureManager.useFeatures(featureFunction, false);
+		});
+	}
+	
+	const std::vector<Feature>& Features::getList() const {
+		return m_features;
+	}
+
+}
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index 1cb6ad3a1187c08cf1aa014ae4ae259591f5c786..4ddd7f8c44c6023a80831bc8b4b092692e84ec86 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -387,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);
diff --git a/src/vkcv/QueueManager.cpp b/src/vkcv/QueueManager.cpp
index 15e958b0de929e53170324ade27a9b3663a15d6a..79e15c9b00e4c67fb956bdcd0e8b1ff05261b2f1 100644
--- a/src/vkcv/QueueManager.cpp
+++ b/src/vkcv/QueueManager.cpp
@@ -44,7 +44,7 @@ namespace vkcv {
         std::vector<int> prios;
         for(auto flag: queueFlags) {
             int prioCount = 0;
-            for (int i = 0; i < qFamilyProperties.size(); i++) {
+            for (size_t i = 0; i < qFamilyProperties.size(); i++) {
                 prioCount += (static_cast<uint32_t>(flag & qFamilyProperties[i].queueFlags) != 0) * qFamilyProperties[i].queueCount;
             }
             prios.push_back(prioCount);
@@ -65,10 +65,14 @@ namespace vkcv {
         std::vector<std::vector<int>> queueFamilyStatus, initialQueueFamilyStatus;
 
         for (auto qFamily : qFamilyProperties) {
-            int graphicsCount = int(static_cast<uint32_t>(qFamily.queueFlags & vk::QueueFlagBits::eGraphics) != 0) * qFamily.queueCount;
-            int computeCount = int(static_cast<uint32_t>(qFamily.queueFlags & vk::QueueFlagBits::eCompute) != 0) * qFamily.queueCount;
-            int transferCount = int(static_cast<uint32_t>(qFamily.queueFlags & vk::QueueFlagBits::eTransfer) != 0) * qFamily.queueCount;
-            queueFamilyStatus.push_back({graphicsCount, computeCount, transferCount});
+            auto graphicsCount = static_cast<uint32_t>(qFamily.queueFlags & vk::QueueFlagBits::eGraphics) != 0? qFamily.queueCount : 0;
+			auto computeCount = static_cast<uint32_t>(qFamily.queueFlags & vk::QueueFlagBits::eCompute) != 0? qFamily.queueCount : 0;
+			auto transferCount = static_cast<uint32_t>(qFamily.queueFlags & vk::QueueFlagBits::eTransfer) != 0? qFamily.queueCount : 0;
+            queueFamilyStatus.push_back({
+				static_cast<int>(graphicsCount),
+				static_cast<int>(computeCount),
+				static_cast<int>(transferCount)
+			});
         }
 
         initialQueueFamilyStatus = queueFamilyStatus;
diff --git a/src/vkcv/ShaderProgram.cpp b/src/vkcv/ShaderProgram.cpp
index 971797d9a42d071a1730ebf31a0b554f92fa361f..134ba7afd203d3222b37dfaf627a9d4fd3ede1c7 100644
--- a/src/vkcv/ShaderProgram.cpp
+++ b/src/vkcv/ShaderProgram.cpp
@@ -195,7 +195,25 @@ namespace vkcv {
         if (maxSetID != -1) {
             if((int32_t)m_DescriptorSets.size() <= maxSetID) m_DescriptorSets.resize(maxSetID + 1);
             for (const auto &binding : bindings) {
-                m_DescriptorSets[binding.first].push_back(binding.second);
+                //checking if descriptor has already been reflected in another shader stage
+                bool bindingFound = false;
+                uint32_t pos = 0;
+                for (const auto& descriptor : m_DescriptorSets[binding.first]) {
+                    if (binding.second.bindingID == descriptor.bindingID) {
+                        if (binding.second.descriptorType == descriptor.descriptorType && binding.second.descriptorCount == descriptor.descriptorCount) {
+                            //updating descriptor binding with another shader stage
+                            ShaderStages updatedShaders = descriptor.shaderStages | shaderStage;
+                            DescriptorBinding newBinding = DescriptorBinding(binding.second.bindingID, binding.second.descriptorType, binding.second.descriptorCount, updatedShaders);
+                            m_DescriptorSets[binding.first][pos] = newBinding;
+                            bindingFound = true;
+                            break;
+                        }
+                        else vkcv_log(LogLevel::ERROR, "Included shaders contain resources with same identifier but different type or count");
+                    }
+                    pos++;
+                }
+                //append new descriptor if it has not been reflected yet
+                if(!bindingFound) m_DescriptorSets[binding.first].push_back(binding.second);
             }
         }
 
diff --git a/src/vkcv/Window.cpp b/src/vkcv/Window.cpp
index aea00fb10d579aea0dc5be789ced3e6582b868bf..072efcd00eb6520fa4f20379721b559668339f6e 100644
--- a/src/vkcv/Window.cpp
+++ b/src/vkcv/Window.cpp
@@ -4,6 +4,7 @@
  * @brief Window class to handle a basic rendering surface and input
  */
 
+#include <thread>
 #include <vector>
 #include <GLFW/glfw3.h>
 
@@ -80,12 +81,17 @@ namespace vkcv {
 			window->e_key.unlock();
 			window->e_char.unlock();
 			window->e_gamepad.unlock();
-    	}
+		}
+
+		glfwPollEvents();
+
+		// 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));
 
-        glfwPollEvents();
-    	
-    	for (int gamepadIndex = GLFW_JOYSTICK_1; gamepadIndex <= GLFW_JOYSTICK_LAST; gamepadIndex++) {
-    		if (glfwJoystickPresent(gamepadIndex)) {
+		for (int gamepadIndex = GLFW_JOYSTICK_1; gamepadIndex <= GLFW_JOYSTICK_LAST; gamepadIndex++) {
+			if (glfwJoystickPresent(gamepadIndex)) {
 				onGamepadEvent(gamepadIndex);
 			}
 		}