diff --git a/projects/rt_ambient_occlusion/CMakeLists.txt b/projects/rt_ambient_occlusion/CMakeLists.txt
index ce4a2fce46c55bdfc778a48a6d48fb8f396fac6d..ac335427ec1bb7bae14c45d1db00e7ab3a2ac97c 100644
--- a/projects/rt_ambient_occlusion/CMakeLists.txt
+++ b/projects/rt_ambient_occlusion/CMakeLists.txt
@@ -14,15 +14,19 @@ target_include_directories(rt_ambient_occlusion SYSTEM BEFORE PRIVATE
 		${vkcv_includes}
 		${vkcv_asset_loader_include}
 		${vkcv_camera_include}
+		${vkcv_gui_include}
 		${vkcv_scene_include}
 		${vkcv_shader_compiler_include}
-		${vkcv_scene_include})
+		${vkcv_scene_include}
+		${vkcv_upscaling_include})
 
 # linking with libraries from all dependencies and the VkCV framework
 target_link_libraries(rt_ambient_occlusion
 		vkcv ${vkcv_libraries}
 		vkcv_asset_loader ${vkcv_asset_loader_libraries}
 		vkcv_camera
+		vkcv_gui
 		vkcv_scene
 		vkcv_shader_compiler
-		vkcv_scene)
+		vkcv_scene
+		vkcv_upscaling)
diff --git a/projects/rt_ambient_occlusion/resources/shaders/ambientOcclusion.rchit b/projects/rt_ambient_occlusion/resources/shaders/ambientOcclusion.rchit
index 9c8a24032ff6136fad922a54a4dc050823648355..7a69ae2186e9f2fb38df9f202976a6a39bda97a5 100644
--- a/projects/rt_ambient_occlusion/resources/shaders/ambientOcclusion.rchit
+++ b/projects/rt_ambient_occlusion/resources/shaders/ambientOcclusion.rchit
@@ -38,12 +38,13 @@ layout(binding = 2, set = 0, scalar) buffer rtObjects {
     ObjDesc objects[];
 };
 
