diff --git a/.gitignore b/.gitignore
index 7ee4ff1903e902c4715c6e2b0c3e784ed5755aaf..76a0fa8e507371af6821b220a402666e79c340a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,8 @@ cmake-build-release/
 
 # GUI configuration files
 imgui.ini
+
+# Generated source and header files for shaders
+*.hxx
+*.cxx
+
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 33b70018e368ecc3ad019ea33e57485814eb233a..84a1e902ace668fbee40346cf71e3c4c7a519f2e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,8 +1,8 @@
 variables:
   RUN:
     value: "all"
-    description: "The tests that should run. Possible values: ubuntu, win, all."
-  GIT_DEPTH: 1
+    description: "The tests that should run. Possible values: ubuntu, win-msvc, win-mingw, mac, all."
+  GIT_DEPTH: 15
 
 stages:
   - build
@@ -17,7 +17,7 @@ build_ubuntu_gcc:
     - ubuntu-gcc-cached
   variables:
     GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 10m
+  timeout: 15m
   retry: 1
   script:
     - mkdir debug
@@ -34,13 +34,13 @@ build_ubuntu_gcc:
 build_win10_msvc:
   only:
     variables:
-      - $RUN =~ /\bwin.*/i || $RUN =~ /\ball.*/i
+      - $RUN =~ /\bwin-msvc.*/i || $RUN =~ /\ball.*/i
   stage: build
   tags: 
     - win10-msvc-cached
   variables:
     GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 10m
+  timeout: 15m
   retry: 0
   script:
     - cd 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\'
@@ -51,6 +51,42 @@ build_win10_msvc:
     - cmake -DCMAKE_BUILD_TYPE=Debug ..
     - cmake --build .
 
+build_win10_mingw:
+  only:
+    variables:
+      - $RUN =~ /\bwin-mingw.*/i || $RUN =~ /\ball.*/i
+  stage: build
+  tags: 
+    - win10-mingw-cached
+  variables:
+    GIT_SUBMODULE_STRATEGY: recursive
+  timeout: 15m
+  retry: 0
+  script:
+    - mkdir debug
+    - cd debug
+    - cmake --no-warn-unused-cli -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_C_COMPILER:FILEPATH=C:\msys64\mingw64\bin\x86_64-w64-mingw32-gcc.exe -DCMAKE_CXX_COMPILER:FILEPATH=C:\msys64\mingw64\bin\x86_64-w64-mingw32-g++.exe .. -G "Unix Makefiles"
+    - cmake --build . -j 8
+
+build_mac_clang:
+  only:
+    variables:
+      - $RUN =~ /\bmac.*/i || $RUN =~ /\ball.*/i
+  stage: build
+  tags: 
+    - catalina-clang-cached
+  variables:
+    GIT_SUBMODULE_STRATEGY: recursive
+  timeout: 15m
+  retry: 1
+  script:
+    - mkdir debug
+    - cd debug
+    - export LDFLAGS="-L/usr/local/opt/llvm/lib"
+    - export CPPFLAGS="-I/usr/local/opt/llvm/include"
+    - cmake -DCMAKE_C_COMPILER="/usr/local/opt/llvm/bin/clang" -DCMAKE_CXX_COMPILER="/usr/local/opt/llvm/bin/clang++" -DCMAKE_BUILD_TYPE=Debug ..
+    - cmake --build .
+
 deploy_doc_develop:
   only:
     variables:
diff --git a/.gitmodules b/.gitmodules
index cfa32fb462987b3e2f4ffec40caaae37b0ed7285..1e5c26deddea8cb725aae8c84513d1dcd18e4cfb 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -28,3 +28,6 @@
 [submodule "lib/VulkanMemoryAllocator-Hpp"]
 	path = lib/VulkanMemoryAllocator-Hpp
 	url = https://github.com/malte-v/VulkanMemoryAllocator-Hpp.git
+[submodule "modules/upscaling/lib/FidelityFX-FSR"]
+	path = modules/upscaling/lib/FidelityFX-FSR
+	url = https://github.com/GPUOpen-Effects/FidelityFX-FSR.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2ae078a428a8e5e640ed8dc7bcc2f4e58e159c6b..dfafe1cd084d4b324c233d502e301c24a5ee95e1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -33,7 +33,8 @@ set(vkcv_flags ${CMAKE_CXX_FLAGS})
 # enabling warnings in the debug build
 if (vkcv_build_debug)
 	if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
-		set(vkcv_flags ${vkcv_flags} " -Weverything")
+		#set(vkcv_flags ${vkcv_flags} " -Weverything")
+		set(vkcv_flags ${vkcv_flags} " -Wextra -Wall")
 	elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
 		set(vkcv_flags ${vkcv_flags} " -Wextra -Wall -pedantic")
 	else()
diff --git a/config/Libraries.cmake b/config/Libraries.cmake
index b0684091d59b659c712aeacecd91e200351e0117..512669ce85a96f8cc94d8181994cfe458fa8b604 100644
--- a/config/Libraries.cmake
+++ b/config/Libraries.cmake
@@ -3,7 +3,13 @@ set(vkcv_config_lib ${vkcv_config}/lib)
 set(vkcv_lib_path ${PROJECT_SOURCE_DIR}/${vkcv_lib})
 
 if(NOT WIN32)
