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/.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/config/Libraries.cmake b/config/Libraries.cmake
index 681f42767b8e1d00da21c7b476712bad98412fbb..512669ce85a96f8cc94d8181994cfe458fa8b604 100644
--- a/config/Libraries.cmake
+++ b/config/Libraries.cmake
@@ -40,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/include/vkcv/Context.hpp b/include/vkcv/Context.hpp
index 3f779572d27dc42f2a29d147d2fbfbd19b0bd929..824713fd1e29cbb8b7e60b22768c0019daaa9938 100644
--- a/include/vkcv/Context.hpp
+++ b/include/vkcv/Context.hpp
@@ -38,12 +38,11 @@ namespace vkcv
         [[nodiscard]]
 		const vma::Allocator& getAllocator() const;
         
-        static Context create(
-			const char*                     applicationName,
-			uint32_t                        applicationVersion,
-			std::vector<vk::QueueFlagBits>  queueFlags,
-			std::vector<const char *>       instanceExtensions,
-			std::vector<const char *>       deviceExtensions);
+        static Context create(const char *applicationName,
+							  uint32_t applicationVersion,
+							  const std::vector<vk::QueueFlagBits>& queueFlags,
+							  const std::vector<const char *>& instanceExtensions,
+							  const std::vector<const char *>& deviceExtensions);
 
     private:
         /**
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index fd2bf56e0766ebc6ae06e76c138f52d4ba41702f..5677dbf6569a182eddba494852d39320f8154711 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
@@ -291,15 +297,21 @@ namespace vkcv
 			const RecordCommandFunction &record,
 			const FinishCommandFunction &finish);
 
-		void submitCommandStream(const CommandStreamHandle handle);
-		void prepareSwapchainImageForPresent(const CommandStreamHandle handle);
-		void prepareImageForSampling(const CommandStreamHandle cmdStream, const ImageHandle image);
-		void prepareImageForStorage(const CommandStreamHandle cmdStream, const ImageHandle image);
-		void recordImageMemoryBarrier(const CommandStreamHandle cmdStream, const ImageHandle image);
-		void recordBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer);
-		void resolveMSAAImage(CommandStreamHandle cmdStream, ImageHandle src, ImageHandle dst);
+		void submitCommandStream(const CommandStreamHandle& handle);
+		void prepareSwapchainImageForPresent(const CommandStreamHandle& handle);
+		void prepareImageForSampling(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void prepareImageForStorage(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void recordImageMemoryBarrier(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void recordBufferMemoryBarrier(const CommandStreamHandle& cmdStream, const BufferHandle& buffer);
+		void resolveMSAAImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst);
 
+		[[nodiscard]]
 		vk::ImageView getSwapchainImageView() const;
+	
+		void recordMemoryBarrier(const CommandStreamHandle& cmdStream);
+		
+		void recordBlitImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst,
+							 SamplerFilterType filterType);
 		
     };
 }
diff --git a/include/vkcv/DescriptorConfig.hpp b/include/vkcv/DescriptorConfig.hpp
index 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 60d3430c8d64454592736f0e658c7f8a770ef2e5..260fbbc6a2a577d0d333656a1eff4f7f3f88cd69 100644
--- a/include/vkcv/DrawcallRecording.hpp
+++ b/include/vkcv/DrawcallRecording.hpp
@@ -19,11 +19,13 @@ namespace vkcv {
     };
 
     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 {
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/modules/CMakeLists.txt b/modules/CMakeLists.txt
index 72d94364136a04d92d0e4dfa9837048c53664637..4b576e7119ebe769eafd1b6abb033b4fb02a3ec1 100644
--- a/modules/CMakeLists.txt
+++ b/modules/CMakeLists.txt
@@ -8,3 +8,4 @@ add_subdirectory(meshlet)
 add_subdirectory(scene)
 add_subdirectory(shader_compiler)
 add_subdirectory(testing)
+add_subdirectory(upscaling)
diff --git a/modules/meshlet/CMakeLists.txt b/modules/meshlet/CMakeLists.txt
index 8c6823876d0c7ca2632fa57903427c25986724db..385115de4ab3f6dc7c540fd77e10ddec0ee458b8 100644
--- a/modules/meshlet/CMakeLists.txt
+++ b/modules/meshlet/CMakeLists.txt
@@ -12,8 +12,8 @@ set(vkcv_meshlet_include ${PROJECT_SOURCE_DIR}/include)
 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/Tipsify.hpp
+		${vkcv_meshlet_source}/vkcv/meshlet/Tipsify.cpp
 )
 
 # adding source files to the module
diff --git a/modules/meshlet/include/vkcv/meshlet/Tipsify.hpp b/modules/meshlet/include/vkcv/meshlet/Tipsify.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a5bcef76b518eb9629eeb5e90614dfd705edc6ed
--- /dev/null
+++ b/modules/meshlet/include/vkcv/meshlet/Tipsify.hpp
@@ -0,0 +1,22 @@
+#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
+     */
+    std::vector<uint32_t> 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/Tipsify.cpp b/modules/meshlet/src/vkcv/meshlet/Tipsify.cpp
index 281830b9013ee1110338855dc73440e27ab8b563..a41d37c15d81d60c4152c58051ad362ba83c77a6 100644
--- a/modules/meshlet/src/vkcv/meshlet/Tipsify.cpp
+++ b/modules/meshlet/src/vkcv/meshlet/Tipsify.cpp
@@ -1,4 +1,269 @@
-//
-// Created by Dradozer on 20.07.2021.
-//
 
