diff --git a/.gitmodules b/.gitmodules
index efa79ed703e1cc83f962cebfd4299621810cf82e..c9eb19ee6ba09ded88a81b5ad253b2ae8c73c814 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -37,3 +37,6 @@
 [submodule "lib/VulkanMemoryAllocator-Hpp"]
 	path = lib/VulkanMemoryAllocator-Hpp
 	url = https://github.com/YaaZ/VulkanMemoryAllocator-Hpp.git
+[submodule "modules/algorithm/lib/FidelityFX-SPD"]
+	path = modules/algorithm/lib/FidelityFX-SPD
+	url = https://github.com/GPUOpen-Effects/FidelityFX-SPD.git
diff --git a/config/Sources.cmake b/config/Sources.cmake
index ad7b1bbf84ea515af449b81c505de24d8cf0ecf9..2efd34c3e0158082ee539413431278aeb41e1a1c 100644
--- a/config/Sources.cmake
+++ b/config/Sources.cmake
@@ -71,9 +71,6 @@ set(vkcv_sources
         ${vkcv_include}/vkcv/QueueManager.hpp
         ${vkcv_source}/vkcv/QueueManager.cpp
 
-        ${vkcv_source}/vkcv/ImageLayoutTransitions.hpp
-        ${vkcv_source}/vkcv/ImageLayoutTransitions.cpp
-
 		${vkcv_include}/vkcv/VertexLayout.hpp
 		${vkcv_source}/vkcv/VertexLayout.cpp
 
@@ -109,4 +106,10 @@ set(vkcv_sources
         
         ${vkcv_include}/vkcv/ImageConfig.hpp
         ${vkcv_source}/vkcv/ImageConfig.cpp
+		
+		${vkcv_include}/vkcv/Downsampler.hpp
+		${vkcv_source}/vkcv/Downsampler.cpp
+		
+		${vkcv_include}/vkcv/BlitDownsampler.hpp
+		${vkcv_source}/vkcv/BlitDownsampler.cpp
 )
diff --git a/include/vkcv/BlitDownsampler.hpp b/include/vkcv/BlitDownsampler.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a7e7e47793e35fbf6f6de084faabfe776266bac3
--- /dev/null
+++ b/include/vkcv/BlitDownsampler.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "Downsampler.hpp"
+
+namespace vkcv {
+	
+	class ImageManager;
+	
+	class BlitDownsampler : public Downsampler {
+		friend class Core;
+	private:
+		ImageManager& m_imageManager;
+		
+		BlitDownsampler(Core& core,
+						ImageManager& imageManager);
+	
+	public:
+		void recordDownsampling(const CommandStreamHandle& cmdStream,
+								const ImageHandle& image) const override;
+	};
+	
+}
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index 6505599489f405d135d33ce1c4aeeba295e2176a..dacf9697ca2cc55389c6288769393dfdd5bff128 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -16,6 +16,7 @@
 #include "Handles.hpp"
 #include "Buffer.hpp"
 #include "Image.hpp"
+#include "BlitDownsampler.hpp"
 #include "GraphicsPipelineConfig.hpp"
 #include "ComputePipelineConfig.hpp"
 #include "CommandResources.hpp"
@@ -92,6 +93,8 @@ namespace vkcv
 		CommandResources    m_CommandResources;
 		SyncResources       m_SyncResources;
 		uint32_t            m_currentSwapchainImageIndex;
+		
+		std::unique_ptr<Downsampler> m_downsampler;
 
 		/**
 		 * Sets up swapchain images
@@ -241,6 +244,14 @@ namespace vkcv
 			bool            supportStorage = false,
 			bool            supportColorAttachment = false,
 			Multisampling   multisampling = Multisampling::None);
+		
+		/**
+		 * @brief Returns the default blit-downsampler.
+		 *
+		 * @return Blit-downsampler
+		 */
+		[[nodiscard]]
+		const Downsampler& getDownsampler() const;
 
         /**
          * Creates a new window and returns it's handle
@@ -558,9 +569,11 @@ namespace vkcv
 		/**
 		 * @brief Submit command stream to GPU for actual execution
 		 * 
-		 * @param handle command stream to submit
+		 * @param[in] handle Command stream to submit
+		 * @param[in] signalRendering Flag to specify if the command stream finishes rendering
 		 */
-		void submitCommandStream(const CommandStreamHandle& handle);
+		void submitCommandStream(const CommandStreamHandle& handle,
+								 bool signalRendering = true);
 
 		/**
 		 * @brief Prepare swapchain image for presentation to screen.
@@ -574,19 +587,29 @@ namespace vkcv
 		 * @brief Prepare image for use as a sampled image.
 		 * Handles internal state such as image format, also acts as a memory barrier
 		 * 
-		 * @param cmdStream Handle of the command stream to record the preparation commands to
-		 * @param image Handle of the image to prepare
+		 * @param[in] cmdStream Handle of the command stream to record the preparation commands to
+		 * @param[in] image Handle of the image to prepare
+		 * @param[in] mipLevelCount Count of mip levels to prepare
+		 * @param[in] mipLevelOffset Offset to start preparing mip levels
 		 */
-		void prepareImageForSampling(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		void prepareImageForSampling(const CommandStreamHandle& cmdStream,
+									 const ImageHandle& image,
+									 uint32_t mipLevelCount = 0,
+									 uint32_t mipLevelOffset = 0);
 
 		/**
 		 * @brief Prepare image for use as a storage image.
 		 * Handles internal state such as image format, also acts as a memory barrier
 		 *
-		 * @param cmdStream Handle of the command stream to record the preparation commands to
-		 * @param image Handle of the image to prepare
-		 */
-		void prepareImageForStorage(const CommandStreamHandle& cmdStream, const ImageHandle& image);
+		 * @param[in] cmdStream Handle of the command stream to record the preparation commands to
+		 * @param[in] image Handle of the image to prepare
+		 * @param[in] mipLevelCount Count of mip levels to prepare
+		 * @param[in] mipLevelOffset Offset to start preparing mip levels
+		 */
+		void prepareImageForStorage(const CommandStreamHandle& cmdStream,
+									const ImageHandle& image,
+									uint32_t mipLevelCount = 0,
+									uint32_t mipLevelOffset = 0);
 		
 		/**
 		 * @brief Manual trigger to record commands to prepare an image for use as an attachment
diff --git a/include/vkcv/DescriptorWrites.hpp b/include/vkcv/DescriptorWrites.hpp
index b8468bfdfd4cd20e59742f410ff8716be4e910fa..9d3269feb96c1828eb0c626bef332b7b9d68ed45 100644
--- a/include/vkcv/DescriptorWrites.hpp
+++ b/include/vkcv/DescriptorWrites.hpp
@@ -20,6 +20,7 @@ namespace vkcv {
 		uint32_t mipLevel;
 		bool useGeneralLayout;
 		uint32_t arrayIndex;
+		uint32_t mipCount;
 	};
 	
 	/**
@@ -29,6 +30,7 @@ namespace vkcv {
 		uint32_t binding;
 		ImageHandle image;
 		uint32_t mipLevel;
+		uint32_t mipCount;
 	};
 	
 	/**
@@ -81,13 +83,15 @@ namespace vkcv {
 		 * @param[in] mipLevel Mip level index
 		 * @param[in] useGeneralLayout Flag to use a general layout
 		 * @param[in] arrayIndex Image array index
+		 * @param[in] mipCount Mip level count
 		 * @return Instance of descriptor writes
 		 */
 		DescriptorWrites& writeSampledImage(uint32_t binding,
 											ImageHandle image,
 											uint32_t mipLevel = 0,
 											bool useGeneralLayout = false,
-											uint32_t arrayIndex = 0);
+											uint32_t arrayIndex = 0,
+											uint32_t mipCount = 1);
 		
 		/**
 		 * @brief Adds an entry to write an image to a given binding
@@ -96,11 +100,13 @@ namespace vkcv {
 		 * @param[in] binding Binding index
 		 * @param[in,out] image Image handle
 		 * @param[in] mipLevel Mip level index
+		 * @param[in] mipCount Mip level count
 		 * @return Instance of descriptor writes
 		 */
 		DescriptorWrites& writeStorageImage(uint32_t binding,
 											ImageHandle image,
-											uint32_t mipLevel = 0);
+											uint32_t mipLevel = 0,
+											uint32_t mipCount = 1);
 		
 		/**
 		 * @brief Adds an entry to write a buffer to a given binding
diff --git a/include/vkcv/Downsampler.hpp b/include/vkcv/Downsampler.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..ae491ea3e7cb7e1ef50f15f699b6e53e66276321
--- /dev/null
+++ b/include/vkcv/Downsampler.hpp
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "Handles.hpp"
+
+namespace vkcv {
+	
+	class Core;
+	
+	/**
+	 * An abstract class to handle downsampling of images for mip generation.
+	 */
+	class Downsampler {
+	protected:
+		/**
+         * Reference to the current Core instance.
+         */
+		Core& m_core;
+		
+	public:
+		/**
+         * Constructor to create a downsampler instance.
+         * @param[in,out] core Reference to a Core instance
+         */
+		explicit Downsampler(Core& core);
+		
+		~Downsampler() = default;
+		
+		/**
+		 * Record the commands of the given downsampler instance to
+         * scale the image down on its own mip levels.
+		 *
+		 * @param[in] cmdStream
+		 * @param[in] image
+		 */
+		virtual void recordDownsampling(const CommandStreamHandle& cmdStream,
+										const ImageHandle& image) const = 0;
+		
+	};
+	
+}
\ No newline at end of file
diff --git a/include/vkcv/FeatureManager.hpp b/include/vkcv/FeatureManager.hpp
index 51e422c949b66fe021ec3a5afd95f50a94e39544..e15ad7670795d4ffaf879248612bbbc5cf8cfb74 100644
--- a/include/vkcv/FeatureManager.hpp
+++ b/include/vkcv/FeatureManager.hpp
@@ -440,13 +440,37 @@ namespace vkcv {
 		}
 		
 		/**
-		 * @brief Return feature structure chain to request activated features.
+		 * @brief Return feature structure chain to request all activated features.
 		 *
 		 * @return Head of feature structure chain
 		 */
 		[[nodiscard]]
 		const vk::PhysicalDeviceFeatures2& getFeatures() const;
 		