-	set(vkcv_libraries  stdc++fs)
+	if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
+		set(vkcv_libraries stdc++fs)
+	endif()
+	
+	if (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
+		list(APPEND vkcv_flags -Xpreprocessor)
+	endif()
 	
 	# optimization for loading times
 	list(APPEND vkcv_flags -pthread)
@@ -34,6 +40,9 @@ endif ()
 # fix dependencies for different Linux distros (looking at you Ubuntu)
 include(${vkcv_config_ext}/CheckLibraries.cmake)
 
+# add custom function to include a file like a shader as string
+include(${vkcv_config_ext}/IncludeShader.cmake)
+
 # cleanup of compiler definitions aka preprocessor variables
 if (vkcv_definitions)
     list(REMOVE_DUPLICATES vkcv_definitions)
diff --git a/config/Sources.cmake b/config/Sources.cmake
index 7ae106e2538c66179ab1ed50408551c43b785bc3..41cd0c20f2106dc02700d9b23227f3e6c34a057a 100644
--- a/config/Sources.cmake
+++ b/config/Sources.cmake
@@ -6,6 +6,9 @@ set(vkcv_sources
 
 		${vkcv_include}/vkcv/Core.hpp
 		${vkcv_source}/vkcv/Core.cpp
+		
+		${vkcv_include}/vkcv/File.hpp
+		${vkcv_source}/vkcv/File.cpp
 
 		${vkcv_include}/vkcv/PassConfig.hpp
 		${vkcv_source}/vkcv/PassConfig.cpp
diff --git a/config/ext/IncludeShader.cmake b/config/ext/IncludeShader.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..e67a8716fb32a953c93a3c6624f0d459a025e950
--- /dev/null
+++ b/config/ext/IncludeShader.cmake
@@ -0,0 +1,75 @@
+
+function(include_shader shader include_dir source_dir)
+	if (NOT EXISTS ${shader})
+		message(WARNING "Shader file does not exist: ${shader}")
+	else()
+		get_filename_component(filename ${shader} NAME)
+		file(SIZE ${shader} filesize)
+		
+		set(include_target_file ${include_dir}/${filename}.hxx)
+		set(source_target_file ${source_dir}/${filename}.cxx)
+		
+		if ((EXISTS ${source_target_file}) AND (EXISTS ${include_target_file}))
+			file(TIMESTAMP ${shader} shader_timestamp "%Y-%m-%dT%H:%M:%S")
+			file(TIMESTAMP ${source_target_file} source_timestamp "%Y-%m-%dT%H:%M:%S")
+			
+			string(COMPARE GREATER ${shader_timestamp} ${source_timestamp} shader_update)
+		else()
+			set(shader_update true)
+		endif()
+		
+		if (shader_update)
+			string(TOUPPER ${filename} varname)
+			string(REPLACE "." "_" varname ${varname})
+			
+			set(shader_header "#pragma once\n")
+			string(APPEND shader_header "// This file is auto-generated via cmake, so don't touch it!\n")
+			string(APPEND shader_header "extern unsigned char ${varname} [${filesize}]\;\n")
+			string(APPEND shader_header "extern unsigned int ${varname}_LEN\;\n")
+			string(APPEND shader_header "const std::string ${varname}_SHADER (reinterpret_cast<const char*>(${varname}), ${varname}_LEN)\;")
+			
+			file(WRITE ${include_target_file} ${shader_header})
+			
+			find_program(xxd_program "xxd")
+			
+			if (EXISTS ${xxd_program})
+				get_filename_component(shader_directory ${shader} DIRECTORY)
+				
+				add_custom_command(
+						OUTPUT ${source_target_file}
+						WORKING_DIRECTORY "${shader_directory}"
+						COMMAND xxd -i -C "${filename}" "${source_target_file}"
+						COMMENT "Processing shader into source files: ${shader}"
+				)
+			else()
+				set(shader_source "// This file is auto-generated via cmake, so don't touch it!\n")
+				string(APPEND shader_source "unsigned char ${varname}[] = {")
+				
+				math(EXPR max_fileoffset "${filesize} - 1" OUTPUT_FORMAT DECIMAL)
+				
+				message(STATUS "Processing shader into source files: ${shader}")
+				
+				foreach(fileoffset RANGE ${max_fileoffset})
+					file(READ ${shader} shader_source_byte OFFSET ${fileoffset} LIMIT 1 HEX)
+					
+					math(EXPR offset_modulo "${fileoffset} % 12" OUTPUT_FORMAT DECIMAL)
+					
+					if (${offset_modulo} EQUAL 0)
+						string(APPEND shader_source "\n  ")
+					endif()
+					
+					if (${fileoffset} LESS ${max_fileoffset})
+						string(APPEND shader_source "0x${shader_source_byte}, ")
+					else()
+						string(APPEND shader_source "0x${shader_source_byte}\n")
+					endif()
+				endforeach()
+				
+				string(APPEND shader_source "}\;\n")
+				string(APPEND shader_source "unsigned int ${varname}_LEN = ${filesize}\;")
+				
+				file(WRITE ${source_target_file} ${shader_source})
+			endif()
+		endif()
+	endif()
+endfunction()
diff --git a/config/lib/vma/vma.cpp b/config/lib/vma/vma.cpp
index 0928b552c10e23914054c44b8de43df722aa2cf0..307c27f096bd1bae2b1deb2ca5994f132adc92cc 100644
--- a/config/lib/vma/vma.cpp
+++ b/config/lib/vma/vma.cpp
@@ -3,5 +3,56 @@
 #define _DEBUG
 #endif
 
+#ifndef _MSVC_LANG
+#ifdef __MINGW32__
+#include <stdint.h>
+#include <stdlib.h>
+
+class VmaMutex {
+public:
+	VmaMutex() : m_locked(false) {}
+	
+	void Lock() {
+		while (m_locked);
+		m_locked = true;
+	}
+	
+	void Unlock() {
+		m_locked = false;
+	}
+private:
+	bool m_locked;
+};
+
+#define VMA_MUTEX VmaMutex
+
+template <typename T>
+T* custom_overestimate_malloc(size_t size) {
+	return new T[size + (sizeof(T) - 1) / sizeof(T)];
+}
+
+void* custom_aligned_malloc(size_t alignment, size_t size) {
+	if (alignment > 4) {
+		return custom_overestimate_malloc<uint64_t>(size);
+	} else
+	if (alignment > 2) {
+		return custom_overestimate_malloc<uint32_t>(size);
+	} else
+	if (alignment > 1) {
+		return custom_overestimate_malloc<uint16_t>(size);
+	} else {
+		return custom_overestimate_malloc<uint8_t>(size);
+	}
+}
+
+void custom_free(void *ptr) {
+	delete[] reinterpret_cast<uint8_t*>(ptr);
+}
+
+#define VMA_SYSTEM_ALIGNED_MALLOC(size, alignment) (custom_aligned_malloc(alignment, size))
+#define VMA_SYSTEM_FREE(ptr) (custom_free(ptr))
+#endif
+#endif
+
 #define VMA_IMPLEMENTATION
 #include "vk_mem_alloc.hpp"
diff --git a/include/vkcv/Context.hpp b/include/vkcv/Context.hpp
index 2ecd9203701510837f49d10c1879efd4890145e9..824713fd1e29cbb8b7e60b22768c0019daaa9938 100644
--- a/include/vkcv/Context.hpp
+++ b/include/vkcv/Context.hpp
@@ -4,6 +4,7 @@
 #include <vk_mem_alloc.hpp>
 
 #include "QueueManager.hpp"
+#include "DrawcallRecording.hpp"
 
 namespace vkcv
 {
@@ -39,9 +40,9 @@ namespace vkcv
         
         static Context create(const char *applicationName,
 							  uint32_t applicationVersion,
-							  std::vector<vk::QueueFlagBits> queueFlags,
-							  std::vector<const char *> instanceExtensions,
-							  std::vector<const char *> deviceExtensions);
+							  const std::vector<vk::QueueFlagBits>& queueFlags,
+							  const std::vector<const char *>& instanceExtensions,
+							  const std::vector<const char *>& deviceExtensions);
 
     private:
         /**
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index 5db834cbb9976d856594263887847d71d42ce264..66a50e5640783048a41f5ca18dbdffc6f5be679b 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -138,9 +138,9 @@ namespace vkcv
         static Core create(Window &window,
                            const char *applicationName,
                            uint32_t applicationVersion,
-                           std::vector<vk::QueueFlagBits> queueFlags    = {},
-                           std::vector<const char*> instanceExtensions  = {},
-                           std::vector<const char*> deviceExtensions    = {});
+                           const std::vector<vk::QueueFlagBits>& queueFlags    = {},
+                           const std::vector<const char*>& instanceExtensions  = {},
+                           const std::vector<const char*>& deviceExtensions    = {});
 
         /**
          * Creates a basic vulkan graphics pipeline using @p config from the pipeline config class and returns it using the @p handle.
@@ -163,7 +163,7 @@ namespace vkcv
          */
         [[nodiscard]]
         PipelineHandle createComputePipeline(
-            const ShaderProgram &config, 
+            const ShaderProgram &shaderProgram,
             const std::vector<vk::DescriptorSetLayout> &descriptorSetLayouts);
 
         /**
@@ -196,11 +196,13 @@ namespace vkcv
          * @param minFilter Minimizing filter
          * @param mipmapMode Mipmapping filter
          * @param addressMode Address mode
+         * @param mipLodBias Mip level of detail bias
          * @return Sampler handle
          */
         [[nodiscard]]
         SamplerHandle createSampler(SamplerFilterType magFilter, SamplerFilterType minFilter,
-									SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode);
+									SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode,
+									float mipLodBias = 0.0f);
 
         /**
          * Creates an #Image with a given format, width, height and depth.
@@ -223,9 +225,13 @@ namespace vkcv
 			Multisampling   multisampling = Multisampling::None);
 
         [[nodiscard]]
-        uint32_t getImageWidth(ImageHandle imageHandle);
+        uint32_t getImageWidth(const ImageHandle& image);
+        
         [[nodiscard]]
-        uint32_t getImageHeight(ImageHandle imageHandle);
+        uint32_t getImageHeight(const ImageHandle& image);
+	
+		[[nodiscard]]
+		vk::Format getImageFormat(const ImageHandle& image);
 
         /** TODO:
          *   @param setDescriptions
@@ -243,13 +249,21 @@ namespace vkcv
 		bool beginFrame(uint32_t& width, uint32_t& height);
 
 		void recordDrawcallsToCmdStream(
-            const CommandStreamHandle       cmdStreamHandle,
+			const CommandStreamHandle       cmdStreamHandle,
 			const PassHandle                renderpassHandle, 
 			const PipelineHandle            pipelineHandle,
 			const PushConstants             &pushConstants,
 			const std::vector<DrawcallInfo> &drawcalls,
 			const std::vector<ImageHandle>  &renderTargets);
 
+		void recordMeshShaderDrawcalls(
+			const CommandStreamHandle               cmdStreamHandle,
+			const PassHandle                        renderpassHandle,
+			const PipelineHandle                    pipelineHandle,
+			const PushConstants&                    pushConstantData,
+            const std::vector<MeshShaderDrawcall>&  drawcalls,
+			const std::vector<ImageHandle>&         renderTargets);
+
 		void recordComputeDispatchToCmdStream(
 			CommandStreamHandle cmdStream,
 			PipelineHandle computePipeline,
@@ -283,16 +297,22 @@ namespace vkcv
 			const RecordCommandFunction &record,
 			const FinishCommandFunction &finish);
 
-		void submitCommandStream(const CommandStreamHandle handle);
-		void prepareSwapchainImageForPresent(const CommandStreamHandle handle);
-		void prepareImageForSampling(const CommandStreamHandle cmdStream, const ImageHandle image);
-		void prepareImageForStorage(const CommandStreamHandle cmdStream, const ImageHandle image);
-		void recordImageMemoryBarrier(const CommandStreamHandle cmdStream, const ImageHandle image);
-		void recordBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer);
-        void readBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer, void *data);
-		void resolveMSAAImage(CommandStreamHandle cmdStream, ImageHandle src, ImageHandle dst);
+		void submitCommandStream(const CommandStreamHandle& handle);
+		void prepareSwapchainImageForPresent(const CommandStreamHandle& handle);
+		void prepareImageForSampling(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void prepareImageForStorage(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void recordImageMemoryBarrier(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void recordBufferMemoryBarrier(const CommandStreamHandle& cmdStream, const BufferHandle& buffer);
+		void readBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer, void *data);
+		void resolveMSAAImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst);
 
+		[[nodiscard]]
 		vk::ImageView getSwapchainImageView() const;
+	
+		void recordMemoryBarrier(const CommandStreamHandle& cmdStream);
+		
+		void recordBlitImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst,
+							 SamplerFilterType filterType);
 		
     };
 }
diff --git a/include/vkcv/DescriptorConfig.hpp b/include/vkcv/DescriptorConfig.hpp
index 776322e6270431f9fa52fd7c3cb4551e5b4bf752..767492eb2b27bd8dff56ef2aeb4769c08eed7200 100644
--- a/include/vkcv/DescriptorConfig.hpp
+++ b/include/vkcv/DescriptorConfig.hpp
@@ -23,7 +23,9 @@ namespace vkcv
         STORAGE_BUFFER,
         SAMPLER,
         IMAGE_SAMPLED,
-		IMAGE_STORAGE
+		IMAGE_STORAGE,
+        UNIFORM_BUFFER_DYNAMIC,
+        STORAGE_BUFFER_DYNAMIC
     };    
     
     /*
diff --git a/include/vkcv/DescriptorWrites.hpp b/include/vkcv/DescriptorWrites.hpp
index f28a6c91e189b13413ffefec0f05e5a0a358ee26..28de2ed7fa6b7e71bfa49b67a337f80f2e05ddcf 100644
--- a/include/vkcv/DescriptorWrites.hpp
+++ b/include/vkcv/DescriptorWrites.hpp
@@ -20,16 +20,15 @@ namespace vkcv {
 		uint32_t	mipLevel;
 	};
 
-	struct UniformBufferDescriptorWrite {
-		inline UniformBufferDescriptorWrite(uint32_t binding, BufferHandle buffer) : binding(binding), buffer(buffer) {};
-		uint32_t		binding;
-		BufferHandle	buffer;
-	};
-
-	struct StorageBufferDescriptorWrite {
-		inline StorageBufferDescriptorWrite(uint32_t binding, BufferHandle buffer) : binding(binding), buffer(buffer) {};
+	struct BufferDescriptorWrite {
+		inline BufferDescriptorWrite(uint32_t binding, BufferHandle buffer, bool dynamic = false,
+									 uint32_t offset = 0, uint32_t size = 0) :
+		binding(binding), buffer(buffer), dynamic(dynamic), offset(offset), size(size) {};
 		uint32_t		binding;
 		BufferHandle	buffer;
+		bool 			dynamic;
+		uint32_t 		offset;
+		uint32_t 		size;
 	};
 
 	struct SamplerDescriptorWrite {
@@ -41,8 +40,8 @@ namespace vkcv {
 	struct DescriptorWrites {
 		std::vector<SampledImageDescriptorWrite>		sampledImageWrites;
 		std::vector<StorageImageDescriptorWrite>		storageImageWrites;
-		std::vector<UniformBufferDescriptorWrite>	    uniformBufferWrites;
-		std::vector<StorageBufferDescriptorWrite>	    storageBufferWrites;
+		std::vector<BufferDescriptorWrite>	    		uniformBufferWrites;
+		std::vector<BufferDescriptorWrite>	    		storageBufferWrites;
 		std::vector<SamplerDescriptorWrite>			    samplerWrites;
 	};
 }
\ No newline at end of file
diff --git a/include/vkcv/DrawcallRecording.hpp b/include/vkcv/DrawcallRecording.hpp
index 572fc2b6b51735bdcd7eb77c1dd9d4a3482a1640..260fbbc6a2a577d0d333656a1eff4f7f3f88cd69 100644
--- a/include/vkcv/DrawcallRecording.hpp
+++ b/include/vkcv/DrawcallRecording.hpp
@@ -13,21 +13,30 @@ namespace vkcv {
         vk::Buffer      buffer;
     };
 
+    enum class IndexBitCount{
+        Bit16,
+        Bit32
+    };
+
     struct DescriptorSetUsage {
-        inline DescriptorSetUsage(uint32_t setLocation, vk::DescriptorSet vulkanHandle) noexcept
-            : setLocation(setLocation), vulkanHandle(vulkanHandle) {}
+        inline DescriptorSetUsage(uint32_t setLocation, vk::DescriptorSet vulkanHandle,
+								  const std::vector<uint32_t>& dynamicOffsets = {}) noexcept
+            : setLocation(setLocation), vulkanHandle(vulkanHandle), dynamicOffsets(dynamicOffsets) {}
 
-        const uint32_t          setLocation;
-        const vk::DescriptorSet vulkanHandle;
+        const uint32_t          	setLocation;
+        const vk::DescriptorSet 	vulkanHandle;
+        const std::vector<uint32_t> dynamicOffsets;
     };
 
     struct Mesh {
-        inline Mesh(std::vector<VertexBufferBinding> vertexBufferBindings, vk::Buffer indexBuffer, size_t indexCount) noexcept
-            : vertexBufferBindings(vertexBufferBindings), indexBuffer(indexBuffer), indexCount(indexCount){}
+        inline Mesh(std::vector<VertexBufferBinding> vertexBufferBindings, vk::Buffer indexBuffer, size_t indexCount, IndexBitCount indexBitCount = IndexBitCount::Bit16) noexcept
+            : vertexBufferBindings(vertexBufferBindings), indexBuffer(indexBuffer), indexCount(indexCount), indexBitCount(indexBitCount){}
 
         std::vector<VertexBufferBinding>    vertexBufferBindings;
         vk::Buffer                          indexBuffer;
         size_t                              indexCount;
+        IndexBitCount                       indexBitCount;
+
     };
 
     struct DrawcallInfo {
@@ -46,4 +55,21 @@ namespace vkcv {
         const PushConstants     &pushConstants,
         const size_t            drawcallIndex);
 
-}
\ No newline at end of file
+    void InitMeshShaderDrawFunctions(vk::Device device);
+
+    struct MeshShaderDrawcall {
+        inline MeshShaderDrawcall(const std::vector<DescriptorSetUsage> descriptorSets, uint32_t taskCount)
+            : descriptorSets(descriptorSets), taskCount(taskCount) {}
+
+        std::vector<DescriptorSetUsage> descriptorSets;
+        uint32_t                        taskCount;
+    };
+
+    void recordMeshShaderDrawcall(
+        vk::CommandBuffer                       cmdBuffer,
+        vk::PipelineLayout                      pipelineLayout,
+        const PushConstants&                 pushConstantData,
+        const uint32_t                          pushConstantOffset,
+        const MeshShaderDrawcall&               drawcall,
+        const uint32_t                          firstTask);
+}
diff --git a/include/vkcv/Event.hpp b/include/vkcv/Event.hpp
index da5cbc72fbb3eee3a71a35c1da6fe32dff06b057..604e3a444dc3bffd2841cb69cd99746d59af523d 100644
--- a/include/vkcv/Event.hpp
+++ b/include/vkcv/Event.hpp
@@ -1,7 +1,12 @@
 #pragma once
 
 #include <functional>
+
+#ifndef __MINGW32__
 #include <mutex>
+#endif
+
+#include <vector>
 
 namespace vkcv {
 	
@@ -27,7 +32,10 @@ namespace vkcv {
     private:
         std::vector< event_function<T...> > m_functions;
         uint32_t m_id_counter;
+	
+#ifndef __MINGW32__
 		std::mutex m_mutex;
+#endif
 
     public:
 
@@ -75,14 +83,18 @@ namespace vkcv {
          * locks the event so its function handles won't be called
          */
         void lock() {
+#ifndef __MINGW32__
 			m_mutex.lock();
+#endif
         }
 	
 		/**
 		* unlocks the event so its function handles can be called after locking
 		*/
         void unlock() {
+#ifndef __MINGW32__
 			m_mutex.unlock();
+#endif
         }
 
         explicit event(bool locked = false) {
diff --git a/include/vkcv/File.hpp b/include/vkcv/File.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..06f1c48593853147140b2c8c68c675d52c9dfaec
--- /dev/null
+++ b/include/vkcv/File.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <filesystem>
+
+namespace vkcv {
+	
+	std::filesystem::path generateTemporaryFilePath();
+	
+	std::filesystem::path generateTemporaryDirectoryPath();
+	
+}
diff --git a/include/vkcv/QueueManager.hpp b/include/vkcv/QueueManager.hpp
index ac043b2d351014ea79fcae0d0fc439bb64a87b72..0919d20d8e07fee67ceb2f393c29b4a53c51b857 100644
--- a/include/vkcv/QueueManager.hpp
+++ b/include/vkcv/QueueManager.hpp
@@ -32,8 +32,8 @@ namespace vkcv {
         const std::vector<Queue> &getTransferQueues() const;
 
         static void queueCreateInfosQueueHandles(vk::PhysicalDevice &physicalDevice,
-                std::vector<float> &queuePriorities,
-                std::vector<vk::QueueFlagBits> &queueFlags,
+                const std::vector<float> &queuePriorities,
+                const std::vector<vk::QueueFlagBits> &queueFlags,
                 std::vector<vk::DeviceQueueCreateInfo> &queueCreateInfos,
                 std::vector<std::pair<int, int>> &queuePairsGraphics,
                 std::vector<std::pair<int, int>> &queuePairsCompute,
diff --git a/include/vkcv/ShaderStage.hpp b/include/vkcv/ShaderStage.hpp
index dca395bdba82a2f1cb38bb0a25196cfd3dab8019..3893bdf5f73408847ceb2b076abfb7d0902bb2f9 100644
--- a/include/vkcv/ShaderStage.hpp
+++ b/include/vkcv/ShaderStage.hpp
@@ -9,7 +9,11 @@ namespace vkcv {
 		TESS_EVAL,
 		GEOMETRY,
 		FRAGMENT,
-		COMPUTE
+		COMPUTE,
+		TASK,
+		MESH
 	};
 
+
+
 }
diff --git a/lib/VulkanMemoryAllocator-Hpp b/lib/VulkanMemoryAllocator-Hpp
index eae6e8d3bd4593e0d7071c85fba2a8f33fbe5dab..3a61240a5354ce56c222969a69825aabb6ba0a21 160000
--- a/lib/VulkanMemoryAllocator-Hpp
+++ b/lib/VulkanMemoryAllocator-Hpp
@@ -1 +1 @@
-Subproject commit eae6e8d3bd4593e0d7071c85fba2a8f33fbe5dab
+Subproject commit 3a61240a5354ce56c222969a69825aabb6ba0a21
diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt
index 802200ad5deb76decbb75e30e1fbd14bff3b7e3b..4b576e7119ebe769eafd1b6abb033b4fb02a3ec1 100644
--- a/modules/CMakeLists.txt
+++ b/modules/CMakeLists.txt
@@ -4,6 +4,8 @@ add_subdirectory(asset_loader)
 add_subdirectory(camera)
 add_subdirectory(gui)
 add_subdirectory(material)
+add_subdirectory(meshlet)
 add_subdirectory(scene)
 add_subdirectory(shader_compiler)
 add_subdirectory(testing)
+add_subdirectory(upscaling)
diff --git a/modules/camera/config/GLM.cmake b/modules/camera/config/GLM.cmake
index efd6444451100b912aa0b5b4a532dc8f448b0b40..f256ccade8c4f44a89744bb7875371324cf2369d 100644
--- a/modules/camera/config/GLM.cmake
+++ b/modules/camera/config/GLM.cmake
@@ -4,18 +4,20 @@ find_package(glm QUIET)
 if (glm_FOUND)
     list(APPEND vkcv_camera_includes ${GLM_INCLUDE_DIRS})
     list(APPEND vkcv_camera_libraries glm)
-
-    list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
-    list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
 else()
     if (EXISTS "${vkcv_camera_lib_path}/glm")
         add_subdirectory(${vkcv_camera_lib}/glm)
-        
+
+        list(APPEND vkcv_camera_includes ${vkcv_camera_lib_path}/glm)
         list(APPEND vkcv_camera_libraries glm)
-        
-        list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
-        list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
     else()
         message(WARNING "GLM is required..! Update the submodules!")
     endif ()
 endif ()
+
+list(APPEND vkcv_camera_definitions GLM_DEPTH_ZERO_TO_ONE)
+list(APPEND vkcv_camera_definitions GLM_FORCE_LEFT_HANDED)
+
+if ((WIN32) AND (${CMAKE_SIZEOF_VOID_P} MATCHES 4))
+    list(APPEND vkcv_camera_definitions GLM_ENABLE_EXPERIMENTAL)
+endif()
diff --git a/modules/meshlet/CMakeLists.txt b/modules/meshlet/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d576466d3d125d3a19640088a9b5725ac7a46b97
--- /dev/null
+++ b/modules/meshlet/CMakeLists.txt
@@ -0,0 +1,36 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_meshlet)
+
+# setting c++ standard for the module
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_meshlet_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_meshlet_include ${PROJECT_SOURCE_DIR}/include)
+
+# Add source and header files to the module
+set(vkcv_meshlet_sources
+		${vkcv_meshlet_include}/vkcv/meshlet/Meshlet.hpp
+		${vkcv_meshlet_source}/vkcv/meshlet/Meshlet.cpp
+
+		${vkcv_meshlet_include}/vkcv/meshlet/Tipsify.hpp
+		${vkcv_meshlet_source}/vkcv/meshlet/Tipsify.cpp
+
+		${vkcv_meshlet_include}/vkcv/meshlet/Forsyth.hpp
+		${vkcv_meshlet_source}/vkcv/meshlet/Forsyth.cpp)
+
+# adding source files to the module
+add_library(vkcv_meshlet STATIC ${vkcv_meshlet_sources})
+
+
+# link the required libraries to the module
+target_link_libraries(vkcv_meshlet vkcv ${vkcv_libraries})
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_meshlet SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_meshlet BEFORE PUBLIC ${vkcv_meshlet_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(vkcv_meshlet vkcv vkcv_asset_loader vkcv_camera)
diff --git a/modules/meshlet/include/vkcv/meshlet/Forsyth.hpp b/modules/meshlet/include/vkcv/meshlet/Forsyth.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..43dc9a3b6bb81ea915268de7a7b53b18efd27638
--- /dev/null
+++ b/modules/meshlet/include/vkcv/meshlet/Forsyth.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "Meshlet.hpp"
+
+namespace vkcv::meshlet
+{
+ /**
+  * Reorders the index buffer, simulating a LRU cache, so that vertices are grouped together in close triangle patches
+  * @param idxBuf current IndexBuffer
+  * @param vertexCount of the mesh
+  * @return new reordered index buffer to replace the input index buffer
+  * References:
+  * https://tomforsyth1000.github.io/papers/fast_vert_cache_opt.html
+  * https://www.martin.st/thesis/efficient_triangle_reordering.pdf
+  * https://github.com/vivkin/forsyth/blob/master/forsyth.h
+  */
+ VertexCacheReorderResult forsythReorder(const std::vector<uint32_t> &idxBuf, const size_t vertexCount);
+}
diff --git a/modules/meshlet/include/vkcv/meshlet/Meshlet.hpp b/modules/meshlet/include/vkcv/meshlet/Meshlet.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9900dffaf28c85753d367ba79bbdf5c19a2cf479
--- /dev/null
+++ b/modules/meshlet/include/vkcv/meshlet/Meshlet.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#include <vector>
+#include <map>
+#include <glm/glm.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+
+namespace vkcv::meshlet {
+
+    struct Vertex {
+        glm::vec3   position;
+        float       padding0;
+        glm::vec3   normal;
+        float       padding1;
+    };
+
+    struct Meshlet {
+        uint32_t    vertexOffset;
+        uint32_t    vertexCount;
+        uint32_t    indexOffset;
+        uint32_t    indexCount;
+        glm::vec3   meanPosition;
+        float       boundingSphereRadius;
+    };
+
+    struct VertexCacheReorderResult {
+        /**
+         * @param indexBuffer new indexBuffer
+         * @param skippedIndices indices that have a spacial break
+         */
+        VertexCacheReorderResult(const std::vector<uint32_t> indexBuffer, const std::vector<uint32_t> skippedIndices)
+                :indexBuffer(indexBuffer), skippedIndices(skippedIndices) {}
+
+        std::vector<uint32_t> indexBuffer;
+        std::vector<uint32_t>  skippedIndices;
+    };
+
+    struct MeshShaderModelData {
+        std::vector<Vertex>     vertices;
+        std::vector<uint32_t>   localIndices;
+        std::vector<Meshlet>    meshlets;
+    };
+
+    std::vector<Vertex> convertToVertices(
+            const std::vector<uint8_t>&         vertexData,
+            const uint64_t                      vertexCount,
+            const vkcv::asset::VertexAttribute& positionAttribute,
+            const vkcv::asset::VertexAttribute& normalAttribute);
+
+    MeshShaderModelData createMeshShaderModelData(
+            const std::vector<Vertex>&      inVertices,
+            const std::vector<uint32_t>&    inIndices,
+            const std::vector<uint32_t>& deadEndIndices = {});
+
+    std::vector<uint32_t> assetLoaderIndicesTo32BitIndices(
+            const std::vector<uint8_t>& indexData,
+            vkcv::asset::IndexType indexType);
+
+}
\ No newline at end of file
diff --git a/modules/meshlet/include/vkcv/meshlet/Tipsify.hpp b/modules/meshlet/include/vkcv/meshlet/Tipsify.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6fb4b37d9c17c82642c3b5e7667c3e8acc50b8c0
--- /dev/null
+++ b/modules/meshlet/include/vkcv/meshlet/Tipsify.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "Meshlet.hpp"
+#include <algorithm>
+#include <iostream>
+
+namespace vkcv::meshlet {
+    /**
+     * reorders the IndexBuffer, so all usages of vertices to triangle are as close as possible
+     * @param indexBuffer32Bit current IndexBuffer
+     * @param vertexCount of the mesh
+     * @param cacheSize of the priority cache <br>
+     * Recommended: 20. Keep the value between 5 and 50 <br>
+     * low:         more random and patchy<br>
+     * high:        closer vertices have higher chance -> leads to sinuous lines
+     * @return new IndexBuffer that replaces the input IndexBuffer, and the indices that are skipped
+     *
+     * https://gfx.cs.princeton.edu/pubs/Sander_2007_%3ETR/tipsy.pdf
+     * https://www.martin.st/thesis/efficient_triangle_reordering.pdf
+     */
+    VertexCacheReorderResult tipsifyMesh(const std::vector<uint32_t> &indexBuffer32Bit,
+                                         const int vertexCount, const unsigned int cacheSize = 20);
+}
\ No newline at end of file
diff --git a/modules/meshlet/src/vkcv/meshlet/Forsyth.cpp b/modules/meshlet/src/vkcv/meshlet/Forsyth.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fd0f160d65b8db81102f9eb6a9d60cf735999d44
--- /dev/null
+++ b/modules/meshlet/src/vkcv/meshlet/Forsyth.cpp
@@ -0,0 +1,317 @@
+#include "vkcv/meshlet/Forsyth.hpp"
+#include <vkcv/Logger.hpp>
+#include <array>
+#include <cmath>
+
+namespace vkcv::meshlet
+{
+    /*
+     * CACHE AND VALENCE
+     * SIZE AND SCORE CONSTANTS
+     * CHANGE AS NEEDED
+     */
+
+    // set these to adjust performance and result quality
+    const size_t VERTEX_CACHE_SIZE = 8;
+    const size_t CACHE_FUNCTION_LENGTH = 32;
+
+    // score function constants
+    const float CACHE_DECAY_POWER = 1.5f;
+    const float LAST_TRI_SCORE = 0.75f;
+
+    const float VALENCE_BOOST_SCALE = 2.0f;
+    const float VALENCE_BOOST_POWER = 0.5f;
+
+    // sizes for precalculated tables
+    // make sure that cache score is always >= vertex_cache_size
+    const size_t CACHE_SCORE_TABLE_SIZE = 32;
+    const size_t VALENCE_SCORE_TABLE_SIZE = 32;
+
+    // precalculated tables
+    std::array<float, CACHE_SCORE_TABLE_SIZE> cachePositionScore = {};
+    std::array<float, VALENCE_SCORE_TABLE_SIZE> valenceScore = {};
+
+    // function to populate the cache position and valence score tables
+    void initScoreTables()
+    {
+        for(size_t i = 0; i < CACHE_SCORE_TABLE_SIZE; i++)
+        {
+            float score = 0.0f;
+            if (i < 3)
+            {
+                score = LAST_TRI_SCORE;
+            }
+            else
+            {
+                const float scaler = 1.0f / static_cast<float>(CACHE_FUNCTION_LENGTH - 3);
+                score = 1.0f - (i - 3) * scaler;
+                score = std::pow(score, CACHE_DECAY_POWER);
+            }
+            cachePositionScore[i] = score;
+        }
+
+        for(size_t i = 0; i < VALENCE_SCORE_TABLE_SIZE; i++)
+        {
+            const float valenceBoost = std::pow(i, -VALENCE_BOOST_POWER);
+            const float score = VALENCE_BOOST_SCALE * valenceBoost;
+
+            valenceScore[i] = score;
+        }
+    }
+
+    /**
+     * Return the vertex' score, depending on its current active triangle count and cache position
+     * Add a valence boost to score, if active triangles are below VALENCE_SCORE_TABLE_SIZE
+     * @param numActiveTris the active triangles on this vertex
+     * @param cachePos the vertex' position in the cache
+     * @return vertex' score
+     */
+    float findVertexScore(uint32_t numActiveTris, int32_t cachePos)
+    {
+        if(numActiveTris == 0)
+            return 0.0f;
+
+        float score = 0.0f;
+
+        if (cachePos >= 0)
+            score = cachePositionScore[cachePos];
+
+        if (numActiveTris < VALENCE_SCORE_TABLE_SIZE)
+            score += valenceScore[numActiveTris];
+
+        return score;
+    }
+
+    VertexCacheReorderResult forsythReorder(const std::vector<uint32_t> &idxBuf, const size_t vertexCount)
+    {
+        std::vector<uint32_t> skippedIndices;
+
+        initScoreTables();
+
+        // get the total triangle count from the index buffer
+        const size_t triangleCount = idxBuf.size() / 3;
+
+        // per-vertex active triangle count
+        std::vector<uint8_t> numActiveTris(vertexCount, 0);
+        // iterate over indices, count total occurrences of each vertex
+        for(const auto index : idxBuf)
+        {
+            if(numActiveTris[index] == UINT8_MAX)
+            {
+                vkcv_log(LogLevel::ERROR, "Unsupported mesh.");
+                vkcv_log(LogLevel::ERROR, "Vertex shared by too many triangles.");
+                return VertexCacheReorderResult({}, {});
+            }
+
+            numActiveTris[index]++;
+        }
+
+
+        // allocate remaining vectors
+        /**
+         * offsets: contains the vertices' offset into the triangleIndices vector
+         * Offset itself is the sum of triangles required by the previous vertices
+         *
+         * lastScore: the vertices' most recent calculated score
+         *
+         * cacheTag: the vertices' most recent cache score
+         *
+         * triangleAdded: boolean flags to denote whether a triangle has been processed or not
+         *
+         * triangleScore: total score of the three vertices making up the triangle
+         *
+         * triangleIndices: indices for the triangles
+         */
+        std::vector<uint32_t> offsets(vertexCount, 0);
+        std::vector<float> lastScore(vertexCount, 0.0f);
+        std::vector<int8_t> cacheTag(vertexCount, -1);
+
+        std::vector<bool> triangleAdded(triangleCount, false);
+        std::vector<float> triangleScore(triangleCount, 0.0f);
+
+        std::vector<int32_t> triangleIndices(idxBuf.size(), 0);
+
+
+        // sum the number of active triangles for all previous vertices
+        // null the number of active triangles afterwards for recalculation in second loop
+        uint32_t sum = 0;
+        for(size_t i = 0; i < vertexCount; i++)
+        {
+            offsets[i] = sum;
+            sum += numActiveTris[i];
+            numActiveTris[i] = 0;
+        }
+        // create the triangle indices, using the newly calculated offsets, and increment numActiveTris
+        // every vertex should be referenced by a triangle index now
+        for(size_t i = 0; i < triangleCount; i++)
+        {
+            for(size_t j = 0; j < 3; j++)
+            {
+                uint32_t v = idxBuf[3 * i + j];
+                triangleIndices[offsets[v] + numActiveTris[v]] = static_cast<int32_t>(i);
+                numActiveTris[v]++;
+            }
+        }
+
+        // calculate and initialize the triangle score, by summing the vertices' score
+        for (size_t i = 0; i < vertexCount; i++)
+        {
+            lastScore[i] = findVertexScore(numActiveTris[i], static_cast<int32_t>(cacheTag[i]));
+
+            for(size_t j = 0; j < numActiveTris[i]; j++)
+            {
+                triangleScore[triangleIndices[offsets[i] + j]] += lastScore[i];
+            }
+        }
+
+        // find best triangle to start reordering with
+        int32_t bestTriangle = -1;
+        float   bestScore    = -1.0f;
+        for(size_t i = 0; i < triangleCount; i++)
+        {
+            if(triangleScore[i] > bestScore)
+            {
+                bestScore = triangleScore[i];
+                bestTriangle = static_cast<int32_t>(i);
+            }
+        }
+
+        // allocate output triangles
+        std::vector<int32_t> outTriangles(triangleCount, 0);
+        uint32_t outPos = 0;
+
+        // initialize cache (with -1)
+        std::array<int32_t, VERTEX_CACHE_SIZE + 3> cache = {};
+        for(auto &element : cache)
+        {
+            element = -1;
+        }
+
+        uint32_t scanPos = 0;
+
+        // begin reordering routine
+        // output the currently best triangle, as long as there are triangles left to output
+        while(bestTriangle >= 0)
+        {
+            // mark best triangle as added
+            triangleAdded[bestTriangle] = true;
+            // output this triangle
+            outTriangles[outPos++] = bestTriangle;
+
+            // push best triangle's vertices into the cache
+            for(size_t i = 0; i < 3; i++)
+            {
+                uint32_t v = idxBuf[3 * bestTriangle + i];
+
+                // get vertex' cache position, if its -1, set its position to the end
+                int8_t endPos = cacheTag[v];
+                if(endPos < 0)
+                    endPos = static_cast<int8_t>(VERTEX_CACHE_SIZE + i);
+
+                // shift vertices' cache entries forward by one
+                for(int8_t j = endPos; j > i; j--)
+                {
+                    cache[j] = cache[j - 1];
+
+                    // if cache slot is valid vertex,
+                    // update the vertex cache tag accordingly
+                    if (cache[j] >= 0)
+                        cacheTag[cache[j]]++;
+                }
+
+                // insert current vertex into its new target slot
+                cache[i] = static_cast<int32_t>(v);
+                cacheTag[v] = static_cast<int8_t>(i);
+
+                // find current triangle in the list of active triangles
+                // remove it by moving the last triangle into the slot the current triangle is holding.
+                for (size_t j = 0; j < numActiveTris[v]; j++)
+                {
+                    if(triangleIndices[offsets[v] + j] == bestTriangle)
+                    {
+                        triangleIndices[offsets[v] + j] = triangleIndices[offsets[v] + numActiveTris[v] - 1];
+                        break;
+                    }
+                }
+                // shorten the list
+                numActiveTris[v]--;
+            }
+
+            // update scores of all triangles in cache
+            for (size_t i = 0; i < cache.size(); i++)
+            {
+                int32_t v = cache[i];
+                if (v < 0)
+                    break;
+
+                // this vertex has been pushed outside of the actual cache
+                if(i >= VERTEX_CACHE_SIZE)
+                {
+                    cacheTag[v] = -1;
+                    cache[i] = -1;
+                }
+
+                float newScore = findVertexScore(numActiveTris[v], cacheTag[v]);
+                float diff = newScore - lastScore[v];
+
+                for(size_t j = 0; j < numActiveTris[v]; j++)
+                {
+                    triangleScore[triangleIndices[offsets[v] + j]] += diff;
+                }
+                lastScore[v] = newScore;
+            }
+
+            // find best triangle reference by vertices in cache
+            bestTriangle = -1;
+            bestScore = -1.0f;
+            for(size_t i = 0; i < VERTEX_CACHE_SIZE; i++)
+            {
+                if (cache[i] < 0)
+                    break;
+
+                int32_t v = cache[i];
+                for(size_t j = 0; j < numActiveTris[v]; j++)
+                {
+                    int32_t t = triangleIndices[offsets[v] + j];
+                    if(triangleScore[t] > bestScore)
+                    {
+                        bestTriangle = t;
+                        bestScore = triangleScore[t];
+                    }
+                }
+            }
+
+            // if no triangle was found at all, continue scanning whole list of triangles
+            if (bestTriangle < 0)
+            {
+                for(; scanPos < triangleCount; scanPos++)
+                {
+                    if(!triangleAdded[scanPos])
+                    {
+                        bestTriangle = scanPos;
+
+                        skippedIndices.push_back(3 * outPos);
+
+                        break;
+                    }
+                }
+            }
+        }
+
+
+        // convert triangle index array into full triangle list
+        std::vector<uint32_t> outIndices(idxBuf.size(), 0);
+        outPos = 0;
+        for(size_t i = 0; i < triangleCount; i++)
+        {
+            int32_t t = outTriangles[i];
+            for(size_t j = 0; j < 3; j++)
+            {
+                int32_t v = idxBuf[3 * t + j];
+                outIndices[outPos++] = static_cast<uint32_t>(v);
+            }
+        }
+
+        return VertexCacheReorderResult(outIndices, skippedIndices);
+    }
+}
\ No newline at end of file
diff --git a/modules/meshlet/src/vkcv/meshlet/Meshlet.cpp b/modules/meshlet/src/vkcv/meshlet/Meshlet.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..abcad7207ed5a6f80cb292ab2f7e855d3b4c7797
--- /dev/null
+++ b/modules/meshlet/src/vkcv/meshlet/Meshlet.cpp
@@ -0,0 +1,167 @@
+
+#include "vkcv/meshlet/Meshlet.hpp"
+#include <vkcv/Logger.hpp>
+#include <cassert>
+#include <iostream>
+
+namespace vkcv::meshlet {
+
+std::vector<vkcv::meshlet::Vertex> convertToVertices(
+        const std::vector<uint8_t>&         vertexData,
+        const uint64_t                      vertexCount,
+        const vkcv::asset::VertexAttribute& positionAttribute,
+        const vkcv::asset::VertexAttribute& normalAttribute) {
+
+    assert(positionAttribute.type   == vkcv::asset::PrimitiveType::POSITION);
+    assert(normalAttribute.type     == vkcv::asset::PrimitiveType::NORMAL);
+
+    std::vector<vkcv::meshlet::Vertex> vertices;
+    vertices.reserve(vertexCount);
+
+    const size_t positionStepSize   = positionAttribute.stride == 0 ? sizeof(glm::vec3) : positionAttribute.stride;
+    const size_t normalStepSize     = normalAttribute.stride   == 0 ? sizeof(glm::vec3) : normalAttribute.stride;
+
+    for (int i = 0; i < vertexCount; i++) {
+        Vertex v;
+
+        const size_t positionOffset = positionAttribute.offset  + positionStepSize  * i;
+        const size_t normalOffset   = normalAttribute.offset    + normalStepSize    * i;
+
+        v.position  = *reinterpret_cast<const glm::vec3*>(&(vertexData[positionOffset]));
+        v.normal    = *reinterpret_cast<const glm::vec3*>(&(vertexData[normalOffset]));
+        vertices.push_back(v);
+    }
+    return vertices;
+}
+
+MeshShaderModelData createMeshShaderModelData(
+        const std::vector<Vertex>&      inVertices,
+        const std::vector<uint32_t>&    inIndices,
+        const std::vector<uint32_t>&    deadEndIndices) {
+
+    MeshShaderModelData data;
+    size_t currentIndex = 0;
+
+    const size_t maxVerticesPerMeshlet = 64;
+    const size_t maxIndicesPerMeshlet  = 126 * 3;
+
+    bool indicesAreLeft = true;
+
+    size_t deadEndIndicesIndex = 0;
+
+    while (indicesAreLeft) {
+        Meshlet meshlet;
+
+        meshlet.indexCount  = 0;
+        meshlet.vertexCount = 0;
+
+        meshlet.indexOffset  = data.localIndices.size();
+        meshlet.vertexOffset = data.vertices.size();
+
+        std::map<uint32_t, uint32_t> globalToLocalIndexMap;
+        std::vector<uint32_t> globalIndicesOrdered;
+
+        while (true) {
+
+            if (deadEndIndicesIndex < deadEndIndices.size()) {
+                const uint32_t deadEndIndex = deadEndIndices[deadEndIndicesIndex];
+                if (deadEndIndex == currentIndex) {
+                    deadEndIndicesIndex++;
+                    break;
+                }
+            }
+
+            indicesAreLeft = currentIndex + 1 <= inIndices.size();
+            if (!indicesAreLeft) {
+                break;
+            }
+
+            bool enoughSpaceForIndices = meshlet.indexCount + 3 < maxIndicesPerMeshlet;
+            if (!enoughSpaceForIndices) {
+                break;
+            }
+
+            size_t vertexCountToAdd = 0;
+            for (int i = 0; i < 3; i++) {
+                const uint32_t globalIndex = inIndices[currentIndex + i];
+                const bool containsVertex  = globalToLocalIndexMap.find(globalIndex) != globalToLocalIndexMap.end();
+                if (!containsVertex) {
+                    vertexCountToAdd++;
+                }
+            }
+
+            bool enoughSpaceForVertices = meshlet.vertexCount + vertexCountToAdd < maxVerticesPerMeshlet;
+            if (!enoughSpaceForVertices) {
+                break;
+            }
+
+            for (int i = 0; i < 3; i++) {
+                const uint32_t globalIndex = inIndices[currentIndex + i];
+
+                uint32_t localIndex;
+                const bool indexAlreadyExists = globalToLocalIndexMap.find(globalIndex) != globalToLocalIndexMap.end();
+                if (indexAlreadyExists) {
+                    localIndex = globalToLocalIndexMap[globalIndex];
+                }
+                else {
+                    localIndex = globalToLocalIndexMap.size();
+                    globalToLocalIndexMap[globalIndex] = localIndex;
+                    globalIndicesOrdered.push_back(globalIndex);
+                }
+
+                data.localIndices.push_back(localIndex);
+            }
+
+            meshlet.indexCount  += 3;
+            currentIndex        += 3;
+            meshlet.vertexCount += vertexCountToAdd;
+        }
+
+        for (const uint32_t globalIndex : globalIndicesOrdered) {
+            const Vertex v = inVertices[globalIndex];
+            data.vertices.push_back(v);
+        }
+
+        // compute mean position
+        meshlet.meanPosition = glm::vec3(0);
+        const uint32_t meshletLastVertexIndex = meshlet.vertexOffset + meshlet.vertexCount;
+
+        for (uint32_t vertexIndex = meshlet.vertexOffset; vertexIndex < meshletLastVertexIndex; vertexIndex++) {
+            const Vertex& v         = data.vertices[vertexIndex];
+            meshlet.meanPosition    += v.position;
+        }
+        meshlet.meanPosition /= meshlet.vertexCount;
+
+        // compute bounding sphere radius
+        meshlet.boundingSphereRadius = 0.f;
+        for (uint32_t vertexIndex = meshlet.vertexOffset; vertexIndex < meshletLastVertexIndex; vertexIndex++) {
+            const Vertex& v = data.vertices[vertexIndex];
+            const float d                   = glm::distance(v.position, meshlet.meanPosition);
+            meshlet.boundingSphereRadius    = glm::max(meshlet.boundingSphereRadius, d);
+        }
+
+        data.meshlets.push_back(meshlet);
+    }
+
+    return data;
+}
+
+std::vector<uint32_t> assetLoaderIndicesTo32BitIndices(const std::vector<uint8_t>& indexData, vkcv::asset::IndexType indexType) {
+    std::vector<uint32_t> indices;
+    if (indexType == vkcv::asset::IndexType::UINT16) {
+        for (int i = 0; i < indexData.size(); i += 2) {
+            const uint16_t index16Bit = *reinterpret_cast<const uint16_t *>(&(indexData[i]));
+            const uint32_t index32Bit = static_cast<uint32_t>(index16Bit);
+            indices.push_back(index32Bit);
+        }
+    } else if (indexType == vkcv::asset::IndexType::UINT32) {
+        for (int i = 0; i < indexData.size(); i += 4) {
+            const uint32_t index32Bit = *reinterpret_cast<const uint32_t *>(&(indexData[i]));
+            indices.push_back(index32Bit);
+        }
+    } else {
+        vkcv_log(vkcv::LogLevel::ERROR, "Unsupported index type");
+    }
+    return indices;
+}
+}
\ No newline at end of file
diff --git a/modules/meshlet/src/vkcv/meshlet/Tipsify.cpp b/modules/meshlet/src/vkcv/meshlet/Tipsify.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c5762100bc37eccbe3e4f6b4c94e5f0e580c53c7
--- /dev/null
+++ b/modules/meshlet/src/vkcv/meshlet/Tipsify.cpp
@@ -0,0 +1,288 @@
+
+#include <vkcv/Logger.hpp>
+#include "vkcv/meshlet/Tipsify.hpp"
+#include <iostream>
+
+namespace vkcv::meshlet {
+
+    const int maxUsedVertices           = 128;
+
+    /**
+     * modulo operation with maxUsedVertices
+     * @param number for modulo operation
+     * @return number between 0 and maxUsedVertices - 1
+     */
+    int mod( int number ){
+        return (number + maxUsedVertices) % maxUsedVertices;
+    }
+
+    /**
+     * searches for the next VertexIndex that was used before or returns any vertexIndex if no used was found
+     * @param livingTriangles
+     * @param usedVerticeStack
+     * @param usedVerticeCount
+     * @param usedVerticeOffset
+     * @param vertexCount
+     * @param lowestLivingVertexIndex
+     * @param currentTriangleIndex
+     * @param skippedIndices
+     * @return a VertexIndex to be used as fanningVertexIndex
+     */
+    int skipDeadEnd(
+            const std::vector<uint8_t> &livingTriangles,
+            const std::vector<uint32_t> &usedVerticeStack,
+            int &usedVerticeCount,
+            int &usedVerticeOffset,
+            int vertexCount,
+            int &lowestLivingVertexIndex,
+            int &currentTriangleIndex,
+            std::vector<uint32_t> &skippedIndices) {
+
+        // returns the latest vertex used that has a living triangle
+        while (mod(usedVerticeCount) != usedVerticeOffset) {
+            // iterate from the latest to the oldest. + maxUsedVertices to always make it a positive number in the range 0 to maxUsedVertices -1
+            int nextVertex = usedVerticeStack[mod(--usedVerticeCount)];
+
+            if (livingTriangles[nextVertex] > 0) {
+                return nextVertex;
+            }
+        }
+        // returns any vertexIndex since no last used has a living triangle
+        while (lowestLivingVertexIndex + 1 < vertexCount) {
+            lowestLivingVertexIndex++;
+            if (livingTriangles[lowestLivingVertexIndex] > 0) {
+                // add index of the vertex to skippedIndices
+                skippedIndices.push_back(static_cast<uint32_t>(currentTriangleIndex * 3));
+                return lowestLivingVertexIndex;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * searches for the best next candidate as a fanningVertexIndex
+     * @param vertexCount
+     * @param lowestLivingVertexIndex
+     * @param cacheSize
+     * @param possibleCandidates
+     * @param numPossibleCandidates
+     * @param lastTimestampCache
+     * @param currentTimeStamp
+     * @param livingTriangles
+     * @param usedVerticeStack
+     * @param usedVerticeCount
+     * @param usedVerticeOffset
+     * @param currentTriangleIndex
+     * @param skippedIndices
+     * @return a VertexIndex to be used as fanningVertexIndex
+     */
+    int getNextVertexIndex(int vertexCount,
+                           int &lowestLivingVertexIndex,
+                           int cacheSize,
+                           const std::vector<uint32_t> &possibleCandidates,
+                           int numPossibleCandidates,
+                           const std::vector<uint32_t> &lastTimestampCache,
+                           int currentTimeStamp,
+                           const std::vector<uint8_t> &livingTriangles,
+                           const std::vector<uint32_t> &usedVerticeStack,
+                           int &usedVerticeCount,
+                           int &usedVerticeOffset,
+                           int &currentTriangleIndex,
+                           std::vector<uint32_t> &skippedIndices) {
+        int nextVertexIndex = -1;
+        int maxPriority     = -1;
+        // calculates the next possibleCandidates that is recently used
+        for (int j = 0; j < numPossibleCandidates; j++) {
+            int vertexIndex = possibleCandidates[j];
+
+            // the candidate needs to be not fanned out yet
+            if (livingTriangles[vertexIndex] > 0) {
+                int priority = -1;
+
+                // prioritizes recent used vertices, but tries not to pick one that has many triangles -> fills holes better
+                if ( currentTimeStamp - lastTimestampCache[vertexIndex] + 2 * livingTriangles[vertexIndex] <=
+                    cacheSize) {
+                    priority = currentTimeStamp - lastTimestampCache[vertexIndex];
+                }
+                // select the vertexIndex with the highest priority
+                if (priority > maxPriority) {
+                    maxPriority     = priority;
+                    nextVertexIndex = vertexIndex;
+                }
+            }
+        }
+
+        // if no candidate is alive, try and find another one
+        if (nextVertexIndex == -1) {
+            nextVertexIndex = skipDeadEnd(
+                    livingTriangles,
+                    usedVerticeStack,
+                    usedVerticeCount,
+                    usedVerticeOffset,
+                    vertexCount,
+                    lowestLivingVertexIndex,
+                    currentTriangleIndex,
+                    skippedIndices);
+        }
+        return nextVertexIndex;
+    }
+
+    VertexCacheReorderResult tipsifyMesh(
+            const std::vector<uint32_t> &indexBuffer32Bit,
+            const int vertexCount,
+            const unsigned int cacheSize) {
+
+        if (indexBuffer32Bit.empty() || vertexCount <= 0) {
+            vkcv_log(LogLevel::ERROR, "Invalid Input.");
+            return VertexCacheReorderResult(indexBuffer32Bit , {});
+        }
+        int triangleCount = indexBuffer32Bit.size() / 3;
+
+       // dynamic array for vertexOccurrence
+        std::vector<uint8_t> vertexOccurrence(vertexCount, 0);
+        // count the occurrence of a vertex in all among all triangles
+        for (size_t i = 0; i < triangleCount * 3; i++) {
+            vertexOccurrence[indexBuffer32Bit[i]]++;
+        }
+
+        int sum = 0;
+        std::vector<uint32_t> offsetVertexOccurrence(vertexCount + 1, 0);
+        // highest offset for later iteration
+        int maxOffset = 0;
+        // calculate the offset of each vertex from the start
+        for (int i = 0; i < vertexCount; i++) {
+            offsetVertexOccurrence[i]   = sum;
+            sum                         += vertexOccurrence[i];
+
+            if (vertexOccurrence[i] > maxOffset) {
+                maxOffset = vertexOccurrence[i];
+            }
+            // reset for reuse
+            vertexOccurrence[i] = 0;
+        }
+        offsetVertexOccurrence[vertexCount] = sum;
+
+        // vertexIndexToTriangle = which vertex belongs to which triangle
+        std::vector<uint32_t> vertexIndexToTriangle(3 * triangleCount, 0);
+        // vertexOccurrence functions as number of usages in all triangles
+        // lowestLivingVertexIndex = number of a triangle
+        for (int i = 0; i < triangleCount; i++) {
+            // get the pointer to the first vertex of the triangle
+            // this allows us to iterate over the indexBuffer with the first vertex of the triangle as start
+            const uint32_t *vertexIndexOfTriangle = &indexBuffer32Bit[i * 3];
+
+            vertexIndexToTriangle[offsetVertexOccurrence[vertexIndexOfTriangle[0]] + vertexOccurrence[vertexIndexOfTriangle[0]]] = i;
+            vertexOccurrence[vertexIndexOfTriangle[0]]++;
+
+            vertexIndexToTriangle[offsetVertexOccurrence[vertexIndexOfTriangle[1]] + vertexOccurrence[vertexIndexOfTriangle[1]]] = i;
+            vertexOccurrence[vertexIndexOfTriangle[1]]++;
+
+            vertexIndexToTriangle[offsetVertexOccurrence[vertexIndexOfTriangle[2]] + vertexOccurrence[vertexIndexOfTriangle[2]]] = i;
+            vertexOccurrence[vertexIndexOfTriangle[2]]++;
+        }
+
+        // counts if a triangle still uses this vertex
+        std::vector<uint8_t>  livingVertices = vertexOccurrence;
+        std::vector<uint32_t> lastTimestampCache(vertexCount, 0);
+
+        // stack of already used vertices, if it'currentTimeStamp full it will write to 0 again
+        std::vector<uint32_t> usedVerticeStack(maxUsedVertices, 0);
+
+        //currently used vertices
+        int usedVerticeCount     = 0;
+        // offset if maxUsedVertices was reached and it loops back to 0
+        int usedVerticeOffset    = 0;
+
+        // saves if a triangle was emitted (used in the IndexBuffer)
+        std::vector<bool> isEmittedTriangles(triangleCount, false);
+
+        // reordered Triangles that get rewritten to the new IndexBuffer
+        std::vector<uint32_t> reorderedTriangleIndexBuffer(triangleCount, 0);
+
+        // offset to the latest not used triangleIndex
+        int triangleOutputOffset    = 0;
+        // vertexIndex to fan out from (fanning VertexIndex)
+        int currentVertexIndex      = 0;
+        int currentTimeStamp        = cacheSize + 1;
+        int lowestLivingVertexIndex = 0;
+
+        std::vector<uint32_t> possibleCandidates(3 * maxOffset);
+
+        int currentTriangleIndex = 0;
+        // list of vertex indices where a deadEnd was reached
+        // useful to know where the mesh is potentially not contiguous
+        std::vector<uint32_t> skippedIndices;
+
+        // run while not all indices are fanned out, -1 equals all are fanned out
+        while (currentVertexIndex >= 0) {
+            // number of possible candidates for a fanning VertexIndex
+            int numPossibleCandidates   = 0;
+            // offset of currentVertexIndex and the next VertexIndex
+            int startOffset             = offsetVertexOccurrence[currentVertexIndex];
+            int endOffset               = offsetVertexOccurrence[currentVertexIndex + 1];
+            // iterates over every triangle of currentVertexIndex
+            for (int offset = startOffset; offset < endOffset; offset++) {
+                int triangleIndex = vertexIndexToTriangle[offset];
+
+                // checks if the triangle is already emitted
+                if (!isEmittedTriangles[triangleIndex]) {
+
+                    // get the pointer to the first vertex of the triangle
+                    // this allows us to iterate over the indexBuffer with the first vertex of the triangle as start
+                    const uint32_t *vertexIndexOfTriangle        = &indexBuffer32Bit[3 * triangleIndex];
+
+                    currentTriangleIndex++;
+
+                    // save emitted vertexIndexOfTriangle to reorderedTriangleIndexBuffer and set it to emitted
+                    reorderedTriangleIndexBuffer[triangleOutputOffset++]    = triangleIndex;
+                    isEmittedTriangles[triangleIndex]                       = true;
+
+                    // save all vertexIndices of the triangle to reuse as soon as possible
+                    for (int j = 0; j < 3; j++) {
+                        int vertexIndex = vertexIndexOfTriangle[j];
+
+                        //save vertexIndex to reuseStack
+                        usedVerticeStack[mod(usedVerticeCount++)] = vertexIndex;
+
+                        // after looping back increase the start, so it only overrides the oldest vertexIndex
+                        if ((mod(usedVerticeCount)) ==
+                            (mod(usedVerticeOffset))) {
+                            usedVerticeOffset = mod(usedVerticeOffset + 1);
+                        }
+                        // add vertex to next possibleCandidates as fanning vertex
+                        possibleCandidates[numPossibleCandidates++] = vertexIndex;
+
+                        // remove one occurrence of the vertex, since the triangle is used
+                        livingVertices[vertexIndex]--;
+
+                        // writes the timestamp (number of iteration) of the last usage, if it wasn't used within the last cacheSize iterations
+                        if (currentTimeStamp - lastTimestampCache[vertexIndex] > cacheSize) {
+                            lastTimestampCache[vertexIndex] = currentTimeStamp;
+                            currentTimeStamp++;
+                        }
+                    }
+                }
+            }
+
+            // search for the next vertexIndex to fan out
+            currentVertexIndex = getNextVertexIndex(
+                    vertexCount, lowestLivingVertexIndex, cacheSize, possibleCandidates, numPossibleCandidates, lastTimestampCache, currentTimeStamp,
+                    livingVertices, usedVerticeStack, usedVerticeCount, usedVerticeOffset, currentTriangleIndex, skippedIndices);
+        }
+
+        std::vector<uint32_t> reorderedIndexBuffer(3 * triangleCount);
+
+        triangleOutputOffset = 0;
+        // rewriting the TriangleIndexBuffer to the new IndexBuffer
+        for (int i = 0; i < triangleCount; i++) {
+            int triangleIndex = reorderedTriangleIndexBuffer[i];
+            // rewriting the triangle index to vertices
+            for (int j = 0; j < 3; j++) {
+                int vertexIndex = indexBuffer32Bit[(3 * triangleIndex) + j];
+                reorderedIndexBuffer[triangleOutputOffset++] = vertexIndex;
+            }
+        }
+
+        return VertexCacheReorderResult(reorderedIndexBuffer, skippedIndices);
+    }
+}
\ No newline at end of file
diff --git a/modules/scene/CMakeLists.txt b/modules/scene/CMakeLists.txt
index 9aa76883a260d26aa6f46d6dabdc8206e4dad387..5edf9a29ad929b3c07b79d4f1ffcb7f1cf2fcd99 100644
--- a/modules/scene/CMakeLists.txt
+++ b/modules/scene/CMakeLists.txt
@@ -13,7 +13,7 @@ set(vkcv_scene_sources
 		${vkcv_scene_include}/vkcv/scene/Bounds.hpp
 		${vkcv_scene_source}/vkcv/scene/Bounds.cpp
 		
-		${vkcv_scene_source}/vkcv/scene/Frustum.hpp
+		${vkcv_scene_include}/vkcv/scene/Frustum.hpp
 		${vkcv_scene_source}/vkcv/scene/Frustum.cpp
 		
 		${vkcv_scene_include}/vkcv/scene/MeshPart.hpp
@@ -21,7 +21,7 @@ set(vkcv_scene_sources
 		
 		${vkcv_scene_include}/vkcv/scene/Mesh.hpp
 		${vkcv_scene_source}/vkcv/scene/Mesh.cpp
-		
+
 		${vkcv_scene_include}/vkcv/scene/Node.hpp
 		${vkcv_scene_source}/vkcv/scene/Node.cpp
 		
@@ -42,4 +42,4 @@ target_include_directories(vkcv_scene SYSTEM BEFORE PRIVATE ${vkcv_include} ${vk
 target_include_directories(vkcv_scene BEFORE PUBLIC ${vkcv_scene_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(vkcv_scene vkcv vkcv_asset_loader vkcv_material vkcv_camera)
\ No newline at end of file
+target_link_libraries(vkcv_scene vkcv vkcv_asset_loader vkcv_material vkcv_camera)
diff --git a/modules/scene/src/vkcv/scene/Frustum.hpp b/modules/scene/include/vkcv/scene/Frustum.hpp
similarity index 100%
rename from modules/scene/src/vkcv/scene/Frustum.hpp
rename to modules/scene/include/vkcv/scene/Frustum.hpp
diff --git a/modules/scene/src/vkcv/scene/Frustum.cpp b/modules/scene/src/vkcv/scene/Frustum.cpp
index c800bb1e4baf4d0feef33c073740fb211da7bf63..1f63eb1d07002d24add81872627777048642dcdb 100644
--- a/modules/scene/src/vkcv/scene/Frustum.cpp
+++ b/modules/scene/src/vkcv/scene/Frustum.cpp
@@ -1,5 +1,5 @@
 
-#include "Frustum.hpp"
+#include "vkcv/scene/Frustum.hpp"
 
 namespace vkcv::scene {
 	
diff --git a/modules/scene/src/vkcv/scene/Mesh.cpp b/modules/scene/src/vkcv/scene/Mesh.cpp
index 53fb81713ed7e14049a21cb91c771d67f2f7086c..af02aedbd71ba4bdfcc30aa7fdcd82796af904f1 100644
--- a/modules/scene/src/vkcv/scene/Mesh.cpp
+++ b/modules/scene/src/vkcv/scene/Mesh.cpp
@@ -1,7 +1,7 @@
 
 #include "vkcv/scene/Mesh.hpp"
 #include "vkcv/scene/Scene.hpp"
-#include "Frustum.hpp"
+#include "vkcv/scene/Frustum.hpp"
 
 namespace vkcv::scene {
 	
diff --git a/modules/scene/src/vkcv/scene/Node.cpp b/modules/scene/src/vkcv/scene/Node.cpp
index 32230099b2f693362bab69d8172a4dee56c4e304..24f62d18e160c7d80f82384829a2130737737ba9 100644
--- a/modules/scene/src/vkcv/scene/Node.cpp
+++ b/modules/scene/src/vkcv/scene/Node.cpp
@@ -1,7 +1,7 @@
 
 #include "vkcv/scene/Node.hpp"
 #include "vkcv/scene/Scene.hpp"
-#include "Frustum.hpp"
+#include "vkcv/scene/Frustum.hpp"
 
 #include <algorithm>
 
diff --git a/modules/shader_compiler/CMakeLists.txt b/modules/shader_compiler/CMakeLists.txt
index 4b674ec41ed4ea5f42dc73187c212e6a69952cec..6fee42bfb571168cd2371e21e231ce417efa41f0 100644
--- a/modules/shader_compiler/CMakeLists.txt
+++ b/modules/shader_compiler/CMakeLists.txt
@@ -10,6 +10,9 @@ set(vkcv_shader_compiler_include ${PROJECT_SOURCE_DIR}/include)
 
 # Add source and header files to the module
 set(vkcv_shader_compiler_sources
+		${vkcv_shader_compiler_include}/vkcv/shader/Compiler.hpp
+		${vkcv_shader_compiler_source}/vkcv/shader/Compiler.cpp
+		
 		${vkcv_shader_compiler_include}/vkcv/shader/GLSLCompiler.hpp
 		${vkcv_shader_compiler_source}/vkcv/shader/GLSLCompiler.cpp
 )
diff --git a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
index d7b7af7178531aea358cecbc8b86a29527173014..5b119ca5c68f997bacfbea6c60d5c965f9a7a54e 100644
--- a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
+++ b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
@@ -1,6 +1,11 @@
 #pragma once
 
+#include <filesystem>
+#include <string>
+#include <unordered_map>
+
 #include <vkcv/Event.hpp>
+#include <vkcv/ShaderStage.hpp>
 
 namespace vkcv::shader {
 	
@@ -8,10 +13,21 @@ namespace vkcv::shader {
 	
 	class Compiler {
 	private:
+	protected:
+		std::unordered_map<std::string, std::string> m_defines;
+		
 	public:
+		virtual bool compileSource(ShaderStage shaderStage, const char* shaderSource,
+								   const ShaderCompiledFunction& compiled,
+								   const std::filesystem::path& includePath) = 0;
+		
 		virtual void compile(ShaderStage shaderStage, const std::filesystem::path& shaderPath,
-							 const ShaderCompiledFunction& compiled, bool update = false) = 0;
+							 const ShaderCompiledFunction& compiled,
+							 const std::filesystem::path& includePath, bool update) = 0;
+		
+		std::string getDefine(const std::string& name) const;
 		
+		void setDefine(const std::string& name, const std::string& value);
 	};
 	
 }
diff --git a/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp b/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
index 7105d93a0c3e153bf3abe1d624d0c13c6f09ac6d..eca84def118625e21df1c645cfc71b6bcddf7393 100644
--- a/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
+++ b/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
@@ -7,7 +7,7 @@
 
 namespace vkcv::shader {
 	
-	class GLSLCompiler {
+	class GLSLCompiler : public Compiler {
 	private:
 	public:
 		GLSLCompiler();
@@ -20,8 +20,13 @@ namespace vkcv::shader {
 		GLSLCompiler& operator=(const GLSLCompiler& other);
 		GLSLCompiler& operator=(GLSLCompiler&& other) = default;
 		
+		bool compileSource(ShaderStage shaderStage, const char* shaderSource,
+						   const ShaderCompiledFunction& compiled,
+						   const std::filesystem::path& includePath);
+		
 		void compile(ShaderStage shaderStage, const std::filesystem::path& shaderPath,
-					 const ShaderCompiledFunction& compiled, bool update = false);
+					 const ShaderCompiledFunction& compiled,
+					 const std::filesystem::path& includePath = "", bool update = false) override;
 		
 	};
 	
diff --git a/modules/shader_compiler/src/vkcv/shader/Compiler.cpp b/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f5ec0435ca8b82dc5f328921f43a39338d1be456
--- /dev/null
+++ b/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
@@ -0,0 +1,14 @@
+
+#include "vkcv/shader/Compiler.hpp"
+
+namespace vkcv::shader {
+	
+	std::string Compiler::getDefine(const std::string &name) const {
+		return m_defines.at(name);
+	}
+	
+	void Compiler::setDefine(const std::string &name, const std::string &value) {
+		m_defines[name] = value;
+	}
+	
+}
diff --git a/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
index ec358188b8e871da6f4d62ffd397f32bfb795ee2..c8878513bf99054e357f1b076dfe12664be763b3 100644
--- a/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
+++ b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
@@ -2,16 +2,18 @@
 #include "vkcv/shader/GLSLCompiler.hpp"
 
 #include <fstream>
+#include <strstream>
 #include <glslang/SPIRV/GlslangToSpv.h>
 #include <glslang/StandAlone/DirStackFileIncluder.h>
 
+#include <vkcv/File.hpp>
 #include <vkcv/Logger.hpp>
 
 namespace vkcv::shader {
 	
 	static uint32_t s_CompilerCount = 0;
 	
-	GLSLCompiler::GLSLCompiler() {
+	GLSLCompiler::GLSLCompiler() : Compiler() {
 		if (s_CompilerCount == 0) {
 			glslang::InitializeProcess();
 		}
@@ -19,7 +21,7 @@ namespace vkcv::shader {
 		s_CompilerCount++;
 	}
 	
-	GLSLCompiler::GLSLCompiler(const GLSLCompiler &other) {
+	GLSLCompiler::GLSLCompiler(const GLSLCompiler &other) : Compiler(other) {
 		s_CompilerCount++;
 	}
 	
@@ -50,6 +52,10 @@ namespace vkcv::shader {
 				return EShLangFragment;
 			case ShaderStage::COMPUTE:
 				return EShLangCompute;
+			case ShaderStage::TASK:
+				return EShLangTaskNV;
+			case ShaderStage::MESH:
+				return EShLangMeshNV;
 			default:
 				return EShLangCount;
 		}
@@ -197,22 +203,45 @@ namespace vkcv::shader {
 		return true;
 	}
 	
-	void GLSLCompiler::compile(ShaderStage shaderStage, const std::filesystem::path &shaderPath,
-							   const ShaderCompiledFunction& compiled, bool update) {
+	bool GLSLCompiler::compileSource(ShaderStage shaderStage, const char* shaderSource,
+									 const ShaderCompiledFunction &compiled,
+									 const std::filesystem::path& includePath) {
 		const EShLanguage language = findShaderLanguage(shaderStage);
 		
 		if (language == EShLangCount) {
-			vkcv_log(LogLevel::ERROR, "Shader stage not supported (%s)", shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "Shader stage not supported");
+			return false;
 		}
 		
-		const std::vector<char> code = readShaderCode(shaderPath);
-		
 		glslang::TShader shader (language);
 		glslang::TProgram program;
 		
+		std::string source (shaderSource);
+		
+		if (!m_defines.empty()) {
+			std::strstream 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()) {
+				pos = 0;
+			}
+			
+			const size_t epos = source.find_last_of("#extension", pos) + 10;
+			if (epos < source.length()) {
+				pos = epos;
+			}
+			
+			pos = source.find('\n', pos) + 1;
+			source = source.insert(pos, defines.str());
+		}
+		
 		const char *shaderStrings [1];
-		shaderStrings[0] = code.data();
+		shaderStrings[0] = source.c_str();
 		
 		shader.setStrings(shaderStrings, 1);
 		
@@ -222,51 +251,53 @@ namespace vkcv::shader {
 		const auto messages = (EShMessages)(
 			EShMsgSpvRules |
 			EShMsgVulkanRules
-			);
+		);
 
 		std::string preprocessedGLSL;
 
 		DirStackFileIncluder includer;
-		includer.pushExternalLocalDirectory(shaderPath.parent_path().string());
+		includer.pushExternalLocalDirectory(includePath.string());
 
-		if (!shader.preprocess(&resources, 100, ENoProfile, false, false, messages, &preprocessedGLSL, includer)) {
-			vkcv_log(LogLevel::ERROR, "Shader parsing failed {\n%s\n%s\n} (%s)",
-				shader.getInfoLog(), shader.getInfoDebugLog(), shaderPath.string().c_str());
-			return;
+		if (!shader.preprocess(&resources, 100, ENoProfile,
+							   false, false,
+							   messages, &preprocessedGLSL, includer)) {
+			vkcv_log(LogLevel::ERROR, "Shader preprocessing failed {\n%s\n%s\n}",
+				shader.getInfoLog(), shader.getInfoDebugLog());
+			return false;
 		}
 		
 		const char* preprocessedCString = preprocessedGLSL.c_str();
 		shader.setStrings(&preprocessedCString, 1);
 
 		if (!shader.parse(&resources, 100, false, messages)) {
-			vkcv_log(LogLevel::ERROR, "Shader parsing failed {\n%s\n%s\n} (%s)",
-					 shader.getInfoLog(), shader.getInfoDebugLog(), shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "Shader parsing failed {\n%s\n%s\n}",
+					 shader.getInfoLog(), shader.getInfoDebugLog());
+			return false;
 		}
 		
 		program.addShader(&shader);
 		
 		if (!program.link(messages)) {
-			vkcv_log(LogLevel::ERROR, "Shader linking failed {\n%s\n%s\n} (%s)",
-					 shader.getInfoLog(), shader.getInfoDebugLog(), shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "Shader linking failed {\n%s\n%s\n}",
+					 shader.getInfoLog(), shader.getInfoDebugLog());
+			return false;
 		}
 		
 		const glslang::TIntermediate* intermediate = program.getIntermediate(language);
 		
 		if (!intermediate) {
-			vkcv_log(LogLevel::ERROR, "No valid intermediate representation (%s)", shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "No valid intermediate representation");
+			return false;
 		}
 		
 		std::vector<uint32_t> spirv;
 		glslang::GlslangToSpv(*intermediate, spirv);
 		
-		const std::filesystem::path tmp_path (std::tmpnam(nullptr));
+		const std::filesystem::path tmp_path = generateTemporaryFilePath();
 		
 		if (!writeSpirvCode(tmp_path, spirv)) {
-			vkcv_log(LogLevel::ERROR, "Spir-V could not be written to disk (%s)", shaderPath.string().c_str());
-			return;
+			vkcv_log(LogLevel::ERROR, "Spir-V could not be written to disk");
+			return false;
 		}
 		
 		if (compiled) {
@@ -274,6 +305,24 @@ namespace vkcv::shader {
 		}
 		
 		std::filesystem::remove(tmp_path);
+		return true;
+	}
+	
+	void GLSLCompiler::compile(ShaderStage shaderStage, const std::filesystem::path &shaderPath,
+							   const ShaderCompiledFunction& compiled,
+							   const std::filesystem::path& includePath, bool update) {
+		const std::vector<char> code = readShaderCode(shaderPath);
+		bool result;
+		
+		if (!includePath.empty()) {
+			result = compileSource(shaderStage, code.data(), compiled, includePath);
+		} else {
+			result = compileSource(shaderStage, code.data(), compiled, shaderPath.parent_path());
+		}
+		
+		if (!result) {
+			vkcv_log(LogLevel::ERROR, "Shader compilation failed: (%s)", shaderPath.string().c_str());
+		}
 		
 		if (update) {
 			// TODO: Shader hot compilation during runtime
diff --git a/modules/upscaling/CMakeLists.txt b/modules/upscaling/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0767e5c4d2d60c001ac9d6792efcd623456284a8
--- /dev/null
+++ b/modules/upscaling/CMakeLists.txt
@@ -0,0 +1,39 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_upscaling)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_upscaling_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_upscaling_include ${PROJECT_SOURCE_DIR}/include)
+
+set(vkcv_upscaling_sources
+		${vkcv_upscaling_include}/vkcv/upscaling/Upscaling.hpp
+		${vkcv_upscaling_source}/vkcv/upscaling/Upscaling.cpp
+		
+		${vkcv_upscaling_include}/vkcv/upscaling/BilinearUpscaling.hpp
+		${vkcv_upscaling_source}/vkcv/upscaling/BilinearUpscaling.cpp
+		
+		${vkcv_upscaling_include}/vkcv/upscaling/FSRUpscaling.hpp
+		${vkcv_upscaling_source}/vkcv/upscaling/FSRUpscaling.cpp
+)
+
+# Setup some path variables to load libraries
+set(vkcv_upscaling_lib lib)
+set(vkcv_upscaling_lib_path ${PROJECT_SOURCE_DIR}/${vkcv_upscaling_lib})
+
+# Check and load FidelityFX_FSR
+include(config/FidelityFX_FSR.cmake)
+
+# adding source files to the project
+add_library(vkcv_upscaling STATIC ${vkcv_upscaling_sources})
+
+# link the required libraries to the module
+target_link_libraries(vkcv_upscaling ${vkcv_upscaling_libraries} vkcv vkcv_shader_compiler)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_upscaling SYSTEM BEFORE PRIVATE ${vkcv_upscaling_includes} ${vkcv_include} ${vkcv_shader_compiler_include})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_upscaling BEFORE PUBLIC ${vkcv_upscaling_include})
diff --git a/modules/upscaling/config/FidelityFX_FSR.cmake b/modules/upscaling/config/FidelityFX_FSR.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..cc52b4189f781f534a933feb7b782b6bec333e5a
--- /dev/null
+++ b/modules/upscaling/config/FidelityFX_FSR.cmake
@@ -0,0 +1,18 @@
+
+if (EXISTS "${vkcv_upscaling_lib_path}/FidelityFX-FSR")
+	include_shader(${vkcv_upscaling_lib_path}/FidelityFX-FSR/ffx-fsr/ffx_a.h ${vkcv_upscaling_include} ${vkcv_upscaling_source})
+	include_shader(${vkcv_upscaling_lib_path}/FidelityFX-FSR/ffx-fsr/ffx_fsr1.h ${vkcv_upscaling_include} ${vkcv_upscaling_source})
+	include_shader(${vkcv_upscaling_lib_path}/FidelityFX-FSR/sample/src/VK/FSR_Pass.glsl ${vkcv_upscaling_include} ${vkcv_upscaling_source})
+	
+	list(APPEND vkcv_upscaling_includes ${vkcv_upscaling_lib}/FidelityFX-FSR/ffx-fsr)
+	
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_source}/ffx_a.h.cxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_source}/ffx_fsr1.h.cxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_source}/FSR_Pass.glsl.cxx)
+	
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/ffx_a.h.hxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/ffx_fsr1.h.hxx)
+	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/FSR_Pass.glsl.hxx)
+else()
+	message(WARNING "FidelityFX-FSR is required..! Update the submodules!")
+endif ()
diff --git a/modules/upscaling/include/vkcv/upscaling/BilinearUpscaling.hpp b/modules/upscaling/include/vkcv/upscaling/BilinearUpscaling.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..52124dc8e36bee7ef7c00de6afcf3457296a7623
--- /dev/null
+++ b/modules/upscaling/include/vkcv/upscaling/BilinearUpscaling.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "Upscaling.hpp"
+
+namespace vkcv::upscaling {
+	
+	class BilinearUpscaling : public Upscaling {
+	private:
+	public:
+		BilinearUpscaling(Core& core);
+		
+		void recordUpscaling(const CommandStreamHandle& cmdStream,
+							 const ImageHandle& input,
+							 const ImageHandle& output) override;
+	
+	};
+
+}
diff --git a/modules/upscaling/include/vkcv/upscaling/FSRUpscaling.hpp b/modules/upscaling/include/vkcv/upscaling/FSRUpscaling.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2a1338b85e3ee60a33215157aaaa15817f2db97f
--- /dev/null
+++ b/modules/upscaling/include/vkcv/upscaling/FSRUpscaling.hpp
@@ -0,0 +1,78 @@
+#pragma once
+
+#include "Upscaling.hpp"
+
+#include <vkcv/ShaderProgram.hpp>
+
+namespace vkcv::upscaling {
+	
+	enum class FSRQualityMode : int {
+		NONE = 0,
+		ULTRA_QUALITY = 1,
+		QUALITY = 2,
+		BALANCED = 3,
+		PERFORMANCE = 4
+	};
+	
+	void getFSRResolution(FSRQualityMode mode,
+						  uint32_t outputWidth, uint32_t outputHeight,
+						  uint32_t &inputWidth, uint32_t &inputHeight);
+	
+	float getFSRLodBias(FSRQualityMode mode);
+	
+	struct FSRConstants {
+		uint32_t Const0 [4];
+		uint32_t Const1 [4];
+		uint32_t Const2 [4];
+		uint32_t Const3 [4];
+		uint32_t Sample [4];
+	};
+	
+	class FSRUpscaling : public Upscaling {
+	private:
+		PipelineHandle m_easuPipeline;
+		PipelineHandle m_rcasPipeline;
+		
+		DescriptorSetHandle m_easuDescriptorSet;
+		DescriptorSetHandle m_rcasDescriptorSet;
+		
+		Buffer<FSRConstants> m_easuConstants;
+		Buffer<FSRConstants> m_rcasConstants;
+		ImageHandle m_intermediateImage;
+		SamplerHandle m_sampler;
+		
+		bool m_hdr;
+		
+		/**
+		 * Sharpness will calculate the rcasAttenuation value
+		 * which should be between 0.0f and 2.0f (default: 0.25f).
+		 *
+		 * rcasAttenuation = (1.0f - sharpness) * 2.0f
+		 *
+		 * So the default value for sharpness should be 0.875f.
+		 *
+		 * Beware that 0.0f or any negative value of sharpness will
+		 * disable the rcas pass completely.
+		 */
+		float m_sharpness;
+	
+	public:
+		explicit FSRUpscaling(Core& core);
+		
+		void recordUpscaling(const CommandStreamHandle& cmdStream,
+							 const ImageHandle& input,
+							 const ImageHandle& output) override;
+		
+		[[nodiscard]]
+		bool isHdrEnabled() const;
+		
+		void setHdrEnabled(bool enabled);
+		
+		[[nodiscard]]
+		float getSharpness() const;
+		
+		void setSharpness(float sharpness);
+		
+	};
+
+}
diff --git a/modules/upscaling/include/vkcv/upscaling/Upscaling.hpp b/modules/upscaling/include/vkcv/upscaling/Upscaling.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c44e878ad78f0a3359599c76f781371505fd3a85
--- /dev/null
+++ b/modules/upscaling/include/vkcv/upscaling/Upscaling.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <vkcv/Core.hpp>
+#include <vkcv/Handles.hpp>
+
+namespace vkcv::upscaling {
+	
+	class Upscaling {
+	protected:
+		Core& m_core;
+	
+	public:
+		Upscaling(Core& core);
+		
+		~Upscaling() = default;
+		
+		virtual void recordUpscaling(const CommandStreamHandle& cmdStream,
+									 const ImageHandle& input,
+							 		 const ImageHandle& output) = 0;
+	
+	};
+	
+}
diff --git a/modules/upscaling/lib/FidelityFX-FSR b/modules/upscaling/lib/FidelityFX-FSR
new file mode 160000
index 0000000000000000000000000000000000000000..bcffc8171efb80e265991301a49670ed755088dd
--- /dev/null
+++ b/modules/upscaling/lib/FidelityFX-FSR
@@ -0,0 +1 @@
+Subproject commit bcffc8171efb80e265991301a49670ed755088dd
diff --git a/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9c36acf5d050e3f4f19223020357b6c32534a2de
--- /dev/null
+++ b/modules/upscaling/src/vkcv/upscaling/BilinearUpscaling.cpp
@@ -0,0 +1,13 @@
+
+#include "vkcv/upscaling/BilinearUpscaling.hpp"
+
+namespace vkcv::upscaling {
+	
+	BilinearUpscaling::BilinearUpscaling(Core &core) : Upscaling(core) {}
+	
+	void BilinearUpscaling::recordUpscaling(const CommandStreamHandle &cmdStream, const ImageHandle &input,
+											const ImageHandle &output) {
+		m_core.recordBlitImage(cmdStream, input, output, SamplerFilterType::LINEAR);
+	}
+
+}
diff --git a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..460a6d0b459fe7d1d2a917a62138fea2e5a40908
--- /dev/null
+++ b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
@@ -0,0 +1,382 @@
+
+#include "vkcv/upscaling/FSRUpscaling.hpp"
+
+#include <stdint.h>
+#include <math.h>
+
+#define A_CPU 1
+#include <ffx_a.h>
+#include <ffx_fsr1.h>
+
+#include "ffx_a.h.hxx"
+#include "ffx_fsr1.h.hxx"
+#include "FSR_Pass.glsl.hxx"
+
+#include <vkcv/File.hpp>
+#include <vkcv/Logger.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+namespace vkcv::upscaling {
+	
+	void getFSRResolution(FSRQualityMode mode,
+						  uint32_t outputWidth, uint32_t outputHeight,
+						  uint32_t &inputWidth, uint32_t &inputHeight) {
+		float scale;
+		
+		switch (mode) {
+			case FSRQualityMode::ULTRA_QUALITY:
+				scale = 1.3f;
+				break;
+			case FSRQualityMode::QUALITY:
+				scale = 1.5f;
+				break;
+			case FSRQualityMode::BALANCED:
+				scale = 1.7f;
+				break;
+			case FSRQualityMode::PERFORMANCE:
+				scale = 2.0f;
+				break;
+			default:
+				scale = 1.0f;
+				break;
+		}
+		
+		inputWidth = static_cast<uint32_t>(
+				std::round(static_cast<float>(outputWidth) / scale)
+		);
+		
+		inputHeight = static_cast<uint32_t>(
+				std::round(static_cast<float>(outputHeight) / scale)
+		);
+	}
+	
+	float getFSRLodBias(FSRQualityMode mode) {
+		switch (mode) {
+			case FSRQualityMode::ULTRA_QUALITY:
+				return -0.38f;
+			case FSRQualityMode::QUALITY:
+				return -0.58f;
+			case FSRQualityMode::BALANCED:
+				return -0.79f;
+			case FSRQualityMode::PERFORMANCE:
+				return -1.0f;
+			default:
+				return 0.0f;
+		}
+	}
+	
+	static std::vector<DescriptorBinding> getDescriptorBindings() {
+		return std::vector<DescriptorBinding>({
+			DescriptorBinding(
+					0, DescriptorType::UNIFORM_BUFFER_DYNAMIC,
+					1, ShaderStage::COMPUTE
+			),
+			DescriptorBinding(
+					1, DescriptorType::IMAGE_SAMPLED,
+					1, ShaderStage::COMPUTE
+			),
+			DescriptorBinding(
+					2, DescriptorType::IMAGE_STORAGE,
+					1, ShaderStage::COMPUTE
+			),
+			DescriptorBinding(
+					3, DescriptorType::SAMPLER,
+					1, ShaderStage::COMPUTE
+			)
+		});
+	}
+	
+	template<typename T>
+	bool checkFeatures(const vk::BaseInStructure* base, vk::StructureType type, bool (*check)(const T& features)) {
+		if (base->sType == type) {
+			return check(*reinterpret_cast<const T*>(base));
+		} else
+		if (base->pNext) {
+			return checkFeatures<T>(base->pNext, type, check);
+		} else {
+			return false;
+		}
+	}
+	
+	static bool checkFloat16(const vk::PhysicalDeviceFloat16Int8FeaturesKHR& features) {
+		return features.shaderFloat16;
+	}
+	
+	static bool check16Storage(const vk::PhysicalDevice16BitStorageFeaturesKHR& features) {
+		return features.storageBuffer16BitAccess;
+	}
+	
+	static bool writeShaderCode(const std::filesystem::path &shaderPath, const std::string& code) {
+		std::ofstream file (shaderPath.string(), std::ios::out);
+		
+		if (!file.is_open()) {
+			vkcv_log(LogLevel::ERROR, "The file could not be opened (%s)", shaderPath.string().c_str());
+			return false;
+		}
+		
+		file.seekp(0);
+		file.write(code.c_str(), static_cast<std::streamsize>(code.length()));
+		file.close();
+		
+		return true;
+	}
+	
+	static bool compileFSRShader(vkcv::shader::GLSLCompiler& compiler,
+								 const shader::ShaderCompiledFunction& compiled) {
+		std::filesystem::path directory = generateTemporaryDirectoryPath();
+		
+		if (!std::filesystem::create_directory(directory)) {
+			vkcv_log(LogLevel::ERROR, "The directory could not be created (%s)", directory.string().c_str());
+			return false;
+		}
+		
+		if (!writeShaderCode(directory / "ffx_a.h", FFX_A_H_SHADER)) {
+			return false;
+		}
+		
+		if (!writeShaderCode(directory / "ffx_fsr1.h", FFX_FSR1_H_SHADER)) {
+			return false;
+		}
+		
+		return compiler.compileSource(vkcv::ShaderStage::COMPUTE,
+									  FSR_PASS_GLSL_SHADER.c_str(),
+									  [&directory, &compiled] (vkcv::ShaderStage shaderStage,
+									  		const std::filesystem::path& path) {
+				if (compiled) {
+					compiled(shaderStage, path);
+				}
+				
+				std::filesystem::remove_all(directory);
+			}, directory
+		);
+	}
+	
+	FSRUpscaling::FSRUpscaling(Core& core) :
+	Upscaling(core),
+	m_easuPipeline(),
+	m_rcasPipeline(),
+	m_easuDescriptorSet(m_core.createDescriptorSet(getDescriptorBindings())),
+	m_rcasDescriptorSet(m_core.createDescriptorSet(getDescriptorBindings())),
+	m_easuConstants(m_core.createBuffer<FSRConstants>(
+			BufferType::UNIFORM,1,
+			BufferMemoryType::HOST_VISIBLE
+	)),
+	m_rcasConstants(m_core.createBuffer<FSRConstants>(
+			BufferType::UNIFORM,1,
+			BufferMemoryType::HOST_VISIBLE
+	)),
+	m_intermediateImage(),
+	m_sampler(m_core.createSampler(
+			SamplerFilterType::LINEAR,
+			SamplerFilterType::LINEAR,
+			SamplerMipmapMode::NEAREST,
+			SamplerAddressMode::CLAMP_TO_EDGE
+	)),
+	m_hdr(false),
+	m_sharpness(0.875f) {
+		vkcv::shader::GLSLCompiler easuCompiler;
+		vkcv::shader::GLSLCompiler rcasCompiler;
+		
+		const auto& features = m_core.getContext().getPhysicalDevice().getFeatures2();
+		const bool float16Support = (
+				checkFeatures<vk::PhysicalDeviceFloat16Int8FeaturesKHR>(
+						reinterpret_cast<const vk::BaseInStructure*>(&features),
+						vk::StructureType::ePhysicalDeviceShaderFloat16Int8FeaturesKHR,
+						checkFloat16
+				) &&
+				checkFeatures<vk::PhysicalDevice16BitStorageFeaturesKHR>(
+						reinterpret_cast<const vk::BaseInStructure*>(&features),
+						vk::StructureType::ePhysicalDevice16BitStorageFeaturesKHR,
+						check16Storage
+				)
+		) || (true); // check doesn't work because chain is empty
+		
+		if (!float16Support) {
+			easuCompiler.setDefine("SAMPLE_SLOW_FALLBACK", "1");
+			rcasCompiler.setDefine("SAMPLE_SLOW_FALLBACK", "1");
+		}
+		
+		easuCompiler.setDefine("SAMPLE_EASU", "1");
+		rcasCompiler.setDefine("SAMPLE_RCAS", "1");
+		
+		{
+			ShaderProgram program;
+			compileFSRShader(easuCompiler, [&program](vkcv::ShaderStage shaderStage,
+					const std::filesystem::path& path) {
+				program.addShader(shaderStage, path);
+			});
+			
+			m_easuPipeline = m_core.createComputePipeline(program, {
+				m_core.getDescriptorSet(m_easuDescriptorSet).layout
+			});
+			
+			DescriptorWrites writes;
+			writes.uniformBufferWrites.emplace_back(
+					0, m_easuConstants.getHandle(),true
+			);
+			
+			writes.samplerWrites.emplace_back(3, m_sampler);
+			
+			m_core.writeDescriptorSet(m_easuDescriptorSet, writes);
+		}
+		
+		{
+			ShaderProgram program;
+			compileFSRShader(rcasCompiler, [&program](vkcv::ShaderStage shaderStage,
+					const std::filesystem::path& path) {
+				program.addShader(shaderStage, path);
+			});
+			
+			m_rcasPipeline = m_core.createComputePipeline(program, {
+				m_core.getDescriptorSet(m_rcasDescriptorSet).layout
+			});
+			
+			DescriptorWrites writes;
+			writes.uniformBufferWrites.emplace_back(
+					0, m_rcasConstants.getHandle(),true
+			);
+			
+			writes.samplerWrites.emplace_back(3, m_sampler);
+			
+			m_core.writeDescriptorSet(m_rcasDescriptorSet, writes);
+		}
+	}
+	
+	void FSRUpscaling::recordUpscaling(const CommandStreamHandle& cmdStream,
+									   const ImageHandle& input,
+									   const ImageHandle& output) {
+		const uint32_t inputWidth = m_core.getImageWidth(input);
+		const uint32_t inputHeight = m_core.getImageHeight(input);
+		
+		const uint32_t outputWidth = m_core.getImageWidth(output);
+		const uint32_t outputHeight = m_core.getImageHeight(output);
+		
+		if ((!m_intermediateImage) ||
+			(outputWidth != m_core.getImageWidth(m_intermediateImage)) ||
+			(outputHeight != m_core.getImageHeight(m_intermediateImage))) {
+			m_intermediateImage = m_core.createImage(
+					m_core.getImageFormat(output),
+					outputWidth, outputHeight,1,
+					false,
+					true
+			).getHandle();
+			
+			m_core.prepareImageForStorage(cmdStream, m_intermediateImage);
+		}
+		
+		const bool rcasEnabled = (
+				(m_sharpness > +0.0f) &&
+				((inputWidth < outputWidth) || (inputHeight < outputHeight))
+		);
+		
+		{
+			FSRConstants consts = {};
+			
+			FsrEasuCon(
+					consts.Const0, consts.Const1, consts.Const2, consts.Const3,
+					static_cast<AF1>(inputWidth), static_cast<AF1>(inputHeight),
+					static_cast<AF1>(inputWidth), static_cast<AF1>(inputHeight),
+					static_cast<AF1>(outputWidth), static_cast<AF1>(outputHeight)
+			);
+			
+			consts.Sample[0] = (((m_hdr) && (!rcasEnabled)) ? 1 : 0);
+			
+			m_easuConstants.fill(&consts);
+		}
+		
+		static const uint32_t threadGroupWorkRegionDim = 16;
+		
+		uint32_t dispatch[3];
+		dispatch[0] = (outputWidth + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
+		dispatch[1] = (outputHeight + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
+		dispatch[2] = 1;
+		
+		m_core.recordBufferMemoryBarrier(cmdStream, m_easuConstants.getHandle());
+		
+		if (rcasEnabled) {
+			{
+				DescriptorWrites writes;
+				writes.sampledImageWrites.emplace_back(1, input);
+				writes.storageImageWrites.emplace_back(2, m_intermediateImage);
+				
+				m_core.writeDescriptorSet(m_easuDescriptorSet, writes);
+			}
+			{
+				DescriptorWrites writes;
+				writes.sampledImageWrites.emplace_back(1, m_intermediateImage);
+				writes.storageImageWrites.emplace_back(2, output);
+				
+				m_core.writeDescriptorSet(m_rcasDescriptorSet, writes);
+			}
+			
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_easuPipeline,
+					dispatch,
+					{DescriptorSetUsage(0, m_core.getDescriptorSet(
+							m_easuDescriptorSet
+					).vulkanHandle, { 0 })},
+					PushConstants(0)
+			);
+			
+			{
+				FSRConstants consts = {};
+				
+				FsrRcasCon(consts.Const0, (1.0f - m_sharpness) * 2.0f);
+				consts.Sample[0] = (m_hdr ? 1 : 0);
+				
+				m_rcasConstants.fill(&consts);
+			}
+			
+			m_core.recordBufferMemoryBarrier(cmdStream, m_rcasConstants.getHandle());
+			m_core.prepareImageForSampling(cmdStream, m_intermediateImage);
+			
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_rcasPipeline,
+					dispatch,
+					{DescriptorSetUsage(0, m_core.getDescriptorSet(
+							m_rcasDescriptorSet
+					).vulkanHandle, { 0 })},
+					PushConstants(0)
+			);
+			
+			m_core.prepareImageForStorage(cmdStream, m_intermediateImage);
+		} else {
+			{
+				DescriptorWrites writes;
+				writes.sampledImageWrites.emplace_back(1, input);
+				writes.storageImageWrites.emplace_back(2, output);
+				
+				m_core.writeDescriptorSet(m_easuDescriptorSet, writes);
+			}
+			
+			m_core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					m_easuPipeline,
+					dispatch,
+					{DescriptorSetUsage(0, m_core.getDescriptorSet(
+							m_easuDescriptorSet
+					).vulkanHandle, { 0 })},
+					PushConstants(0)
+			);
+		}
+	}
+	
+	bool FSRUpscaling::isHdrEnabled() const {
+		return m_hdr;
+	}
+	
+	void FSRUpscaling::setHdrEnabled(bool enabled) {
+		m_hdr = enabled;
+	}
+	
+	float FSRUpscaling::getSharpness() const {
+		return m_sharpness;
+	}
+	
+	void FSRUpscaling::setSharpness(float sharpness) {
+		m_sharpness = (sharpness < 0.0f ? 0.0f : (sharpness > 1.0f ? 1.0f : sharpness));
+	}
+	
+}
diff --git a/modules/upscaling/src/vkcv/upscaling/Upscaling.cpp b/modules/upscaling/src/vkcv/upscaling/Upscaling.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b0c3dee9b1c799c0e1f07b59b03d3ad46bd453ed
--- /dev/null
+++ b/modules/upscaling/src/vkcv/upscaling/Upscaling.cpp
@@ -0,0 +1,8 @@
+
+#include "vkcv/upscaling/Upscaling.hpp"
+
+namespace vkcv::upscaling {
+	
+	Upscaling::Upscaling(Core &core) : m_core(core) {}
+	
+}
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index 86034ffb07e7c2f40463f711d2027af8aade573f..6f179ebae0dedc1fc10060b14ce11133665a1538 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -5,4 +5,5 @@ add_subdirectory(first_mesh)
 add_subdirectory(first_scene)
 add_subdirectory(particle_simulation)
 add_subdirectory(voxelization)
-add_subdirectory(neural_network)
\ No newline at end of file
+add_subdirectory(mesh_shader)
+add_subdirectory(neural_network)
diff --git a/projects/mesh_shader/.gitignore b/projects/mesh_shader/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..7e24fd7b853bfb0a29d8b30879ef1cb95ad141c0
--- /dev/null
+++ b/projects/mesh_shader/.gitignore
@@ -0,0 +1 @@
+first_triangle
\ No newline at end of file
diff --git a/projects/mesh_shader/CMakeLists.txt b/projects/mesh_shader/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1aa5d5ff3977a47dce75a38329216d550b1b9311
--- /dev/null
+++ b/projects/mesh_shader/CMakeLists.txt
@@ -0,0 +1,30 @@
+cmake_minimum_required(VERSION 3.16)
+project(mesh_shader)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# this should fix the execution path to load local files from the project
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+# adding source files to the project
+add_executable(mesh_shader src/main.cpp)
+
+target_sources(mesh_shader PRIVATE)
+
+# this should fix the execution path to load local files from the project (for MSVC)
+if(MSVC)
+	set_target_properties(mesh_shader PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+	set_target_properties(mesh_shader PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+
+	# in addition to setting the output directory, the working directory has to be set
+	# by default visual studio sets the working directory to the build directory, when using the debugger
+	set_target_properties(mesh_shader PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+endif()
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(mesh_shader SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_meshlet_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(mesh_shader vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_testing vkcv_camera vkcv_meshlet vkcv_shader_compiler vkcv_gui)
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/Bunny/Bunny.glb b/projects/mesh_shader/resources/Bunny/Bunny.glb
new file mode 100644
index 0000000000000000000000000000000000000000..181f1f92f1906e1e1ba900768580203efe19e9be
--- /dev/null
+++ b/projects/mesh_shader/resources/Bunny/Bunny.glb
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b8bc6fab11929ca11bdf4e892ffb03b621b10307f705cdea17d82d3dee3b9aae
+size 4045836
diff --git a/projects/mesh_shader/resources/monke.glb b/projects/mesh_shader/resources/monke.glb
new file mode 100644
index 0000000000000000000000000000000000000000..47d0b9131f15a8f0697318d0a47302c71cad1db8
--- /dev/null
+++ b/projects/mesh_shader/resources/monke.glb
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:597584db90a3f51088beea6652d8320e82cb025f9d3d036b89e54ad72c732a06
+size 98612
diff --git a/projects/mesh_shader/resources/shaders/common.inc b/projects/mesh_shader/resources/shaders/common.inc
new file mode 100644
index 0000000000000000000000000000000000000000..280ffee215a8b8342b78d1f5558d63a05e16859b
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/common.inc
@@ -0,0 +1,4 @@
+struct ObjectMatrices{
+    mat4 model;
+    mat4 mvp;
+};
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/meshlet.inc b/projects/mesh_shader/resources/shaders/meshlet.inc
new file mode 100644
index 0000000000000000000000000000000000000000..0594f62ceead8ffca09b585305075eb6046f3c46
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/meshlet.inc
@@ -0,0 +1,8 @@
+struct Meshlet{
+    uint    vertexOffset;
+    uint    vertexCount;
+    uint    indexOffset;
+    uint    indexCount;
+    vec3    meanPosition;
+    float   boundingSphereRadius;
+};
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/shader.frag b/projects/mesh_shader/resources/shaders/shader.frag
new file mode 100644
index 0000000000000000000000000000000000000000..f4f6982f2089e6c8e102027f3b8763bb38f8e59c
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/shader.frag
@@ -0,0 +1,32 @@
+#version 450
+#extension GL_ARB_separate_shader_objects : enable
+
+layout(location = 0) in  vec3 passNormal;
+layout(location = 1) in  flat uint passTaskIndex;
+layout(location = 0) out vec3 outColor;
+
+uint lowbias32(uint x)
+{
+    x ^= x >> 16;
+    x *= 0x7feb352dU;
+    x ^= x >> 15;
+    x *= 0x846ca68bU;
+    x ^= x >> 16;
+    return x;
+}
+
+float hashToFloat(uint hash){
+    return (hash % 255) / 255.f;
+}
+
+vec3 colorFromIndex(uint i){
+    return vec3(
+        hashToFloat(lowbias32(i+0)),
+        hashToFloat(lowbias32(i+1)),
+        hashToFloat(lowbias32(i+2)));
+}
+
+void main() {
+	outColor = normalize(passNormal) * 0.5 + 0.5;
+    outColor = colorFromIndex(passTaskIndex);
+}
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/shader.mesh b/projects/mesh_shader/resources/shaders/shader.mesh
new file mode 100644
index 0000000000000000000000000000000000000000..30c98610f4776204ff526c57c1f793e371194629
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/shader.mesh
@@ -0,0 +1,78 @@
+#version 460
+#extension GL_ARB_separate_shader_objects   : enable
+#extension GL_GOOGLE_include_directive      : enable
+#extension GL_NV_mesh_shader                : require
+
+#include "meshlet.inc"
+
+layout(local_size_x=32) in;
+
+layout(triangles) out;
+layout(max_vertices=64, max_primitives=126) out;
+
+layout(location = 0) out vec3 passNormal[];
+layout(location = 1) out uint passTaskIndex[];
+
+struct Vertex
+{
+    vec3 position;  float padding0;
+    vec3 normal;    float padding1;
+};
+
+layout(std430, binding = 0) readonly buffer vertexBuffer
+{
+    Vertex vertices[];
+};
+
+layout(std430, binding = 1) readonly buffer indexBuffer
+{
+    uint localIndices[]; // breaks for 16 bit indices
+};
+
+layout(std430, binding = 2) readonly buffer meshletBuffer
+{
+    Meshlet meshlets[];
+};
+
+taskNV in Task {
+  uint meshletIndices[32];
+  mat4 mvp;
+} IN;
+
+void main()	{
+    
+    uint meshletIndex = IN.meshletIndices[gl_WorkGroupID.x];
+    Meshlet meshlet = meshlets[meshletIndex];
+    
+    // set vertices
+    for(uint i = 0; i < 2; i++){
+    
+        uint workIndex = gl_LocalInvocationID.x + 32 * i;
+        if(workIndex >= meshlet.vertexCount){
+            break;
+        }
+    
+        uint vertexIndex    = meshlet.vertexOffset + workIndex;
+        Vertex vertex       = vertices[vertexIndex];
+    
+        gl_MeshVerticesNV[workIndex].gl_Position    = IN.mvp * vec4(vertex.position, 1);
+        passNormal[workIndex]                       = vertex.normal;
+        passTaskIndex[workIndex]                    = meshletIndex;
+    }
+    
+    // set local indices
+    for(uint i = 0; i < 12; i++){
+    
+        uint workIndex = gl_LocalInvocationID.x + i * 32;
+        if(workIndex >= meshlet.indexCount){
+            break;
+        }    
+        
+        uint indexBufferIndex               = meshlet.indexOffset + workIndex;
+        gl_PrimitiveIndicesNV[workIndex]    = localIndices[indexBufferIndex];
+    }
+    
+    if(gl_LocalInvocationID.x == 0){
+        gl_PrimitiveCountNV = meshlet.indexCount / 3;
+    }
+}
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/shader.task b/projects/mesh_shader/resources/shaders/shader.task
new file mode 100644
index 0000000000000000000000000000000000000000..7a692e98e6384767191d76cef940e295ca127d62
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/shader.task
@@ -0,0 +1,78 @@
+#version 460
+#extension GL_ARB_separate_shader_objects   : enable
+#extension GL_NV_mesh_shader                : require
+#extension GL_GOOGLE_include_directive      : enable
+
+#include "meshlet.inc"
+#include "common.inc"
+
+layout(local_size_x=32) in;
+
+taskNV out Task {
+  uint meshletIndices[32];
+  mat4 mvp;
+} OUT;
+
+layout( push_constant ) uniform constants{
+    uint matrixIndex;
+    uint meshletCount;
+};
+
+// TODO: reuse mesh stage binding at location 2 after required fix in framework
+layout(std430, binding = 5) readonly buffer meshletBuffer
+{
+    Meshlet meshlets[];
+};
+
+struct Plane{
+    vec3    pointOnPlane;
+    float   padding0;
+    vec3    normal;
+    float   padding1;
+};
+
+layout(set=0, binding=3, std140) uniform cameraPlaneBuffer{
+    Plane cameraPlanes[6];
+};
+
+layout(std430, binding = 4) readonly buffer matrixBuffer
+{
+    ObjectMatrices objectMatrices[];
+};
+
+shared uint taskCount;
+
+bool isSphereInsideFrustum(vec3 spherePos, float sphereRadius, Plane cameraPlanes[6]){
+    bool isInside = true;
+    for(int i = 0; i < 6; i++){
+        Plane p     = cameraPlanes[i];
+        isInside    = isInside && dot(p.normal, spherePos - p.pointOnPlane) - sphereRadius < 0;
+    }
+    return isInside;
+}
+
+void main() {
+
+    if(gl_LocalInvocationID.x >= meshletCount){
+        return;
+    }
+    
+    uint meshletIndex   = gl_GlobalInvocationID.x;
+    Meshlet meshlet     = meshlets[meshletIndex]; 
+    
+    if(gl_LocalInvocationID.x == 0){
+        taskCount = 0;
+    }
+    
+    // TODO: scaling support
+    vec3 meshletPositionWorld = (vec4(meshlet.meanPosition, 1) * objectMatrices[matrixIndex].model).xyz;
+    if(isSphereInsideFrustum(meshletPositionWorld, meshlet.boundingSphereRadius, cameraPlanes)){
+        uint outIndex = atomicAdd(taskCount, 1);
+        OUT.meshletIndices[outIndex] = gl_GlobalInvocationID.x;
+    }
+
+    if(gl_LocalInvocationID.x == 0){
+        gl_TaskCountNV              = taskCount;
+        OUT.mvp = objectMatrices[matrixIndex].mvp;
+    }
+}
\ No newline at end of file
diff --git a/projects/mesh_shader/resources/shaders/shader.vert b/projects/mesh_shader/resources/shaders/shader.vert
new file mode 100644
index 0000000000000000000000000000000000000000..fca5057976f995183c040195bdbd592c63f1074e
--- /dev/null
+++ b/projects/mesh_shader/resources/shaders/shader.vert
@@ -0,0 +1,29 @@
+#version 450
+#extension GL_ARB_separate_shader_objects   : enable
+#extension GL_GOOGLE_include_directive      : enable
+
+#include "common.inc"
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 1) in vec3 inNormal;
+
+layout(location = 0) out vec3 passNormal;
+layout(location = 1) out uint dummyOutput;
+
+layout(std430, binding = 0) readonly buffer matrixBuffer
+{
+    ObjectMatrices objectMatrices[];
+};
+
+layout( push_constant ) uniform constants{
+    uint matrixIndex;
+    uint padding; // pad to same size as mesh shader constants
+};
+
+
+void main()	{
+	gl_Position = objectMatrices[matrixIndex].mvp * vec4(inPosition, 1.0);
+	passNormal  = inNormal;
+    
+    dummyOutput = padding * 0;  // padding must be used, else compiler shrinks constant size
+}
\ No newline at end of file
diff --git a/projects/mesh_shader/src/main.cpp b/projects/mesh_shader/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3a94de5842f3e70625729c9755b8c88048ece2ec
--- /dev/null
+++ b/projects/mesh_shader/src/main.cpp
@@ -0,0 +1,392 @@
+#include <iostream>
+#include <vkcv/Core.hpp>
+#include <GLFW/glfw3.h>
+#include <vkcv/camera/CameraManager.hpp>
+#include <chrono>
+
+#include <vkcv/shader/GLSLCompiler.hpp>
+#include <vkcv/gui/GUI.hpp>
+#include <vkcv/asset/asset_loader.hpp>
+#include <vkcv/meshlet/Meshlet.hpp>
+#include <vkcv/meshlet/Tipsify.hpp>
+#include <vkcv/meshlet/Forsyth.hpp>
+
+struct Plane {
+	glm::vec3 pointOnPlane;
+	float padding0;
+	glm::vec3 normal;
+	float padding1;
+};
+
+struct CameraPlanes {
+	Plane planes[6];
+};
+
+CameraPlanes computeCameraPlanes(const vkcv::camera::Camera& camera) {
+	const float     fov     = camera.getFov();
+	const glm::vec3 pos     = camera.getPosition();
+	const float     ratio   = camera.getRatio();
+	const glm::vec3 forward = glm::normalize(camera.getFront());
+	float near;
+	float far;
+	camera.getNearFar(near, far);
+
+	glm::vec3 up    = glm::vec3(0, -1, 0);
+	glm::vec3 right = glm::normalize(glm::cross(forward, up));
+	up              = glm::cross(forward, right);
+
+	const glm::vec3 nearCenter      = pos + forward * near;
+	const glm::vec3 farCenter       = pos + forward * far;
+
+	const float tanFovHalf          = glm::tan(fov / 2);
+
+	const glm::vec3 nearUpCenter    = nearCenter + up    * tanFovHalf * near;
+	const glm::vec3 nearDownCenter  = nearCenter - up    * tanFovHalf * near;
+	const glm::vec3 nearRightCenter = nearCenter + right * tanFovHalf * near * ratio;
+	const glm::vec3 nearLeftCenter  = nearCenter - right * tanFovHalf * near * ratio;
+
+	const glm::vec3 farUpCenter     = farCenter + up    * tanFovHalf * far;
+	const glm::vec3 farDownCenter   = farCenter - up    * tanFovHalf * far;
+	const glm::vec3 farRightCenter  = farCenter + right * tanFovHalf * far * ratio;
+	const glm::vec3 farLeftCenter   = farCenter - right * tanFovHalf * far * ratio;
+
+	CameraPlanes cameraPlanes;
+	// near
+	cameraPlanes.planes[0].pointOnPlane = nearCenter;
+	cameraPlanes.planes[0].normal       = -forward;
+	// far
+	cameraPlanes.planes[1].pointOnPlane = farCenter;
+	cameraPlanes.planes[1].normal       = forward;
+
+	// top
+	cameraPlanes.planes[2].pointOnPlane = nearUpCenter;
+	cameraPlanes.planes[2].normal       = glm::normalize(glm::cross(farUpCenter - nearUpCenter, right));
+	// bot
+	cameraPlanes.planes[3].pointOnPlane = nearDownCenter;
+	cameraPlanes.planes[3].normal       = glm::normalize(glm::cross(right, farDownCenter - nearDownCenter));
+
+	// right
+	cameraPlanes.planes[4].pointOnPlane = nearRightCenter;
+	cameraPlanes.planes[4].normal       = glm::normalize(glm::cross(up, farRightCenter - nearRightCenter));
+	// left
+	cameraPlanes.planes[5].pointOnPlane = nearLeftCenter;
+	cameraPlanes.planes[5].normal       = glm::normalize(glm::cross(farLeftCenter - nearLeftCenter, up));
+
+	return cameraPlanes;
+}
+
+int main(int argc, const char** argv) {
+	const char* applicationName = "Mesh shader";
+
+	const int windowWidth = 1280;
+	const int windowHeight = 720;
+	vkcv::Window window = vkcv::Window::create(
+		applicationName,
+		windowWidth,
+		windowHeight,
+		false
+	);
+
+	vkcv::Core core = vkcv::Core::create(
+		window,
+		applicationName,
+		VK_MAKE_VERSION(0, 0, 1),
+		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
+		{},
+		{ "VK_KHR_swapchain", VK_NV_MESH_SHADER_EXTENSION_NAME }
+	);
+
+    vkcv::gui::GUI gui (core, window);
+
+    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);
+
+    assert(!mesh.vertexGroups.empty());
+
+    auto vertexBuffer = core.createBuffer<uint8_t>(
+            vkcv::BufferType::VERTEX,
+            mesh.vertexGroups[0].vertexBuffer.data.size(),
+            vkcv::BufferMemoryType::DEVICE_LOCAL
+    );
+    vertexBuffer.fill(mesh.vertexGroups[0].vertexBuffer.data);
+
+    auto indexBuffer = core.createBuffer<uint8_t>(
+            vkcv::BufferType::INDEX,
+            mesh.vertexGroups[0].indexBuffer.data.size(),
+            vkcv::BufferMemoryType::DEVICE_LOCAL
+    );
+    indexBuffer.fill(mesh.vertexGroups[0].indexBuffer.data);
+
+	// format data for mesh shader
+	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
+
+	std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
+		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
+	});
+
+	const std::vector<vkcv::VertexBufferBinding> vertexBufferBindings = {
+			vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[0].offset), vertexBuffer.getVulkanHandle()),
+			vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[1].offset), vertexBuffer.getVulkanHandle()),
+			vkcv::VertexBufferBinding(static_cast<vk::DeviceSize>(attributes[2].offset), vertexBuffer.getVulkanHandle()) };
+
+	const auto& bunny = mesh.vertexGroups[0];
+	std::vector<vkcv::meshlet::Vertex> interleavedVertices = vkcv::meshlet::convertToVertices(bunny.vertexBuffer.data, bunny.numVertices, attributes[0], attributes[1]);
+	// mesh shader buffers
+	const auto& assetLoaderIndexBuffer                    = mesh.vertexGroups[0].indexBuffer;
+	std::vector<uint32_t> indexBuffer32Bit                = vkcv::meshlet::assetLoaderIndicesTo32BitIndices(assetLoaderIndexBuffer.data, assetLoaderIndexBuffer.type);
+    vkcv::meshlet::VertexCacheReorderResult tipsifyResult = vkcv::meshlet::tipsifyMesh(indexBuffer32Bit, interleavedVertices.size());
+    vkcv::meshlet::VertexCacheReorderResult forsythResult = vkcv::meshlet::forsythReorder(indexBuffer32Bit, interleavedVertices.size());
+
+    const auto meshShaderModelData = createMeshShaderModelData(interleavedVertices, forsythResult.indexBuffer, forsythResult.skippedIndices);
+
+	auto meshShaderVertexBuffer = core.createBuffer<vkcv::meshlet::Vertex>(
+		vkcv::BufferType::STORAGE,
+		meshShaderModelData.vertices.size());
+	meshShaderVertexBuffer.fill(meshShaderModelData.vertices);
+
+	auto meshShaderIndexBuffer = core.createBuffer<uint32_t>(
+		vkcv::BufferType::STORAGE,
+		meshShaderModelData.localIndices.size());
+	meshShaderIndexBuffer.fill(meshShaderModelData.localIndices);
+
+	auto meshletBuffer = core.createBuffer<vkcv::meshlet::Meshlet>(
+		vkcv::BufferType::STORAGE,
+		meshShaderModelData.meshlets.size(),
+		vkcv::BufferMemoryType::DEVICE_LOCAL
+		);
+	meshletBuffer.fill(meshShaderModelData.meshlets);
+
+	// attachments
+	const vkcv::AttachmentDescription present_color_attachment(
+		vkcv::AttachmentOperation::STORE,
+		vkcv::AttachmentOperation::CLEAR,
+		core.getSwapchain().getFormat());
+
+    const vkcv::AttachmentDescription depth_attachment(
+            vkcv::AttachmentOperation::STORE,
+            vkcv::AttachmentOperation::CLEAR,
+            vk::Format::eD32Sfloat
+    );
+
+	vkcv::PassConfig bunnyPassDefinition({ present_color_attachment, depth_attachment });
+	vkcv::PassHandle renderPass = core.createPass(bunnyPassDefinition);
+
+	if (!renderPass)
+	{
+		std::cout << "Error. Could not create renderpass. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	vkcv::ShaderProgram bunnyShaderProgram{};
+	vkcv::shader::GLSLCompiler compiler;
+	
+	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"),
+					 [&bunnyShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		 bunnyShaderProgram.addShader(shaderStage, path);
+	});
+	
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+					 [&bunnyShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		bunnyShaderProgram.addShader(shaderStage, path);
+	});
+
+    const std::vector<vkcv::VertexAttachment> vertexAttachments = bunnyShaderProgram.getVertexAttachments();
+    std::vector<vkcv::VertexBinding> bindings;
+    for (size_t i = 0; i < vertexAttachments.size(); i++) {
+        bindings.push_back(vkcv::VertexBinding(i, { vertexAttachments[i] }));
+    }
+    const vkcv::VertexLayout bunnyLayout (bindings);
+
+	vkcv::DescriptorSetHandle vertexShaderDescriptorSet = core.createDescriptorSet(bunnyShaderProgram.getReflectedDescriptors()[0]);
+
+	const vkcv::PipelineConfig bunnyPipelineDefinition {
+			bunnyShaderProgram,
+			(uint32_t)windowWidth,
+			(uint32_t)windowHeight,
+			renderPass,
+			{ bunnyLayout },
+			{ core.getDescriptorSet(vertexShaderDescriptorSet).layout },
+			false
+	};
+
+	struct ObjectMatrices {
+		glm::mat4 model;
+		glm::mat4 mvp;
+	};
+	const size_t objectCount = 1;
+	vkcv::Buffer<ObjectMatrices> matrixBuffer = core.createBuffer<ObjectMatrices>(vkcv::BufferType::STORAGE, objectCount);
+
+	vkcv::DescriptorWrites vertexShaderDescriptorWrites;
+	vertexShaderDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, matrixBuffer.getHandle()) };
+	core.writeDescriptorSet(vertexShaderDescriptorSet, vertexShaderDescriptorWrites);
+
+	vkcv::PipelineHandle bunnyPipeline = core.createGraphicsPipeline(bunnyPipelineDefinition);
+
+	if (!bunnyPipeline)
+	{
+		std::cout << "Error. Could not create graphics pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	// mesh shader
+	vkcv::ShaderProgram meshShaderProgram;
+	compiler.compile(vkcv::ShaderStage::TASK, std::filesystem::path("resources/shaders/shader.task"),
+		[&meshShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		meshShaderProgram.addShader(shaderStage, path);
+	});
+
+	compiler.compile(vkcv::ShaderStage::MESH, std::filesystem::path("resources/shaders/shader.mesh"),
+		[&meshShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		meshShaderProgram.addShader(shaderStage, path);
+	});
+
+	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
+		[&meshShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		meshShaderProgram.addShader(shaderStage, path);
+	});
+
+	uint32_t setID = 0;
+	vkcv::DescriptorSetHandle meshShaderDescriptorSet = core.createDescriptorSet( meshShaderProgram.getReflectedDescriptors()[setID]);
+	const vkcv::VertexLayout meshShaderLayout(bindings);
+
+	const vkcv::PipelineConfig meshShaderPipelineDefinition{
+		meshShaderProgram,
+		(uint32_t)windowWidth,
+		(uint32_t)windowHeight,
+		renderPass,
+		{meshShaderLayout},
+		{core.getDescriptorSet(meshShaderDescriptorSet).layout},
+		false
+	};
+
+	vkcv::PipelineHandle meshShaderPipeline = core.createGraphicsPipeline(meshShaderPipelineDefinition);
+
+	if (!meshShaderPipeline)
+	{
+		std::cout << "Error. Could not create mesh shader pipeline. Exiting." << std::endl;
+		return EXIT_FAILURE;
+	}
+
+	vkcv::Buffer<CameraPlanes> cameraPlaneBuffer = core.createBuffer<CameraPlanes>(vkcv::BufferType::UNIFORM, 1);
+
+	vkcv::DescriptorWrites meshShaderWrites;
+	meshShaderWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(0, meshShaderVertexBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(1, meshShaderIndexBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(2, meshletBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(4, matrixBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(5, meshletBuffer.getHandle()),
+	};
+	meshShaderWrites.uniformBufferWrites = {
+		vkcv::BufferDescriptorWrite(3, cameraPlaneBuffer.getHandle()),
+	};
+
+    core.writeDescriptorSet( meshShaderDescriptorSet, meshShaderWrites);
+
+    vkcv::ImageHandle depthBuffer = core.createImage(vk::Format::eD32Sfloat, windowWidth, windowHeight, 1, false).getHandle();
+
+    auto start = std::chrono::system_clock::now();
+
+	vkcv::ImageHandle swapchainImageHandle = vkcv::ImageHandle::createSwapchainImageHandle();
+
+    const vkcv::Mesh renderMesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), mesh.vertexGroups[0].numIndices, vkcv::IndexBitCount::Bit32);
+
+	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
+
+	vkcv::camera::CameraManager cameraManager(window);
+	uint32_t camIndex0 = cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	
+	cameraManager.getCamera(camIndex0).setPosition(glm::vec3(0, 0, -2));
+
+	bool useMeshShader          = true;
+	bool updateFrustumPlanes    = true;
+
+	while (window.isWindowOpen())
+	{
+		vkcv::Window::pollEvents();
+
+		uint32_t swapchainWidth, swapchainHeight; // No resizing = No problem
+		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
+			continue;
+		}
+		
+		auto end = std::chrono::system_clock::now();
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
+		start = end;
+		
+		cameraManager.update(0.000001 * static_cast<double>(deltatime.count()));
+
+		const vkcv::camera::Camera& camera = cameraManager.getActiveCamera();
+
+		ObjectMatrices objectMatrices;
+		objectMatrices.model    = *reinterpret_cast<glm::mat4*>(&mesh.meshes.front().modelMatrix);
+		objectMatrices.mvp      = camera.getMVP() * objectMatrices.model;
+
+		matrixBuffer.fill({ objectMatrices });
+
+		struct PushConstants {
+			uint32_t matrixIndex;
+			uint32_t meshletCount;
+		};
+		PushConstants pushConstants{ 0, static_cast<uint32_t>(meshShaderModelData.meshlets.size()) };
+
+		if (updateFrustumPlanes) {
+			const CameraPlanes cameraPlanes = computeCameraPlanes(camera);
+			cameraPlaneBuffer.fill({ cameraPlanes });
+		}
+
+		const std::vector<vkcv::ImageHandle> renderTargets = { swapchainInput, depthBuffer };
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+
+		vkcv::PushConstants pushConstantData(sizeof(pushConstants));
+		pushConstantData.appendDrawcall(pushConstants);
+
+		if (useMeshShader) {
+
+			vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(meshShaderDescriptorSet).vulkanHandle);
+			const uint32_t taskCount = (meshShaderModelData.meshlets.size() + 31) / 32;
+
+			core.recordMeshShaderDrawcalls(
+				cmdStream,
+				renderPass,
+				meshShaderPipeline,
+				pushConstantData,
+				{ vkcv::MeshShaderDrawcall({descriptorUsage}, taskCount)},
+				{ renderTargets });
+		}
+		else {
+
+			vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(vertexShaderDescriptorSet).vulkanHandle);
+
+			core.recordDrawcallsToCmdStream(
+				cmdStream,
+				renderPass,
+				bunnyPipeline,
+				pushConstantData,
+				{ vkcv::DrawcallInfo(renderMesh, { descriptorUsage }) },
+				{ renderTargets });
+		}
+
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+		
+		gui.beginGUI();
+		
+		ImGui::Begin("Settings");
+		ImGui::Checkbox("Use mesh shader", &useMeshShader);
+		ImGui::Checkbox("Update frustum culling", &updateFrustumPlanes);
+
+		ImGui::End();
+		
+		gui.endGUI();
+
+		core.endFrame();
+	}
+	return 0;
+}
diff --git a/projects/neural_network/src/main.cpp b/projects/neural_network/src/main.cpp
index 2645f2077fa4392ce24a38f72fb5deeccb23c2ca..648b025e31cfed2cf06706d61ce9e013ae5a07ca 100644
--- a/projects/neural_network/src/main.cpp
+++ b/projects/neural_network/src/main.cpp
@@ -55,7 +55,7 @@ int main(int argc, const char** argv) {
     vkcv::PipelineHandle computePipeline = core.createComputePipeline(computeShaderProgram, { core.getDescriptorSet(computeDescriptorSet).layout });
 
     vkcv::DescriptorWrites computeWrites;
-    computeWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0,inputBuffer.getHandle()) };
+    computeWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0,inputBuffer.getHandle()) };
     core.writeDescriptorSet(computeDescriptorSet,  computeWrites);
 
     if (!computePipeline)