+#include <vkcv/Logger.hpp>
+#include "vkcv/meshlet/Tipsify.hpp"
+#include <iostream>
+
+const int maxUsedVertices = 128;
+
+namespace vkcv::meshlet {
+
+    /**
+     * 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
+     * @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) {
+
+        // 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) {
+                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
+     * @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 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);
+        }
+        return nextVertexIndex;
+    }
+
+    std::vector<uint32_t> 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 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);
+
+        // 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];
+
+                    // 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);
+        }
+
+        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 reorderedIndexBuffer;
+    }
+}
\ No newline at end of file
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 4a351f41f5fd3186bf3e606ded132c083a63a7e5..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++;
 	}
 	
@@ -201,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);
 		
@@ -226,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) {
@@ -278,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/mesh_shader/resources/shaders/shader.frag b/projects/mesh_shader/resources/shaders/shader.frag
index 17bf8f960bb6996f039c4e73e547db802f1ceab4..f4f6982f2089e6c8e102027f3b8763bb38f8e59c 100644
--- a/projects/mesh_shader/resources/shaders/shader.frag
+++ b/projects/mesh_shader/resources/shaders/shader.frag
@@ -28,5 +28,5 @@ vec3 colorFromIndex(uint i){
 
 void main() {
 	outColor = normalize(passNormal) * 0.5 + 0.5;
-    //outColor = colorFromIndex(passTaskIndex);
+    outColor = colorFromIndex(passTaskIndex);
 }
\ No newline at end of file
diff --git a/projects/mesh_shader/src/main.cpp b/projects/mesh_shader/src/main.cpp
index d7441d0dd9f0a875e70fd01e4182454977d981a8..99529b5305b6979ced4f113ab18c0cc79a53a4e4 100644
--- a/projects/mesh_shader/src/main.cpp
+++ b/projects/mesh_shader/src/main.cpp
@@ -8,7 +8,7 @@
 #include <vkcv/gui/GUI.hpp>
 #include <vkcv/asset/asset_loader.hpp>
 #include <vkcv/meshlet/Meshlet.hpp>
-//#include <vkcv/meshlet/Tipsify.hpp>
+#include <vkcv/meshlet/Tipsify.hpp>
 
 struct Plane {
 	glm::vec3 pointOnPlane;
@@ -138,10 +138,10 @@ int main(int argc, const char** argv) {
 	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);
-//    std::vector<uint32_t> reorderedIndexBuffer32Bit  = vkcv::meshlet::tipsifyMesh(indexBuffer32Bit, interleavedVertices.size(), 2);
+	std::vector<uint32_t> indexBuffer32Bit          = vkcv::meshlet::assetLoaderIndicesTo32BitIndices(assetLoaderIndexBuffer.data, assetLoaderIndexBuffer.type);
+    std::vector<uint32_t> reorderedIndexBuffer32Bit = vkcv::meshlet::tipsifyMesh(indexBuffer32Bit, interleavedVertices.size());
 
-    const auto meshShaderModelData = createMeshShaderModelData(interleavedVertices, indexBuffer32Bit);
+    const auto meshShaderModelData = createMeshShaderModelData(interleavedVertices, reorderedIndexBuffer32Bit);
 
 	auto meshShaderVertexBuffer = core.createBuffer<vkcv::meshlet::Vertex>(
 		vkcv::BufferType::STORAGE,
@@ -221,7 +221,7 @@ int main(int argc, const char** argv) {
 	vkcv::Buffer<ObjectMatrices> matrixBuffer = core.createBuffer<ObjectMatrices>(vkcv::BufferType::STORAGE, objectCount);
 
 	vkcv::DescriptorWrites vertexShaderDescriptorWrites;
-	vertexShaderDescriptorWrites.storageBufferWrites = { vkcv::StorageBufferDescriptorWrite(0, matrixBuffer.getHandle()) };
+	vertexShaderDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, matrixBuffer.getHandle()) };
 	core.writeDescriptorSet(vertexShaderDescriptorSet, vertexShaderDescriptorWrites);
 
 	vkcv::PipelineHandle bunnyPipeline = core.createGraphicsPipeline(bunnyPipelineDefinition);
@@ -275,14 +275,14 @@ int main(int argc, const char** argv) {
 
 	vkcv::DescriptorWrites meshShaderWrites;
 	meshShaderWrites.storageBufferWrites = {
-		vkcv::StorageBufferDescriptorWrite(0, meshShaderVertexBuffer.getHandle()),
-		vkcv::StorageBufferDescriptorWrite(1, meshShaderIndexBuffer.getHandle()),
-		vkcv::StorageBufferDescriptorWrite(2, meshletBuffer.getHandle()),
-		vkcv::StorageBufferDescriptorWrite(4, matrixBuffer.getHandle()),
-		vkcv::StorageBufferDescriptorWrite(5, meshletBuffer.getHandle()),
+		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::UniformBufferDescriptorWrite(3, cameraPlaneBuffer.getHandle())
+		vkcv::BufferDescriptorWrite(3, cameraPlaneBuffer.getHandle()),
 	};
 
     core.writeDescriptorSet( meshShaderDescriptorSet, meshShaderWrites);
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 2084700cae62a378b061aab056b3c5bac99b96e4..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;
@@ -179,7 +179,7 @@ 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) {
@@ -188,13 +188,12 @@ namespace vkcv
 		}
 		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) {
+	
+	Context Context::create(const char *applicationName,
+							uint32_t applicationVersion,
+							const std::vector<vk::QueueFlagBits>& queueFlags,
+							const std::vector<const char *>& instanceExtensions,
+							const std::vector<const char *>& deviceExtensions) {
 		// check for layer support
 		
 		const std::vector<vk::LayerProperties>& layerProperties = vk::enumerateInstanceLayerProperties();
@@ -233,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,
@@ -248,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
@@ -296,22 +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::PhysicalDeviceFeatures2 deviceFeatures;
-		deviceFeatures.features.fragmentStoresAndAtomics    = true;
-		deviceFeatures.features.geometryShader              = true;
-		deviceFeatures.features.depthClamp                 = true;
-
+		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 meshShading;
+		vk::PhysicalDeviceMeshShaderFeaturesNV meshShadingFeatures;
 		if (usingMeshShaders) {
-			meshShading.taskShader = true;
-			meshShading.meshShader = true;
-			deviceFeatures.pNext = &meshShading;
+			meshShadingFeatures.taskShader = true;
+			meshShadingFeatures.meshShader = true;
+            deviceFeatures2.setPNext(&meshShadingFeatures);
 		}
-
-		deviceCreateInfo.pNext = &deviceFeatures;
+		
+		if (shaderFloat16) {
+			deviceFeatures2.setPNext(&deviceShaderFloat16Int8Features);
+		}
+		
+		if (storage16bit) {
+			deviceShaderFloat16Int8Features.setPNext(&device16BitStorageFeatures);
+		}
+		
+		deviceCreateInfo.setPNext(&deviceFeatures2);
 
 		// Ablauf
 		// qCreateInfos erstellen --> braucht das Device
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index 963573a2d1e0766610d4f478e67af3bcbc9956b1..e8e172dd236ac5cb49d0e2caf03599c198a07092 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 {
@@ -489,7 +490,8 @@ namespace vkcv
 					pipelineLayout,
 					usage.setLocation,
 					{ usage.vulkanHandle },
-					{});
+					usage.dynamicOffsets
+				);
 			}
 			if (pushConstants.getSizePerDrawcall() > 0) {
 				cmdBuffer.pushConstants(
@@ -592,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 };
@@ -600,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(
@@ -632,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)
@@ -660,38 +667,38 @@ namespace vkcv
 		return m_DescriptorManager->getDescriptorSet(handle);
 	}
 
-	void Core::prepareSwapchainImageForPresent(const CommandStreamHandle cmdStream) {
+	void Core::prepareSwapchainImageForPresent(const CommandStreamHandle& cmdStream) {
 		auto swapchainHandle = ImageHandle::createSwapchainImageHandle();
 		recordCommandsToStream(cmdStream, [swapchainHandle, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(swapchainHandle, vk::ImageLayout::ePresentSrcKHR, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::prepareImageForSampling(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::prepareImageForSampling(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eShaderReadOnlyOptimal, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::prepareImageForStorage(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::prepareImageForStorage(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(image, vk::ImageLayout::eGeneral, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::recordImageMemoryBarrier(const CommandStreamHandle cmdStream, const ImageHandle image) {
+	void Core::recordImageMemoryBarrier(const CommandStreamHandle& cmdStream, const ImageHandle& image) {
 		recordCommandsToStream(cmdStream, [image, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageMemoryBarrier(image, cmdBuffer);
 		}, nullptr);
 	}
 
-	void Core::recordBufferMemoryBarrier(const CommandStreamHandle cmdStream, const BufferHandle buffer) {
+	void Core::recordBufferMemoryBarrier(const CommandStreamHandle& cmdStream, const BufferHandle& buffer) {
 		recordCommandsToStream(cmdStream, [buffer, this](const vk::CommandBuffer cmdBuffer) {
 			m_BufferManager->recordBufferMemoryBarrier(buffer, cmdBuffer);
 		}, nullptr);
 	}
 	
-	void Core::resolveMSAAImage(CommandStreamHandle cmdStream, ImageHandle src, ImageHandle dst) {
+	void Core::resolveMSAAImage(const CommandStreamHandle& cmdStream, const ImageHandle& src, const ImageHandle& dst) {
 		recordCommandsToStream(cmdStream, [src, dst, this](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordMSAAResolve(cmdBuffer, src, dst);
 		}, nullptr);
@@ -700,5 +707,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/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/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;