+		/**
+		 * @brief Checks all activated features for a specific feature and returns its state.
+		 *
+		 * @tparam T Template parameter to use specific base structure types
+		 * @param[in] type Vulkan structure type identifier
+		 * @param[in] featureTestFunction Function to test feature structure with for requested attributes
+		 * @return True, if the requested attributes are available, else false
+		 */
+		template<typename T>
+		bool checkFeatures(vk::StructureType type,
+						   const std::function<bool(const T&)>& featureTestFunction) const {
+			const auto* base = reinterpret_cast<const vk::BaseInStructure*>(&getFeatures());
+			
+			while (base) {
+				if ((base->sType == type) && (featureTestFunction(*reinterpret_cast<const T*>(base)))) {
+					return true;
+				}
+				
+				base = base->pNext;
+			}
+			
+			return false;
+		}
+		
 	};
 	
 }
diff --git a/include/vkcv/Image.hpp b/include/vkcv/Image.hpp
index 5d5359288d4b1b1f38bfabf062c9cc4cd0ca9ad1..3cb115fd9b84bed4b03717581193fb174dd27889 100644
--- a/include/vkcv/Image.hpp
+++ b/include/vkcv/Image.hpp
@@ -12,6 +12,7 @@
 
 namespace vkcv {
 	
+	class Downsampler;
 	class ImageManager;
 
 	/**
@@ -95,19 +96,15 @@ namespace vkcv {
 		 */
 		void fill(const void* data, size_t size = SIZE_MAX);
 
-		/**
-		 * @brief Generates the entire mip chain from mip level zero,
-		 * returns after operation is finished
-		 */
-		void generateMipChainImmediate();
-
 		/**
 		 * @brief Records mip chain generation to command stream,
 		 * mip level zero is used as source
 		 * 
 		 * @param[out] cmdStream Command stream that the commands are recorded into
+		 * @param[in] downsampler Downsampler to generate mip levels with
 		 */
-		void recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream);
+		void recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream,
+									  const Downsampler &downsampler);
 		
 	private:
 	    // TODO: const qualifier removed, very hacky!!!
diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt
index 0a7f0f0ab1b71f78c6a548caf3d82442a05dcb2b..1bd5a48df5ea3c53a26509e76aa8daed0fea5b10 100644
--- a/modules/CMakeLists.txt
+++ b/modules/CMakeLists.txt
@@ -3,6 +3,7 @@ set(vkcv_modules_includes)
 set(vkcv_modules_libraries)
 
 # Add new modules here:
+add_subdirectory(algorithm)
 add_subdirectory(asset_loader)
 add_subdirectory(camera)
 add_subdirectory(effects)