diff --git a/projects/particle_simulation/src/BloomAndFlares.cpp b/projects/particle_simulation/src/BloomAndFlares.cpp
index 98d53c2a1a2c08d40473858b47aacf34da30f7ed..5961aae664a39dfb9bd597ffa7648c9b67999af4 100644
--- a/projects/particle_simulation/src/BloomAndFlares.cpp
+++ b/projects/particle_simulation/src/BloomAndFlares.cpp
@@ -263,6 +263,10 @@ void BloomAndFlares::execWholePipeline(const vkcv::CommandStreamHandle &cmdStrea
 
 void BloomAndFlares::updateImageDimensions(uint32_t width, uint32_t height)
 {
+    if ((width == m_Width) && (height == m_Height)) {
+        return;
+    }
+    
     m_Width  = width;
     m_Height = height;
 
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
index 0d83644b866f5f89fb33c68f1d5a79fcee8c028a..07ba6b194ce72dbad15a921ca13a4814c6d4f5df 100644
--- a/projects/particle_simulation/src/main.cpp
+++ b/projects/particle_simulation/src/main.cpp
@@ -163,13 +163,13 @@ int main(int argc, const char **argv) {
     particleBuffer.fill(particleSystem.getParticles());
 
     vkcv::DescriptorWrites setWrites;
-    setWrites.uniformBufferWrites = {vkcv::UniformBufferDescriptorWrite(0,color.getHandle()),
-                                     vkcv::UniformBufferDescriptorWrite(1,position.getHandle())};
-    setWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(2,particleBuffer.getHandle())};
+    setWrites.uniformBufferWrites = {vkcv::BufferDescriptorWrite(0,color.getHandle()),
+                                     vkcv::BufferDescriptorWrite(1,position.getHandle())};
+    setWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(2,particleBuffer.getHandle())};
     core.writeDescriptorSet(descriptorSet, setWrites);
 
     vkcv::DescriptorWrites computeWrites;
-    computeWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0,particleBuffer.getHandle())};
+    computeWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0,particleBuffer.getHandle())};
     core.writeDescriptorSet(computeDescriptorSet, computeWrites);
 
     if (!particlePipeline || !computePipeline)
