diff --git a/include/vkcv/FeatureManager.hpp b/include/vkcv/FeatureManager.hpp
index f465d6740c20014ca880fce5e881d5e6c99b32d9..8dcdf0e419d4a36a01c209ec2a6af9b28e33ab8f 100644
--- a/include/vkcv/FeatureManager.hpp
+++ b/include/vkcv/FeatureManager.hpp
@@ -324,6 +324,26 @@ namespace vkcv {
 		 */
 		[[nodiscard]] bool checkSupport(const vk::PhysicalDeviceVulkan13Features &features,
 										bool required) const;
+		
+		/**
+		 * @brief Checks support of the @p vk::PhysicalDeviceCoherentMemoryFeaturesAMD.
+		 *
+		 * @param[in] features The features
+		 * @param[in] required True, if the @p features are required, else false
+		 * @return @p True, if the @p features are supported, else @p false
+		 */
+		[[nodiscard]] bool checkSupport(const vk::PhysicalDeviceCoherentMemoryFeaturesAMD &features,
+										bool required) const;
+		
+		/**
+		 * @brief Checks support of the @p vk::PhysicalDeviceSubgroupSizeControlFeatures.
+		 *
+		 * @param[in] features The features
+		 * @param[in] required True, if the @p features are required, else false
+		 * @return @p True, if the @p features are supported, else @p false
+		 */
+		[[nodiscard]] bool checkSupport(const vk::PhysicalDeviceSubgroupSizeControlFeatures &features,
+										bool required) const;
 
 		/**
 		 * @brief Searches for a base structure of a given structure type.
diff --git a/modules/upscaling/include/vkcv/upscaling/FSR2Upscaling.hpp b/modules/upscaling/include/vkcv/upscaling/FSR2Upscaling.hpp
index d61eef8d7e18ea1aee1bb28a6dc2f41ddb40636c..95dbd9a804ab6ec6a0232cafac7bc43dcf8b5d6d 100644
--- a/modules/upscaling/include/vkcv/upscaling/FSR2Upscaling.hpp
+++ b/modules/upscaling/include/vkcv/upscaling/FSR2Upscaling.hpp
@@ -4,9 +4,8 @@
 
 #include <vector>
 
-#define FFX_GCC
-#include <ffx_fsr2.h>
-#undef FFX_GCC
+struct FfxFsr2ContextDescription;
+struct FfxFsr2Context;
 
 namespace vkcv::upscaling {
 
@@ -81,8 +80,8 @@ namespace vkcv::upscaling {
 	private:
 		std::vector<char> m_scratchBuffer;
 		
-		FfxFsr2ContextDescription m_description;
-		FfxFsr2Context m_context;
+		std::unique_ptr<FfxFsr2ContextDescription> m_description;
+		std::unique_ptr<FfxFsr2Context> m_context;
 		
 		ImageHandle m_depth;
 		ImageHandle m_velocity;
@@ -155,7 +154,7 @@ namespace vkcv::upscaling {
 		void calcJitterOffset(uint32_t renderWidth,
 							 uint32_t renderHeight,
 							 float& jitterOffsetX,
-							 float& jitterOffsetY);
+							 float& jitterOffsetY) const;
 		
 		/**
 		 * Bind the depth buffer image to use with the FSR2
diff --git a/modules/upscaling/src/vkcv/upscaling/FSR2Upscaling.cpp b/modules/upscaling/src/vkcv/upscaling/FSR2Upscaling.cpp
index 2737d5f707163071590969d3856624de84b12abe..0478596daef709ef8b5ae583221d402d36d76dba 100644
--- a/modules/upscaling/src/vkcv/upscaling/FSR2Upscaling.cpp
+++ b/modules/upscaling/src/vkcv/upscaling/FSR2Upscaling.cpp
@@ -4,6 +4,7 @@
 #include <cmath>
 
 #define FFX_GCC
+#include <ffx_fsr2.h>
 #include <ffx_fsr2_vk.h>
 #undef FFX_GCC
 
@@ -60,54 +61,87 @@ namespace vkcv::upscaling {
 									 uint32_t displayHeight,
 									 uint32_t renderWidth,
 									 uint32_t renderHeight) {
-		m_description.displaySize.width = displayWidth;
-		m_description.displaySize.height = displayHeight;
+		m_description->displaySize.width = displayWidth;
+		m_description->displaySize.height = displayHeight;
 		
-		m_description.maxRenderSize.width = renderWidth;
-		m_description.maxRenderSize.height = renderHeight;
+		m_description->maxRenderSize.width = renderWidth;
+		m_description->maxRenderSize.height = renderHeight;
 		
-		m_description.flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE;
+		m_description->flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE;
 		
 		if (m_hdr) {
-			m_description.flags |= FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE;
+			m_description->flags |= FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE;
 		}
 		
-		assert(ffxFsr2ContextCreate(&m_context, &m_description) == FFX_OK);
+		if ((m_description->displaySize.width * m_description->displaySize.height <= 1) ||
+			(m_description->maxRenderSize.width * m_description->maxRenderSize.height <= 1)) {
+			return;
+		}
+		
+		if (!m_context) {
+			m_context.reset(new FfxFsr2Context());
+		}
+		
+		memset(m_context.get(), 0, sizeof(*m_context));
+		assert(ffxFsr2ContextCreate(m_context.get(), m_description.get()) == FFX_OK);
 	}
 	
 	void FSR2Upscaling::destroyFSR2Context() {
 		m_core.getContext().getDevice().waitIdle();
 		
-		assert(ffxFsr2ContextDestroy(&m_context) == FFX_OK);
+		if (m_context) {
+			assert(ffxFsr2ContextDestroy(m_context.get()) == FFX_OK);
+			m_context.reset(nullptr);
+		}
 		
 		m_frameIndex = 0;
 	}
 	
-	FSR2Upscaling::FSR2Upscaling(Core &core) : Upscaling(core) {
+	FSR2Upscaling::FSR2Upscaling(Core &core) :
+	Upscaling(core),
+	m_scratchBuffer(),
+	
+	m_description(new FfxFsr2ContextDescription()),
+	m_context(nullptr),
+	
+	m_depth(),
+	m_velocity(),
+	
+	m_frameIndex(0),
+	
+	m_frameDeltaTime(0.0f),
+	m_reset(false),
+	
+	m_near(0.0f),
+	m_far(0.0f),
+	m_fov(0.0f),
+	
+	m_hdr(false),
+	m_sharpness(0.875f) {
 		const auto& physicalDevice = core.getContext().getPhysicalDevice();
 		
-		memset(&m_description, 0, sizeof(m_description));
+		memset(m_description.get(), 0, sizeof(*m_description));
 		
 		m_scratchBuffer.resize(ffxFsr2GetScratchMemorySizeVK(physicalDevice));
 		
 		assert(ffxFsr2GetInterfaceVK(
-				&(m_description.callbacks),
+				&(m_description->callbacks),
 				m_scratchBuffer.data(),
 				m_scratchBuffer.size(),
 				physicalDevice,
 				vkGetDeviceProcAddr
 		) == FFX_OK);
 		
-		m_description.device = ffxGetDeviceVK(core.getContext().getDevice());
+		m_description->device = ffxGetDeviceVK(core.getContext().getDevice());
 		
-		createFSR2Context(0, 0, 0, 0);
+		createFSR2Context(1, 1, 1, 1);
 	}
 	
 	FSR2Upscaling::~FSR2Upscaling() {
 		destroyFSR2Context();
 		
 		m_scratchBuffer.clear();
-		m_description.callbacks.scratchBuffer = nullptr;
+		m_description->callbacks.scratchBuffer = nullptr;
 	}
 	
 	void FSR2Upscaling::update(float deltaTime, bool reset) {
@@ -122,10 +156,10 @@ namespace vkcv::upscaling {
 	void FSR2Upscaling::calcJitterOffset(uint32_t renderWidth,
 										 uint32_t renderHeight,
 										 float &jitterOffsetX,
-										 float &jitterOffsetY) {
+										 float &jitterOffsetY) const {
 		const int32_t phaseCount = ffxFsr2GetJitterPhaseCount(
-				renderWidth,
-				renderHeight
+				static_cast<int32_t>(renderWidth),
+				static_cast<int32_t>(renderHeight)
 		);
 		
 		const int32_t phaseIndex = (static_cast<int32_t>(m_frameIndex) % phaseCount);
@@ -136,6 +170,9 @@ namespace vkcv::upscaling {
 				phaseIndex,
 				phaseCount
 		) == FFX_OK);
+		
+		jitterOffsetX *= +2.0f / renderWidth;
+		jitterOffsetY *= -2.0f / renderHeight;
 	}
 	
 	void FSR2Upscaling::bindDepthBuffer(const ImageHandle &depthInput) {
@@ -146,8 +183,13 @@ namespace vkcv::upscaling {
 		m_velocity = velocityInput;
 	}
 	
-	void FSR2Upscaling::recordUpscaling(const CommandStreamHandle &cmdStream, const ImageHandle &colorInput,
+	void FSR2Upscaling::recordUpscaling(const CommandStreamHandle &cmdStream,
+										const ImageHandle &colorInput,
 										const ImageHandle &output) {
+		m_core.recordBeginDebugLabel(cmdStream, "vkcv::upscaling::FSR2Upscaling", {
+				1.0f, 0.05f, 0.05f, 1.0f
+		});
+		
 		FfxFsr2DispatchDescription dispatch;
 		memset(&dispatch, 0, sizeof(dispatch));
 		
@@ -157,11 +199,11 @@ namespace vkcv::upscaling {
 		const uint32_t outputWidth = m_core.getImageWidth(output);
 		const uint32_t outputHeight = m_core.getImageHeight(output);
 		
-		if ((m_description.displaySize.width != outputWidth) ||
-			(m_description.displaySize.height != outputHeight) ||
-			(m_description.maxRenderSize.width < inputWidth) ||
-			(m_description.maxRenderSize.height < inputHeight) ||
-			(m_hdr != ((m_description.flags & FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE) != 0))) {
+		if ((m_description->displaySize.width != outputWidth) ||
+			(m_description->displaySize.height != outputHeight) ||
+			(m_description->maxRenderSize.width < inputWidth) ||
+			(m_description->maxRenderSize.height < inputHeight) ||
+			(m_hdr != ((m_description->flags & FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE) != 0))) {
 			destroyFSR2Context();
 			
 			createFSR2Context(
@@ -172,109 +214,114 @@ namespace vkcv::upscaling {
 			);
 		}
 		
-		const bool sharpeningEnabled = (
-				(m_sharpness > +0.0f) &&
-				((inputWidth < outputWidth) || (inputHeight < outputHeight))
-		);
-		
-		dispatch.color = ffxGetTextureResourceVK(
-				&m_context,
-				m_core.getVulkanImage(colorInput),
-				m_core.getVulkanImageView(colorInput),
-				inputWidth,
-				inputHeight,
-				static_cast<VkFormat>(m_core.getImageFormat(colorInput))
-		);
-		
-		dispatch.depth = ffxGetTextureResourceVK(
-				&m_context,
-				m_core.getVulkanImage(m_depth),
-				m_core.getVulkanImageView(m_depth),
-				m_core.getImageWidth(m_depth),
-				m_core.getImageHeight(m_depth),
-				static_cast<VkFormat>(m_core.getImageFormat(m_depth))
-		);
-		
-		dispatch.motionVectors = ffxGetTextureResourceVK(
-				&m_context,
-				m_core.getVulkanImage(m_velocity),
-				m_core.getVulkanImageView(m_velocity),
-				m_core.getImageWidth(m_velocity),
-				m_core.getImageHeight(m_velocity),
-				static_cast<VkFormat>(m_core.getImageFormat(m_velocity))
-		);
-		
-		dispatch.exposure = ffxGetTextureResourceVK(
-				&m_context,
-				nullptr,
-				nullptr,
-				1,
-				1,
-				VK_FORMAT_UNDEFINED
-		);
-		
-		dispatch.reactive = ffxGetTextureResourceVK(
-				&m_context,
-				nullptr,
-				nullptr,
-				1,
-				1,
-				VK_FORMAT_UNDEFINED
-		);
-		
-		dispatch.transparencyAndComposition = ffxGetTextureResourceVK(
-				&m_context,
-				nullptr,
-				nullptr,
-				1,
-				1,
-				VK_FORMAT_UNDEFINED
-		);
-		
-		dispatch.output = ffxGetTextureResourceVK(
-				&m_context,
-				m_core.getVulkanImage(output),
-				m_core.getVulkanImageView(output),
-				outputWidth,
-				outputHeight,
-				static_cast<VkFormat>(m_core.getImageFormat(output))
-		);
-		
-		calcJitterOffset(
-				inputWidth,
-				inputHeight,
-				dispatch.jitterOffset.x,
-				dispatch.jitterOffset.y
-		);
-		
-		dispatch.motionVectorScale.x = static_cast<float>(inputWidth);
-		dispatch.motionVectorScale.y = static_cast<float>(inputHeight);
-		
-		dispatch.renderSize.width = inputWidth;
-		dispatch.renderSize.height = inputHeight;
-		
-		dispatch.enableSharpening = sharpeningEnabled;
-		dispatch.sharpness = m_sharpness;
-		
-		dispatch.frameTimeDelta = m_frameDeltaTime * 1000.0f; // from seconds to milliseconds
-		dispatch.preExposure = 1.0f;
-		dispatch.reset = m_reset;
-		
-		dispatch.cameraNear = m_near;
-		dispatch.cameraFar = m_far;
-		dispatch.cameraFovAngleVertical = m_fov;
-		
-		m_core.recordCommandsToStream(cmdStream, [&](const vk::CommandBuffer& cmdBuffer) {
-			dispatch.commandList = ffxGetCommandListVK(cmdBuffer);
+		if (m_context) {
+			const bool sharpeningEnabled = (
+					(m_sharpness > +0.0f) &&
+					((inputWidth < outputWidth) || (inputHeight < outputHeight))
+			);
+			
+			dispatch.color = ffxGetTextureResourceVK(
+					m_context.get(),
+					m_core.getVulkanImage(colorInput),
+					m_core.getVulkanImageView(colorInput),
+					inputWidth,
+					inputHeight,
+					static_cast<VkFormat>(m_core.getImageFormat(colorInput))
+			);
 			
-			assert(ffxFsr2ContextDispatch(
-					&m_context,
-					&dispatch
-			) == FFX_OK);
+			dispatch.depth = ffxGetTextureResourceVK(
+					m_context.get(),
+					m_core.getVulkanImage(m_depth),
+					m_core.getVulkanImageView(m_depth),
+					m_core.getImageWidth(m_depth),
+					m_core.getImageHeight(m_depth),
+					static_cast<VkFormat>(m_core.getImageFormat(m_depth))
+			);
+			
+			dispatch.motionVectors = ffxGetTextureResourceVK(
+					m_context.get(),
+					m_core.getVulkanImage(m_velocity),
+					m_core.getVulkanImageView(m_velocity),
+					m_core.getImageWidth(m_velocity),
+					m_core.getImageHeight(m_velocity),
+					static_cast<VkFormat>(m_core.getImageFormat(m_velocity))
+			);
+			
+			dispatch.exposure = ffxGetTextureResourceVK(
+					m_context.get(),
+					nullptr,
+					nullptr,
+					1,
+					1,
+					VK_FORMAT_UNDEFINED
+			);
+			
+			dispatch.reactive = ffxGetTextureResourceVK(
+					m_context.get(),
+					nullptr,
+					nullptr,
+					1,
+					1,
+					VK_FORMAT_UNDEFINED
+			);
 			
-			m_frameIndex++;
-			m_reset = false;
-		}, nullptr);
+			dispatch.transparencyAndComposition = ffxGetTextureResourceVK(
+					m_context.get(),
+					nullptr,
+					nullptr,
+					1,
+					1,
+					VK_FORMAT_UNDEFINED
+			);
+			
+			dispatch.output = ffxGetTextureResourceVK(
+					m_context.get(),
+					m_core.getVulkanImage(output),
+					m_core.getVulkanImageView(output),
+					outputWidth,
+					outputHeight,
+					static_cast<VkFormat>(m_core.getImageFormat(output))
+			);
+			
+			calcJitterOffset(
+					inputWidth,
+					inputHeight,
+					dispatch.jitterOffset.x,
+					dispatch.jitterOffset.y
+			);
+			
+			dispatch.motionVectorScale.x = static_cast<float>(+2.0f);
+			dispatch.motionVectorScale.y = static_cast<float>(-2.0f);
+			
+			dispatch.renderSize.width = inputWidth;
+			dispatch.renderSize.height = inputHeight;
+			
+			dispatch.enableSharpening = sharpeningEnabled;
+			dispatch.sharpness = m_sharpness;
+			
+			dispatch.frameTimeDelta = m_frameDeltaTime * 1000.0f; // from seconds to milliseconds
+			dispatch.preExposure = 1.0f;
+			dispatch.reset = m_reset;
+			
+			dispatch.cameraNear = m_near;
+			dispatch.cameraFar = m_far;
+			dispatch.cameraFovAngleVertical = m_fov;
+			
+			m_core.recordCommandsToStream(cmdStream, [&](const vk::CommandBuffer& cmdBuffer) {
+				dispatch.commandList = ffxGetCommandListVK(cmdBuffer);
+				
+				assert(ffxFsr2ContextDispatch(
+						m_context.get(),
+						&dispatch
+				) == FFX_OK);
+				
+				m_frameIndex++;
+				m_reset = false;
+			}, nullptr);
+		}
+		
+		m_core.updateImageLayoutManual(output, vk::ImageLayout::eGeneral);
+		m_core.recordEndDebugLabel(cmdStream);
 	}
 	
 	void FSR2Upscaling::setCamera(float near, float far, float fov) {
diff --git a/projects/indirect_dispatch/CMakeLists.txt b/projects/indirect_dispatch/CMakeLists.txt
index 53b0d1b15dc2eeb32308e16ad81d476ed502d370..ad59d5b24efa1af1b387253b4c007fd25c50500f 100644
--- a/projects/indirect_dispatch/CMakeLists.txt
+++ b/projects/indirect_dispatch/CMakeLists.txt
@@ -25,7 +25,25 @@ target_sources(indirect_dispatch PRIVATE
     src/MotionBlurSetup.cpp)
 
 # including headers of dependencies and the VkCV framework
-target_include_directories(indirect_dispatch SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
+target_include_directories(indirect_dispatch SYSTEM BEFORE PRIVATE
+		${vkcv_include}
+		${vkcv_includes}
+		${vkcv_testing_include}
+		${vkcv_camera_include}
+		${vkcv_shader_compiler_include}
+		${vkcv_gui_include}
+		${vkcv_upscaling_include}
+)
 
 # linking with libraries from all dependencies and the VkCV framework
-target_link_libraries(indirect_dispatch vkcv ${vkcv_libraries} vkcv_asset_loader ${vkcv_asset_loader_libraries} vkcv_testing vkcv_camera vkcv_shader_compiler vkcv_gui)
\ No newline at end of file
+target_link_libraries(indirect_dispatch
+		vkcv
+		${vkcv_libraries}
+		vkcv_asset_loader
+		${vkcv_asset_loader_libraries}
+		vkcv_testing
+		vkcv_camera
+		vkcv_shader_compiler
+		vkcv_gui
+		vkcv_upscaling
+)
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/App.cpp b/projects/indirect_dispatch/src/App.cpp
index f17a3df647359018411e5bebcc3175295359678b..f871338af2670087eaf851901790631a4711f680 100644
--- a/projects/indirect_dispatch/src/App.cpp
+++ b/projects/indirect_dispatch/src/App.cpp
@@ -4,6 +4,11 @@
 #include <vkcv/Sampler.hpp>
 #include <vkcv/gui/GUI.hpp>
 
+#include <vkcv/upscaling/FSR2Upscaling.hpp>
+#include <vkcv/upscaling/FSRUpscaling.hpp>
+#include <vkcv/upscaling/NISUpscaling.hpp>
+#include <vkcv/upscaling/BilinearUpscaling.hpp>
+
 #include <chrono>
 #include <functional>
 
@@ -35,7 +40,6 @@ App::App() :
 	m_cameraManager(m_core.getWindow(m_windowHandle)){}
 
 bool App::initialize() {
-
 	if (!loadMeshPass(m_core, &m_meshPass))
 		return false;
 
@@ -64,7 +68,12 @@ bool App::initialize() {
 		return false;
 
 	m_linearSampler = vkcv::samplerLinear(m_core, true);
-	m_renderTargets = createRenderTargets(m_core, m_windowWidth, m_windowHeight);
+	m_renderTargets = createRenderTargets(
+			m_core,
+			m_windowWidth,
+			m_windowHeight,
+			vkcv::upscaling::FSR2QualityMode::NONE
+	);
 
 	auto cameraHandle = m_cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
 	m_cameraManager.getCamera(cameraHandle).setPosition(glm::vec3(0, 1, -3));
@@ -143,11 +152,46 @@ void App::run() {
 			}
 		}
 	});
-
+	
+	vkcv::upscaling::FSR2Upscaling fsr2 (m_core);
+	
+	fsr2.bindDepthBuffer(m_renderTargets.depthBuffer);
+	fsr2.bindVelocityBuffer(m_renderTargets.motionBuffer);
+	
+	vkcv::upscaling::FSR2QualityMode fsrMode = vkcv::upscaling::FSR2QualityMode::NONE;
+	vkcv::upscaling::FSR2QualityMode oldFsrMode = fsrMode;
+	
+	int fsrModeIndex = static_cast<int>(fsrMode);
+	
+	const std::vector<const char*> fsrModeNames = {
+			"None",
+			"Quality",
+			"Balanced",
+			"Performance",
+			"Ultra Performance"
+	};
+	
+	bool fsrMipLoadBiasFlag = true;
+	bool fsrMipLoadBiasFlagBackup = fsrMipLoadBiasFlag;
+	
+	vkcv::upscaling::FSRUpscaling fsr1 (m_core);
+	vkcv::upscaling::BilinearUpscaling bilinear (m_core);
+	vkcv::upscaling::NISUpscaling nis (m_core);
+	
+	const std::vector<const char*> modeNames = {
+			"FSR Upscaling 1.0",
+			"FSR Upscaling 2.1.1",
+			"NIS Upscaling",
+			"Bilinear Upscaling"
+	};
+	
+	int upscalingMode = 3;
+	
+	vkcv::SamplerHandle fsr2Sampler;
+	
 	auto frameEndTime = std::chrono::system_clock::now();
 
 	while (vkcv::Window::hasOpenWindow()) {
-
 		vkcv::Window::pollEvents();
 
 		if (!freezeFrame) {
@@ -167,13 +211,42 @@ void App::run() {
 		if (!m_core.beginFrame(swapchainWidth, swapchainHeight,m_windowHandle))
 			continue;
 
-		const bool hasResolutionChanged = (swapchainWidth != m_windowWidth) || (swapchainHeight != m_windowHeight);
+		const bool hasResolutionChanged = (
+				(swapchainWidth != m_windowWidth) ||
+				(swapchainHeight != m_windowHeight) ||
+				(oldFsrMode != fsrMode) ||
+				(fsrMipLoadBiasFlagBackup != fsrMipLoadBiasFlag)
+		);
+		
 		if (hasResolutionChanged) {
 			m_windowWidth  = swapchainWidth;
 			m_windowHeight = swapchainHeight;
-
-			m_renderTargets = createRenderTargets(m_core, m_windowWidth, m_windowHeight);
+			oldFsrMode = fsrMode;
+			fsrMipLoadBiasFlagBackup = fsrMipLoadBiasFlag;
+			
+			fsr2Sampler = m_core.createSampler(
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerFilterType::LINEAR,
+					vkcv::SamplerMipmapMode::LINEAR,
+					vkcv::SamplerAddressMode::REPEAT,
+					fsrMipLoadBiasFlag? vkcv::upscaling::getFSR2LodBias(fsrMode) : 0.0f
+			);
+			
+			vkcv::DescriptorWrites meshPassDescriptorWrites;
+			meshPassDescriptorWrites.writeSampler(1, fsr2Sampler);
+			m_core.writeDescriptorSet(m_meshPass.descriptorSet, meshPassDescriptorWrites);
+
+			m_renderTargets = createRenderTargets(
+					m_core,
+					m_windowWidth,
+					m_windowHeight,
+					fsrMode
+			);
+			
 			m_motionBlur.setResolution(m_windowWidth, m_windowHeight);
+			
+			fsr2.bindDepthBuffer(m_renderTargets.depthBuffer);
+			fsr2.bindVelocityBuffer(m_renderTargets.motionBuffer);
 		}
 
 		if(!freezeFrame)
@@ -183,20 +256,41 @@ void App::run() {
 		const float fDeltaTimeSeconds = microsecondToSecond * std::chrono::duration_cast<std::chrono::microseconds>(frameEndTime - frameStartTime).count();
 
 		m_cameraManager.update(fDeltaTimeSeconds);
-
-		const auto      time                = frameEndTime - appStartTime;
-		const float     fCurrentTime        = std::chrono::duration_cast<std::chrono::milliseconds>(time).count() * 0.001f;
+		fsr2.update(fDeltaTimeSeconds, false);
+		
+		const auto& camera = m_cameraManager.getActiveCamera();
+		float near, far;
+		
+		camera.getNearFar(near, far);
+		fsr2.setCamera(near, far, camera.getFov());
+
+		const auto  time         = frameEndTime - appStartTime;
+		const float fCurrentTime = std::chrono::duration_cast<std::chrono::milliseconds>(time).count() * 0.001f;
+		
+		float jitterX, jitterY;
+		
+		fsr2.calcJitterOffset(
+				m_core.getImageWidth(m_renderTargets.colorBuffer),
+				m_core.getImageHeight(m_renderTargets.colorBuffer),
+				jitterX,
+				jitterY
+		);
+		
+		const glm::mat4 jitterMatrix = glm::translate(
+				glm::identity<glm::mat4>(),
+				glm::vec3(jitterX, jitterY, 0.0f)
+		);
 
 		// update matrices
 		if (!freezeFrame) {
-
-			viewProjection = m_cameraManager.getActiveCamera().getMVP();
+			viewProjection = camera.getMVP();
 
 			for (Object& obj : sceneObjects) {
 				if (obj.modelMatrixUpdate) {
 					obj.modelMatrixUpdate(fCurrentTime, obj);
 				}
-				obj.mvp = viewProjection * obj.modelMatrix;
+				
+				obj.mvp = jitterMatrix * viewProjection * obj.modelMatrix;
 			}
 		}
 
@@ -212,7 +306,8 @@ void App::run() {
 
 		const std::vector<vkcv::ImageHandle> prepassRenderTargets = {
 			m_renderTargets.motionBuffer,
-			m_renderTargets.depthBuffer };
+			m_renderTargets.depthBuffer
+		};
 
 		std::vector<vkcv::InstanceDrawcall> prepassSceneDrawcalls;
 		for (const Object& obj : sceneObjects) {
@@ -225,12 +320,15 @@ void App::run() {
 			prepassPushConstants,
 			prepassSceneDrawcalls,
 			prepassRenderTargets,
-			m_windowHandle);
+			m_windowHandle
+		);
 
 		// sky prepass
 		glm::mat4 skyPrepassMatrices[2] = {
 			viewProjection,
-			viewProjectionPrevious };
+			viewProjectionPrevious
+		};
+		
 		vkcv::PushConstants skyPrepassPushConstants(sizeof(glm::mat4) * 2);
 		skyPrepassPushConstants.appendDrawcall(skyPrepassMatrices);
 
@@ -240,12 +338,14 @@ void App::run() {
 			skyPrepassPushConstants,
 			{ skyDrawcall },
 			prepassRenderTargets,
-			m_windowHandle);
+			m_windowHandle
+		);
 
 		// main pass
 		const std::vector<vkcv::ImageHandle> renderTargets   = { 
 			m_renderTargets.colorBuffer, 
-			m_renderTargets.depthBuffer };
+			m_renderTargets.depthBuffer
+		};
 
 		vkcv::PushConstants meshPushConstants(2 * sizeof(glm::mat4));
 		for (const Object& obj : sceneObjects) {
@@ -266,11 +366,12 @@ void App::run() {
 			meshPushConstants,
 			forwardSceneDrawcalls,
 			renderTargets,
-			m_windowHandle);
+			m_windowHandle
+		);
 
 		// sky
 		vkcv::PushConstants skyPushConstants = vkcv::pushConstants<glm::mat4>();
-		skyPushConstants.appendDrawcall(viewProjection);
+		skyPushConstants.appendDrawcall(jitterMatrix * viewProjection);
 
 		m_core.recordDrawcallsToCmdStream(
 			cmdStream,
@@ -278,35 +379,82 @@ void App::run() {
 			skyPushConstants,
 			{ skyDrawcall },
 			renderTargets,
-			m_windowHandle);
+			m_windowHandle
+		);
+		
+		// upscaling
+		m_core.prepareImageForSampling(cmdStream, m_renderTargets.colorBuffer);
+		
+		switch (upscalingMode) {
+			case 0:
+				m_core.prepareImageForStorage(cmdStream, m_renderTargets.finalBuffer);
+				
+				fsr1.recordUpscaling(
+						cmdStream,
+						m_renderTargets.colorBuffer,
+						m_renderTargets.finalBuffer
+				);
+				break;
+			case 1:
+				m_core.prepareImageForSampling(cmdStream, m_renderTargets.depthBuffer);
+				m_core.prepareImageForSampling(cmdStream, m_renderTargets.motionBuffer);
+				
+				m_core.prepareImageForSampling(cmdStream, m_renderTargets.finalBuffer);
+				
+				fsr2.recordUpscaling(
+						cmdStream,
+						m_renderTargets.colorBuffer,
+						m_renderTargets.finalBuffer
+				);
+				break;
+			case 2:
+				m_core.prepareImageForStorage(cmdStream, m_renderTargets.finalBuffer);
+				
+				nis.recordUpscaling(
+						cmdStream,
+						m_renderTargets.colorBuffer,
+						m_renderTargets.finalBuffer
+				);
+				break;
+			case 3:
+				m_core.prepareImageForStorage(cmdStream, m_renderTargets.finalBuffer);
+				
+				bilinear.recordUpscaling(
+						cmdStream,
+						m_renderTargets.colorBuffer,
+						m_renderTargets.finalBuffer
+				);
+				break;
+			default:
+				break;
+		}
+		
+		m_core.prepareImageForSampling(cmdStream, m_renderTargets.finalBuffer);
 
 		// motion blur
 		vkcv::ImageHandle motionBlurOutput;
 
 		if (motionVectorVisualisationMode == eMotionVectorVisualisationMode::None) {
-			float cameraNear;
-			float cameraFar;
-			m_cameraManager.getActiveCamera().getNearFar(cameraNear, cameraFar);
-
 			motionBlurOutput = m_motionBlur.render(
 				cmdStream,
 				m_renderTargets.motionBuffer,
-				m_renderTargets.colorBuffer,
+				m_renderTargets.finalBuffer,
 				m_renderTargets.depthBuffer,
 				motionBlurMode,
-				cameraNear,
-				cameraFar,
+				near,
+				far,
 				fDeltaTimeSeconds,
 				static_cast<float>(cameraShutterSpeedInverse),
 				motionBlurTileOffsetLength,
-				motionBlurFastPathThreshold);
-		}
-		else {
+				motionBlurFastPathThreshold
+			);
+		} else {
 			motionBlurOutput = m_motionBlur.renderMotionVectorVisualisation(
 				cmdStream,
 				m_renderTargets.motionBuffer,
 				motionVectorVisualisationMode,
-				motionVectorVisualisationRange);
+				motionVectorVisualisationRange
+			);
 		}
 
 		// gamma correction
@@ -330,7 +478,8 @@ void App::run() {
 			m_gammaCorrectionPass.pipeline,
 			fullScreenImageDispatch,
 			{ vkcv::useDescriptorSet(0, m_gammaCorrectionPass.descriptorSet) },
-			vkcv::PushConstants(0));
+			vkcv::PushConstants(0)
+		);
 
 		m_core.prepareSwapchainImageForPresent(cmdStream);
 		m_core.submitCommandStream(cmdStream);
@@ -364,6 +513,21 @@ void App::run() {
 		ImGui::InputFloat("Object mean height",         &objectMeanHeight);
 		ImGui::InputFloat("Object rotation speed X",    &objectRotationSpeedX);
 		ImGui::InputFloat("Object rotation speed Y",    &objectRotationSpeedY);
+		
+		float sharpness = fsr2.getSharpness();
+		
+		ImGui::Combo("FSR Quality Mode", &fsrModeIndex, fsrModeNames.data(), fsrModeNames.size());
+		ImGui::DragFloat("FSR Sharpness", &sharpness, 0.001, 0.0f, 1.0f);
+		ImGui::Checkbox("FSR Mip Lod Bias", &fsrMipLoadBiasFlag);
+		ImGui::Combo("Upscaling Mode", &upscalingMode, modeNames.data(), modeNames.size());
+		
+		if ((fsrModeIndex >= 0) && (fsrModeIndex <= 4)) {
+			fsrMode = static_cast<vkcv::upscaling::FSR2QualityMode>(fsrModeIndex);
+		}
+		
+		fsr1.setSharpness(sharpness);
+		fsr2.setSharpness(sharpness);
+		nis.setSharpness(sharpness);
 
 		ImGui::End();
 		gui.endGUI();
diff --git a/projects/indirect_dispatch/src/AppSetup.cpp b/projects/indirect_dispatch/src/AppSetup.cpp
index 26cfbbc38e2a190326b8a17adde5b8e4f44ec1cc..bc6090a7829944cc503eb978b9540095781c0133 100644
--- a/projects/indirect_dispatch/src/AppSetup.cpp
+++ b/projects/indirect_dispatch/src/AppSetup.cpp
@@ -299,36 +299,63 @@ bool loadComputePass(vkcv::Core& core, const std::filesystem::path& path, Comput
 	return true;
 }
 
-AppRenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const uint32_t height) {
+AppRenderTargets createRenderTargets(vkcv::Core& core,
+									 uint32_t width,
+									 uint32_t height,
+									 vkcv::upscaling::FSR2QualityMode mode) {
 	AppRenderTargets targets;
+	uint32_t renderWidth, renderHeight;
+	
+	vkcv::upscaling::getFSR2Resolution(
+			mode,
+			width,
+			height,
+			renderWidth,
+			renderHeight
+	);
 
 	targets.depthBuffer = core.createImage(
-		AppConfig::depthBufferFormat,
-		width,
-		height,
-		1,
-		false
+			AppConfig::depthBufferFormat,
+			renderWidth,
+			renderHeight,
+			1,
+			false
 	);
 
 	targets.colorBuffer = core.createImage(
-		AppConfig::colorBufferFormat,
-		width,
-		height,
-		1,
-		false,
-		false,
-		true
+			AppConfig::colorBufferFormat,
+			renderWidth,
+			renderHeight,
+			1,
+			false,
+			false,
+			true
 	);
 
 	targets.motionBuffer = core.createImage(
-		AppConfig::motionBufferFormat,
-		width,
-		height,
-		1,
-		false,
-		false,
-		true
+			AppConfig::motionBufferFormat,
+			renderWidth,
+			renderHeight,
+			1,
+			false,
+			false,
+			true
+	);
+	
+	targets.finalBuffer = core.createImage(
+			AppConfig::colorBufferFormat,
+			width,
+			height,
+			1,
+			false,
+			true,
+			true
 	);
+	
+	core.setDebugLabel(targets.depthBuffer, "Depth buffer");
+	core.setDebugLabel(targets.colorBuffer, "Color buffer");
+	core.setDebugLabel(targets.motionBuffer, "Motion buffer");
+	core.setDebugLabel(targets.finalBuffer, "Final buffer");
 
 	return targets;
 }
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/AppSetup.hpp b/projects/indirect_dispatch/src/AppSetup.hpp
index 41e020c357a3d868775a581170596e1748e39700..d06910b5f02325d27ee7f2a0e0fbfe4cf7560dab 100644
--- a/projects/indirect_dispatch/src/AppSetup.hpp
+++ b/projects/indirect_dispatch/src/AppSetup.hpp
@@ -1,10 +1,12 @@
 #pragma once
 #include <vkcv/Core.hpp>
+#include <vkcv/upscaling/FSR2Upscaling.hpp>
 
 struct AppRenderTargets {
 	vkcv::ImageHandle depthBuffer;
 	vkcv::ImageHandle colorBuffer;
 	vkcv::ImageHandle motionBuffer;
+	vkcv::ImageHandle finalBuffer;
 };
 
 struct GraphicPassHandles {
@@ -46,4 +48,7 @@ bool loadSkyPrePass(vkcv::Core& core, GraphicPassHandles* outHandles);
 
 bool loadComputePass(vkcv::Core& core, const std::filesystem::path& path, ComputePassHandles* outComputePass);
 
-AppRenderTargets createRenderTargets(vkcv::Core& core, const uint32_t width, const uint32_t height);
\ No newline at end of file
+AppRenderTargets createRenderTargets(vkcv::Core& core,
+									 uint32_t width,
+									 uint32_t height,
+									 vkcv::upscaling::FSR2QualityMode mode);
\ No newline at end of file
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index ac2b2b6a19c23e039178da43705a4f1033fa6682..381761a766ca2c6f0d9d622a9b56c183f13fd47f 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -637,7 +637,8 @@ int main(int argc, const char** argv) {
 				width, height
 		);
 
-		if ((width != fsrWidth) || ((height != fsrHeight)) || (fsrMipLoadBiasFlagBackup != fsrMipLoadBiasFlag)) {
+		if ((width != fsrWidth) || ((height != fsrHeight)) ||
+			(fsrMipLoadBiasFlagBackup != fsrMipLoadBiasFlag)) {
 			fsrWidth = width;
 			fsrHeight = height;
 			fsrMipLoadBiasFlagBackup = fsrMipLoadBiasFlag;
diff --git a/src/vkcv/Context.cpp b/src/vkcv/Context.cpp
index c0458f237151ece361ac066432643d182aaa199e..8b23bc552075c7980a3abf4a61c9eed03ddaff0e 100644
--- a/src/vkcv/Context.cpp
+++ b/src/vkcv/Context.cpp
@@ -413,7 +413,8 @@ namespace vkcv {
 				[](vk::PhysicalDeviceShaderFloat16Int8Features &features) {
 					features.setShaderFloat16(true);
 				},
-				false);
+				false
+			);
 		}
 
 		if (featureManager.useExtension(VK_KHR_16BIT_STORAGE_EXTENSION_NAME, false)) {
@@ -421,7 +422,26 @@ namespace vkcv {
 				[](vk::PhysicalDevice16BitStorageFeatures &features) {
 					features.setStorageBuffer16BitAccess(true);
 				},
-				false);
+				false
+			);
+		}
+		
+		if (featureManager.useExtension(VK_AMD_DEVICE_COHERENT_MEMORY_EXTENSION_NAME, false)) {
+			featureManager.useFeatures<vk::PhysicalDeviceCoherentMemoryFeaturesAMD>(
+				[](vk::PhysicalDeviceCoherentMemoryFeaturesAMD &features) {
+					features.setDeviceCoherentMemory(true);
+				},
+				false
+			);
+		}
+		
+		if (featureManager.useExtension(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME, false)) {
+			featureManager.useFeatures<vk::PhysicalDeviceSubgroupSizeControlFeatures>(
+					[](vk::PhysicalDeviceSubgroupSizeControlFeatures &features) {
+						features.setSubgroupSizeControl(true);
+					},
+					false
+			);
 		}
 
 		featureManager.useFeatures([](vk::PhysicalDeviceFeatures &features) {
@@ -456,13 +476,37 @@ namespace vkcv {
 
 		vk::Device device = physicalDevice.createDevice(deviceCreateInfo);
 
-		QueueManager queueManager =
-			QueueManager::create(device, queuePairsGraphics, queuePairsCompute, queuePairsTransfer);
+		QueueManager queueManager = QueueManager::create(
+				device,
+				queuePairsGraphics,
+				queuePairsCompute,
+				queuePairsTransfer
+		);
+		
+		const bool coherentDeviceMemory = featureManager.checkFeatures<vk::PhysicalDeviceCoherentMemoryFeaturesAMD>(
+				vk::StructureType::ePhysicalDeviceCoherentMemoryFeaturesAMD,
+				[](const vk::PhysicalDeviceCoherentMemoryFeaturesAMD &features) {
+					return features.deviceCoherentMemory;
+				}
+		);
 
 		vma::AllocatorCreateFlags vmaFlags;
-		const vma::AllocatorCreateInfo allocatorCreateInfo(vmaFlags, physicalDevice, device, 0,
-														   nullptr, nullptr, nullptr, nullptr,
-														   instance, VK_HEADER_VERSION_COMPLETE);
+		if (coherentDeviceMemory) {
+			vmaFlags |= vma::AllocatorCreateFlagBits::eAmdDeviceCoherentMemory;
+		}
+		
+		const vma::AllocatorCreateInfo allocatorCreateInfo(
+				vmaFlags,
+				physicalDevice,
+				device,
+				0,
+				nullptr,
+				nullptr,
+				nullptr,
+				nullptr,
+				instance,
+				VK_HEADER_VERSION_COMPLETE
+		);
 
 		vma::Allocator allocator = vma::createAllocator(allocatorCreateInfo);
 
diff --git a/src/vkcv/FeatureManager.cpp b/src/vkcv/FeatureManager.cpp
index 12503a321b4bd7d11e2e453e87d7a8901f968555..c83597e9f0b1026404496bc515bdf5d53fd7bbcc 100644
--- a/src/vkcv/FeatureManager.cpp
+++ b/src/vkcv/FeatureManager.cpp
@@ -476,6 +476,25 @@ namespace vkcv {
 
 		return true;
 	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceCoherentMemoryFeaturesAMD &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceCoherentMemoryFeaturesAMD);
+		
+		vkcv_check_feature(deviceCoherentMemory);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceSubgroupSizeControlFeatures &features,
+									  bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceSubgroupSizeControlFeatures);
+		
+		vkcv_check_feature(subgroupSizeControl);
+		vkcv_check_feature(computeFullSubgroups);
+		
+		return true;
+	}
 
 	vk::BaseOutStructure* FeatureManager::findFeatureStructure(vk::StructureType type) const {
 		for (auto &base : m_featuresExtensions) {
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index 993243cb5644dd3a1527f3c50efadc9fdc9aa299..e4961f760513cf5377f79edeec180ca1b6595ab4 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -345,14 +345,25 @@ namespace vkcv {
 		if ((!mipLevelCount) || (mipLevelOffset + mipLevelCount > mipLevelsMax))
 			mipLevelCount = mipLevelsMax - mipLevelOffset;
 
-		vk::ImageSubresourceRange imageSubresourceRange(aspectFlags, mipLevelOffset, mipLevelCount,
-														0, image.m_layers);
-
+		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);
+		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,