diff --git a/modules/algorithm/CMakeLists.txt b/modules/algorithm/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d4eeaa261d10f50bd6c4bc8a43455cb252be50cf
--- /dev/null
+++ b/modules/algorithm/CMakeLists.txt
@@ -0,0 +1,41 @@
+cmake_minimum_required(VERSION 3.16)
+project(vkcv_algorithm)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(vkcv_algorithm_source ${PROJECT_SOURCE_DIR}/src)
+set(vkcv_algorithm_include ${PROJECT_SOURCE_DIR}/include)
+
+set(vkcv_algorithm_sources
+		${vkcv_algorithm_include}/vkcv/algorithm/SinglePassDownsampler.hpp
+		${vkcv_algorithm_source}/vkcv/algorithm/SinglePassDownsampler.cpp
+)
+
+# Setup some path variables to load libraries
+set(vkcv_algorithm_lib lib)
+set(vkcv_algorithm_lib_path ${PROJECT_SOURCE_DIR}/${vkcv_algorithm_lib})
+
+# Check and load FidelityFX_SPD
+include(config/FidelityFX_SPD.cmake)
+
+# adding source files to the project
+add_library(vkcv_algorithm ${vkcv_build_attribute} ${vkcv_algorithm_sources})
+
+# link the required libraries to the module
+target_link_libraries(vkcv_algorithm ${vkcv_algorithm_libraries} vkcv vkcv_shader_compiler vkcv_camera vkcv_asset_loader)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(vkcv_algorithm SYSTEM BEFORE PRIVATE ${vkcv_algorithm_includes} ${vkcv_include} ${vkcv_includes} ${vkcv_shader_compiler_include})
+
+# add the own include directory for public headers
+target_include_directories(vkcv_algorithm BEFORE PUBLIC ${vkcv_algorithm_include})
+
+if (vkcv_parent_scope)
+	list(APPEND vkcv_modules_includes ${vkcv_algorithm_include})
+	list(APPEND vkcv_modules_libraries vkcv_algorithm)
+	
+	set(vkcv_modules_includes ${vkcv_modules_includes} PARENT_SCOPE)
+	set(vkcv_modules_libraries ${vkcv_modules_libraries} PARENT_SCOPE)
+endif()
diff --git a/modules/algorithm/config/FidelityFX_SPD.cmake b/modules/algorithm/config/FidelityFX_SPD.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..eec11bfde46e7079417cabfbdc47a9e387877bff
--- /dev/null
+++ b/modules/algorithm/config/FidelityFX_SPD.cmake
@@ -0,0 +1,21 @@
+
+use_git_submodule("${vkcv_algorithm_lib_path}/FidelityFX-SPD" ffx_spd_status)
+
+if (${ffx_spd_status})
+	include_shader(${vkcv_algorithm_lib_path}/FidelityFX-SPD/ffx-spd/ffx_a.h ${vkcv_algorithm_include} ${vkcv_algorithm_source})
+	include_shader(${vkcv_algorithm_lib_path}/FidelityFX-SPD/ffx-spd/ffx_spd.h ${vkcv_algorithm_include} ${vkcv_algorithm_source})
+	include_shader(${vkcv_algorithm_lib_path}/FidelityFX-SPD/sample/src/VK/SPDIntegration.glsl ${vkcv_algorithm_include} ${vkcv_algorithm_source})
+	include_shader(${vkcv_algorithm_lib_path}/FidelityFX-SPD/sample/src/VK/SPDIntegrationLinearSampler.glsl ${vkcv_algorithm_include} ${vkcv_algorithm_source})
+
+	list(APPEND vkcv_algorithm_includes ${vkcv_algorithm_lib}/FidelityFX-SPD/ffx-spd)
+	
+	list(APPEND vkcv_algorithm_sources ${vkcv_algorithm_source}/ffx_a.h.cxx)
+	list(APPEND vkcv_algorithm_sources ${vkcv_algorithm_source}/ffx_spd.h.cxx)
+	list(APPEND vkcv_algorithm_sources ${vkcv_algorithm_source}/SPDIntegration.glsl.cxx)
+	list(APPEND vkcv_algorithm_sources ${vkcv_algorithm_source}/SPDIntegrationLinearSampler.glsl.cxx)
+	
+	list(APPEND vkcv_algorithm_sources ${vkcv_algorithm_include}/ffx_a.h.hxx)
+	list(APPEND vkcv_algorithm_sources ${vkcv_algorithm_include}/ffx_spd.h.hxx)
+	list(APPEND vkcv_algorithm_sources ${vkcv_algorithm_include}/SPDIntegration.glsl.hxx)
+	list(APPEND vkcv_algorithm_sources ${vkcv_algorithm_include}/SPDIntegrationLinearSampler.glsl.hxx)
+endif ()
diff --git a/modules/algorithm/include/vkcv/algorithm/SinglePassDownsampler.hpp b/modules/algorithm/include/vkcv/algorithm/SinglePassDownsampler.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..c2869affda1ca0af33ad511f661ac41ca2e036d8
--- /dev/null
+++ b/modules/algorithm/include/vkcv/algorithm/SinglePassDownsampler.hpp
@@ -0,0 +1,45 @@
+#pragma once
+
+#include <vkcv/Core.hpp>
+#include <vkcv/Downsampler.hpp>
+#include <vkcv/ShaderProgram.hpp>
+
+namespace vkcv::algorithm {
+
+#define SPD_MAX_MIP_LEVELS 12
+	
+	struct SPDConstants {
+		int mips;
+		int numWorkGroupsPerSlice;
+		int workGroupOffset[2];
+	};
+	
+	struct SPDConstantsSampler {
+		int mips;
+		int numWorkGroupsPerSlice;
+		int workGroupOffset[2];
+		float invInputSize[2];
+		float padding[2];
+	};
+
+	class SinglePassDownsampler : public vkcv::Downsampler {
+	private:
+		ComputePipelineHandle m_pipeline;
+		
+		DescriptorSetLayoutHandle m_descriptorSetLayout;
+		DescriptorSetHandle m_descriptorSet;
+		
+		Buffer<uint32_t> m_globalCounter;
+		
+		SamplerHandle m_sampler;
+		
+	public:
+		explicit SinglePassDownsampler(Core& core,
+									   const SamplerHandle &sampler = SamplerHandle());
+		
+		void recordDownsampling(const CommandStreamHandle& cmdStream,
+								const ImageHandle& image) const override;
+	
+	};
+
+}
diff --git a/modules/algorithm/lib/FidelityFX-SPD b/modules/algorithm/lib/FidelityFX-SPD
new file mode 160000
index 0000000000000000000000000000000000000000..7c796c6d9fa6a9439e3610478148cfd742d97daf
--- /dev/null
+++ b/modules/algorithm/lib/FidelityFX-SPD
@@ -0,0 +1 @@
+Subproject commit 7c796c6d9fa6a9439e3610478148cfd742d97daf
diff --git a/modules/algorithm/src/vkcv/algorithm/SinglePassDownsampler.cpp b/modules/algorithm/src/vkcv/algorithm/SinglePassDownsampler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d715fbdabc7cbcfb8a52a11997df23abe98499ef
--- /dev/null
+++ b/modules/algorithm/src/vkcv/algorithm/SinglePassDownsampler.cpp
@@ -0,0 +1,287 @@
+
+#include "vkcv/algorithm/SinglePassDownsampler.hpp"
+
+#include <cstdint>
+#include <cmath>
+
+#define A_CPU 1
+#include <ffx_a.h>
+#include <ffx_spd.h>
+
+#include "ffx_a.h.hxx"
+#include "ffx_spd.h.hxx"
+#include "SPDIntegration.glsl.hxx"
+#include "SPDIntegrationLinearSampler.glsl.hxx"
+
+#include <vkcv/File.hpp>
+#include <vkcv/Logger.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+namespace vkcv::algorithm {
+	
+	static DescriptorBindings getDescriptorBindings(const SamplerHandle &sampler) {
+		DescriptorBindings descriptorBindings = {};
+		
+		auto binding_0 = DescriptorBinding {
+				0,
+				DescriptorType::IMAGE_STORAGE,
+				SPD_MAX_MIP_LEVELS + 1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_1 = DescriptorBinding {
+				1,
+				DescriptorType::IMAGE_STORAGE,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_2 = DescriptorBinding{
+				2,
+				DescriptorType::STORAGE_BUFFER,
+				1,
+				ShaderStage::COMPUTE,
+				false
+		};
+		
+		auto binding_3 = DescriptorBinding{
+				3,
+				DescriptorType::IMAGE_SAMPLED,
+				1,
+				ShaderStage::COMPUTE
+		};
+		
+		auto binding_4 = DescriptorBinding{
+				4,
+				DescriptorType::SAMPLER,
+				1,
+				ShaderStage::COMPUTE
+		};
+		
+		descriptorBindings.insert(std::make_pair(0, binding_0));
+		descriptorBindings.insert(std::make_pair(1, binding_1));
+		descriptorBindings.insert(std::make_pair(2, binding_2));
+		
+		if (sampler) {
+			descriptorBindings.insert(std::make_pair(3, binding_3));
+			descriptorBindings.insert(std::make_pair(4, binding_4));
+		}
+		
+		return descriptorBindings;
+	}
+	
+	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 compileSPDShader(vkcv::shader::GLSLCompiler& compiler,
+								 const std::string &source,
+								 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_spd.h", FFX_SPD_H_SHADER)) {
+			return false;
+		}
+		
+		return compiler.compileSource(
+				vkcv::ShaderStage::COMPUTE,
+				source.c_str(),
+				[&directory, &compiled] (vkcv::ShaderStage shaderStage,
+										 const std::filesystem::path& path) {
+					if (compiled) {
+						compiled(shaderStage, path);
+					}
+					
+					std::filesystem::remove_all(directory);
+				}, directory
+		);
+	}
+	
+	SinglePassDownsampler::SinglePassDownsampler(Core &core,
+												 const SamplerHandle &sampler) :
+		 vkcv::Downsampler(core),
+		 m_pipeline(),
+		
+		 m_descriptorSetLayout(m_core.createDescriptorSetLayout(getDescriptorBindings(sampler))),
+		 m_descriptorSet(m_core.createDescriptorSet(m_descriptorSetLayout)),
+		
+		 m_globalCounter(m_core.createBuffer<uint32_t>(
+				 vkcv::BufferType::STORAGE,
+				 6
+		 )),
+		 
+		 m_sampler(sampler) {
+		vkcv::shader::GLSLCompiler compiler (vkcv::shader::GLSLCompileTarget::SUBGROUP_OP);
+		
+		vk::PhysicalDeviceSubgroupProperties subgroupProperties;
+		
+		vk::PhysicalDeviceProperties2 properties2;
+		properties2.pNext = &subgroupProperties;
+		m_core.getContext().getPhysicalDevice().getProperties2(&properties2);
+		
+		const bool no_subgroup_quad = !(subgroupProperties.supportedOperations & vk::SubgroupFeatureFlagBits::eQuad);
+		
+		if (no_subgroup_quad) {
+			compiler.setDefine("SPD_NO_WAVE_OPERATIONS", "1");
+		}
+		
+		const auto& featureManager = m_core.getContext().getFeatureManager();
+		
+		const bool float16Support = (
+				featureManager.checkFeatures<vk::PhysicalDeviceFloat16Int8FeaturesKHR>(
+						vk::StructureType::ePhysicalDeviceShaderFloat16Int8FeaturesKHR,
+						[](const vk::PhysicalDeviceFloat16Int8FeaturesKHR& features) {
+							return features.shaderFloat16;
+						}
+				) &&
+				featureManager.checkFeatures<vk::PhysicalDevice16BitStorageFeaturesKHR>(
+						vk::StructureType::ePhysicalDevice16BitStorageFeaturesKHR,
+						[](const vk::PhysicalDevice16BitStorageFeaturesKHR& features) {
+							return features.storageBuffer16BitAccess;
+						}
+				) &&
+				((no_subgroup_quad) ||
+				(featureManager.checkFeatures<vk::PhysicalDeviceShaderSubgroupExtendedTypesFeatures>(
+						vk::StructureType::ePhysicalDeviceShaderSubgroupExtendedTypesFeatures,
+						[](const vk::PhysicalDeviceShaderSubgroupExtendedTypesFeatures& features) {
+							return features.shaderSubgroupExtendedTypes;
+						}
+				)))
+		);
+		
+		if (float16Support) {
+			compiler.setDefine("A_HALF", "1");
+			compiler.setDefine("SPD_PACKED_ONLY", "1");
+		}
+		
+		ShaderProgram program;
+		if (m_sampler) {
+			compileSPDShader(
+					compiler,
+					SPDINTEGRATIONLINEARSAMPLER_GLSL_SHADER,
+					[&program](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+						program.addShader(shaderStage, path);
+					}
+			);
+		} else {
+			compileSPDShader(
+					compiler,
+					SPDINTEGRATION_GLSL_SHADER,
+					[&program](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
+						program.addShader(shaderStage, path);
+					}
+			);
+		}
+		
+		m_pipeline = m_core.createComputePipeline({program,{
+				m_descriptorSetLayout
+		}});
+		
+		uint32_t zeroes [m_globalCounter.getCount()];
+		memset(zeroes, 0, m_globalCounter.getSize());
+		m_globalCounter.fill(zeroes);
+		
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageBuffer(2, m_globalCounter.getHandle());
+		
+		if (m_sampler) {
+			writes.writeSampler(4, m_sampler);
+		}
+		
+		m_core.writeDescriptorSet(m_descriptorSet, writes);
+	}
+	
+	void SinglePassDownsampler::recordDownsampling(const CommandStreamHandle &cmdStream,
+												   const ImageHandle &image) const {
+		vkcv::DescriptorWrites writes;
+		writes.writeStorageImage(1, image, 6);
+		
+		if (m_sampler) {
+			writes.writeStorageImage(0, image, 0, m_core.getImageMipLevels(image) - 1);
+			writes.writeSampledImage(3, image);
+		} else {
+			writes.writeStorageImage(0, image, 0, m_core.getImageMipLevels(image));
+		}
+		
+		m_core.writeDescriptorSet(m_descriptorSet, writes);
+		
+		varAU2(dispatchThreadGroupCountXY);
+		varAU2(workGroupOffset);
+		varAU2(numWorkGroupsAndMips);
+		varAU4(rectInfo) = initAU4(
+				0,
+				0,
+				m_core.getImageWidth(image),
+				m_core.getImageHeight(image)
+		);
+		
+		SpdSetup(dispatchThreadGroupCountXY, workGroupOffset, numWorkGroupsAndMips, rectInfo);
+		
+		if (m_sampler) {
+			m_core.prepareImageForStorage(cmdStream, image, m_core.getImageMipLevels(image) - 1, 1);
+			m_core.prepareImageForSampling(cmdStream, image, 1);
+		} else {
+			m_core.prepareImageForStorage(cmdStream, image);
+		}
+		
+		uint32_t dispatch [3];
+		dispatch[0] = dispatchThreadGroupCountXY[0];
+		dispatch[1] = dispatchThreadGroupCountXY[1];
+		dispatch[2] = 1; // TODO: No array size supported by images yet!
+		
+		vkcv::PushConstants pushConstants (m_sampler? sizeof(SPDConstantsSampler) : sizeof(SPDConstants));
+		
+		if (m_sampler) {
+			SPDConstantsSampler data;
+			data.numWorkGroupsPerSlice = numWorkGroupsAndMips[0];
+			data.mips = numWorkGroupsAndMips[1];
+			data.workGroupOffset[0] = workGroupOffset[0];
+			data.workGroupOffset[1] = workGroupOffset[1];
+			data.invInputSize[0] = 1.0f / m_core.getImageWidth(image);
+			data.invInputSize[1] = 1.0f / m_core.getImageHeight(image);
+			
+			pushConstants.appendDrawcall<SPDConstantsSampler>(data);
+		} else {
+			SPDConstants data;
+			data.numWorkGroupsPerSlice = numWorkGroupsAndMips[0];
+			data.mips = numWorkGroupsAndMips[1];
+			data.workGroupOffset[0] = workGroupOffset[0];
+			data.workGroupOffset[1] = workGroupOffset[1];
+			
+			pushConstants.appendDrawcall<SPDConstants>(data);
+		}
+		
+		m_core.recordComputeDispatchToCmdStream(cmdStream, m_pipeline, dispatch, {
+			vkcv::DescriptorSetUsage(0, m_descriptorSet)
+		}, pushConstants);
+		
+		if (m_sampler) {
+			m_core.prepareImageForSampling(cmdStream, image, m_core.getImageMipLevels(image) - 1, 1);
+		} else {
+			m_core.prepareImageForSampling(cmdStream, image);
+		}
+	}
+	
+}
diff --git a/modules/material/include/vkcv/material/Material.hpp b/modules/material/include/vkcv/material/Material.hpp
index 7251bda151cab9f812114f8a7a707597c476ad21..c47d0c5ba084e845b8a364ce0ebf5e71539bfd9a 100644
--- a/modules/material/include/vkcv/material/Material.hpp
+++ b/modules/material/include/vkcv/material/Material.hpp
@@ -3,6 +3,7 @@
 #include <vector>
 
 #include <vkcv/Core.hpp>