diff --git a/projects/voxelization/CMakeLists.txt b/projects/voxelization/CMakeLists.txt
index c962409f2e14994f0c38b923de7b9b1a4d198cab..d2f533b0f9c7313ddcc6046fb29378c3a507d1fe 100644
--- a/projects/voxelization/CMakeLists.txt
+++ b/projects/voxelization/CMakeLists.txt
@@ -30,7 +30,7 @@ if(MSVC)
 endif()
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(voxelization SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
+target_include_directories(voxelization SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include} ${vkcv_upscaling_include})
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(voxelization vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler vkcv_gui)
+target_link_libraries(voxelization vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_camera vkcv_shader_compiler vkcv_gui vkcv_upscaling)
diff --git a/projects/voxelization/resources/shaders/postEffects.comp b/projects/voxelization/resources/shaders/postEffects.comp
new file mode 100644
index 0000000000000000000000000000000000000000..c0f9fe1a764bcdabac5501e2f82692c6f476e9e6
--- /dev/null
+++ b/projects/voxelization/resources/shaders/postEffects.comp
@@ -0,0 +1,149 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "luma.inc"
+
+layout(set=0, binding=0)        uniform texture2D   inTexture;
+layout(set=0, binding=1)        uniform sampler     textureSampler;
+layout(set=0, binding=2, rgba8) uniform image2D     outImage;
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    float time;
+};
+
+// from: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
+vec3 ACESFilm(vec3 x)
+{
+    float a = 2.51f;
+    float b = 0.03f;
+    float c = 2.43f;
+    float d = 0.59f;
+    float e = 0.14f;
+    return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0, 1);
+}
+
+// From Dave Hoskins: https://www.shadertoy.com/view/4djSRW.
+float hash(vec3 p3){
+    p3 = fract(p3 * 0.1031);
+    p3 += dot(p3,p3.yzx + 19.19);
+    return fract((p3.x + p3.y) * p3.z);
+}
+
+// From iq: https://www.shadertoy.com/view/4sfGzS.
+float noise(vec3 x){
+    vec3 i = floor(x);
+    vec3 f = fract(x);
+    f = f*f*(3.0-2.0*f);
+    return mix(mix(mix(hash(i+vec3(0, 0, 0)),
+    hash(i+vec3(1, 0, 0)),f.x),
+    mix(hash(i+vec3(0, 1, 0)),
+    hash(i+vec3(1, 1, 0)),f.x),f.y),
+    mix(mix(hash(i+vec3(0, 0, 1)),
+    hash(i+vec3(1, 0, 1)),f.x),
+    mix(hash(i+vec3(0, 1, 1)),
+    hash(i+vec3(1, 1, 1)),f.x),f.y),f.z);
+}
+
+// From: https://www.shadertoy.com/view/3sGSWVF
+// Slightly high-passed continuous value-noise.
+float grainSource(vec3 x, float strength, float pitch){
+    float center = noise(x);
+    float v1 = center - noise(vec3( 1, 0, 0)/pitch + x) + 0.5;
+    float v2 = center - noise(vec3( 0, 1, 0)/pitch + x) + 0.5;
+    float v3 = center - noise(vec3(-1, 0, 0)/pitch + x) + 0.5;
+    float v4 = center - noise(vec3( 0,-1, 0)/pitch + x) + 0.5;
+
+    float total = (v1 + v2 + v3 + v4) / 4.0;
+    return mix(1, 0.5 + total, strength);
+}
+
+vec3 applyGrain(ivec2 uv, vec3 c){
+    float grainLift     = 0.6;
+    float grainStrength = 0.4;
+    float grainTimeFactor = 0.1;
+
+    float timeColorOffset = 1.2;
+    vec3 grain = vec3(
+    grainSource(vec3(uv, floor(grainTimeFactor*time)),                   grainStrength, grainLift),
+    grainSource(vec3(uv, floor(grainTimeFactor*time + timeColorOffset)), grainStrength, grainLift),
+    grainSource(vec3(uv, floor(grainTimeFactor*time - timeColorOffset)), grainStrength, grainLift));
+
+    return c * grain;
+}
+
+vec2 computeDistortedUV(vec2 uv, float aspectRatio){
+    uv          = uv * 2 - 1;
+    float   r2  = dot(uv, uv);
+    float   k1  = 0.02f;
+
+    float maxR2     = dot(vec2(1), vec2(1));
+    float maxFactor = maxR2 * k1;
+
+    // correction only needed for pincushion distortion
+    maxFactor       = min(maxFactor, 0);
+
+    uv /= 1 + r2*k1;
+
+    // correction to avoid going out of [-1, 1] range when using barrel distortion
+    uv *= 1 + maxFactor;
+
+    return uv * 0.5 + 0.5;
+}
+
+float computeLocalContrast(vec2 uv){
+    float lumaMin = 100;
+    float lumaMax = 0;
+
+    vec2 pixelSize = vec2(1) / textureSize(sampler2D(inTexture, textureSampler), 0);
+
+    for(int x = -1; x <= 1; x++){
+        for(int y = -1; y <= 1; y++){
+            vec3 c = texture(sampler2D(inTexture, textureSampler), uv + vec2(x, y) * pixelSize).rgb;
+            float luma  = computeLuma(c);
+            lumaMin     = min(lumaMin, luma);
+            lumaMax     = max(lumaMax, luma);
+        }
+    }
+
+    return lumaMax - lumaMin;
+}
+
+vec3 computeChromaticAberrationScale(vec2 uv){
+    float   localContrast   = computeLocalContrast(uv);
+    vec3    colorScales     = vec3(-1, 0, 1);
+    float   aberrationScale = 0.004;
+    vec3    maxScaleFactors = colorScales * aberrationScale;
+    float   factor          = clamp(localContrast, 0, 1);
+    return mix(vec3(0), maxScaleFactors, factor);
+}
+
+vec3 sampleColorChromaticAberration(vec2 uv){
+    vec2 toCenter       = (vec2(0.5) - uv);
+
+    vec3 scaleFactors = computeChromaticAberrationScale(uv);
+
+    float r = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.r).r;
+    float g = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.g).g;
+    float b = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.b).b;
+    return vec3(r, g, b);
+}
+
+void main(){
+
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
+        return;
+    }
+    ivec2   textureRes  = textureSize(sampler2D(inTexture, textureSampler), 0);
+    ivec2   coord       = ivec2(gl_GlobalInvocationID.xy);
+    vec2    uv          = vec2(coord) / textureRes;
+    float   aspectRatio = float(textureRes.x) / textureRes.y;
+    uv                  = computeDistortedUV(uv, aspectRatio);
+
+    vec3 tonemapped    = sampleColorChromaticAberration(uv);
+    tonemapped          = applyGrain(coord, tonemapped);
+
+    vec3 gammaCorrected = pow(tonemapped, vec3(1.f / 2.2f));
+    imageStore(outImage, coord, vec4(gammaCorrected, 0.f));
+}
\ No newline at end of file
diff --git a/projects/voxelization/resources/shaders/tonemapping.comp b/projects/voxelization/resources/shaders/tonemapping.comp
index 8fa07d39ebb56eab857cdccb755a6558f5ae1ec3..ffadc9a71e207f97fec9a8815aa1c61bc709c369 100644
--- a/projects/voxelization/resources/shaders/tonemapping.comp
+++ b/projects/voxelization/resources/shaders/tonemapping.comp
@@ -1,18 +1,12 @@
 #version 440
 #extension GL_GOOGLE_include_directive : enable
 