-layout(binding = 3, set = 0, scalar) buffer rtInstanceCount {
-    int instanceCount;
+layout(binding = 3, set = 0, scalar) buffer rtContext {
+    uint instanceCount;
+    uint sampleCount;
 };
 
 void main() {
-    int instanceIndex = gl_InstanceID + gl_GeometryIndexEXT * instanceCount;
+    int instanceIndex = gl_InstanceID + gl_GeometryIndexEXT * int(instanceCount);
 
     if (instanceIndex >= objects.length()) {
         payload.hitSky = 1.0f;
diff --git a/projects/rt_ambient_occlusion/resources/shaders/ambientOcclusion.rgen b/projects/rt_ambient_occlusion/resources/shaders/ambientOcclusion.rgen
index f0646e779a9ad225eadcabbdf314d6bacfeac48a..cecbfcc9aaff3dea184112c9e8988a290dbb9518 100644
--- a/projects/rt_ambient_occlusion/resources/shaders/ambientOcclusion.rgen
+++ b/projects/rt_ambient_occlusion/resources/shaders/ambientOcclusion.rgen
@@ -1,5 +1,6 @@
 #version 460
 #extension GL_EXT_ray_tracing : require
+#extension GL_EXT_scalar_block_layout : require
 
 #define M_PI 3.1415926535897932384626433832795
 
@@ -13,6 +14,11 @@ layout(location = 0) rayPayloadEXT Payload {
 layout(binding = 0, set = 0, rgba16) uniform image2D outImg;            // the output image -> maybe use 16 bit values?
 layout(binding = 1, set = 0) uniform accelerationStructureEXT tlas;     // top level acceleration structure
 
+layout(binding = 3, set = 0, scalar) buffer rtContext {
+    uint instanceCount;
+    uint sampleCount;
+};
+
 layout( push_constant ) uniform constants {
     vec4 camera_position;   // as origin for ray generation
     vec4 camera_right;      // for computing ray direction
@@ -102,7 +108,7 @@ float CastShadowRay(vec3 orig, vec3 dir) {
   return payload.hitSky;
 }
 
-vec3 sampleCosineDistribution(vec2 xi){
+vec3 sampleCosineDistribution(vec2 xi) {
 	float phi = 2 * M_PI * xi.y;
 	return vec3(
 		sqrt(xi.x) * cos(phi),
@@ -117,7 +123,7 @@ struct Basis{
 	vec3 forward;
 };
 
-Basis buildBasisAroundNormal(vec3 N){
+Basis buildBasisAroundNormal(vec3 N) {
 	Basis 	basis;
 	basis.up 		= N;
 	basis.right 	= abs(basis.up.x) < 0.99 ?  vec3(1, 0, 0) : vec3(0, 0, 1);
@@ -126,18 +132,7 @@ Basis buildBasisAroundNormal(vec3 N){
 	return basis;
 }
 
-vec3 sampleTangentToWorldSpace(vec3 tangentSpaceSample, vec3 N){
-	Basis tangentBasis = buildBasisAroundNormal(N);
-	return (
-		tangentBasis.right		* tangentSpaceSample.x +
-		tangentBasis.up			* tangentSpaceSample.y +
-		tangentBasis.forward 	* tangentSpaceSample.z
-    );
-}
-
-void main(){
-    uint rayCount = 16;    // the amount of rays to be casted
-
+void main() {
     initRandom(gl_LaunchIDEXT.xy);
 
     uvec2 pixel = gl_LaunchIDEXT.xy;
@@ -145,22 +140,30 @@ void main(){
     vec3 pos, norm;  // AO rays from where?
     TraceCameraRay(pixelIsSky, pos, norm);
 
-    if (pixelIsSky){
+    if (pixelIsSky) {
         // Don't compute ambient occlusion for the sky
         imageStore(outImg, ivec2(pixel), vec4(0.8,0.8,0.8,1.0));
         return;
     }
 
+    Basis tangentBasis = buildBasisAroundNormal(norm);
+
     // Compute ambient occlusion
     float aoValue = 0.0f;
-    for(uint i = 0; i < rayCount; i++){
+
+    for (uint i = 0; i < sampleCount; i++) {
         vec3 sampleTangentSpace = sampleCosineDistribution(random());
-        vec3 sampleWorldSpace   = sampleTangentToWorldSpace(sampleTangentSpace, norm);
 
-        aoValue += CastShadowRay(pos, sampleWorldSpace);
+        vec3 sampleWorldSpace  = (
+            tangentBasis.right * sampleTangentSpace.x +
+            tangentBasis.up * sampleTangentSpace.y +
+            tangentBasis.forward * sampleTangentSpace.z
+        );
+
+        aoValue += CastShadowRay(pos, sampleWorldSpace) * max(-dot(norm, -sampleWorldSpace), 0);
     }
 
-    aoValue /= rayCount;
+    aoValue /= max(sampleCount, 1);
     
     imageStore(outImg, ivec2(pixel), vec4(vec3(aoValue), 1));
 }
diff --git a/projects/rt_ambient_occlusion/src/main.cpp b/projects/rt_ambient_occlusion/src/main.cpp
index 4e587f2ecf35668623bfba615cf65de0673644ea..49d6156405af2149c0fe0ec38c72635554af690e 100644
--- a/projects/rt_ambient_occlusion/src/main.cpp
+++ b/projects/rt_ambient_occlusion/src/main.cpp
@@ -1,7 +1,9 @@
 #include <vkcv/Core.hpp>
 #include <vkcv/camera/CameraManager.hpp>
+#include <vkcv/gui/GUI.hpp>
 #include <vkcv/shader/GLSLCompiler.hpp>
 #include <vkcv/scene/Scene.hpp>
+#include <vkcv/upscaling/FSRUpscaling.hpp>
 
 /**
  * Note: This project is based on the following tutorial https://github.com/Apress/Ray-Tracing-Gems-II/tree/main/Chapter_16.
@@ -147,16 +149,38 @@ int main(int argc, const char** argv) {
 	
 	objDescBuffer.fill(objDescList);
 	
-	auto instanceCountBuffer = vkcv::buffer<uint32_t>(core, vkcv::BufferType::STORAGE, 1);
-	instanceCountBuffer.fill(&instanceCount);
+	auto contextBuffer = vkcv::buffer<uint32_t>(
+			core, vkcv::BufferType::STORAGE, 2
+	);
+	
+	uint32_t* context = contextBuffer.map();
+	
+	context[0] = instanceCount;
+	context[1] = 16;
 	
 	{
 		vkcv::DescriptorWrites writes;
 		writes.writeAcceleration(1, { scene_tlas });
 		writes.writeStorageBuffer(2, objDescBuffer.getHandle());
-		writes.writeStorageBuffer(3, instanceCountBuffer.getHandle());
+		writes.writeStorageBuffer(3, contextBuffer.getHandle());
 		core.writeDescriptorSet(descriptorSetHandles[0], writes);
 	}
+	
+	vkcv::upscaling::FSRUpscaling upscaling (core);
+	
+	uint32_t fsrWidth = core.getWindow(windowHandle).getWidth();
+	uint32_t fsrHeight = core.getWindow(windowHandle).getHeight();
+	
+	vkcv::upscaling::FSRQualityMode fsrMode = vkcv::upscaling::FSRQualityMode::NONE;
+	int fsrModeIndex = static_cast<int>(fsrMode);
+	
+	const std::vector<const char*> fsrModeNames = {
+			"None",
+			"Ultra Quality",
+			"Quality",
+			"Balanced",
+			"Performance"
+	};
 
 	struct RaytracingPushConstantData {
 	    glm::vec4 camera_position;   // as origin for ray generation
@@ -169,25 +193,69 @@ int main(int argc, const char** argv) {
 			shaderProgram,
 			descriptorSetLayoutHandles
 	));
-
-	vkcv::ImageHandle depthBuffer;
+	
+	const vk::Format depthBufferFormat = vk::Format::eD32Sfloat;
+	const vk::Format colorBufferFormat = vk::Format::eR16G16B16A16Sfloat;
+	
+	vkcv::ImageConfig depthBufferConfig (
+			fsrWidth,
+			fsrHeight
+	);
+	
+	vkcv::ImageHandle depthBuffer = core.createImage(
+			depthBufferFormat,
+			depthBufferConfig
+	);
+	
+	vkcv::ImageConfig colorBufferConfig (
+			fsrWidth,
+			fsrHeight
+	);
+	
+	colorBufferConfig.setSupportingStorage(true);
+	colorBufferConfig.setSupportingColorAttachment(true);
+	
+	vkcv::ImageHandle colorBuffer;
 
 	const vkcv::ImageHandle swapchainInput = vkcv::ImageHandle::createSwapchainImageHandle();
 	
+	vkcv::gui::GUI gui (core, windowHandle);
+	
 	core.run([&](const vkcv::WindowHandle &windowHandle, double t, double dt,
 				 uint32_t swapchainWidth, uint32_t swapchainHeight) {
-		if ((!depthBuffer) ||
-			(swapchainWidth != core.getImageWidth(depthBuffer)) ||
-			((swapchainHeight != core.getImageHeight(depthBuffer)))) {
+		uint32_t width, height;
+		vkcv::upscaling::getFSRResolution(
+				fsrMode,
+				swapchainWidth, swapchainHeight,
+				width, height
+		);
+		
+		if ((!colorBuffer) || (!depthBuffer) ||
+			(width != fsrWidth) || (height != fsrHeight)) {
+			fsrWidth = width;
+			fsrHeight = height;
+			
+			depthBufferConfig.setWidth(fsrWidth);
+			depthBufferConfig.setHeight(fsrHeight);
+			
 			depthBuffer = core.createImage(
-					vk::Format::eD32Sfloat,
-					vkcv::ImageConfig(
-							swapchainWidth,
-							swapchainHeight
-					)
+					depthBufferFormat,
+					depthBufferConfig
+			);
+			
+			colorBufferConfig.setWidth(fsrWidth);
+			colorBufferConfig.setHeight(fsrHeight);
+			
+			colorBuffer = core.createImage(
+					colorBufferFormat,
+					colorBufferConfig
 			);
 		}
 		
+		if ((!colorBuffer) || (!depthBuffer)) {
+			return;
+		}
+		
 		cameraManager.update(dt);
 
 		const std::vector<vkcv::ImageHandle> renderTargets = { swapchainInput, depthBuffer };
@@ -205,26 +273,59 @@ int main(int argc, const char** argv) {
 		
 		{
 			vkcv::DescriptorWrites writes;
-			writes.writeStorageImage(0, swapchainInput);
+			writes.writeStorageImage(0, colorBuffer);
 			core.writeDescriptorSet(shaderDescriptorSet, writes);
 		}
 
 		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
 
-		core.prepareImageForStorage(cmdStream, swapchainInput);
+		core.prepareImageForStorage(cmdStream, colorBuffer);
 
 		core.recordRayGenerationToCmdStream(
 			cmdStream,
 			pipeline,
-			vkcv::DispatchSize(swapchainWidth, swapchainHeight),
+			vkcv::DispatchSize(width, height),
 			{ vkcv::useDescriptorSet(0, shaderDescriptorSet) },
 			pushConstants,
 			windowHandle
 		);
+		
+		core.prepareImageForSampling(cmdStream, colorBuffer);
+		core.prepareImageForStorage(cmdStream, swapchainInput);
+		
+		upscaling.recordUpscaling(cmdStream, colorBuffer, swapchainInput);
 
 		core.prepareSwapchainImageForPresent(cmdStream);
 		core.submitCommandStream(cmdStream);
+		
+		gui.beginGUI();
+		
+		ImGui::Begin("Settings");
+		
+		int sampleCount = static_cast<int>(context[1]);
+		
+		ImGui::SliderInt("Samples", &sampleCount, 1, 128);
+		
+		if (static_cast<uint32_t>(sampleCount) != context[1]) {
+			context[1] = static_cast<uint32_t>(sampleCount);
+		}
+		
+		float sharpness = upscaling.getSharpness();
+		
+		ImGui::Combo("FSR Quality Mode", &fsrModeIndex, fsrModeNames.data(), fsrModeNames.size());
+		ImGui::DragFloat("FSR Sharpness", &sharpness, 0.001, 0.0f, 1.0f);
+		
+		if ((fsrModeIndex >= 0) && (fsrModeIndex <= 4)) {
+			fsrMode = static_cast<vkcv::upscaling::FSRQualityMode>(fsrModeIndex);
+		}
+		
+		upscaling.setSharpness(sharpness);
+		
+		ImGui::End();
+		
+		gui.endGUI();
 	});
-
+	
+	contextBuffer.unmap();
 	return 0;
 }
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index 4e65cc45f83915861ef4b67c388e5ae35ef9d35c..5ecbd70f93e47c7e4a0a1a85dc0eef8e4cc00279 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -414,6 +414,10 @@ namespace vkcv {
 			mipLevelCount = mipLevelsMax - mipLevelOffset;
 		}
 		
+		if (mipLevelCount <= 0) {
+			return {};
+		}
+		
 		vk::ImageSubresourceRange imageSubresourceRange(
 				aspectFlags,
 				mipLevelOffset,
@@ -480,14 +484,16 @@ namespace vkcv {
 				newLayout
 		);
 		
-		cmdBuffer.pipelineBarrier(
-				vk::PipelineStageFlagBits::eAllCommands,
-				vk::PipelineStageFlagBits::eAllCommands,
-				{},
-				nullptr,
-				nullptr,
-				transitionBarrier
-		);
+		if (transitionBarrier.subresourceRange.levelCount > 0) {
+			cmdBuffer.pipelineBarrier(
+					vk::PipelineStageFlagBits::eAllCommands,
+					vk::PipelineStageFlagBits::eAllCommands,
+					{},
+					nullptr,
+					nullptr,
+					transitionBarrier
+			);
+		}
 		
 		for (auto& layer : image.m_layers) {
 			layer = newLayout;