+#include <vkcv/Downsampler.hpp>
 #include <vkcv/Handles.hpp>
 
 namespace vkcv::material {
@@ -146,6 +147,15 @@ namespace vkcv::material {
          * @return true if the material is invalid, otherwise false
          */
 		bool operator!() const;
+		
+		/**
+		 * @brief Records mip chain generation to command stream of the whole material.
+		 *
+		 * @param[out] cmdStream Command stream that the commands are recorded into
+		 * @param[in] downsampler Downsampler to generate mip levels with
+		 */
+		void recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream,
+									  const Downsampler &downsampler);
 
         /**
          * Returns the descriptor bindings required by a given material
diff --git a/modules/material/src/vkcv/material/Material.cpp b/modules/material/src/vkcv/material/Material.cpp
index 2bd18e8342a24d6311182c9c0c2d640d3cbfa185..a1054ed021fb087397e2fae5899777507fe21fa8 100644
--- a/modules/material/src/vkcv/material/Material.cpp
+++ b/modules/material/src/vkcv/material/Material.cpp
@@ -27,6 +27,13 @@ namespace vkcv::material {
 		return (m_Type == MaterialType::UNKNOWN);
 	}
 	
+	void Material::recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream,
+											const Downsampler &downsampler) {
+		for (auto& texture : m_Textures) {
+			downsampler.recordDownsampling(cmdStream, texture.m_Image);
+		}
+	}
+	
 	const DescriptorBindings& Material::getDescriptorBindings(MaterialType type)
 	{
 		static DescriptorBindings pbr_bindings = {};
diff --git a/modules/scene/src/vkcv/scene/Scene.cpp b/modules/scene/src/vkcv/scene/Scene.cpp
index cd02004f56ea3dfddcd20618b7859a0699d92d71..695e568444d4fe916c67b79965034a1c30d50623 100644
--- a/modules/scene/src/vkcv/scene/Scene.cpp
+++ b/modules/scene/src/vkcv/scene/Scene.cpp
@@ -274,6 +274,14 @@ namespace vkcv::scene {
 			scene.getNode(root).loadMesh(asset_scene, mesh);
 		}
 		
+		auto mipStream = core.createCommandStream(vkcv::QueueType::Graphics);
+		
+		for (auto& material : scene.m_materials) {
+			material.m_data.recordMipChainGeneration(mipStream, core.getDownsampler());
+		}
+		
+		core.submitCommandStream(mipStream, false);
+		
 		scene.getNode(root).splitMeshesToSubNodes(128);
 		return scene;
 	}
diff --git a/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp b/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
index e457874ad2abc4cc3bfe9ab74fdb8d35446b5db1..face4cba350122f22feee6bae878397dccf4a070 100644
--- a/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
+++ b/modules/shader_compiler/include/vkcv/shader/GLSLCompiler.hpp
@@ -11,17 +11,28 @@ namespace vkcv::shader {
      * @addtogroup vkcv_shader
      * @{
      */
+     
+	enum class GLSLCompileTarget {
+		SUBGROUP_OP,
+		RAY_TRACING,
+		
+		UNKNOWN
+	};
 
     /**
      * A class to handle GLSL runtime shader compilation.
      */
 	class GLSLCompiler : public Compiler {
 	private:
+		GLSLCompileTarget m_target;
+		
 	public:
         /**
          * The constructor of a runtime GLSL shader compiler instance.
+         *
+         * @param[in] target Compile target (optional)
          */
-		GLSLCompiler();
+		GLSLCompiler(GLSLCompileTarget target = GLSLCompileTarget::UNKNOWN);
 
         /**
          * The copy-constructor of a runtime GLSL shader compiler instance.
diff --git a/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
index c639118f5a4482eb9db2cee46057e5662b472181..78a82113f89bd54914471d64fee6e11700548431 100644
--- a/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
+++ b/modules/shader_compiler/src/vkcv/shader/GLSLCompiler.cpp
@@ -13,7 +13,9 @@ namespace vkcv::shader {
 	
 	static uint32_t s_CompilerCount = 0;
 	
-	GLSLCompiler::GLSLCompiler() : Compiler() {
+	GLSLCompiler::GLSLCompiler(GLSLCompileTarget target) :
+		Compiler(),
+		m_target(target) {
 		if (s_CompilerCount == 0) {
 			glslang::InitializeProcess();
 		}
@@ -56,17 +58,17 @@ namespace vkcv::shader {
 				return EShLangTaskNV;
 			case ShaderStage::MESH:
 				return EShLangMeshNV;
-			case ShaderStage::RAY_GEN: // for RTX
+			case ShaderStage::RAY_GEN:
 			    return EShLangRayGen;
-			case ShaderStage::RAY_CLOSEST_HIT: // for RTX
+			case ShaderStage::RAY_CLOSEST_HIT:
 			    return EShLangClosestHit;
-			case ShaderStage::RAY_MISS: // for RTX
+			case ShaderStage::RAY_MISS:
 			    return EShLangMiss;
-			case ShaderStage::RAY_INTERSECTION: // for RTX
+			case ShaderStage::RAY_INTERSECTION:
 				return EShLangIntersect;
-			case ShaderStage::RAY_ANY_HIT: // for RTX
+			case ShaderStage::RAY_ANY_HIT:
 				return EShLangAnyHit;
-			case ShaderStage::RAY_CALLABLE: // for RTX
+			case ShaderStage::RAY_CALLABLE:
 				return EShLangCallable;
 			default:
 				return EShLangCount;
@@ -226,18 +228,20 @@ namespace vkcv::shader {
 		}
 		
 		glslang::TShader shader (language);
-
-		// configure environment for rtx shaders by adjusting versions of Vulkan and SPIR-V, else rtx shader cannot be compiled.
-		if((shaderStage == ShaderStage::RAY_GEN) || (shaderStage == ShaderStage::RAY_ANY_HIT)
-		|| (shaderStage == ShaderStage::RAY_CLOSEST_HIT) || (shaderStage == ShaderStage::RAY_MISS)
-		|| (shaderStage == ShaderStage::RAY_INTERSECTION) || (shaderStage == ShaderStage::RAY_CALLABLE)){
-
-		    shader.setEnvClient(glslang::EShClientVulkan,glslang::EShTargetVulkan_1_2);
-		    shader.setEnvTarget(glslang::EShTargetSpv,glslang::EShTargetSpv_1_4);
+		switch (m_target) {
+			case GLSLCompileTarget::SUBGROUP_OP:
+				shader.setEnvClient(glslang::EShClientVulkan,glslang::EShTargetVulkan_1_1);
+				shader.setEnvTarget(glslang::EShTargetSpv,glslang::EShTargetSpv_1_3);
+				break;
+			case GLSLCompileTarget::RAY_TRACING:
+				shader.setEnvClient(glslang::EShClientVulkan,glslang::EShTargetVulkan_1_2);
+				shader.setEnvTarget(glslang::EShTargetSpv,glslang::EShTargetSpv_1_4);
+				break;
+			default:
+				break;
 		}
 
 		glslang::TProgram program;
-		
 		std::string source (shaderSource);
 		
 		if (!m_defines.empty()) {
diff --git a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
index a2009440acffd1e9a82c4832b0d2cf4e1c92b34b..a470cd21aba04faf7087e883cadfecb322876553 100644
--- a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
+++ b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
@@ -107,26 +107,6 @@ namespace vkcv::upscaling {
 	    return descriptorBindings;
 	}
 	
-	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);
 		
@@ -205,17 +185,20 @@ namespace vkcv::upscaling {
 		vkcv::shader::GLSLCompiler easuCompiler;
 		vkcv::shader::GLSLCompiler rcasCompiler;
 		
-		const auto& features = m_core.getContext().getFeatureManager().getFeatures();
+		const auto& featureManager = m_core.getContext().getFeatureManager();
+		
 		const bool float16Support = (
-				checkFeatures<vk::PhysicalDeviceFloat16Int8FeaturesKHR>(
-						reinterpret_cast<const vk::BaseInStructure*>(&features),
+				featureManager.checkFeatures<vk::PhysicalDeviceFloat16Int8FeaturesKHR>(
 						vk::StructureType::ePhysicalDeviceShaderFloat16Int8FeaturesKHR,
-						checkFloat16
+						[](const vk::PhysicalDeviceFloat16Int8FeaturesKHR& features) {
+							return features.shaderFloat16;
+						}
 				) &&
-				checkFeatures<vk::PhysicalDevice16BitStorageFeaturesKHR>(
-						reinterpret_cast<const vk::BaseInStructure*>(&features),
+				featureManager.checkFeatures<vk::PhysicalDevice16BitStorageFeaturesKHR>(
 						vk::StructureType::ePhysicalDevice16BitStorageFeaturesKHR,
-						check16Storage
+						[](const vk::PhysicalDevice16BitStorageFeaturesKHR& features) {
+							return features.storageBuffer16BitAccess;
+						}
 				)
 		);
 		
diff --git a/projects/bindless_textures/src/main.cpp b/projects/bindless_textures/src/main.cpp
index 0986b0910b6e7486c2dc3fc244269672a58d0730..811e1162588fa699b4d74045504dc9c3d4de4f4c 100644
--- a/projects/bindless_textures/src/main.cpp
+++ b/projects/bindless_textures/src/main.cpp
@@ -56,15 +56,14 @@ int main(int argc, const char** argv) {
                                         "resources/cube/Grass001_1K_Displacement.jpg",
                                         "resources/cube/Grass001_1K_Normal.jpg",
                                         "resources/cube/Grass001_1K_Roughness.jpg" };
-    for(uint32_t i = 0; i < 5; i++)
+	
+    for (uint32_t i = 0; i < 5; i++)
     {
         std::filesystem::path grassPath(grassPaths[i]);
         vkcv::asset::Texture grassTexture = vkcv::asset::loadTexture(grassPath);
 
         vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, grassTexture.width, grassTexture.height);
         texture.fill(grassTexture.data.data());
-        texture.generateMipChainImmediate();
-        texture.switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
 
         texturesArray.push_back(texture);
     }
@@ -167,12 +166,20 @@ int main(int argc, const char** argv) {
 		return EXIT_FAILURE;
 	}
 	
-	vkcv::asset::Texture &tex = mesh.textures[0];
-	vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, tex.w, tex.h);
-	texture.fill(tex.data.data());
-	texture.generateMipChainImmediate();
-	texture.switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
-	texturesArray.push_back(texture);
+	{
+		vkcv::asset::Texture &tex = mesh.textures[0];
+		vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, tex.w, tex.h);
+		texture.fill(tex.data.data());
+		texturesArray.push_back(texture);
+	}
+	
+	auto downsampleStream = core.createCommandStream(vkcv::QueueType::Graphics);
+	
+	for (auto& texture : texturesArray) {
+		texture.recordMipChainGeneration(downsampleStream, core.getDownsampler());
+	}
+	
+	core.submitCommandStream(downsampleStream, false);
 
 	vkcv::SamplerHandle sampler = core.createSampler(
 		vkcv::SamplerFilterType::LINEAR,
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index fe8acb3cfbb65de71763c2c30b50daf5f598234d..fa671a5507a7621586497048128e8959f72cdf08 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -127,8 +127,12 @@ int main(int argc, const char** argv) {
 	vkcv::asset::Texture &tex = mesh.textures[0];
 	vkcv::Image texture = core.createImage(vk::Format::eR8G8B8A8Srgb, tex.w, tex.h);
 	texture.fill(tex.data.data());
-	texture.generateMipChainImmediate();
-	texture.switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
+	
+	{
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+		texture.recordMipChainGeneration(cmdStream, core.getDownsampler());
+		core.submitCommandStream(cmdStream, false);
+	}
 
 	vkcv::SamplerHandle sampler = core.createSampler(
 		vkcv::SamplerFilterType::LINEAR,
diff --git a/projects/indirect_dispatch/src/AppSetup.cpp b/projects/indirect_dispatch/src/AppSetup.cpp
index 342d2d705f831bc98705c9ecaaa5b369befda7b6..933f20db73313e25bc9e4863f2bf4530df26a366 100644
--- a/projects/indirect_dispatch/src/AppSetup.cpp
+++ b/projects/indirect_dispatch/src/AppSetup.cpp
@@ -75,8 +75,12 @@ bool loadImage(vkcv::Core& core, const std::filesystem::path& path, vkcv::ImageH
 		true);
 
 	image.fill(textureData.data.data(), textureData.data.size());
-	image.generateMipChainImmediate();
-	image.switchLayout(vk::ImageLayout::eReadOnlyOptimalKHR);
+	
+	{
+		auto mipStream = core.createCommandStream(vkcv::QueueType::Graphics);
+		image.recordMipChainGeneration(mipStream, core.getDownsampler());
+		core.submitCommandStream(mipStream, false);
+	}
 
 	*outImage = image.getHandle();
 	return true;
diff --git a/projects/indirect_draw/src/main.cpp b/projects/indirect_draw/src/main.cpp
index 16e3e83994a1a7ca14fa2163799ee8d034eb8deb..26f523958e9704d7fc090ceba818591d904a2e92 100644
--- a/projects/indirect_draw/src/main.cpp
+++ b/projects/indirect_draw/src/main.cpp
@@ -172,7 +172,9 @@ void compileMeshForIndirectDraw(vkcv::Core &core,
     vkcv::Image pseudoImg = core.createImage(vk::Format::eR8G8B8A8Srgb, 2, 2);
     std::vector<uint8_t> pseudoData = {0, 0, 0, 0};
     pseudoImg.fill(pseudoData.data());
-    pseudoImg.switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
+	
+	auto mipStream = core.createCommandStream(vkcv::QueueType::Graphics);
+	pseudoImg.recordMipChainGeneration(mipStream, core.getDownsampler());
 
 	uint32_t vertexOffset = 0;
     for (const auto &mesh : scene.meshes)
@@ -194,8 +196,7 @@ void compileMeshForIndirectDraw(vkcv::Core &core,
 
                 vkcv::Image baseColorImg = core.createImage(vk::Format::eR8G8B8A8Srgb, baseColor.w, baseColor.h);
                 baseColorImg.fill(baseColor.data.data());
-                baseColorImg.generateMipChainImmediate();
-                baseColorImg.switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
+				baseColorImg.recordMipChainGeneration(mipStream, core.getDownsampler());
 
                 compiledMat.baseColor.push_back(baseColorImg);
             }
@@ -254,6 +255,8 @@ void compileMeshForIndirectDraw(vkcv::Core &core,
 			}
         }
     }
+	
+	core.submitCommandStream(mipStream, false);
 }
 
 int main(int argc, const char** argv) {
diff --git a/projects/rtx_ambient_occlusion/src/main.cpp b/projects/rtx_ambient_occlusion/src/main.cpp
index 0276f05db5bece4416578d89da1fd7a7b31336ca..e1bfd2231bc8047f12ba7d71e6a176f1fd85d92c 100644
--- a/projects/rtx_ambient_occlusion/src/main.cpp
+++ b/projects/rtx_ambient_occlusion/src/main.cpp
@@ -43,7 +43,7 @@ int main(int argc, const char** argv) {
     std::vector<float> vertices = teapot.getVertices();
     std::vector<uint32_t> indices = teapot.getIndices();
 
-	vkcv::shader::GLSLCompiler compiler;
+	vkcv::shader::GLSLCompiler compiler (vkcv::shader::GLSLCompileTarget::RAY_TRACING);
 
 	vkcv::ShaderProgram rtxShaderProgram;
 	compiler.compile(vkcv::ShaderStage::RAY_GEN, std::filesystem::path("resources/shaders/ambientOcclusion.rgen"),
diff --git a/projects/voxelization/CMakeLists.txt b/projects/voxelization/CMakeLists.txt
index 0f0585d9d5d9a26cb2daec486f3df8c417a01527..22a4848aa5f5d0b4cdaf61cb48a6dafde4bb66d9 100644
--- a/projects/voxelization/CMakeLists.txt
+++ b/projects/voxelization/CMakeLists.txt
@@ -28,7 +28,8 @@ target_include_directories(voxelization SYSTEM BEFORE PRIVATE
 		${vkcv_shader_compiler_include}
 		${vkcv_gui_include}
 		${vkcv_upscaling_include}
-		${vkcv_effects_include})
+		${vkcv_effects_include}
+		${vkcv_algorithm_include})
 
 # linking with libraries from all dependencies and the VkCV framework
 target_link_libraries(voxelization
@@ -40,4 +41,5 @@ target_link_libraries(voxelization
 		vkcv_shader_compiler
 		vkcv_gui
 		vkcv_upscaling
-		vkcv_effects)
+		vkcv_effects
+		vkcv_algorithm)
diff --git a/projects/voxelization/src/ShadowMapping.cpp b/projects/voxelization/src/ShadowMapping.cpp
index 53a11ceb1306c8a567834e943d70a8862a34afa8..2d09f8a3137816e441c0fd4cf0e46da030356c5b 100644
--- a/projects/voxelization/src/ShadowMapping.cpp
+++ b/projects/voxelization/src/ShadowMapping.cpp
@@ -321,8 +321,7 @@ void ShadowMapping::recordShadowMapRendering(
 		dispatchCount,
 		{ vkcv::DescriptorSetUsage(0, m_shadowBlurYDescriptorSet) },
 		vkcv::PushConstants(0));
-	m_shadowMap.recordMipChainGeneration(cmdStream);
-	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
+	m_shadowMap.recordMipChainGeneration(cmdStream, m_corePtr->getDownsampler());
 
 	m_corePtr->recordEndDebugLabel(cmdStream);
 }
diff --git a/projects/voxelization/src/Voxelization.cpp b/projects/voxelization/src/Voxelization.cpp
index 616eed83e0687e03c1d9bfe0ee3127a72457d241..523a7a672ba09a259c77e2ee568a06c92533832e 100644
--- a/projects/voxelization/src/Voxelization.cpp
+++ b/projects/voxelization/src/Voxelization.cpp
@@ -314,8 +314,7 @@ void Voxelization::voxelizeMeshes(
 
 	// intermediate image mipchain
 	m_corePtr->recordBeginDebugLabel(cmdStream, "Intermediate Voxel mipmap generation", { 1, 1, 1, 1 });
-	m_voxelImageIntermediate.recordMipChainGeneration(cmdStream);
-	m_corePtr->prepareImageForSampling(cmdStream, m_voxelImageIntermediate.getHandle());
+	m_voxelImageIntermediate.recordMipChainGeneration(cmdStream, m_corePtr->getDownsampler());
 	m_corePtr->recordEndDebugLabel(cmdStream);
 
 	// secondary bounce
@@ -327,14 +326,12 @@ void Voxelization::voxelizeMeshes(
 		bufferToImageDispatchCount,
 		{ vkcv::DescriptorSetUsage(0, m_secondaryBounceDescriptorSet) },
 		vkcv::PushConstants(0));
-	m_voxelImage.recordMipChainGeneration(cmdStream);
-	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImage.getHandle());
+	m_voxelImage.recordMipChainGeneration(cmdStream, m_corePtr->getDownsampler());
 	m_corePtr->recordEndDebugLabel(cmdStream);
 
 	// final image mipchain
 	m_corePtr->recordBeginDebugLabel(cmdStream, "Voxel mipmap generation", { 1, 1, 1, 1 });
-	m_voxelImage.recordMipChainGeneration(cmdStream);
-	m_corePtr->prepareImageForSampling(cmdStream, m_voxelImage.getHandle());
+	m_voxelImage.recordMipChainGeneration(cmdStream, m_corePtr->getDownsampler());
 	m_corePtr->recordEndDebugLabel(cmdStream);
 }
 
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index 02533e447724ad8d83c7af415b01acc2350126eb..02dc237e0fa84ac5a10c3a601556cda784b8d9c8 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -12,6 +12,7 @@
 #include <vkcv/upscaling/BilinearUpscaling.hpp>
 #include <vkcv/upscaling/NISUpscaling.hpp>
 #include <vkcv/effects/BloomAndFlaresEffect.hpp>
+#include <vkcv/algorithm/SinglePassDownsampler.hpp>
 
 int main(int argc, const char** argv) {
 	const char* applicationName = "Voxelization";
@@ -21,6 +22,14 @@ int main(int argc, const char** argv) {
 
 	vkcv::Features features;
 	features.requireExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+	
+	features.tryExtensionFeature<vk::PhysicalDeviceShaderSubgroupExtendedTypesFeatures>(
+		VK_KHR_SHADER_SUBGROUP_EXTENDED_TYPES_EXTENSION_NAME,
+		[](vk::PhysicalDeviceShaderSubgroupExtendedTypesFeatures& features) {
+			features.setShaderSubgroupExtendedTypes(true);
+		}
+	);
+	
 	features.tryExtensionFeature<vk::PhysicalDevice16BitStorageFeatures>(
 		VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME,
 		[](vk::PhysicalDevice16BitStorageFeatures& features) {
@@ -44,6 +53,7 @@ int main(int argc, const char** argv) {
 			{ vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },
 			features
 	);
+	
 	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, true);
 	vkcv::Window& window = core.getWindow(windowHandle);
 
@@ -240,6 +250,10 @@ int main(int argc, const char** argv) {
 	std::vector<vkcv::DescriptorSetLayoutHandle> materialDescriptorSetLayouts;
 	std::vector<vkcv::DescriptorSetHandle> materialDescriptorSets;
 	std::vector<vkcv::Image> sceneImages;
+	
+	const vkcv::Downsampler &downsampler = core.getDownsampler();
+	
+	auto mipStream = core.createCommandStream(vkcv::QueueType::Graphics);
 
 	for (const auto& material : scene.materials) {
 		int albedoIndex     = material.baseColor;
@@ -269,22 +283,19 @@ int main(int argc, const char** argv) {
 		// albedo texture
 		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Srgb, albedoTexture.w, albedoTexture.h, 1, true));
 		sceneImages.back().fill(albedoTexture.data.data());
-		sceneImages.back().generateMipChainImmediate();
-		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
+		sceneImages.back().recordMipChainGeneration(mipStream, downsampler);
 		const vkcv::ImageHandle albedoHandle = sceneImages.back().getHandle();
 
 		// normal texture
 		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Unorm, normalTexture.w, normalTexture.h, 1, true));
 		sceneImages.back().fill(normalTexture.data.data());
-		sceneImages.back().generateMipChainImmediate();
-		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
+		sceneImages.back().recordMipChainGeneration(mipStream, downsampler);
 		const vkcv::ImageHandle normalHandle = sceneImages.back().getHandle();
 
 		// specular texture
 		sceneImages.push_back(core.createImage(vk::Format::eR8G8B8A8Unorm, specularTexture.w, specularTexture.h, 1, true));
 		sceneImages.back().fill(specularTexture.data.data());
-		sceneImages.back().generateMipChainImmediate();
-		sceneImages.back().switchLayout(vk::ImageLayout::eShaderReadOnlyOptimal);
+		sceneImages.back().recordMipChainGeneration(mipStream, downsampler);
 		const vkcv::ImageHandle specularHandle = sceneImages.back().getHandle();
 
 		vkcv::DescriptorWrites setWrites;
@@ -299,6 +310,8 @@ int main(int argc, const char** argv) {
 		setWrites.writeSampler(1, colorSampler);
 		core.writeDescriptorSet(materialDescriptorSets.back(), setWrites);
 	}
+	
+	core.submitCommandStream(mipStream, false);
 
 	std::vector<vkcv::DescriptorSetLayoutHandle> perMeshDescriptorSetLayouts;
 	std::vector<vkcv::DescriptorSetHandle> perMeshDescriptorSets;
diff --git a/src/vkcv/BlitDownsampler.cpp b/src/vkcv/BlitDownsampler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ce031549fd0ed831c6d771602b8205ce8401fc2c
--- /dev/null
+++ b/src/vkcv/BlitDownsampler.cpp
@@ -0,0 +1,20 @@
+
+#include "vkcv/BlitDownsampler.hpp"
+
+#include "vkcv/Core.hpp"
+#include "ImageManager.hpp"
+
+namespace vkcv {
+	
+	BlitDownsampler::BlitDownsampler(Core &core,
+									 ImageManager& imageManager) :
+		Downsampler(core),
+		m_imageManager(imageManager) {}
+	
+	void BlitDownsampler::recordDownsampling(const CommandStreamHandle &cmdStream,
+											 const ImageHandle &image) const {
+		m_imageManager.recordImageMipChainGenerationToCmdStream(cmdStream, image);
+		m_core.prepareImageForSampling(cmdStream, image);
+	}
+	
+}
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index df3170d1524b69d23a67c91d1ec94829bcc08c8c..383dcbb98e97e58bb20bfa0836aeec17201cf532 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -15,10 +15,10 @@
 #include "ImageManager.hpp"
 #include "DescriptorManager.hpp"
 #include "WindowManager.hpp"
-#include "ImageLayoutTransitions.hpp"
 #include "CommandStreamManager.hpp"
 #include <cmath>
 #include "vkcv/Logger.hpp"
+#include "vkcv/BlitDownsampler.hpp"
 
 namespace vkcv
 {
@@ -62,13 +62,15 @@ namespace vkcv
 			m_WindowManager(std::make_unique<WindowManager>()),
 			m_SwapchainManager(std::make_unique<SwapchainManager>()),
             m_CommandResources(commandResources),
-            m_SyncResources(syncResources)
+            m_SyncResources(syncResources),
+			m_downsampler(nullptr)
 	{
 		m_BufferManager->m_core = this;
 		m_BufferManager->init();
 		m_CommandStreamManager->init(this);
 		m_SwapchainManager->m_context = &m_Context;
 		m_ImageManager->m_core = this;
+		m_downsampler = std::unique_ptr<Downsampler>(new BlitDownsampler(*this, *m_ImageManager));
 	}
 
 	Core::~Core() noexcept {
@@ -231,7 +233,7 @@ namespace vkcv
 			const bool isDepthImage = isDepthFormat(imageManager.getImageFormat(handle));
 			const vk::ImageLayout targetLayout =
 				isDepthImage ? vk::ImageLayout::eDepthStencilAttachmentOptimal : vk::ImageLayout::eColorAttachmentOptimal;
-			imageManager.recordImageLayoutTransition(handle, targetLayout, cmdBuffer);
+			imageManager.recordImageLayoutTransition(handle, 0, 0, targetLayout, cmdBuffer);
 		}
 	}
 
@@ -809,10 +811,16 @@ namespace vkcv
 		}
 	}
 
-	void Core::submitCommandStream(const CommandStreamHandle& handle) {
+	void Core::submitCommandStream(const CommandStreamHandle& handle,
+								   bool signalRendering) {
 		std::vector<vk::Semaphore> waitSemaphores;
+		
 		// FIXME: add proper user controllable sync
-		std::vector<vk::Semaphore> signalSemaphores = { m_SyncResources.renderFinished };
+		std::vector<vk::Semaphore> signalSemaphores;
+		if (signalRendering) {
+			signalSemaphores.push_back(m_SyncResources.renderFinished);
+		}
+		
 		m_CommandStreamManager->submitCommandStreamSynchronous(handle, waitSemaphores, signalSemaphores);
 	}
 
@@ -849,6 +857,10 @@ namespace vkcv
 			supportColorAttachment,
 			multisampling);
 	}
+	
+	const Downsampler &Core::getDownsampler() const {
+		return *m_downsampler;
+	}
 
 	WindowHandle Core::createWindow(
 			const char *applicationName,
@@ -928,19 +940,43 @@ namespace vkcv
 	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);
+			m_ImageManager->recordImageLayoutTransition(
+					swapchainHandle,
+					0,
+					0,
+					vk::ImageLayout::ePresentSrcKHR,
+					cmdBuffer
+			);
 		}, nullptr);
 	}
 
-	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);
+	void Core::prepareImageForSampling(const CommandStreamHandle& cmdStream,
+									   const ImageHandle& image,
+									   uint32_t mipLevelCount,
+									   uint32_t mipLevelOffset) {
+		recordCommandsToStream(cmdStream, [image, mipLevelCount, mipLevelOffset, this](const vk::CommandBuffer cmdBuffer) {
+			m_ImageManager->recordImageLayoutTransition(
+					image,
+					mipLevelCount,
+					mipLevelOffset,
+					vk::ImageLayout::eShaderReadOnlyOptimal,
+					cmdBuffer
+			);
 		}, nullptr);
 	}
 
-	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);
+	void Core::prepareImageForStorage(const CommandStreamHandle& cmdStream,
+									  const ImageHandle& image,
+									  uint32_t mipLevelCount,
+									  uint32_t mipLevelOffset) {
+		recordCommandsToStream(cmdStream, [image, mipLevelCount, mipLevelOffset, this](const vk::CommandBuffer cmdBuffer) {
+			m_ImageManager->recordImageLayoutTransition(
+					image,
+					mipLevelCount,
+					mipLevelOffset,
+					vk::ImageLayout::eGeneral,
+					cmdBuffer
+			);
 		}, nullptr);
 	}
 
@@ -996,11 +1032,11 @@ namespace vkcv
 							   SamplerFilterType filterType) {
 		recordCommandsToStream(cmdStream, [&](const vk::CommandBuffer cmdBuffer) {
 			m_ImageManager->recordImageLayoutTransition(
-					src, vk::ImageLayout::eTransferSrcOptimal, cmdBuffer
+					src, 0, 0, vk::ImageLayout::eTransferSrcOptimal, cmdBuffer
 			);
 			
 			m_ImageManager->recordImageLayoutTransition(
-					dst, vk::ImageLayout::eTransferDstOptimal, cmdBuffer
+					dst, 0, 0, vk::ImageLayout::eTransferDstOptimal, cmdBuffer
 			);
 			
 			const std::array<vk::Offset3D, 2> srcOffsets = {
diff --git a/src/vkcv/DescriptorManager.cpp b/src/vkcv/DescriptorManager.cpp
index d12a5287e2317bdfc21fb15872681138b4f0d64f..a7e2f06c98fb24608f667d529b226736bddace7d 100644
--- a/src/vkcv/DescriptorManager.cpp
+++ b/src/vkcv/DescriptorManager.cpp
@@ -163,6 +163,7 @@ namespace vkcv
 		size_t bufferInfoIndex;
 		uint32_t binding;
 		uint32_t arrayElementIndex;
+		uint32_t descriptorCount;
 		vk::DescriptorType type;
     };
 
@@ -181,20 +182,27 @@ namespace vkcv
 
 		for (const auto& write : writes.getSampledImageWrites())
 		{
-		    vk::ImageLayout layout = write.useGeneralLayout ? vk::ImageLayout::eGeneral : vk::ImageLayout::eShaderReadOnlyOptimal;
-			const vk::DescriptorImageInfo imageInfo(
-				nullptr,
-				imageManager.getVulkanImageView(write.image, write.mipLevel),
-                layout
+		    const vk::ImageLayout layout = (write.useGeneralLayout?
+					vk::ImageLayout::eGeneral :
+					vk::ImageLayout::eShaderReadOnlyOptimal
 			);
 			
-			imageInfos.push_back(imageInfo);
+			for (uint32_t i = 0; i < write.mipCount; i++) {
+				const vk::DescriptorImageInfo imageInfo(
+						nullptr,
+						imageManager.getVulkanImageView(write.image, write.mipLevel + i),
+						layout
+				);
+				
+				imageInfos.push_back(imageInfo);
+			}
 			
 			WriteDescriptorSetInfo vulkanWrite = {
-					imageInfos.size(),
+					imageInfos.size() + 1 - write.mipCount,
 					0,
 					write.binding,
 					write.arrayIndex,
+					write.mipCount,
 					vk::DescriptorType::eSampledImage,
 			};
 			
@@ -202,19 +210,22 @@ namespace vkcv
 		}
 
 		for (const auto& write : writes.getStorageImageWrites()) {
-			const vk::DescriptorImageInfo imageInfo(
-				nullptr,
-				imageManager.getVulkanImageView(write.image, write.mipLevel),
-				vk::ImageLayout::eGeneral
-			);
-			
-			imageInfos.push_back(imageInfo);
+			for (uint32_t i = 0; i < write.mipCount; i++) {
+				const vk::DescriptorImageInfo imageInfo(
+						nullptr,
+						imageManager.getVulkanImageView(write.image, write.mipLevel + i),
+						vk::ImageLayout::eGeneral
+				);
+				
+				imageInfos.push_back(imageInfo);
+			}
 			
 			WriteDescriptorSetInfo vulkanWrite = {
-					imageInfos.size(),
+					imageInfos.size() + 1 - write.mipCount,
 					0,
 					write.binding,
 					0,
+					write.mipCount,
 					vk::DescriptorType::eStorageImage
 			};
 			
@@ -240,6 +251,7 @@ namespace vkcv
 					bufferInfos.size(),
 					write.binding,
 					0,
+					1,
 					write.dynamic?
 					vk::DescriptorType::eUniformBufferDynamic :
 					vk::DescriptorType::eUniformBuffer
@@ -267,6 +279,7 @@ namespace vkcv
 					bufferInfos.size(),
 					write.binding,
 					0,
+					1,
 					write.dynamic?
 					vk::DescriptorType::eStorageBufferDynamic :
 					vk::DescriptorType::eStorageBuffer
@@ -291,6 +304,7 @@ namespace vkcv
 					0,
 					write.binding,
 					0,
+					1,
 					vk::DescriptorType::eSampler
 			};
 			
@@ -304,7 +318,7 @@ namespace vkcv
 					set,
 					write.binding,
 					write.arrayElementIndex,
-					1,
+					write.descriptorCount,
 					write.type,
 					(write.imageInfoIndex > 0? &(imageInfos[write.imageInfoIndex - 1]) : nullptr),
 					(write.bufferInfoIndex > 0? &(bufferInfos[write.bufferInfoIndex - 1]) : nullptr)
diff --git a/src/vkcv/DescriptorWrites.cpp b/src/vkcv/DescriptorWrites.cpp
index 2dbf1b253d45c32ee1d7883aa0363c5640d023bb..68333b452cb5cef59a115812df17c2f89d06332e 100644
--- a/src/vkcv/DescriptorWrites.cpp
+++ b/src/vkcv/DescriptorWrites.cpp
@@ -7,15 +7,17 @@ namespace vkcv {
 														  ImageHandle image,
 														  uint32_t mipLevel,
 														  bool useGeneralLayout,
-														  uint32_t arrayIndex) {
-		m_sampledImageWrites.emplace_back(binding, image, mipLevel, useGeneralLayout, arrayIndex);
+														  uint32_t arrayIndex,
+														  uint32_t mipCount) {
+		m_sampledImageWrites.emplace_back(binding, image, mipLevel, useGeneralLayout, arrayIndex, mipCount);
 		return *this;
 	}
 	
 	DescriptorWrites &DescriptorWrites::writeStorageImage(uint32_t binding,
 														  ImageHandle image,
-														  uint32_t mipLevel) {
-		m_storageImageWrites.emplace_back(binding, image, mipLevel);
+														  uint32_t mipLevel,
+														  uint32_t mipCount) {
+		m_storageImageWrites.emplace_back(binding, image, mipLevel, mipCount);
 		return *this;
 	}
 	
diff --git a/src/vkcv/Downsampler.cpp b/src/vkcv/Downsampler.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a857eec3f12af6fca2d05d598c357988ffce5114
--- /dev/null
+++ b/src/vkcv/Downsampler.cpp
@@ -0,0 +1,9 @@
+
+#include "vkcv/Downsampler.hpp"
+
+namespace vkcv {
+	
+	Downsampler::Downsampler(Core& core)
+	: m_core(core) {}
+	
+}
diff --git a/src/vkcv/FeatureManager.cpp b/src/vkcv/FeatureManager.cpp
index af4426252d840bdffd7d210d900be64d6b74f6f3..bfceeb6300ece6dd3387d0785d8611a7289aa82c 100644
--- a/src/vkcv/FeatureManager.cpp
+++ b/src/vkcv/FeatureManager.cpp
@@ -576,4 +576,5 @@ m_physicalDevice.getFeatures2(&query)
 	const vk::PhysicalDeviceFeatures2& FeatureManager::getFeatures() const {
 		return m_featuresBase;
 	}
+	
 }
diff --git a/src/vkcv/Image.cpp b/src/vkcv/Image.cpp
index 15a2fc5240176742f50141407a3c72b531757ee9..32cbb05634bb3850c8071ab6a91e66bb85f29ba8 100644
--- a/src/vkcv/Image.cpp
+++ b/src/vkcv/Image.cpp
@@ -4,6 +4,8 @@
  * @brief class for image handles
  */
 #include "vkcv/Image.hpp"
+
+#include "vkcv/Downsampler.hpp"
 #include "ImageManager.hpp"
 
 namespace vkcv{
@@ -68,12 +70,9 @@ namespace vkcv{
 		m_manager->fillImage(m_handle, data, size);
 	}
 
-	void Image::generateMipChainImmediate() {
-		m_manager->generateImageMipChainImmediate(m_handle);
-	}
-
-	void Image::recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream) {
-		m_manager->recordImageMipChainGenerationToCmdStream(cmdStream, m_handle);
+	void Image::recordMipChainGeneration(const vkcv::CommandStreamHandle& cmdStream,
+										 const Downsampler &downsampler) {
+		downsampler.recordDownsampling(cmdStream, m_handle);
 	}
 	
 	Image::Image(ImageManager* manager, const ImageHandle& handle) :
diff --git a/src/vkcv/ImageLayoutTransitions.cpp b/src/vkcv/ImageLayoutTransitions.cpp
deleted file mode 100644
index 14b226847a15b5e9cadffc28555e76b88b61e6a3..0000000000000000000000000000000000000000
--- a/src/vkcv/ImageLayoutTransitions.cpp
+++ /dev/null
@@ -1,68 +0,0 @@
-#include "ImageLayoutTransitions.hpp"
-#include "vkcv/Image.hpp"
-
-namespace vkcv {
-	vk::ImageMemoryBarrier createImageLayoutTransitionBarrier(const ImageManager::Image &image, vk::ImageLayout newLayout) {
-
-		vk::ImageAspectFlags aspectFlags;
-		if (isDepthFormat(image.m_format)) {
-			aspectFlags = vk::ImageAspectFlagBits::eDepth;
-		}
-		else {
-			aspectFlags = vk::ImageAspectFlagBits::eColor;
-		}
-
-		vk::ImageSubresourceRange imageSubresourceRange(
-			aspectFlags,
-			0,
-			image.m_viewPerMip.size(),
-			0,
-			image.m_layers
-		);
-
-		// TODO: precise AccessFlagBits, will require a lot of context
-		return vk::ImageMemoryBarrier(
-			vk::AccessFlagBits::eMemoryWrite,
-			vk::AccessFlagBits::eMemoryRead,
-			image.m_layout,
-			newLayout,
-			VK_QUEUE_FAMILY_IGNORED,
-			VK_QUEUE_FAMILY_IGNORED,
-			image.m_handle,
-			imageSubresourceRange);
-	}
-
-	vk::ImageMemoryBarrier createSwapchainImageLayoutTransitionBarrier(
-		vk::Image       vulkanHandle, 
-		vk::ImageLayout oldLayout, 
-		vk::ImageLayout newLayout) {
-
-		vk::ImageSubresourceRange imageSubresourceRange(
-			vk::ImageAspectFlagBits::eColor,
-			0,
-			1,
-			0,
-			1);
-
-		// TODO: precise AccessFlagBits, will require a lot of context
-		return vk::ImageMemoryBarrier(
-			vk::AccessFlagBits::eMemoryWrite,
-			vk::AccessFlagBits::eMemoryRead,
-			oldLayout,
-			newLayout,
-			VK_QUEUE_FAMILY_IGNORED,
-			VK_QUEUE_FAMILY_IGNORED,
-			vulkanHandle,
-			imageSubresourceRange);
-	}
-
-	void recordImageBarrier(vk::CommandBuffer cmdBuffer, vk::ImageMemoryBarrier barrier) {
-		cmdBuffer.pipelineBarrier(
-			vk::PipelineStageFlagBits::eAllCommands,
-			vk::PipelineStageFlagBits::eAllCommands,
-			{},
-			nullptr,
-			nullptr,
-			barrier);
-	}
-}
\ No newline at end of file
diff --git a/src/vkcv/ImageLayoutTransitions.hpp b/src/vkcv/ImageLayoutTransitions.hpp
deleted file mode 100644
index 5c147f133a6492746ad410367e5e627be000d7be..0000000000000000000000000000000000000000
--- a/src/vkcv/ImageLayoutTransitions.hpp
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma once
-#include <vulkan/vulkan.hpp>
-#include "ImageManager.hpp"
-
-namespace vkcv {
-	vk::ImageMemoryBarrier createImageLayoutTransitionBarrier(const ImageManager::Image& image, vk::ImageLayout newLayout);
-	vk::ImageMemoryBarrier createSwapchainImageLayoutTransitionBarrier(
-		vk::Image       vulkanHandle,
-		vk::ImageLayout oldLayout,
-		vk::ImageLayout newLayout);
-
-	void recordImageBarrier(vk::CommandBuffer cmdBuffer, vk::ImageMemoryBarrier barrier);
-}
\ No newline at end of file
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index bfee504f599d941e625e6ef5c6045805c33a29a1..e7112579c7316ca8f886c66a63cdf74749a287a9 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -5,7 +5,6 @@
  */
 #include "ImageManager.hpp"
 #include "vkcv/Core.hpp"
-#include "ImageLayoutTransitions.hpp"
 #include "vkcv/Logger.hpp"
 
 #include <algorithm>
@@ -276,6 +275,46 @@ namespace vkcv {
 		return image.m_viewPerMip[mipLevel];
 	}
 	
+	static vk::ImageMemoryBarrier createImageLayoutTransitionBarrier(const ImageManager::Image &image,
+																	 uint32_t mipLevelCount,
+																	 uint32_t mipLevelOffset,
+																	 vk::ImageLayout newLayout) {
+		vk::ImageAspectFlags aspectFlags;
+		if (isDepthFormat(image.m_format)) {
+			aspectFlags = vk::ImageAspectFlagBits::eDepth;
+		} else {
+			aspectFlags = vk::ImageAspectFlagBits::eColor;
+		}
+		
+		const uint32_t mipLevelsMax = image.m_viewPerMip.size();
+		
+		if (mipLevelOffset > mipLevelsMax)
+			mipLevelOffset = mipLevelsMax;
+		
+		if ((!mipLevelCount) || (mipLevelOffset + mipLevelCount > mipLevelsMax))
+			mipLevelCount = mipLevelsMax - mipLevelOffset;
+		
+		vk::ImageSubresourceRange imageSubresourceRange(
+				aspectFlags,
+				mipLevelOffset,
+				mipLevelCount,
+				0,
+				image.m_layers
+		);
+		
+		// TODO: precise AccessFlagBits, will require a lot of context
+		return vk::ImageMemoryBarrier(
+				vk::AccessFlagBits::eMemoryWrite,
+				vk::AccessFlagBits::eMemoryRead,
+				image.m_layout,
+				newLayout,
+				VK_QUEUE_FAMILY_IGNORED,
+				VK_QUEUE_FAMILY_IGNORED,
+				image.m_handle,
+				imageSubresourceRange
+		);
+	}
+	
 	void ImageManager::switchImageLayoutImmediate(const ImageHandle& handle, vk::ImageLayout newLayout) {
 		uint64_t id = handle.getId();
 		
@@ -287,7 +326,7 @@ namespace vkcv {
 		}
 		
 		auto& image = isSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[id];
-		const auto transitionBarrier = createImageLayoutTransitionBarrier(image, newLayout);
+		const auto transitionBarrier = createImageLayoutTransitionBarrier(image, 0, 0, newLayout);
 		
 		SubmitInfo submitInfo;
 		submitInfo.queueType = QueueType::Graphics;
@@ -309,11 +348,11 @@ namespace vkcv {
 		image.m_layout = newLayout;
 	}
 
-	void ImageManager::recordImageLayoutTransition(
-		const ImageHandle& handle, 
-		vk::ImageLayout newLayout, 
-		vk::CommandBuffer cmdBuffer) {
-
+	void ImageManager::recordImageLayoutTransition(const ImageHandle& handle,
+												   uint32_t mipLevelCount,
+												   uint32_t mipLevelOffset,
+												   vk::ImageLayout newLayout,
+												   vk::CommandBuffer cmdBuffer) {
 		const uint64_t id = handle.getId();
 		const bool isSwapchainImage = handle.isSwapchainImage();
 
@@ -323,8 +362,22 @@ namespace vkcv {
 		}
 
 		auto& image = isSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[id];
-		const auto transitionBarrier = createImageLayoutTransitionBarrier(image, newLayout);
-		recordImageBarrier(cmdBuffer, transitionBarrier);
+		const auto transitionBarrier = createImageLayoutTransitionBarrier(
+				image,
+				mipLevelCount,
+				mipLevelOffset,
+				newLayout
+		);
+		
+		cmdBuffer.pipelineBarrier(
+				vk::PipelineStageFlagBits::eAllCommands,
+				vk::PipelineStageFlagBits::eAllCommands,
+				{},
+				nullptr,
+				nullptr,
+				transitionBarrier
+		);
+		
 		image.m_layout = newLayout;
 	}
 
@@ -341,8 +394,16 @@ namespace vkcv {
 		}
 
 		auto& image = isSwapchainImage ? m_swapchainImages[m_currentSwapchainInputImage] : m_images[id];
-		const auto transitionBarrier = createImageLayoutTransitionBarrier(image, image.m_layout);
-		recordImageBarrier(cmdBuffer, transitionBarrier);
+		const auto transitionBarrier = createImageLayoutTransitionBarrier(image, 0, 0, image.m_layout);
+		
+		cmdBuffer.pipelineBarrier(
+				vk::PipelineStageFlagBits::eAllCommands,
+				vk::PipelineStageFlagBits::eAllCommands,
+				{},
+				nullptr,
+				nullptr,
+				transitionBarrier
+		);
 	}
 	
 	constexpr uint32_t getBytesPerPixel(vk::Format format) {
@@ -452,7 +513,7 @@ namespace vkcv {
 		}
 
 		auto& image = m_images[id];
-		recordImageLayoutTransition(handle, vk::ImageLayout::eGeneral, cmdBuffer);
+		recordImageLayoutTransition(handle, 0, 0, vk::ImageLayout::eGeneral, cmdBuffer);
 
 		vk::ImageAspectFlags aspectMask = isDepthImageFormat(image.m_format) ?
 			vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor;
@@ -497,22 +558,6 @@ namespace vkcv {
 		}
 	}
 
-	void ImageManager::generateImageMipChainImmediate(const ImageHandle& handle) {
-		SubmitInfo submitInfo;
-		submitInfo.queueType = QueueType::Graphics;
-
-		if (handle.isSwapchainImage()) {
-			vkcv_log(vkcv::LogLevel::ERROR, "You cannot generate a mip chain for the swapchain, what are you smoking?");
-			return;
-		}
-
-		const auto record = [this, handle](const vk::CommandBuffer cmdBuffer) {
-			recordImageMipGenerationToCmdBuffer(cmdBuffer, handle);
-		};
-
-		m_core->recordAndSubmitCommandsImmediate(submitInfo, record, nullptr);
-	}
-
 	void ImageManager::recordImageMipChainGenerationToCmdStream(
 		const vkcv::CommandStreamHandle& cmdStream,
 		const ImageHandle& handle) {
@@ -549,8 +594,8 @@ namespace vkcv {
 			vk::Offset3D(0, 0, 0), 
 			vk::Extent3D(dstImage.m_width, dstImage.m_height, dstImage.m_depth));
 
-		recordImageLayoutTransition(src, vk::ImageLayout::eTransferSrcOptimal, cmdBuffer);
-		recordImageLayoutTransition(dst, vk::ImageLayout::eTransferDstOptimal, cmdBuffer);
+		recordImageLayoutTransition(src, 0, 0, vk::ImageLayout::eTransferSrcOptimal, cmdBuffer);
+		recordImageLayoutTransition(dst, 0, 0, vk::ImageLayout::eTransferDstOptimal, cmdBuffer);
 
 		cmdBuffer.resolveImage(
 			srcImage.m_handle,
diff --git a/src/vkcv/ImageManager.hpp b/src/vkcv/ImageManager.hpp
index aedf726031ef43ac723b93f3dbd1276d92f3f704..5ac21b55a37d657929d409415b62013590bbb78f 100644
--- a/src/vkcv/ImageManager.hpp
+++ b/src/vkcv/ImageManager.hpp
@@ -97,8 +97,11 @@ namespace vkcv {
 		vk::ImageView getVulkanImageView(const ImageHandle& handle, size_t mipLevel = 0) const;
 
 		void switchImageLayoutImmediate(const ImageHandle& handle, vk::ImageLayout newLayout);
+		
 		void recordImageLayoutTransition(
-			const ImageHandle& handle, 
+			const ImageHandle& handle,
+			uint32_t mipLevelCount,
+			uint32_t mipLevelOffset,
 			vk::ImageLayout newLayout, 
 			vk::CommandBuffer cmdBuffer);
 
@@ -107,7 +110,6 @@ namespace vkcv {
 			vk::CommandBuffer cmdBuffer);
 
 		void fillImage(const ImageHandle& handle, const void* data, size_t size);
-		void generateImageMipChainImmediate(const ImageHandle& handle);
 		void recordImageMipChainGenerationToCmdStream(const vkcv::CommandStreamHandle& cmdStream, const ImageHandle& handle);
 		void recordMSAAResolve(vk::CommandBuffer cmdBuffer, ImageHandle src, ImageHandle dst);