-#include "luma.inc"
-
 layout(set=0, binding=0)        uniform texture2D   inTexture;
 layout(set=0, binding=1)        uniform sampler     textureSampler;
 layout(set=0, binding=2, rgba8) uniform image2D     outImage;
 
 layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
 
-layout( push_constant ) uniform constants{
-    float time;
-};
-
 // from: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
 vec3 ACESFilm(vec3 x)
 {
@@ -24,112 +18,6 @@ vec3 ACESFilm(vec3 x)
     return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0, 1);
 }
 
-// From Dave Hoskins: https://www.shadertoy.com/view/4djSRW.
-float hash(vec3 p3){
-    p3 = fract(p3 * 0.1031);
-    p3 += dot(p3,p3.yzx + 19.19);
-    return fract((p3.x + p3.y) * p3.z);
-}
-
-// From iq: https://www.shadertoy.com/view/4sfGzS.
-float noise(vec3 x){
-    vec3 i = floor(x);
-    vec3 f = fract(x);
-    f = f*f*(3.0-2.0*f);
-    return mix(mix(mix(hash(i+vec3(0, 0, 0)), 
-                       hash(i+vec3(1, 0, 0)),f.x),
-                   mix(hash(i+vec3(0, 1, 0)), 
-                       hash(i+vec3(1, 1, 0)),f.x),f.y),
-               mix(mix(hash(i+vec3(0, 0, 1)), 
-                       hash(i+vec3(1, 0, 1)),f.x),
-                   mix(hash(i+vec3(0, 1, 1)), 
-                       hash(i+vec3(1, 1, 1)),f.x),f.y),f.z);
-}
-
-// From: https://www.shadertoy.com/view/3sGSWVF
-// Slightly high-passed continuous value-noise.
-float grainSource(vec3 x, float strength, float pitch){
-    float center = noise(x);
-	float v1 = center - noise(vec3( 1, 0, 0)/pitch + x) + 0.5;
-	float v2 = center - noise(vec3( 0, 1, 0)/pitch + x) + 0.5;
-	float v3 = center - noise(vec3(-1, 0, 0)/pitch + x) + 0.5;
-	float v4 = center - noise(vec3( 0,-1, 0)/pitch + x) + 0.5;
-    
-	float total = (v1 + v2 + v3 + v4) / 4.0;
-	return mix(1, 0.5 + total, strength);
-}
-
-vec3 applyGrain(ivec2 uv, vec3 c){
-    float grainLift     = 0.6;
-    float grainStrength = 0.4;
-    float grainTimeFactor = 0.1;
-    
-    float timeColorOffset = 1.2;
-    vec3 grain = vec3(
-        grainSource(vec3(uv, floor(grainTimeFactor*time)),                   grainStrength, grainLift),
-        grainSource(vec3(uv, floor(grainTimeFactor*time + timeColorOffset)), grainStrength, grainLift),
-        grainSource(vec3(uv, floor(grainTimeFactor*time - timeColorOffset)), grainStrength, grainLift));
-    
-    return c * grain;
-}
-
-vec2 computeDistortedUV(vec2 uv, float aspectRatio){
-    uv          = uv * 2 - 1;
-    float   r2  = dot(uv, uv);
-    float   k1  = 0.02f;
-    
-    float maxR2     = dot(vec2(1), vec2(1));
-    float maxFactor = maxR2 * k1;
-    
-    // correction only needed for pincushion distortion
-    maxFactor       = min(maxFactor, 0);
-    
-    uv /= 1 + r2*k1;
-    
-    // correction to avoid going out of [-1, 1] range when using barrel distortion 
-    uv *= 1 + maxFactor;
-    
-    return uv * 0.5 + 0.5;
-}
-
-float computeLocalContrast(vec2 uv){
-    float lumaMin = 100;
-    float lumaMax = 0;
-    
-    vec2 pixelSize = vec2(1) / textureSize(sampler2D(inTexture, textureSampler), 0);
-    
-    for(int x = -1; x <= 1; x++){
-        for(int y = -1; y <= 1; y++){
-            vec3 c = texture(sampler2D(inTexture, textureSampler), uv + vec2(x, y) * pixelSize).rgb;
-            float luma  = computeLuma(c);
-            lumaMin     = min(lumaMin, luma);
-            lumaMax     = max(lumaMax, luma);
-        }
-    }
-    
-    return lumaMax - lumaMin;
-}
-
-vec3 computeChromaticAberrationScale(vec2 uv){
-    float   localContrast   = computeLocalContrast(uv);
-    vec3    colorScales     = vec3(-1, 0, 1);
-    float   aberrationScale = 0.004;
-    vec3    maxScaleFactors = colorScales * aberrationScale;
-    float   factor          = clamp(localContrast, 0, 1);
-    return mix(vec3(0), maxScaleFactors, factor);
-}
-
-vec3 sampleColorChromaticAberration(vec2 uv){
-    vec2 toCenter       = (vec2(0.5) - uv);
-    
-    vec3 scaleFactors = computeChromaticAberrationScale(uv);
-    
-    float r = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.r).r;
-    float g = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.g).g;
-    float b = texture(sampler2D(inTexture, textureSampler), uv + toCenter * scaleFactors.b).b;
-    return vec3(r, g, b);
-}
-
 void main(){
 
     if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage)))){
@@ -138,12 +26,9 @@ void main(){
     ivec2   textureRes  = textureSize(sampler2D(inTexture, textureSampler), 0);
     ivec2   coord       = ivec2(gl_GlobalInvocationID.xy);
     vec2    uv          = vec2(coord) / textureRes;
-    float   aspectRatio = float(textureRes.x) / textureRes.y;
-    uv                  = computeDistortedUV(uv, aspectRatio);
-    vec3 linearColor    = sampleColorChromaticAberration(uv);
+
+    vec3 linearColor    = texture(sampler2D(inTexture, textureSampler), uv).rgb;
     vec3 tonemapped     = ACESFilm(linearColor);
-    tonemapped          = applyGrain(coord, tonemapped);
-    
-    vec3 gammaCorrected = pow(tonemapped, vec3(1.f / 2.2f));
-    imageStore(outImage, coord, vec4(gammaCorrected, 0.f));
+
+    imageStore(outImage, coord, vec4(tonemapped, 0.f));
 }
\ No newline at end of file
diff --git a/projects/voxelization/src/Voxelization.cpp b/projects/voxelization/src/Voxelization.cpp
index bbf161ddeb0899a1ce61279b4c476fb19cb906d7..f7e03709c6423ef0e3c43251afb28e887b9be61f 100644
--- a/projects/voxelization/src/Voxelization.cpp
+++ b/projects/voxelization/src/Voxelization.cpp
@@ -119,10 +119,10 @@ Voxelization::Voxelization(
 	m_voxelizationPipe = m_corePtr->createGraphicsPipeline(voxelizationPipeConfig);
 
 	vkcv::DescriptorWrites voxelizationDescriptorWrites;
-	voxelizationDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	voxelizationDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
 	voxelizationDescriptorWrites.uniformBufferWrites = { 
-		vkcv::UniformBufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()),
-		vkcv::UniformBufferDescriptorWrite(3, lightInfoBuffer)
+		vkcv::BufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(3, lightInfoBuffer)
 	};
 	voxelizationDescriptorWrites.sampledImageWrites = { vkcv::SampledImageDescriptorWrite(4, shadowMap) };
 	voxelizationDescriptorWrites.samplerWrites      = { vkcv::SamplerDescriptorWrite(5, shadowSampler) };
@@ -180,7 +180,7 @@ Voxelization::Voxelization(
 		{ m_corePtr->getDescriptorSet(m_voxelResetDescriptorSet).layout });
 
 	vkcv::DescriptorWrites resetVoxelWrites;
-	resetVoxelWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	resetVoxelWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
 	m_corePtr->writeDescriptorSet(m_voxelResetDescriptorSet, resetVoxelWrites);
 
 	// buffer to image
@@ -192,7 +192,7 @@ Voxelization::Voxelization(
 		{ m_corePtr->getDescriptorSet(m_bufferToImageDescriptorSet).layout });
 
 	vkcv::DescriptorWrites bufferToImageDescriptorWrites;
-	bufferToImageDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	bufferToImageDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
 	bufferToImageDescriptorWrites.storageImageWrites = { vkcv::StorageImageDescriptorWrite(1, m_voxelImageIntermediate.getHandle()) };
 	m_corePtr->writeDescriptorSet(m_bufferToImageDescriptorSet, bufferToImageDescriptorWrites);
 
@@ -205,11 +205,11 @@ Voxelization::Voxelization(
 		{ m_corePtr->getDescriptorSet(m_secondaryBounceDescriptorSet).layout });
 
 	vkcv::DescriptorWrites secondaryBounceDescriptorWrites;
-	secondaryBounceDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
+	secondaryBounceDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
 	secondaryBounceDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(1, m_voxelImageIntermediate.getHandle()) };
 	secondaryBounceDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(2, voxelSampler) };
 	secondaryBounceDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(3, m_voxelImage.getHandle()) };
-	secondaryBounceDescriptorWrites.uniformBufferWrites = { vkcv::UniformBufferDescriptorWrite(4, m_voxelInfoBuffer.getHandle()) };
+	secondaryBounceDescriptorWrites.uniformBufferWrites = { vkcv::BufferDescriptorWrite(4, m_voxelInfoBuffer.getHandle()) };
 	m_corePtr->writeDescriptorSet(m_secondaryBounceDescriptorSet, secondaryBounceDescriptorWrites);
 }
 
@@ -331,7 +331,7 @@ void Voxelization::renderVoxelVisualisation(
 	voxelVisualisationDescriptorWrite.storageImageWrites =
 	{ vkcv::StorageImageDescriptorWrite(0, m_voxelImage.getHandle(), mipLevel) };
 	voxelVisualisationDescriptorWrite.uniformBufferWrites =
-	{ vkcv::UniformBufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()) };
+	{ vkcv::BufferDescriptorWrite(1, m_voxelInfoBuffer.getHandle()) };
 	m_corePtr->writeDescriptorSet(m_visualisationDescriptorSet, voxelVisualisationDescriptorWrite);
 
 	uint32_t drawVoxelCount = voxelCount / exp2(mipLevel);
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index ca9951490e57b4b6afa3bbee986a55342a40582e..e7f9caa493714d30f13f64c292f1b6e51e5170b1 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -11,6 +11,8 @@
 #include "vkcv/gui/GUI.hpp"
 #include "ShadowMapping.hpp"
 #include "BloomAndFlares.hpp"
+#include <vkcv/upscaling/FSRUpscaling.hpp>
+#include <vkcv/upscaling/BilinearUpscaling.hpp>
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "Voxelization";
@@ -27,11 +29,11 @@ int main(int argc, const char** argv) {
 		true
 	);
 
-	bool    isFullscreen            = false;
-	int     windowedWidthBackup     = windowWidth;
-	int     windowedHeightBackup    = windowHeight;
-	int     windowedPosXBackup;
-	int     windowedPosYBackup;
+	bool     isFullscreen            = false;
+	uint32_t windowedWidthBackup     = windowWidth;
+	uint32_t windowedHeightBackup    = windowHeight;
+	int      windowedPosXBackup;
+	int      windowedPosYBackup;
     glfwGetWindowPos(window.getWindow(), &windowedPosXBackup, &windowedPosYBackup);
 
 	window.e_key.add([&](int key, int scancode, int action, int mods) {
@@ -85,7 +87,7 @@ int main(int argc, const char** argv) {
 		VK_MAKE_VERSION(0, 0, 1),
 		{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
 		{},
-		{ "VK_KHR_swapchain" }
+		{ "VK_KHR_swapchain", "VK_KHR_shader_float16_int8", "VK_KHR_16bit_storage" }
 	);
 
 	vkcv::asset::Scene mesh;
@@ -393,6 +395,9 @@ int main(int argc, const char** argv) {
 	else {
 		resolvedColorBuffer = colorBuffer;
 	}
+	
+	vkcv::ImageHandle swapBuffer = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true).getHandle();
+	vkcv::ImageHandle swapBuffer2 = core.createImage(colorBufferFormat, windowWidth, windowHeight, 1, false, true).getHandle();
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 
@@ -421,6 +426,18 @@ int main(int argc, const char** argv) {
 	vkcv::PipelineHandle tonemappingPipeline = core.createComputePipeline(
 		tonemappingProgram,
 		{ core.getDescriptorSet(tonemappingDescriptorSet).layout });
+	
+	// tonemapping compute shader
+	vkcv::ShaderProgram postEffectsProgram;
+	compiler.compile(vkcv::ShaderStage::COMPUTE, "resources/shaders/postEffects.comp",
+		[&](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+		postEffectsProgram.addShader(shaderStage, path);
+	});
+	vkcv::DescriptorSetHandle postEffectsDescriptorSet = core.createDescriptorSet(
+			postEffectsProgram.getReflectedDescriptors()[0]);
+	vkcv::PipelineHandle postEffectsPipeline = core.createComputePipeline(
+			postEffectsProgram,
+			{ core.getDescriptorSet(postEffectsDescriptorSet).layout });
 
 	// resolve compute shader
 	vkcv::ShaderProgram resolveProgram;
@@ -438,7 +455,8 @@ int main(int argc, const char** argv) {
 		vkcv::SamplerFilterType::NEAREST,
 		vkcv::SamplerFilterType::NEAREST,
 		vkcv::SamplerMipmapMode::NEAREST,
-		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	);
 
 	// model matrices per mesh
 	std::vector<glm::mat4> modelMatrices;
@@ -473,7 +491,8 @@ int main(int argc, const char** argv) {
 		vkcv::SamplerFilterType::LINEAR,
 		vkcv::SamplerFilterType::LINEAR,
 		vkcv::SamplerMipmapMode::LINEAR,
-		vkcv::SamplerAddressMode::CLAMP_TO_EDGE);
+		vkcv::SamplerAddressMode::CLAMP_TO_EDGE
+	);
 
 	ShadowMapping shadowMapping(&core, vertexLayout);
 
@@ -511,10 +530,10 @@ int main(int argc, const char** argv) {
 	// write forward pass descriptor set
 	vkcv::DescriptorWrites forwardDescriptorWrites;
 	forwardDescriptorWrites.uniformBufferWrites = {
-		vkcv::UniformBufferDescriptorWrite(0, shadowMapping.getLightInfoBuffer()),
-		vkcv::UniformBufferDescriptorWrite(3, cameraPosBuffer.getHandle()),
-		vkcv::UniformBufferDescriptorWrite(6, voxelization.getVoxelInfoBufferHandle()),
-		vkcv::UniformBufferDescriptorWrite(7, volumetricSettingsBuffer.getHandle())};
+		vkcv::BufferDescriptorWrite(0, shadowMapping.getLightInfoBuffer()),
+		vkcv::BufferDescriptorWrite(3, cameraPosBuffer.getHandle()),
+		vkcv::BufferDescriptorWrite(6, voxelization.getVoxelInfoBufferHandle()),
+		vkcv::BufferDescriptorWrite(7, volumetricSettingsBuffer.getHandle())};
 	forwardDescriptorWrites.sampledImageWrites = {
 		vkcv::SampledImageDescriptorWrite(1, shadowMapping.getShadowMap()),
 		vkcv::SampledImageDescriptorWrite(4, voxelization.getVoxelImageHandle()) };
@@ -523,6 +542,27 @@ int main(int argc, const char** argv) {
 		vkcv::SamplerDescriptorWrite(5, voxelSampler) };
 	core.writeDescriptorSet(forwardShadingDescriptorSet, forwardDescriptorWrites);
 
+	vkcv::upscaling::FSRUpscaling upscaling (core);
+	uint32_t fsrWidth = windowWidth, fsrHeight = windowHeight;
+	
+	vkcv::upscaling::FSRQualityMode fsrMode = vkcv::upscaling::FSRQualityMode::NONE;
+	int fsrModeIndex = static_cast<int>(fsrMode);
+	
+	const std::vector<const char*> fsrModeNames = {
+			"None",
+			"Ultra Quality",
+			"Quality",
+			"Balanced",
+			"Performance"
+	};
+	
+	bool fsrMipLoadBiasFlag = true;
+	bool fsrMipLoadBiasFlagBackup = fsrMipLoadBiasFlag;
+	
+	vkcv::upscaling::BilinearUpscaling upscaling1 (core);
+	
+	bool bilinearUpscaling = false;
+	
 	vkcv::gui::GUI gui(core, window);
 
 	glm::vec2   lightAnglesDegree               = glm::vec2(90.f, 0.f);
@@ -550,22 +590,72 @@ int main(int argc, const char** argv) {
 		if (!core.beginFrame(swapchainWidth, swapchainHeight)) {
 			continue;
 		}
-
-		if ((swapchainWidth != windowWidth) || ((swapchainHeight != windowHeight))) {
-			depthBuffer         = core.createImage(depthBufferFormat, swapchainWidth, swapchainHeight, 1, false, false, false, msaa).getHandle();
-			colorBuffer         = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, colorBufferRequiresStorage, true, msaa).getHandle();
+		
+		uint32_t width, height;
+		vkcv::upscaling::getFSRResolution(
+				fsrMode,
+				swapchainWidth, swapchainHeight,
+				width, height
+		);
+
+		if ((width != fsrWidth) || ((height != fsrHeight)) || (fsrMipLoadBiasFlagBackup != fsrMipLoadBiasFlag)) {
+			fsrWidth = width;
+			fsrHeight = height;
+			fsrMipLoadBiasFlagBackup = fsrMipLoadBiasFlag;
+			
+			colorSampler = core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT,
+					fsrMipLoadBiasFlag? vkcv::upscaling::getFSRLodBias(fsrMode) : 0.0f
+			);
+			
+			for (size_t i = 0; i < scene.materials.size(); i++) {
+				vkcv::DescriptorWrites setWrites;
+				setWrites.samplerWrites = {
+						vkcv::SamplerDescriptorWrite(1, colorSampler),
+				};
+				core.writeDescriptorSet(materialDescriptorSets[i], setWrites);
+			}
+			
+			depthBuffer = core.createImage(
+					depthBufferFormat,
+					fsrWidth, fsrHeight, 1,
+					false, false, false,
+					msaa
+			).getHandle();
+			
+			colorBuffer = core.createImage(
+					colorBufferFormat,
+					fsrWidth, fsrHeight, 1,
+					false, colorBufferRequiresStorage, true,
+					msaa
+			).getHandle();
 
 			if (usingMsaa) {
-				resolvedColorBuffer = core.createImage(colorBufferFormat, swapchainWidth, swapchainHeight, 1, false, true, true).getHandle();
-			}
-			else {
+				resolvedColorBuffer = core.createImage(
+						colorBufferFormat,
+						fsrWidth, fsrHeight, 1,
+						false, true, true
+				).getHandle();
+			} else {
 				resolvedColorBuffer = colorBuffer;
 			}
-
-			windowWidth = swapchainWidth;
-			windowHeight = swapchainHeight;
-
-			bloomFlares.updateImageDimensions(windowWidth, windowHeight);
+			
+			swapBuffer = core.createImage(
+					colorBufferFormat,
+					fsrWidth, fsrHeight, 1,
+					false, true
+			).getHandle();
+			
+			swapBuffer2 = core.createImage(
+					colorBufferFormat,
+					swapchainWidth, swapchainHeight, 1,
+					false, true
+			).getHandle();
+			
+			bloomFlares.updateImageDimensions(swapchainWidth, swapchainHeight);
 		}
 
 		auto end = std::chrono::system_clock::now();
@@ -575,9 +665,17 @@ int main(int argc, const char** argv) {
 		vkcv::DescriptorWrites tonemappingDescriptorWrites;
 		tonemappingDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, resolvedColorBuffer) };
 		tonemappingDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(1, colorSampler) };
-		tonemappingDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, swapchainInput) };
+		tonemappingDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, swapBuffer) };
 
 		core.writeDescriptorSet(tonemappingDescriptorSet, tonemappingDescriptorWrites);
+		
+		// update descriptor sets which use swapchain image
+		vkcv::DescriptorWrites postEffectsDescriptorWrites;
+		postEffectsDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, swapBuffer2) };
+		postEffectsDescriptorWrites.samplerWrites       = { vkcv::SamplerDescriptorWrite(1, colorSampler) };
+		postEffectsDescriptorWrites.storageImageWrites  = { vkcv::StorageImageDescriptorWrite(2, swapchainInput) };
+		
+		core.writeDescriptorSet(postEffectsDescriptorSet, postEffectsDescriptorWrites);
 
 		// update resolve descriptor, color images could be changed
 		vkcv::DescriptorWrites resolveDescriptorWrites;
@@ -679,11 +777,18 @@ int main(int argc, const char** argv) {
 			renderTargets);
 
 		const uint32_t fullscreenLocalGroupSize = 8;
-		const uint32_t fulsscreenDispatchCount[3] = {
-			static_cast<uint32_t>(glm::ceil(windowWidth  / static_cast<float>(fullscreenLocalGroupSize))),
-			static_cast<uint32_t>(glm::ceil(windowHeight / static_cast<float>(fullscreenLocalGroupSize))),
-			1
-		};
+		
+		uint32_t fulsscreenDispatchCount [3];
+		
+		fulsscreenDispatchCount[0] = static_cast<uint32_t>(
+				glm::ceil(fsrWidth  / static_cast<float>(fullscreenLocalGroupSize))
+		);
+		
+		fulsscreenDispatchCount[1] = static_cast<uint32_t>(
+				glm::ceil(fsrHeight / static_cast<float>(fullscreenLocalGroupSize))
+		);
+		
+		fulsscreenDispatchCount[2] = 1;
 
 		if (usingMsaa) {
 			if (msaaCustomResolve) {
@@ -706,24 +811,58 @@ int main(int argc, const char** argv) {
 			}
 		}
 
-		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer, windowWidth, windowHeight, 
-			glm::normalize(cameraManager.getActiveCamera().getFront()));
+		bloomFlares.execWholePipeline(cmdStream, resolvedColorBuffer, fsrWidth, fsrHeight,
+			glm::normalize(cameraManager.getActiveCamera().getFront())
+		);
 
-		core.prepareImageForStorage(cmdStream, swapchainInput);
+		core.prepareImageForStorage(cmdStream, swapBuffer);
 		core.prepareImageForSampling(cmdStream, resolvedColorBuffer);
-
-		auto timeSinceStart = std::chrono::duration_cast<std::chrono::microseconds>(end - appStartTime);
-		float timeF         = static_cast<float>(timeSinceStart.count()) * 0.01;
-
-		vkcv::PushConstants timePushConstants (sizeof(timeF));
-		timePushConstants.appendDrawcall(timeF);
 		
 		core.recordComputeDispatchToCmdStream(
 			cmdStream, 
 			tonemappingPipeline, 
 			fulsscreenDispatchCount,
-			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptorSet).vulkanHandle) },
-			timePushConstants);
+			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(
+					tonemappingDescriptorSet
+			).vulkanHandle) },
+			vkcv::PushConstants(0)
+		);
+		
+		core.prepareImageForStorage(cmdStream, swapBuffer2);
+		core.prepareImageForSampling(cmdStream, swapBuffer);
+		
+		if (bilinearUpscaling) {
+			upscaling1.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
+		} else {
+			upscaling.recordUpscaling(cmdStream, swapBuffer, swapBuffer2);
+		}
+		
+		core.prepareImageForStorage(cmdStream, swapchainInput);
+		core.prepareImageForSampling(cmdStream, swapBuffer2);
+		
+		auto timeSinceStart = std::chrono::duration_cast<std::chrono::microseconds>(end - appStartTime);
+		float timeF         = static_cast<float>(timeSinceStart.count()) * 0.01f;
+		
+		vkcv::PushConstants timePushConstants (sizeof(timeF));
+		timePushConstants.appendDrawcall(timeF);
+		
+		fulsscreenDispatchCount[0] = static_cast<uint32_t>(
+				glm::ceil(swapchainWidth  / static_cast<float>(fullscreenLocalGroupSize))
+		);
+		
+		fulsscreenDispatchCount[1] = static_cast<uint32_t>(
+				glm::ceil(swapchainHeight / static_cast<float>(fullscreenLocalGroupSize))
+		);
+		
+		core.recordComputeDispatchToCmdStream(
+				cmdStream,
+				postEffectsPipeline,
+				fulsscreenDispatchCount,
+				{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(
+						postEffectsDescriptorSet
+				).vulkanHandle) },
+				timePushConstants
+		);
 
 		// present and end
 		core.prepareSwapchainImageForPresent(cmdStream);
@@ -751,12 +890,25 @@ int main(int argc, const char** argv) {
 			ImGui::DragFloat("Voxelization extent", &voxelizationExtent, 1.f, 0.f);
 			voxelizationExtent = std::max(voxelizationExtent, 1.f);
 			voxelVisualisationMip = std::max(voxelVisualisationMip, 0);
-
+			
 			ImGui::ColorEdit3("Scattering color", &scatteringColor.x);
 			ImGui::DragFloat("Scattering density", &scatteringDensity, 0.0001);
 			ImGui::ColorEdit3("Absorption color", &absorptionColor.x);
 			ImGui::DragFloat("Absorption density", &absorptionDensity, 0.0001);
 			ImGui::DragFloat("Volumetric ambient", &volumetricAmbient, 0.002);
+			
+			float fsrSharpness = upscaling.getSharpness();
+			
+			ImGui::Combo("FSR Quality Mode", &fsrModeIndex, fsrModeNames.data(), fsrModeNames.size());
+			ImGui::DragFloat("FSR Sharpness", &fsrSharpness, 0.001, 0.0f, 1.0f);
+			ImGui::Checkbox("FSR Mip Lod Bias", &fsrMipLoadBiasFlag);
+			ImGui::Checkbox("Bilinear Upscaling", &bilinearUpscaling);
+			
+			if ((fsrModeIndex >= 0) && (fsrModeIndex <= 4)) {
+				fsrMode = static_cast<vkcv::upscaling::FSRQualityMode>(fsrModeIndex);
+			}
+			
+			upscaling.setSharpness(fsrSharpness);
 
 			if (ImGui::Button("Reload forward pass")) {
 
diff --git a/src/vkcv/Context.cpp b/src/vkcv/Context.cpp
index 5db50869498600fa8e926c0feff2cb5fda1eb22e..2e30fb961d0b0931e4ff8796dd92b2cbd0b5f734 100644
--- a/src/vkcv/Context.cpp
+++ b/src/vkcv/Context.cpp
@@ -151,7 +151,7 @@ namespace vkcv
 	 * @param check The elements to be checked
 	 * @return True, if all elements in "check" are supported
 	*/
-	bool checkSupport(std::vector<const char*>& supported, std::vector<const char*>& check)
+	bool checkSupport(const std::vector<const char*>& supported, const std::vector<const char*>& check)
 	{
 		for (auto checkElem : check) {
 			bool found = false;
@@ -180,11 +180,20 @@ namespace vkcv
 		return extensions;
 	}
 	
+	bool isPresentInCharPtrVector(const std::vector<const char*>& v, const char* term){
+		for (const auto& entry : v) {
+			if (strcmp(entry, term) != 0) {
+				return true;
+			}
+		}
+		return false;
+	}
+	
 	Context Context::create(const char *applicationName,
 							uint32_t applicationVersion,
-							std::vector<vk::QueueFlagBits> queueFlags,
-							std::vector<const char *> instanceExtensions,
-							std::vector<const char *> deviceExtensions) {
+							const std::vector<vk::QueueFlagBits>& queueFlags,
+							const std::vector<const char *>& instanceExtensions,
+							const std::vector<const char *>& deviceExtensions) {
 		// check for layer support
 		
 		const std::vector<vk::LayerProperties>& layerProperties = vk::enumerateInstanceLayerProperties();
@@ -223,7 +232,7 @@ namespace vkcv
 		
 		// for GLFW: get all required extensions
 		std::vector<const char*> requiredExtensions = getRequiredExtensions();
-		instanceExtensions.insert(instanceExtensions.end(), requiredExtensions.begin(), requiredExtensions.end());
+		requiredExtensions.insert(requiredExtensions.end(), instanceExtensions.begin(), instanceExtensions.end());
 		
 		const vk::ApplicationInfo applicationInfo(
 				applicationName,
@@ -238,8 +247,8 @@ namespace vkcv
 				&applicationInfo,
 				0,
 				nullptr,
-				static_cast<uint32_t>(instanceExtensions.size()),
-				instanceExtensions.data()
+				static_cast<uint32_t>(requiredExtensions.size()),
+				requiredExtensions.data()
 		);
 
 #ifndef NDEBUG
@@ -286,13 +295,39 @@ namespace vkcv
 		deviceCreateInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size());
 		deviceCreateInfo.ppEnabledLayerNames = validationLayers.data();
 #endif
-
+		const bool shaderFloat16 = checkSupport(deviceExtensions, { "VK_KHR_shader_float16_int8" });
+		const bool storage16bit  = checkSupport(deviceExtensions, { "VK_KHR_16bit_storage" });
+		
 		// FIXME: check if device feature is supported
-		vk::PhysicalDeviceFeatures deviceFeatures;
-		deviceFeatures.fragmentStoresAndAtomics = true;
-		deviceFeatures.geometryShader = true;
-		deviceFeatures.depthClamp = true;
-		deviceCreateInfo.pEnabledFeatures = &deviceFeatures;
+		vk::PhysicalDeviceShaderFloat16Int8Features deviceShaderFloat16Int8Features;
+		deviceShaderFloat16Int8Features.shaderFloat16 = shaderFloat16;
+		
+		vk::PhysicalDevice16BitStorageFeatures device16BitStorageFeatures;
+		device16BitStorageFeatures.storageBuffer16BitAccess = storage16bit;
+		
+		vk::PhysicalDeviceFeatures2 deviceFeatures2;
+		deviceFeatures2.features.fragmentStoresAndAtomics = true;
+		deviceFeatures2.features.geometryShader = true;
+		deviceFeatures2.features.depthClamp = true;
+		deviceFeatures2.features.shaderInt16 = true;
+		
+		const bool usingMeshShaders = isPresentInCharPtrVector(deviceExtensions, VK_NV_MESH_SHADER_EXTENSION_NAME);
+		vk::PhysicalDeviceMeshShaderFeaturesNV meshShadingFeatures;
+		if (usingMeshShaders) {
+			meshShadingFeatures.taskShader = true;
+			meshShadingFeatures.meshShader = true;
+            deviceFeatures2.setPNext(&meshShadingFeatures);
+		}
+		
+		if (shaderFloat16) {
+			deviceFeatures2.setPNext(&deviceShaderFloat16Int8Features);
+		}
+		
+		if (storage16bit) {
+			deviceShaderFloat16Int8Features.setPNext(&device16BitStorageFeatures);
+		}
+		
+		deviceCreateInfo.setPNext(&deviceFeatures2);
 
 		// Ablauf
 		// qCreateInfos erstellen --> braucht das Device
@@ -300,6 +335,11 @@ namespace vkcv
 		// jetzt koennen wir mit dem device die queues erstellen
 		
 		vk::Device device = physicalDevice.createDevice(deviceCreateInfo);
+
+		if (usingMeshShaders)
+		{
+			InitMeshShaderDrawFunctions(device);
+		}
 		
 		QueueManager queueManager = QueueManager::create(
 				device,
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index bdfbdeef1411f9728969fd43c16bbec5a1a8c136..aaf8a7a6aeacc4af7fdfc4b42914dcd54a754e57 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -53,9 +53,9 @@ namespace vkcv
     Core Core::create(Window &window,
                       const char *applicationName,
                       uint32_t applicationVersion,
-                      std::vector<vk::QueueFlagBits> queueFlags,
-                      std::vector<const char *> instanceExtensions,
-                      std::vector<const char *> deviceExtensions)
+                      const std::vector<vk::QueueFlagBits>& queueFlags,
+                      const std::vector<const char *>& instanceExtensions,
+                      const std::vector<const char *>& deviceExtensions)
     {
         Context context = Context::create(
         		applicationName, applicationVersion,
@@ -90,8 +90,8 @@ namespace vkcv
     Core::Core(Context &&context, Window &window, const Swapchain& swapChain,  std::vector<vk::ImageView> swapchainImageViews,
         const CommandResources& commandResources, const SyncResources& syncResources) noexcept :
             m_Context(std::move(context)),
+			m_swapchain(swapChain),
             m_window(window),
-            m_swapchain(swapChain),
             m_PassManager{std::make_unique<PassManager>(m_Context.m_Device)},
             m_PipelineManager{std::make_unique<PipelineManager>(m_Context.m_Device)},
             m_DescriptorManager(std::make_unique<DescriptorManager>(m_Context.m_Device)),
@@ -118,7 +118,8 @@ namespace vkcv
 			swapchainImageViews, 
 			swapChain.getExtent().width,
 			swapChain.getExtent().height,
-			swapChain.getFormat());
+			swapChain.getFormat()
+		);
 	}
 
 	Core::~Core() noexcept {
@@ -227,130 +228,246 @@ namespace vkcv
 		return (m_currentSwapchainImageIndex != std::numeric_limits<uint32_t>::max());
 	}
 
-	void Core::recordDrawcallsToCmdStream(
-		const CommandStreamHandle       cmdStreamHandle,
-		const PassHandle                renderpassHandle, 
-		const PipelineHandle            pipelineHandle, 
-        const PushConstants             &pushConstants,
-        const std::vector<DrawcallInfo> &drawcalls,
-		const std::vector<ImageHandle>  &renderTargets) {
+	std::array<uint32_t, 2> getWidthHeightFromRenderTargets(
+		const std::vector<ImageHandle>& renderTargets,
+		const Swapchain& swapchain,
+		const ImageManager& imageManager) {
 
-		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
-			return;
-		}
+		std::array<uint32_t, 2> widthHeight;
 
-		uint32_t width;
-		uint32_t height;
 		if (renderTargets.size() > 0) {
 			const vkcv::ImageHandle firstImage = renderTargets[0];
 			if (firstImage.isSwapchainImage()) {
-				const auto& swapchainExtent = m_swapchain.getExtent();
-				width = swapchainExtent.width;
-				height = swapchainExtent.height;
+				const auto& swapchainExtent = swapchain.getExtent();
+				widthHeight[0] = swapchainExtent.width;
+				widthHeight[1] = swapchainExtent.height;
 			}
 			else {
-				width = m_ImageManager->getImageWidth(firstImage);
-				height = m_ImageManager->getImageHeight(firstImage);
+				widthHeight[0] = imageManager.getImageWidth(firstImage);
+				widthHeight[1] = imageManager.getImageHeight(firstImage);
 			}
 		}
 		else {
-			width = 1;
-			height = 1;
+			widthHeight[0] = 1;
+			widthHeight[1] = 1;
 		}
 		// TODO: validate that width/height match for all attachments
+		return widthHeight;
+	}
 
-		const vk::RenderPass renderpass = m_PassManager->getVkPass(renderpassHandle);
-		const PassConfig passConfig = m_PassManager->getPassConfig(renderpassHandle);
-
-		const vk::Pipeline pipeline		= m_PipelineManager->getVkPipeline(pipelineHandle);
-		const vk::PipelineLayout pipelineLayout = m_PipelineManager->getVkPipelineLayout(pipelineHandle);
-		const vk::Rect2D renderArea(vk::Offset2D(0, 0), vk::Extent2D(width, height));
+	vk::Framebuffer createFramebuffer(
+		const std::vector<ImageHandle>& renderTargets,
+		const ImageManager&             imageManager,
+		const Swapchain&                swapchain,
+		vk::RenderPass                  renderpass,
+		vk::Device                      device) {
 
 		std::vector<vk::ImageView> attachmentsViews;
-		for (const ImageHandle& handle : renderTargets) {
-			vk::ImageView targetHandle;
-			const auto cmdBuffer = m_CommandStreamManager->getStreamCommandBuffer(cmdStreamHandle);
+		for (const ImageHandle handle : renderTargets) {
+			vk::ImageView targetHandle = imageManager.getVulkanImageView(handle);
+			attachmentsViews.push_back(targetHandle);
+		}
+
+		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, swapchain, imageManager);
 
-			targetHandle = m_ImageManager->getVulkanImageView(handle);
-			const bool isDepthImage = isDepthFormat(m_ImageManager->getImageFormat(handle));
-			const vk::ImageLayout targetLayout = 
+		const vk::FramebufferCreateInfo createInfo(
+			{},
+			renderpass,
+			static_cast<uint32_t>(attachmentsViews.size()),
+			attachmentsViews.data(),
+			widthHeight[0],
+			widthHeight[1],
+			1);
+
+		return device.createFramebuffer(createInfo);
+	}
+
+	void transitionRendertargetsToAttachmentLayout(
+		const std::vector<ImageHandle>& renderTargets,
+		ImageManager&                   imageManager,
+		const vk::CommandBuffer         cmdBuffer) {
+
+		for (const ImageHandle handle : renderTargets) {
+			vk::ImageView targetHandle = imageManager.getVulkanImageView(handle);
+			const bool isDepthImage = isDepthFormat(imageManager.getImageFormat(handle));
+			const vk::ImageLayout targetLayout =
 				isDepthImage ? vk::ImageLayout::eDepthStencilAttachmentOptimal : vk::ImageLayout::eColorAttachmentOptimal;
-			m_ImageManager->recordImageLayoutTransition(handle, targetLayout, cmdBuffer);
-			attachmentsViews.push_back(targetHandle);
+			imageManager.recordImageLayoutTransition(handle, targetLayout, cmdBuffer);
 		}
-		
-        const vk::FramebufferCreateInfo createInfo(
-            {},
-            renderpass,
-            static_cast<uint32_t>(attachmentsViews.size()),
-            attachmentsViews.data(),
-            width,
-            height,
-            1
-		);
-		
-		vk::Framebuffer framebuffer = m_Context.m_Device.createFramebuffer(createInfo);
-        
-        if (!framebuffer) {
-			vkcv_log(LogLevel::ERROR, "Failed to create temporary framebuffer");
-            return;
-        }
+	}
 
-        vk::Viewport dynamicViewport(
-        		0.0f, 0.0f,
-            	static_cast<float>(width), static_cast<float>(height),
-            0.0f, 1.0f
+	std::vector<vk::ClearValue> createAttachmentClearValues(const std::vector<AttachmentDescription>& attachments) {
+		std::vector<vk::ClearValue> clearValues;
+		for (const auto& attachment : attachments) {
+			if (attachment.load_operation == AttachmentOperation::CLEAR) {
+				float clear = 0.0f;
+
+				if (isDepthFormat(attachment.format)) {
+					clear = 1.0f;
+				}
+
+				clearValues.emplace_back(std::array<float, 4>{
+					clear,
+						clear,
+						clear,
+						1.f
+				});
+			}
+		}
+		return clearValues;
+	}
+
+	void recordDynamicViewport(vk::CommandBuffer cmdBuffer, uint32_t width, uint32_t height) {
+		vk::Viewport dynamicViewport(
+			0.0f, 0.0f,
+			static_cast<float>(width), static_cast<float>(height),
+			0.0f, 1.0f
 		);
 
-        vk::Rect2D dynamicScissor({0, 0}, {width, height});
+		vk::Rect2D dynamicScissor({ 0, 0 }, { width, height });
+
+		cmdBuffer.setViewport(0, 1, &dynamicViewport);
+		cmdBuffer.setScissor(0, 1, &dynamicScissor);
+	}
+
+	void Core::recordDrawcallsToCmdStream(
+		const CommandStreamHandle       cmdStreamHandle,
+		const PassHandle                renderpassHandle, 
+		const PipelineHandle            pipelineHandle, 
+        const PushConstants             &pushConstantData,
+        const std::vector<DrawcallInfo> &drawcalls,
+		const std::vector<ImageHandle>  &renderTargets) {
+
+		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
+			return;
+		}
+
+		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, m_swapchain, *m_ImageManager);
+		const auto width  = widthHeight[0];
+		const auto height = widthHeight[1];
+
+		const vk::RenderPass        renderpass      = m_PassManager->getVkPass(renderpassHandle);
+		const PassConfig            passConfig      = m_PassManager->getPassConfig(renderpassHandle);
+
+		const vk::Pipeline          pipeline        = m_PipelineManager->getVkPipeline(pipelineHandle);
+		const vk::PipelineLayout    pipelineLayout  = m_PipelineManager->getVkPipelineLayout(pipelineHandle);
+		const vk::Rect2D            renderArea(vk::Offset2D(0, 0), vk::Extent2D(width, height));
+
+		vk::CommandBuffer cmdBuffer = m_CommandStreamManager->getStreamCommandBuffer(cmdStreamHandle);
+		transitionRendertargetsToAttachmentLayout(renderTargets, *m_ImageManager, cmdBuffer);
+
+		const vk::Framebuffer framebuffer = createFramebuffer(renderTargets, *m_ImageManager, m_swapchain, renderpass, m_Context.m_Device);
+
+		if (!framebuffer) {
+			vkcv_log(LogLevel::ERROR, "Failed to create temporary framebuffer");
+			return;
+		}
 
 		SubmitInfo submitInfo;
 		submitInfo.queueType = QueueType::Graphics;
 		submitInfo.signalSemaphores = { m_SyncResources.renderFinished };
 
 		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
-            std::vector<vk::ClearValue> clearValues;
 
-            for (const auto& attachment : passConfig.attachments) {
-                if (attachment.load_operation == AttachmentOperation::CLEAR) {
-                    float clear = 0.0f;
+			const std::vector<vk::ClearValue> clearValues = createAttachmentClearValues(passConfig.attachments);
 
-                    if (isDepthFormat(attachment.format)) {
-                        clear = 1.0f;
-                    }
+			const vk::RenderPassBeginInfo beginInfo(renderpass, framebuffer, renderArea, clearValues.size(), clearValues.data());
+			cmdBuffer.beginRenderPass(beginInfo, {}, {});
 
-                    clearValues.emplace_back(std::array<float, 4>{
-                            clear,
-                            clear,
-                            clear,
-                            1.f
-                    });
-                }
-            }
+			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});
 
-            const vk::RenderPassBeginInfo beginInfo(renderpass, framebuffer, renderArea, clearValues.size(), clearValues.data());
-            const vk::SubpassContents subpassContents = {};
-            cmdBuffer.beginRenderPass(beginInfo, subpassContents, {});
+			const PipelineConfig &pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
+			if(pipeConfig.m_UseDynamicViewport)
+			{
+				recordDynamicViewport(cmdBuffer, width, height);
+			}
 
-            cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});
+			for (int i = 0; i < drawcalls.size(); i++) {
+				recordDrawcall(drawcalls[i], cmdBuffer, pipelineLayout, pushConstantData, i);
+			}
 
-            const PipelineConfig &pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
-            if (pipeConfig.m_UseDynamicViewport) {
-                cmdBuffer.setViewport(0, 1, &dynamicViewport);
-                cmdBuffer.setScissor(0, 1, &dynamicScissor);
-            }
+        vk::Rect2D dynamicScissor({0, 0}, {width, height});
+			cmdBuffer.endRenderPass();
+		};
 
-            for (size_t i = 0; i < drawcalls.size(); i++) {
-                recordDrawcall(drawcalls[i], cmdBuffer, pipelineLayout, pushConstants, i);
-            }
+		auto finishFunction = [framebuffer, this]()
+		{
+			m_Context.m_Device.destroy(framebuffer);
+		};
+
+		recordCommandsToStream(cmdStreamHandle, submitFunction, finishFunction);
+	}
+
+	void Core::recordMeshShaderDrawcalls(
+		const CommandStreamHandle                           cmdStreamHandle,
+		const PassHandle                                    renderpassHandle,
+		const PipelineHandle                                pipelineHandle,
+		const PushConstants&                                pushConstantData,
+		const std::vector<MeshShaderDrawcall>&              drawcalls,
+		const std::vector<ImageHandle>&                     renderTargets) {
+
+		if (m_currentSwapchainImageIndex == std::numeric_limits<uint32_t>::max()) {
+			return;
+		}
+
+		const std::array<uint32_t, 2> widthHeight = getWidthHeightFromRenderTargets(renderTargets, m_swapchain, *m_ImageManager);
+		const auto width  = widthHeight[0];
+		const auto height = widthHeight[1];
+
+		const vk::RenderPass        renderpass = m_PassManager->getVkPass(renderpassHandle);
+		const PassConfig            passConfig = m_PassManager->getPassConfig(renderpassHandle);
+
+		const vk::Pipeline          pipeline = m_PipelineManager->getVkPipeline(pipelineHandle);
+		const vk::PipelineLayout    pipelineLayout = m_PipelineManager->getVkPipelineLayout(pipelineHandle);
+		const vk::Rect2D            renderArea(vk::Offset2D(0, 0), vk::Extent2D(width, height));
+
+		vk::CommandBuffer cmdBuffer = m_CommandStreamManager->getStreamCommandBuffer(cmdStreamHandle);
+		transitionRendertargetsToAttachmentLayout(renderTargets, *m_ImageManager, cmdBuffer);
+
+		const vk::Framebuffer framebuffer = createFramebuffer(renderTargets, *m_ImageManager, m_swapchain, renderpass, m_Context.m_Device);
+
+		if (!framebuffer) {
+			vkcv_log(LogLevel::ERROR, "Failed to create temporary framebuffer");
+			return;
+		}
+
+		SubmitInfo submitInfo;
+		submitInfo.queueType = QueueType::Graphics;
+		submitInfo.signalSemaphores = { m_SyncResources.renderFinished };
+
+		auto submitFunction = [&](const vk::CommandBuffer& cmdBuffer) {
+
+			const std::vector<vk::ClearValue> clearValues = createAttachmentClearValues(passConfig.attachments);
+
+			const vk::RenderPassBeginInfo beginInfo(renderpass, framebuffer, renderArea, clearValues.size(), clearValues.data());
+			cmdBuffer.beginRenderPass(beginInfo, {}, {});
+
+			cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline, {});
 
-            cmdBuffer.endRenderPass();
-        };
+			const PipelineConfig& pipeConfig = m_PipelineManager->getPipelineConfig(pipelineHandle);
+			if (pipeConfig.m_UseDynamicViewport)
+			{
+				recordDynamicViewport(cmdBuffer, width, height);
+			}
+
+			for (int i = 0; i < drawcalls.size(); i++) {
+                const uint32_t pushConstantOffset = i * pushConstantData.getSizePerDrawcall();
+                recordMeshShaderDrawcall(
+                    cmdBuffer,
+                    pipelineLayout,
+                    pushConstantData,
+                    pushConstantOffset,
+                    drawcalls[i],
+                    0);
+			}
 
-        auto finishFunction = [framebuffer, this]()
-        {
-            m_Context.m_Device.destroy(framebuffer);
-        };
+			cmdBuffer.endRenderPass();
+		};
+
+		auto finishFunction = [framebuffer, this]()
+		{
+			m_Context.m_Device.destroy(framebuffer);
+		};
 
 		recordCommandsToStream(cmdStreamHandle, submitFunction, finishFunction);
 	}
@@ -373,7 +490,8 @@ namespace vkcv
 					pipelineLayout,
 					usage.setLocation,
 					{ usage.vulkanHandle },
-					{});
+					usage.dynamicOffsets
+				);
 			}
 			if (pushConstants.getSizePerDrawcall() > 0) {
 				cmdBuffer.pushConstants(
@@ -476,7 +594,7 @@ namespace vkcv
 		}
 	}
 
-	void Core::submitCommandStream(const CommandStreamHandle handle) {
+	void Core::submitCommandStream(const CommandStreamHandle& handle) {
 		std::vector<vk::Semaphore> waitSemaphores;
 		// FIXME: add proper user controllable sync
 		std::vector<vk::Semaphore> signalSemaphores = { m_SyncResources.renderFinished };
@@ -484,8 +602,9 @@ namespace vkcv
 	}
 
 	SamplerHandle Core::createSampler(SamplerFilterType magFilter, SamplerFilterType minFilter,
-									  SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode) {
-		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode);
+									  SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode,
+									  float mipLodBias) {
+		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode, mipLodBias);
 	}
 
 	Image Core::createImage(
@@ -516,14 +635,18 @@ namespace vkcv
 			multisampling);
 	}
 
-	uint32_t Core::getImageWidth(ImageHandle imageHandle)
+	uint32_t Core::getImageWidth(const ImageHandle& image)
 	{
-		return m_ImageManager->getImageWidth(imageHandle);
+		return m_ImageManager->getImageWidth(image);
 	}
 
-	uint32_t Core::getImageHeight(ImageHandle imageHandle)
+	uint32_t Core::getImageHeight(const ImageHandle& image)
 	{
-		return m_ImageManager->getImageHeight(imageHandle);
+		return m_ImageManager->getImageHeight(image);
+	}
+	
+	vk::Format Core::getImageFormat(const ImageHandle& image) {
+		return m_ImageManager->getImageFormat(image);
 	}
 
     DescriptorSetHandle Core::createDescriptorSet(const std::vector<DescriptorBinding>& bindings)
@@ -544,32 +667,32 @@ namespace vkcv
 		return m_DescriptorManager->getDescriptorSet(handle);
 	}
 
-	void Core::prepareSwapchainImageForPresent(const CommandStreamHandle cmdStream) {
+	void Core::prepareSwapchainImageForPresent(const CommandStreamHandle& cmdStream) {
 		auto swapchainHandle = ImageHandle::createSwapchainImageHandle();
 		recordCommandsToStream(cmdStream, [swapchainHandle, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(swapchainHandle, vk::ImageLayout::ePresentSrcKHR, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::prepareImageForSampling(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::prepareImageForSampling(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eShaderReadOnlyOptimal, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::prepareImageForStorage(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::prepareImageForStorage(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eGeneral, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::recordImageMemoryBarrier(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::recordImageMemoryBarrier(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageMemoryBarrier(image, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::recordBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer) {
+	void Core::recordBufferMemoryBarrier(const CommandStreamHandle& cmdStream, const BufferHandle& buffer) {
 		recordCommandsToStream(cmdStream, [buffer, this](const vk::CommandBuffer cmdBuffer) {
 			m_BufferManager->recordBufferMemoryBarrier(buffer, cmdBuffer);
 		}, nullptr);
@@ -581,7 +704,7 @@ namespace vkcv
 		}, nullptr);
 	}
 	
-	void Core::resolveMSAAImage(CommandStreamHandle cmdStream, ImageHandle src, ImageHandle dst) {
+	void Core::resolveMSAAImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst) {
 		recordCommandsToStream(cmdStream, [src, dst, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordMSAAResolve(cmdBuffer, src, dst);
 		}, nullptr);
@@ -590,5 +713,86 @@ namespace vkcv
 	vk::ImageView Core::getSwapchainImageView() const {
     	return m_ImageManager->getVulkanImageView(vkcv::ImageHandle::createSwapchainImageHandle());
     }
+    
+    void Core::recordMemoryBarrier(const CommandStreamHandle& cmdStream) {
+		recordCommandsToStream(cmdStream, [](const vk::CommandBuffer cmdBuffer) {
+			vk::MemoryBarrier barrier (
+					vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead,
+					vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eMemoryRead
+			);
+			
+			cmdBuffer.pipelineBarrier(
+					vk::PipelineStageFlagBits::eAllCommands,
+					vk::PipelineStageFlagBits::eAllCommands,
+					vk::DependencyFlags(),
+					1, &barrier,
+					0, nullptr,
+					0, nullptr
+			);
+		}, nullptr);
+	}
+	
+	void Core::recordBlitImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst,
+							   SamplerFilterType filterType) {
+		recordCommandsToStream(cmdStream, [&](const vk::CommandBuffer cmdBuffer) {
+			m_ImageManager->recordImageLayoutTransition(
+					src, vk::ImageLayout::eTransferSrcOptimal, cmdBuffer
+			);
+			
+			m_ImageManager->recordImageLayoutTransition(
+					dst, vk::ImageLayout::eTransferDstOptimal, cmdBuffer
+			);
+			
+			const std::array<vk::Offset3D, 2> srcOffsets = {
+					vk::Offset3D(0, 0, 0),
+					vk::Offset3D(
+							m_ImageManager->getImageWidth(src),
+							m_ImageManager->getImageHeight(src),
+							1
+					)
+			};
+			
+			const std::array<vk::Offset3D, 2> dstOffsets = {
+					vk::Offset3D(0, 0, 0),
+					vk::Offset3D(
+							m_ImageManager->getImageWidth(dst),
+							m_ImageManager->getImageHeight(dst),
+							1
+					)
+			};
+			
+			const bool srcDepth = isDepthFormat(m_ImageManager->getImageFormat(src));
+			const bool dstDepth = isDepthFormat(m_ImageManager->getImageFormat(dst));
+			
+			const vk::ImageBlit blit = vk::ImageBlit(
+					vk::ImageSubresourceLayers(
+							srcDepth?
+							vk::ImageAspectFlagBits::eDepth :
+							vk::ImageAspectFlagBits::eColor,
+							0, 0, 1
+					),
+					srcOffsets,
+					vk::ImageSubresourceLayers(
+							dstDepth?
+							vk::ImageAspectFlagBits::eDepth :
+							vk::ImageAspectFlagBits::eColor,
+							0, 0, 1
+					),
+					dstOffsets
+			);
+			
+			cmdBuffer.blitImage(
+					m_ImageManager->getVulkanImage(src),
+					vk::ImageLayout::eTransferSrcOptimal,
+					m_ImageManager->getVulkanImage(dst),
+					vk::ImageLayout::eTransferDstOptimal,
+					1,
+					&blit,
+					filterType == SamplerFilterType::LINEAR?
+					vk::Filter::eLinear :
+					vk::Filter::eNearest
+			);
+		}, nullptr);
+	}
 	
 }
diff --git a/src/vkcv/DescriptorManager.cpp b/src/vkcv/DescriptorManager.cpp
index 07ca97b5ade9b69eed724000d9c7b388818d6725..0df359a15883847c132c429ed2945ac7624fb865 100644
--- a/src/vkcv/DescriptorManager.cpp
+++ b/src/vkcv/DescriptorManager.cpp
@@ -11,10 +11,14 @@ namespace vkcv
          * Allocate the set size for the descriptor pools, namely 1000 units of each descriptor type below.
 		 * Finally, create an initial pool.
          */
-		m_PoolSizes = { vk::DescriptorPoolSize(vk::DescriptorType::eSampler, 1000),
-													vk::DescriptorPoolSize(vk::DescriptorType::eSampledImage, 1000),
-													vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 1000),
-													vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 1000) };
+		m_PoolSizes = {
+				vk::DescriptorPoolSize(vk::DescriptorType::eSampler, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eSampledImage, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eUniformBuffer, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eStorageBuffer, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eUniformBufferDynamic, 1000),
+				vk::DescriptorPoolSize(vk::DescriptorType::eStorageBufferDynamic, 1000)
+		};
 
 		m_PoolInfo = vk::DescriptorPoolCreateInfo(
 				vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet,
@@ -105,7 +109,6 @@ namespace vkcv
 		const ImageManager		&imageManager, 
 		const BufferManager		&bufferManager,
 		const SamplerManager	&samplerManager) {
-
 		vk::DescriptorSet set = m_DescriptorSets[handle.getId()].vulkanHandle;
 
 		std::vector<vk::DescriptorImageInfo> imageInfos;
@@ -153,10 +156,15 @@ namespace vkcv
 		}
 
 		for (const auto& write : writes.uniformBufferWrites) {
+			const size_t size = bufferManager.getBufferSize(write.buffer);
+			const uint32_t offset = std::clamp<uint32_t>(write.offset, 0, size);
+			
 			const vk::DescriptorBufferInfo bufferInfo(
 				bufferManager.getBuffer(write.buffer),
-				static_cast<uint32_t>(0),
-				bufferManager.getBufferSize(write.buffer)
+				offset,
+				write.size == 0? size : std::min<uint32_t>(
+						write.size, size - offset
+				)
 			);
 			
 			bufferInfos.push_back(bufferInfo);
@@ -165,6 +173,8 @@ namespace vkcv
 					0,
 					bufferInfos.size(),
 					write.binding,
+					write.dynamic?
+					vk::DescriptorType::eUniformBufferDynamic :
 					vk::DescriptorType::eUniformBuffer
 			};
 			
@@ -172,10 +182,15 @@ namespace vkcv
 		}
 
 		for (const auto& write : writes.storageBufferWrites) {
+			const size_t size = bufferManager.getBufferSize(write.buffer);
+			const uint32_t offset = std::clamp<uint32_t>(write.offset, 0, size);
+			
 			const vk::DescriptorBufferInfo bufferInfo(
 				bufferManager.getBuffer(write.buffer),
-				static_cast<uint32_t>(0),
-				bufferManager.getBufferSize(write.buffer)
+				offset,
+				write.size == 0? size : std::min<uint32_t>(
+						write.size, size - offset
+				)
 			);
 			
 			bufferInfos.push_back(bufferInfo);
@@ -184,6 +199,8 @@ namespace vkcv
 					0,
 					bufferInfos.size(),
 					write.binding,
+					write.dynamic?
+					vk::DescriptorType::eStorageBufferDynamic :
 					vk::DescriptorType::eStorageBuffer
 			};
 			
@@ -239,8 +256,12 @@ namespace vkcv
         {
             case DescriptorType::UNIFORM_BUFFER:
                 return vk::DescriptorType::eUniformBuffer;
+			case DescriptorType::UNIFORM_BUFFER_DYNAMIC:
+				return vk::DescriptorType::eUniformBufferDynamic;
             case DescriptorType::STORAGE_BUFFER:
                 return vk::DescriptorType::eStorageBuffer;
+			case DescriptorType::STORAGE_BUFFER_DYNAMIC:
+				return vk::DescriptorType::eStorageBufferDynamic;
             case DescriptorType::SAMPLER:
                 return vk::DescriptorType::eSampler;
             case DescriptorType::IMAGE_SAMPLED:
diff --git a/src/vkcv/DrawcallRecording.cpp b/src/vkcv/DrawcallRecording.cpp
index 32ed00e98f7ef72f0c391f61924444c26844869b..d89ace3859717f753534402507a713a78bfb6876 100644
--- a/src/vkcv/DrawcallRecording.cpp
+++ b/src/vkcv/DrawcallRecording.cpp
@@ -1,7 +1,18 @@
 #include <vkcv/DrawcallRecording.hpp>
+#include <vkcv/Logger.hpp>
 
 namespace vkcv {
 
+    vk::IndexType getIndexType(IndexBitCount indexByteCount){
+        switch (indexByteCount) {
+            case IndexBitCount::Bit16: return vk::IndexType::eUint16;
+            case IndexBitCount::Bit32: return vk::IndexType::eUint32;
+            default:
+                vkcv_log(LogLevel::ERROR, "unknown Enum");
+                return vk::IndexType::eUint16;
+        }
+    }
+
     void recordDrawcall(
         const DrawcallInfo      &drawcall,
         vk::CommandBuffer       cmdBuffer,
@@ -33,11 +44,59 @@ namespace vkcv {
         }
 
         if (drawcall.mesh.indexBuffer) {
-            cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, vk::IndexType::eUint16);	//FIXME: choose proper size
+            cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, getIndexType(drawcall.mesh.indexBitCount));
             cmdBuffer.drawIndexed(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
         }
         else {
-            cmdBuffer.draw(drawcall.mesh.indexCount, 1, 0, 0, {});
+            cmdBuffer.draw(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
         }
     }
+
+
+
+    struct MeshShaderFunctions
+    {
+        PFN_vkCmdDrawMeshTasksNV cmdDrawMeshTasks                           = nullptr;
+        PFN_vkCmdDrawMeshTasksIndirectNV cmdDrawMeshTasksIndirect           = nullptr;
+        PFN_vkCmdDrawMeshTasksIndirectCountNV cmdDrawMeshTasksIndirectCount = nullptr;
+    } MeshShaderFunctions;
+
+    void InitMeshShaderDrawFunctions(vk::Device device)
+    {
+        MeshShaderFunctions.cmdDrawMeshTasks = PFN_vkCmdDrawMeshTasksNV(device.getProcAddr("vkCmdDrawMeshTasksNV"));
+        MeshShaderFunctions.cmdDrawMeshTasksIndirect = PFN_vkCmdDrawMeshTasksIndirectNV(device.getProcAddr("vkCmdDrawMeshTasksIndirectNV"));
+        MeshShaderFunctions.cmdDrawMeshTasksIndirectCount = PFN_vkCmdDrawMeshTasksIndirectCountNV (device.getProcAddr( "vkCmdDrawMeshTasksIndirectCountNV"));
+    }
+
+    void recordMeshShaderDrawcall(
+        vk::CommandBuffer                       cmdBuffer,
+        vk::PipelineLayout                      pipelineLayout,
+        const PushConstants&                    pushConstantData,
+        const uint32_t                          pushConstantOffset,
+        const MeshShaderDrawcall&               drawcall,
+        const uint32_t                          firstTask) {
+
+        for (const auto& descriptorUsage : drawcall.descriptorSets) {
+            cmdBuffer.bindDescriptorSets(
+                vk::PipelineBindPoint::eGraphics,
+                pipelineLayout,
+                descriptorUsage.setLocation,
+                descriptorUsage.vulkanHandle,
+                nullptr);
+        }
+
+        // char* cast because void* does not support pointer arithmetic
+        const void* drawcallPushConstantData = pushConstantOffset + (char*)pushConstantData.getData();
+
+        if (pushConstantData.getData()) {
+            cmdBuffer.pushConstants(
+                pipelineLayout,
+                vk::ShaderStageFlagBits::eAll,
+                0,
+                pushConstantData.getSizePerDrawcall(),
+                drawcallPushConstantData);
+        }
+
+        MeshShaderFunctions.cmdDrawMeshTasks(VkCommandBuffer(cmdBuffer), drawcall.taskCount, firstTask);
+    }
 }
diff --git a/src/vkcv/File.cpp b/src/vkcv/File.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6006b90f74e0a41f83483f2a1efbe5bda4c4e9f8
--- /dev/null
+++ b/src/vkcv/File.cpp
@@ -0,0 +1,60 @@
+
+#include "vkcv/File.hpp"
+
+#include <stdlib.h>
+
+#ifdef _WIN32
+#include <io.h>
+#else
+#include <unistd.h>
+#endif
+
+#include "vkcv/Logger.hpp"
+
+namespace vkcv {
+	
+	std::filesystem::path generateTemporaryFilePath() {
+		std::filesystem::path tmp = generateTemporaryDirectoryPath();
+		
+		if (std::filesystem::is_directory(tmp)) {
+			return std::filesystem::path(tmp.string() + "W"); // add W for Wambo
+		} else {
+			return tmp;
+		}
+	}
+	
+	std::filesystem::path generateTemporaryDirectoryPath() {
+		std::error_code code;
+		auto tmp = std::filesystem::temp_directory_path(code);
+		
+		if (tmp.empty()) {
+			tmp = std::filesystem::current_path();
+		}
+		
+		char name [16] = "vkcv_tmp_XXXXXX";
+		
+#ifdef _WIN32
+		int err = _mktemp_s(name, 16);
+		
+		if (err != 0) {
+			vkcv_log(LogLevel::ERROR, "Temporary file path could not be generated");
+			return "";
+		}
+#else
+		int fd = mkstemp(name); // creates a file locally
+		
+		if (fd == -1) {
+			vkcv_log(LogLevel::ERROR, "Temporary file path could not be generated");
+			return "";
+		}
+		
+		close(fd);
+		remove(name); // removes the local file again
+#endif
+		
+		return tmp / name;
+	}
+	
+	
+	
+}
diff --git a/src/vkcv/ImageManager.hpp b/src/vkcv/ImageManager.hpp
index 646b3211f761f0c71596fc088b39b784d5f39a5c..4d99422118e8d464ea75d9f013b471f3dd40fd8c 100644
--- a/src/vkcv/ImageManager.hpp
+++ b/src/vkcv/ImageManager.hpp
@@ -13,6 +13,8 @@
 #include "vkcv/ImageConfig.hpp"
 
 namespace vkcv {
+	
+	bool isDepthImageFormat(vk::Format format);
 
 	class ImageManager
 	{
diff --git a/src/vkcv/PipelineManager.cpp b/src/vkcv/PipelineManager.cpp
index 8b1f0b68be3a72f60103ca0dd8136f2c923513a5..244f6723f70e5ea938c005b74b286e192d68443c 100644
--- a/src/vkcv/PipelineManager.cpp
+++ b/src/vkcv/PipelineManager.cpp
@@ -44,95 +44,190 @@ namespace vkcv
 
     vk::PrimitiveTopology primitiveTopologyToVulkanPrimitiveTopology(const PrimitiveTopology topology) {
         switch (topology) {
-        case(PrimitiveTopology::PointList):     return vk::PrimitiveTopology::ePointList;
-        case(PrimitiveTopology::LineList):      return vk::PrimitiveTopology::eLineList;
-        case(PrimitiveTopology::TriangleList):  return vk::PrimitiveTopology::eTriangleList;
-        default: std::cout << "Error: Unknown primitive topology type" << std::endl; return vk::PrimitiveTopology::eTriangleList;
+            case(PrimitiveTopology::PointList):     return vk::PrimitiveTopology::ePointList;
+            case(PrimitiveTopology::LineList):      return vk::PrimitiveTopology::eLineList;
+            case(PrimitiveTopology::TriangleList):  return vk::PrimitiveTopology::eTriangleList;
+            default: std::cout << "Error: Unknown primitive topology type" << std::endl; return vk::PrimitiveTopology::eTriangleList;
         }
     }
 
     vk::CompareOp depthTestToVkCompareOp(DepthTest depthTest) {
         switch (depthTest) {
-        case(DepthTest::None):          return vk::CompareOp::eAlways;
-        case(DepthTest::Less):          return vk::CompareOp::eLess;
-        case(DepthTest::LessEqual):     return vk::CompareOp::eLessOrEqual;
-        case(DepthTest::Greater):       return vk::CompareOp::eGreater;
-        case(DepthTest::GreatherEqual): return vk::CompareOp::eGreaterOrEqual;
-        case(DepthTest::Equal):         return vk::CompareOp::eEqual;
-        default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown depth test enum"); return vk::CompareOp::eAlways;
+            case(DepthTest::None):          return vk::CompareOp::eAlways;
+            case(DepthTest::Less):          return vk::CompareOp::eLess;
+            case(DepthTest::LessEqual):     return vk::CompareOp::eLessOrEqual;
+            case(DepthTest::Greater):       return vk::CompareOp::eGreater;
+            case(DepthTest::GreatherEqual): return vk::CompareOp::eGreaterOrEqual;
+            case(DepthTest::Equal):         return vk::CompareOp::eEqual;
+            default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown depth test enum"); return vk::CompareOp::eAlways;
         }
     }
+        
+	vk::ShaderStageFlagBits shaderStageToVkShaderStage(vkcv::ShaderStage stage) {
+		switch (stage) {
+            case vkcv::ShaderStage::VERTEX:         return vk::ShaderStageFlagBits::eVertex;
+            case vkcv::ShaderStage::FRAGMENT:       return vk::ShaderStageFlagBits::eFragment;
+            case vkcv::ShaderStage::GEOMETRY:       return vk::ShaderStageFlagBits::eGeometry;
+            case vkcv::ShaderStage::TESS_CONTROL:   return vk::ShaderStageFlagBits::eTessellationControl;
+            case vkcv::ShaderStage::TESS_EVAL:      return vk::ShaderStageFlagBits::eTessellationEvaluation;
+            case vkcv::ShaderStage::COMPUTE:        return vk::ShaderStageFlagBits::eCompute;
+            case vkcv::ShaderStage::TASK:           return vk::ShaderStageFlagBits::eTaskNV;
+            case vkcv::ShaderStage::MESH:           return vk::ShaderStageFlagBits::eMeshNV;
+            default: vkcv_log(vkcv::LogLevel::ERROR, "Unknown shader stage"); return vk::ShaderStageFlagBits::eAll;
+		}
+	}
+
+    bool createPipelineShaderStageCreateInfo(
+        const vkcv::ShaderProgram&          shaderProgram, 
+        ShaderStage                         stage,
+        vk::Device                          device,
+        vk::PipelineShaderStageCreateInfo*  outCreateInfo) {
+
+        assert(outCreateInfo);
+        std::vector<char>           code = shaderProgram.getShader(stage).shaderCode;
+        vk::ShaderModuleCreateInfo  vertexModuleInfo({}, code.size(), reinterpret_cast<uint32_t*>(code.data()));
+        vk::ShaderModule            shaderModule;
+        if (device.createShaderModule(&vertexModuleInfo, nullptr, &shaderModule) != vk::Result::eSuccess)
+            return false;
+
+        const static auto entryName = "main";
+
+        *outCreateInfo = vk::PipelineShaderStageCreateInfo(
+            {},
+            shaderStageToVkShaderStage(stage),
+            shaderModule,
+            entryName,
+            nullptr);
+        return true;
+    }
 
     PipelineHandle PipelineManager::createPipeline(const PipelineConfig &config, PassManager& passManager)
     {
 		const vk::RenderPass &pass = passManager.getVkPass(config.m_PassHandle);
     	
+		const bool existsTaskShader = config.m_ShaderProgram.existsShader(ShaderStage::TASK);
+		const bool existsMeshShader = config.m_ShaderProgram.existsShader(ShaderStage::MESH);
+
         const bool existsVertexShader = config.m_ShaderProgram.existsShader(ShaderStage::VERTEX);
+
+        const bool validGeometryStages = existsVertexShader || (existsTaskShader && existsMeshShader);
+
         const bool existsFragmentShader = config.m_ShaderProgram.existsShader(ShaderStage::FRAGMENT);
-        if (!(existsVertexShader && existsFragmentShader))
+        if (!validGeometryStages)
         {
-			vkcv_log(LogLevel::ERROR, "Requires vertex and fragment shader code");
+			vkcv_log(LogLevel::ERROR, "Requires vertex or task and mesh shader");
             return PipelineHandle();
         }
-
-        // vertex shader stage
-        std::vector<char> vertexCode = config.m_ShaderProgram.getShader(ShaderStage::VERTEX).shaderCode;
-        vk::ShaderModuleCreateInfo vertexModuleInfo({}, vertexCode.size(), reinterpret_cast<uint32_t*>(vertexCode.data()));
-        vk::ShaderModule vertexModule{};
-        if (m_Device.createShaderModule(&vertexModuleInfo, nullptr, &vertexModule) != vk::Result::eSuccess)
+        if (!existsFragmentShader) {
+            vkcv_log(LogLevel::ERROR, "Requires fragment shader code");
             return PipelineHandle();
+        }
 
-        vk::PipelineShaderStageCreateInfo pipelineVertexShaderStageInfo(
-                {},
-                vk::ShaderStageFlagBits::eVertex,
-                vertexModule,
-                "main",
-                nullptr
-        );
+        std::vector<vk::PipelineShaderStageCreateInfo> shaderStages;
+        auto destroyShaderModules = [&shaderStages, this] {
+            for (auto stage : shaderStages) {
+                m_Device.destroyShaderModule(stage.module);
+            }
+            shaderStages.clear();
+        };
+
+        if (existsVertexShader) {
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram, 
+                vkcv::ShaderStage::VERTEX, 
+                m_Device, 
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
+        }
+
+        if (existsTaskShader) {
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram,
+                vkcv::ShaderStage::TASK,
+                m_Device,
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
+        }
+
+        if (existsMeshShader) {
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram,
+                vkcv::ShaderStage::MESH,
+                m_Device,
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
+        }
 
         // fragment shader stage
-        std::vector<char> fragCode = config.m_ShaderProgram.getShader(ShaderStage::FRAGMENT).shaderCode;
-        vk::ShaderModuleCreateInfo fragmentModuleInfo({}, fragCode.size(), reinterpret_cast<uint32_t*>(fragCode.data()));
-        vk::ShaderModule fragmentModule{};
-        if (m_Device.createShaderModule(&fragmentModuleInfo, nullptr, &fragmentModule) != vk::Result::eSuccess)
         {
-            m_Device.destroy(vertexModule);
-            return PipelineHandle();
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram,
+                vkcv::ShaderStage::FRAGMENT,
+                m_Device,
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
         }
 
-        vk::PipelineShaderStageCreateInfo pipelineFragmentShaderStageInfo(
-                {},
-                vk::ShaderStageFlagBits::eFragment,
-                fragmentModule,
-                "main",
-                nullptr
-        );
-
         // vertex input state
 
         // Fill up VertexInputBindingDescription and VertexInputAttributeDescription Containers
         std::vector<vk::VertexInputAttributeDescription>	vertexAttributeDescriptions;
 		std::vector<vk::VertexInputBindingDescription>		vertexBindingDescriptions;
 
-        const VertexLayout &layout = config.m_VertexLayout;
-
-        // iterate over the layout's specified, mutually exclusive buffer bindings that make up a vertex buffer
-        for (const auto &vertexBinding : layout.vertexBindings)
-        {
-            vertexBindingDescriptions.emplace_back(vertexBinding.bindingLocation,
-                                                   vertexBinding.stride,
-                                                   vk::VertexInputRate::eVertex);
-
-            // iterate over the bindings' specified, mutually exclusive vertex input attachments that make up a vertex
-            for(const auto &vertexAttachment: vertexBinding.vertexAttachments)
-            {
-                vertexAttributeDescriptions.emplace_back(vertexAttachment.inputLocation,
-                                                         vertexBinding.bindingLocation,
-                                                         vertexFormatToVulkanFormat(vertexAttachment.format),
-                                                         vertexAttachment.offset % vertexBinding.stride);
+		if (existsVertexShader) {
+			const VertexLayout& layout = config.m_VertexLayout;
+
+			// iterate over the layout's specified, mutually exclusive buffer bindings that make up a vertex buffer
+			for (const auto& vertexBinding : layout.vertexBindings)
+			{
+				vertexBindingDescriptions.emplace_back(vertexBinding.bindingLocation,
+					vertexBinding.stride,
+					vk::VertexInputRate::eVertex);
+
+				// iterate over the bindings' specified, mutually exclusive vertex input attachments that make up a vertex
+				for (const auto& vertexAttachment : vertexBinding.vertexAttachments)
+				{
+					vertexAttributeDescriptions.emplace_back(vertexAttachment.inputLocation,
+						vertexBinding.bindingLocation,
+						vertexFormatToVulkanFormat(vertexAttachment.format),
+						vertexAttachment.offset % vertexBinding.stride);
+
+				}
+			}
 
-            }
-        }
+		}
 
         // Handover Containers to PipelineVertexInputStateCreateIngo Struct
         vk::PipelineVertexInputStateCreateInfo pipelineVertexInputStateCreateInfo(
@@ -240,8 +335,7 @@ namespace vkcv
         vk::PipelineLayout vkPipelineLayout{};
         if (m_Device.createPipelineLayout(&pipelineLayoutCreateInfo, nullptr, &vkPipelineLayout) != vk::Result::eSuccess)
         {
-            m_Device.destroy(vertexModule);
-            m_Device.destroy(fragmentModule);
+            destroyShaderModules();
             return PipelineHandle();
         }
 	
@@ -276,25 +370,28 @@ namespace vkcv
 		    dynamicStates.push_back(vk::DynamicState::eScissor);
         }
 
-        vk::PipelineDynamicStateCreateInfo dynamicStateCreateInfo({},
-                                                            static_cast<uint32_t>(dynamicStates.size()),
-                                                            dynamicStates.data());
-
-        // graphics pipeline create
-        std::vector<vk::PipelineShaderStageCreateInfo> shaderStages = { pipelineVertexShaderStageInfo, pipelineFragmentShaderStageInfo };
-
-		const char *geometryShaderName = "main";	// outside of if to make sure it stays in scope
-		vk::ShaderModule geometryModule;
-		if (config.m_ShaderProgram.existsShader(ShaderStage::GEOMETRY)) {
-			const vkcv::Shader geometryShader = config.m_ShaderProgram.getShader(ShaderStage::GEOMETRY);
-			const auto& geometryCode = geometryShader.shaderCode;
-			const vk::ShaderModuleCreateInfo geometryModuleInfo({}, geometryCode.size(), reinterpret_cast<const uint32_t*>(geometryCode.data()));
-			if (m_Device.createShaderModule(&geometryModuleInfo, nullptr, &geometryModule) != vk::Result::eSuccess) {
-				return PipelineHandle();
-			}
-			vk::PipelineShaderStageCreateInfo geometryStage({}, vk::ShaderStageFlagBits::eGeometry, geometryModule, geometryShaderName);
-			shaderStages.push_back(geometryStage);
-		}
+        vk::PipelineDynamicStateCreateInfo dynamicStateCreateInfo(
+            {},
+            static_cast<uint32_t>(dynamicStates.size()),
+            dynamicStates.data());
+
+        const bool existsGeometryShader = config.m_ShaderProgram.existsShader(vkcv::ShaderStage::GEOMETRY);
+        if (existsGeometryShader) {
+            vk::PipelineShaderStageCreateInfo createInfo;
+            const bool success = createPipelineShaderStageCreateInfo(
+                config.m_ShaderProgram,
+                vkcv::ShaderStage::GEOMETRY,
+                m_Device,
+                &createInfo);
+
+            if (success) {
+                shaderStages.push_back(createInfo);
+            }
+            else {
+                destroyShaderModules();
+                return PipelineHandle();
+            }
+        }
 
         const vk::GraphicsPipelineCreateInfo graphicsPipelineCreateInfo(
                 {},
@@ -319,20 +416,11 @@ namespace vkcv
         vk::Pipeline vkPipeline{};
         if (m_Device.createGraphicsPipelines(nullptr, 1, &graphicsPipelineCreateInfo, nullptr, &vkPipeline) != vk::Result::eSuccess)
         {
-            m_Device.destroy(vertexModule);
-            m_Device.destroy(fragmentModule);
-            if (geometryModule) {
-                m_Device.destroy(geometryModule);
-            }
-            m_Device.destroy();
+            destroyShaderModules();
             return PipelineHandle();
         }
 
-        m_Device.destroy(vertexModule);
-        m_Device.destroy(fragmentModule);
-        if (geometryModule) {
-            m_Device.destroy(geometryModule);
-        }
+        destroyShaderModules();
         
         const uint64_t id = m_Pipelines.size();
         m_Pipelines.push_back({ vkPipeline, vkPipelineLayout, config });
@@ -457,4 +545,4 @@ namespace vkcv
         vk::ShaderModuleCreateInfo moduleInfo({}, code.size(), reinterpret_cast<uint32_t*>(code.data()));
         return m_Device.createShaderModule(&moduleInfo, nullptr, &module);
     }
-}
\ No newline at end of file
+}
diff --git a/src/vkcv/QueueManager.cpp b/src/vkcv/QueueManager.cpp
index b4891c6be387b817b87f059f4155f5708d4f4710..15e958b0de929e53170324ade27a9b3663a15d6a 100644
--- a/src/vkcv/QueueManager.cpp
+++ b/src/vkcv/QueueManager.cpp
@@ -27,8 +27,8 @@ namespace vkcv {
      * @throws std::runtime_error If the requested queues from @p queueFlags are not creatable due to insufficient availability.
      */
     void QueueManager::queueCreateInfosQueueHandles(vk::PhysicalDevice &physicalDevice,
-                                      std::vector<float> &queuePriorities,
-                                      std::vector<vk::QueueFlagBits> &queueFlags,
+                                      const std::vector<float> &queuePriorities,
+                                      const std::vector<vk::QueueFlagBits> &queueFlags,
                                       std::vector<vk::DeviceQueueCreateInfo> &queueCreateInfos,
                                       std::vector<std::pair<int, int>> &queuePairsGraphics,
                                       std::vector<std::pair<int, int>> &queuePairsCompute,
diff --git a/src/vkcv/SamplerManager.cpp b/src/vkcv/SamplerManager.cpp
index a6ebb95b5e237dcd06ed8041b3f16489f7339d6a..792e6f16b4a05af41a164a1eda9dd7423594857e 100644
--- a/src/vkcv/SamplerManager.cpp
+++ b/src/vkcv/SamplerManager.cpp
@@ -17,7 +17,8 @@ namespace vkcv {
 	SamplerHandle SamplerManager::createSampler(SamplerFilterType magFilter,
 												SamplerFilterType minFilter,
 												SamplerMipmapMode mipmapMode,
-												SamplerAddressMode addressMode) {
+												SamplerAddressMode addressMode,
+												float mipLodBias) {
 		vk::Filter vkMagFilter;
 		vk::Filter vkMinFilter;
 		vk::SamplerMipmapMode vkMipmapMode;
@@ -81,13 +82,13 @@ namespace vkcv {
 				vkAddressMode,
 				vkAddressMode,
 				vkAddressMode,
-				0.0f,
+				mipLodBias,
 				false,
 				16.0f,
 				false,
 				vk::CompareOp::eAlways,
-				0.0f,
-				16.0f,
+				-1000.0f,
+				1000.0f,
 				vk::BorderColor::eIntOpaqueBlack,
 				false
 		);
diff --git a/src/vkcv/SamplerManager.hpp b/src/vkcv/SamplerManager.hpp
index 511176d4f87633a8691ca730ecc383e2588d8cf0..aea47a03714b417314a09dfc0be855df31fbb557 100644
--- a/src/vkcv/SamplerManager.hpp
+++ b/src/vkcv/SamplerManager.hpp
@@ -32,7 +32,8 @@ namespace vkcv {
 		SamplerHandle createSampler(SamplerFilterType magFilter,
 							  		SamplerFilterType minFilter,
 							  		SamplerMipmapMode mipmapMode,
-							  		SamplerAddressMode addressMode);
+							  		SamplerAddressMode addressMode,
+							  		float mipLodBias);
 		
 		[[nodiscard]]
 		vk::Sampler getVulkanSampler(const SamplerHandle& handle) const;
diff --git a/src/vkcv/Window.cpp b/src/vkcv/Window.cpp
index 025cb388c6880cc8132b454c799d39e2b530ceb3..aea00fb10d579aea0dc5be789ced3e6582b868bf 100644
--- a/src/vkcv/Window.cpp
+++ b/src/vkcv/Window.cpp
@@ -4,7 +4,9 @@
  * @brief Window class to handle a basic rendering surface and input
  */
 
+#include <vector>
 #include <GLFW/glfw3.h>
+
 #include "vkcv/Window.hpp"
 
 namespace vkcv {