diff --git a/projects/indirect_dispatch/resources/shaders/motionBlur.comp b/projects/indirect_dispatch/resources/shaders/motionBlur.comp
index 503a7afa77249b1183e03e808d4879f63b20b24e..7d71df17db177718a211120ab951975825ddd7e6 100644
--- a/projects/indirect_dispatch/resources/shaders/motionBlur.comp
+++ b/projects/indirect_dispatch/resources/shaders/motionBlur.comp
@@ -1,6 +1,8 @@
 #version 440
 #extension GL_GOOGLE_include_directive : enable
+
 #include "motionBlurConfig.inc"
+#include "motionBlurWorkTile.inc"
 
 layout(set=0, binding=0)                    uniform texture2D   inColor;
 layout(set=0, binding=1)                    uniform texture2D   inDepth;
@@ -9,7 +11,11 @@ layout(set=0, binding=3)                    uniform texture2D   inMotionNeighbou
 layout(set=0, binding=4)                    uniform sampler     nearestSampler;
 layout(set=0, binding=5, r11f_g11f_b10f)    uniform image2D     outImage;
 
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+layout(set=0, binding=6) buffer copyPathTileBuffer {
+    WorkTiles workTiles;
+};
+
+layout(local_size_x = motionTileSize, local_size_y = motionTileSize, local_size_z = 1) in;
 
 layout( push_constant ) uniform constants{
     // computed from delta time and shutter speed
@@ -127,11 +133,14 @@ float dither(ivec2 coord){
 
 void main(){
 
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage))))
+    uint    tileIndex       = gl_WorkGroupID.x;
+    ivec2   tileCoordinates = workTiles.tileXY[tileIndex];
+    ivec2   coord           = ivec2(tileCoordinates * motionTileSize + gl_LocalInvocationID.xy);
+
+    if(any(greaterThanEqual(coord, imageSize(outImage))))
         return;
    
     ivec2   textureRes  = textureSize(sampler2D(inColor, nearestSampler), 0);
-    ivec2   coord       = ivec2(gl_GlobalInvocationID.xy);
     vec2    uv          = vec2(coord + 0.5) / textureRes;   // + 0.5 to shift uv into pixel center
     
     // the motion tile lookup is jittered, so the hard edges in the blur are replaced by noise
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurColorCopy.comp b/projects/indirect_dispatch/resources/shaders/motionBlurColorCopy.comp
index 890b814e0feadb646971148978d1b91952904ca9..3afbe9f3a7d027cc253223de91b4ed30285479f3 100644
--- a/projects/indirect_dispatch/resources/shaders/motionBlurColorCopy.comp
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurColorCopy.comp
@@ -1,20 +1,29 @@
 #version 440
 #extension GL_GOOGLE_include_directive : enable
+
 #include "motionBlurConfig.inc"
+#include "motionBlurWorkTile.inc"
 
 layout(set=0, binding=0)                    uniform texture2D   inColor;
 layout(set=0, binding=1)                    uniform sampler     nearestSampler;
 layout(set=0, binding=2, r11f_g11f_b10f)    uniform image2D     outImage;
 
-layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+layout(set=0, binding=3) buffer copyPathTileBuffer {
+    WorkTiles workTiles;
+};
+
+layout(local_size_x = motionTileSize, local_size_y = motionTileSize, local_size_z = 1) in;
 
 void main(){
 
-    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, imageSize(outImage))))
+    uint    tileIndex       = gl_WorkGroupID.x;
+    ivec2   tileCoordinates = workTiles.tileXY[tileIndex];
+    ivec2   coordinate      = ivec2(tileCoordinates * motionTileSize + gl_LocalInvocationID.xy);
+    
+    if(any(greaterThanEqual(coordinate, imageSize(outImage))))
         return;
-   
-    ivec2   coord = ivec2(gl_GlobalInvocationID.xy);
-    vec3    color = texelFetch(sampler2D(inColor, nearestSampler), coord, 0).rgb;
     
-    imageStore(outImage, coord, vec4(color, 0.f));
+    vec3 color = texelFetch(sampler2D(inColor, nearestSampler), coordinate, 0).rgb;
+    
+    imageStore(outImage, coordinate, vec4(color, 0.f));
 }
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurConfig.inc b/projects/indirect_dispatch/resources/shaders/motionBlurConfig.inc
index fdd915c8aa857ee3bf91fe31d8f6a224bfe85612..25e647d091c4463883fec665f9fab04b95a28907 100644
--- a/projects/indirect_dispatch/resources/shaders/motionBlurConfig.inc
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurConfig.inc
@@ -1 +1,8 @@
-const int motionTileSize = 20;
\ No newline at end of file
+#ifndef MOTION_BLUR_CONFIG
+#define MOTION_BLUR_CONFIG
+
+const int motionTileSize        = 24;
+const int maxMotionBlurWidth    = 3840;
+const int maxMotionBlurHeight   = 2160;
+
+#endif // #ifndef MOTION_BLUR_CONFIG
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurIndirectArguments.comp b/projects/indirect_dispatch/resources/shaders/motionBlurIndirectArguments.comp
deleted file mode 100644
index 1d225cf864c979ddf902b33201b5e00a1783ae56..0000000000000000000000000000000000000000
--- a/projects/indirect_dispatch/resources/shaders/motionBlurIndirectArguments.comp
+++ /dev/null
@@ -1,20 +0,0 @@
-#version 440
-#extension GL_GOOGLE_include_directive : enable
-
-layout(set=0, binding=0) buffer indirectArgumentBuffer {
-    uint dispatchArgs[3];
-};
-
-layout( push_constant ) uniform constants{
-    uint width;
-    uint height;
-};
-
-layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
-
-void main(){
-
-    dispatchArgs[0] = (width  + 7) / 8;
-    dispatchArgs[1] = (height + 7) / 8;
-    dispatchArgs[2] = 1;
-}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurTileClassification.comp b/projects/indirect_dispatch/resources/shaders/motionBlurTileClassification.comp
new file mode 100644
index 0000000000000000000000000000000000000000..a7be26b830d52d244354501353cf34366145398c
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurTileClassification.comp
@@ -0,0 +1,43 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlurWorkTile.inc"
+
+layout(set=0, binding=0) uniform texture2D  inVelocityTile;
+layout(set=0, binding=1) uniform sampler    nearestSampler;
+
+layout(set=0, binding=2) buffer fullPathTileBuffer {
+    WorkTiles fullPathTiles;
+};
+
+layout(set=0, binding=3) buffer copyPathTileBuffer {
+    WorkTiles copyPathTiles;
+};
+
+layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
+
+layout( push_constant ) uniform constants{
+    uint width;
+    uint height;
+};
+
+void main(){
+    
+    ivec2 tileCoord = ivec2(gl_GlobalInvocationID.xy);
+    
+    if(any(greaterThanEqual(gl_GlobalInvocationID.xy, textureSize(sampler2D(inVelocityTile, nearestSampler), 0))))
+        return;
+    
+    vec2    motion          = texelFetch(sampler2D(inVelocityTile, nearestSampler), tileCoord, 0).rg;
+    vec2    motionPixel     = motion * vec2(width, height);
+    float   velocityPixel   = length(motionPixel);
+    
+    if(velocityPixel > 0.5){
+        uint index                  = atomicAdd(fullPathTiles.tileCount, 1);
+        fullPathTiles.tileXY[index] = tileCoord;
+    }
+    else{
+        uint index                  = atomicAdd(copyPathTiles.tileCount, 1);
+        copyPathTiles.tileXY[index] = tileCoord;
+    }
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurTileClassificationVis.comp b/projects/indirect_dispatch/resources/shaders/motionBlurTileClassificationVis.comp
new file mode 100644
index 0000000000000000000000000000000000000000..7889f454ee8a8a5d9f3026da2b34012f38a3f4b6
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurTileClassificationVis.comp
@@ -0,0 +1,47 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlurConfig.inc"
+#include "motionBlurWorkTile.inc"
+
+layout(set=0, binding=0)                    uniform texture2D   inColor;
+layout(set=0, binding=1)                    uniform sampler     nearestSampler;
+layout(set=0, binding=2, r11f_g11f_b10f)    uniform image2D     outImage;
+
+layout(set=0, binding=3) buffer fullPathTileBuffer {
+    WorkTiles fullPathTiles;
+};
+
+layout(set=0, binding=4) buffer copyPathTileBuffer {
+    WorkTiles copyPathTiles;
+};
+
+layout(local_size_x = motionTileSize, local_size_y = motionTileSize, local_size_z = 1) in;
+
+void main(){
+    
+    uint tileIndexFullPath = gl_WorkGroupID.x;
+    uint tileIndexCopyPath = gl_WorkGroupID.x - fullPathTiles.tileCount;
+    
+    vec3    debugColor;
+    ivec2   tileCoordinates;
+    
+    if(tileIndexFullPath < fullPathTiles.tileCount){
+        debugColor      = vec3(1, 0, 0);
+        tileCoordinates = fullPathTiles.tileXY[tileIndexFullPath];
+    }
+    else if(tileIndexCopyPath < copyPathTiles.tileCount){
+        debugColor      = vec3(0, 1, 0);
+        tileCoordinates = copyPathTiles.tileXY[tileIndexCopyPath];
+    }
+    else{
+        return;
+    }
+    
+    ivec2   coordinate  = ivec2(tileCoordinates * motionTileSize + gl_LocalInvocationID.xy);
+    vec3    color       = texelFetch(sampler2D(inColor, nearestSampler), coordinate, 0).rgb;
+    
+    color = mix(color, debugColor, 0.5);
+    
+    imageStore(outImage, coordinate, vec4(color, 0));
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurWorkTile.inc b/projects/indirect_dispatch/resources/shaders/motionBlurWorkTile.inc
new file mode 100644
index 0000000000000000000000000000000000000000..8577f100aac524b93eecac406606a962bc52d222
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurWorkTile.inc
@@ -0,0 +1,19 @@
+#ifndef MOTION_BLUR_WORK_TILE
+#define MOTION_BLUR_WORK_TILE
+
+#include "motionBlurConfig.inc"
+
+const int maxTileCount = 
+    (maxMotionBlurWidth  + motionTileSize - 1) / motionTileSize * 
+    (maxMotionBlurHeight + motionTileSize - 1) / motionTileSize;
+
+struct WorkTiles{
+    uint    tileCount;
+    // dispatch Y/Z are here so the buffer can be used directly as an indirect dispatch argument buffer
+    uint    dispatchY;
+    uint    dispatchZ;
+    
+    ivec2   tileXY[maxTileCount];
+};
+
+#endif // #ifndef MOTION_BLUR_WORK_TILE
\ No newline at end of file
diff --git a/projects/indirect_dispatch/resources/shaders/motionBlurWorkTileReset.comp b/projects/indirect_dispatch/resources/shaders/motionBlurWorkTileReset.comp
new file mode 100644
index 0000000000000000000000000000000000000000..916a4e4d752061e3662e05ce1188be3e3d4c262c
--- /dev/null
+++ b/projects/indirect_dispatch/resources/shaders/motionBlurWorkTileReset.comp
@@ -0,0 +1,24 @@
+#version 440
+#extension GL_GOOGLE_include_directive : enable
+
+#include "motionBlurWorkTile.inc"
+
+layout(set=0, binding=0) buffer fullPathTileBuffer {
+    WorkTiles fullPathTiles;
+};
+
+layout(set=0, binding=1) buffer copyPathTileBuffer {
+    WorkTiles copyPathTiles;
+};
+
+layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
+
+void main(){
+    fullPathTiles.tileCount = 0;
+    fullPathTiles.dispatchY = 1;
+    fullPathTiles.dispatchZ = 1;
+    
+    copyPathTiles.tileCount = 0;
+    copyPathTiles.dispatchY = 1;
+    copyPathTiles.dispatchZ = 1;
+}
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlur.cpp b/projects/indirect_dispatch/src/MotionBlur.cpp
index 3a8594f68871139ce3eeb7d9e3d8ec2fb1058dcd..223b74ca262664eb4cfeb4106b47f92ede70931a 100644
--- a/projects/indirect_dispatch/src/MotionBlur.cpp
+++ b/projects/indirect_dispatch/src/MotionBlur.cpp
@@ -10,8 +10,8 @@ std::array<uint32_t, 3> computeFullscreenDispatchSize(
 
 	// optimized divide and ceil
 	return std::array<uint32_t, 3>{
-		static_cast<uint32_t>(imageWidth  + (localGroupSize - 1) / localGroupSize),
-		static_cast<uint32_t>(imageHeight + (localGroupSize - 1) / localGroupSize),
+		static_cast<uint32_t>(imageWidth  + (localGroupSize - 1)) / localGroupSize,
+		static_cast<uint32_t>(imageHeight + (localGroupSize - 1)) / localGroupSize,
 		static_cast<uint32_t>(1) };
 }
 
@@ -36,18 +36,42 @@ bool MotionBlur::initialize(vkcv::Core* corePtr, const uint32_t targetWidth, con
 	if (!loadComputePass(*m_core, "resources/shaders/motionVectorVisualisation.comp", &m_motionVectorVisualisationPass))
 		return false;
 
-	if (!loadComputePass(*m_core, "resources/shaders/motionBlurIndirectArguments.comp", &m_indirectArgumentPass))
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlurColorCopy.comp", &m_colorCopyPass))
 		return false;
 
-	if (!loadComputePass(*m_core, "resources/shaders/motionBlurColorCopy.comp", &m_colorCopyPass))
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlurTileClassification.comp", &m_tileClassificationPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlurWorkTileReset.comp", &m_tileResetPass))
+		return false;
+
+	if (!loadComputePass(*m_core, "resources/shaders/motionBlurTileClassificationVis.comp", &m_tileVisualisationPass))
 		return false;
 
-	m_indirectArgumentBuffer = m_core->createBuffer<uint32_t>(vkcv::BufferType::STORAGE, 3, vkcv::BufferMemoryType::DEVICE_LOCAL, true).getHandle();
+	// work tile buffers and descriptors
+	const uint32_t workTileBufferSize = static_cast<uint32_t>(2 * sizeof(uint32_t)) * (3 +
+		((MotionBlurConfig::maxWidth + MotionBlurConfig::maxMotionTileSize - 1) / MotionBlurConfig::maxMotionTileSize) *
+		((MotionBlurConfig::maxHeight + MotionBlurConfig::maxMotionTileSize - 1) / MotionBlurConfig::maxMotionTileSize));
+
+	m_copyPathWorkTileBuffer = m_core->createBuffer<uint32_t>(
+		vkcv::BufferType::STORAGE, 
+		workTileBufferSize, 
+		vkcv::BufferMemoryType::DEVICE_LOCAL, 
+		true).getHandle();
+
+	m_fullPathWorkTileBuffer = m_core->createBuffer<uint32_t>(
+		vkcv::BufferType::STORAGE, 
+		workTileBufferSize, 
+		vkcv::BufferMemoryType::DEVICE_LOCAL, 
+		true).getHandle();
+
+	vkcv::DescriptorWrites tileResetDescriptorWrites;
+	tileResetDescriptorWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(0, m_fullPathWorkTileBuffer),
+		vkcv::BufferDescriptorWrite(1, m_copyPathWorkTileBuffer) };
+
+	m_core->writeDescriptorSet(m_tileResetPass.descriptorSet, tileResetDescriptorWrites);
 
-	vkcv::DescriptorWrites indirectArgumentDescriptorWrites;
-	indirectArgumentDescriptorWrites.storageBufferWrites = 
-		{ vkcv::BufferDescriptorWrite(0, m_indirectArgumentBuffer) };
-	m_core->writeDescriptorSet(m_indirectArgumentPass.descriptorSet, indirectArgumentDescriptorWrites);
 
 	m_renderTargets = MotionBlurSetup::createRenderTargets(targetWidth, targetHeight, *m_core);
 
@@ -79,25 +103,57 @@ vkcv::ImageHandle MotionBlur::render(
 
 	computeMotionTiles(cmdStream, motionBufferFullRes);
 
-	// write indirect dispatch argument buffer
-	struct IndirectArgumentConstants {
+	// work tile reset
+	const uint32_t dispatchSizeOne[3] = { 1, 1, 1 };
+
+	m_core->recordComputeDispatchToCmdStream(
+		cmdStream,
+		m_tileResetPass.pipeline,
+		dispatchSizeOne,
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileResetPass.descriptorSet).vulkanHandle) },
+		vkcv::PushConstants(0));
+
+	m_core->recordBufferMemoryBarrier(cmdStream, m_fullPathWorkTileBuffer);
+	m_core->recordBufferMemoryBarrier(cmdStream, m_copyPathWorkTileBuffer);
+
+	// work tile classification
+	vkcv::DescriptorWrites tileClassificationDescriptorWrites;
+	tileClassificationDescriptorWrites.sampledImageWrites = {
+		vkcv::SampledImageDescriptorWrite(0, m_renderTargets.motionMaxNeighbourhood) };
+	tileClassificationDescriptorWrites.samplerWrites = {
+		vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
+	tileClassificationDescriptorWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(2, m_fullPathWorkTileBuffer),
+		vkcv::BufferDescriptorWrite(3, m_copyPathWorkTileBuffer) };
+
+	m_core->writeDescriptorSet(m_tileClassificationPass.descriptorSet, tileClassificationDescriptorWrites);
+
+	const auto tileClassificationDispatch = computeFullscreenDispatchSize(
+		m_core->getImageWidth(m_renderTargets.motionMaxNeighbourhood), 
+		m_core->getImageHeight(m_renderTargets.motionMaxNeighbourhood),
+		8);
+
+	struct ResolutionConstants {
 		uint32_t width;
 		uint32_t height;
 	};
-	vkcv::PushConstants indirectArgumentPassPushConstants(sizeof(IndirectArgumentConstants));
-	IndirectArgumentConstants indirectArgumentConstants;
-	indirectArgumentConstants.width  = m_core->getImageWidth( m_renderTargets.outputColor);
-	indirectArgumentConstants.height = m_core->getImageHeight(m_renderTargets.outputColor);
-	indirectArgumentPassPushConstants.appendDrawcall(indirectArgumentConstants);
+	vkcv::PushConstants resolutionPushConstants(sizeof(ResolutionConstants));
+	ResolutionConstants resolutionConstants;
+	resolutionConstants.width = m_core->getImageWidth(m_renderTargets.outputColor);
+	resolutionConstants.height = m_core->getImageHeight(m_renderTargets.outputColor);
+	resolutionPushConstants.appendDrawcall(resolutionConstants);
 
-	const uint32_t dispatchSizeOne[3] = { 1, 1, 1 };
+	m_core->prepareImageForSampling(cmdStream, m_renderTargets.motionMaxNeighbourhood);
 
 	m_core->recordComputeDispatchToCmdStream(
 		cmdStream,
-		m_indirectArgumentPass.pipeline,
-		dispatchSizeOne,
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_indirectArgumentPass.descriptorSet).vulkanHandle) },
-		indirectArgumentPassPushConstants);
+		m_tileClassificationPass.pipeline,
+		tileClassificationDispatch.data(),
+		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileClassificationPass.descriptorSet).vulkanHandle) },
+		resolutionPushConstants);
+
+	m_core->recordBufferMemoryBarrier(cmdStream, m_fullPathWorkTileBuffer);
+	m_core->recordBufferMemoryBarrier(cmdStream, m_copyPathWorkTileBuffer);
 
 	// usually this is the neighbourhood max, but other modes can be used for comparison/debugging
 	vkcv::ImageHandle inputMotionTiles;
@@ -122,6 +178,8 @@ vkcv::ImageHandle MotionBlur::render(
 		vkcv::SamplerDescriptorWrite(4, m_nearestSampler) };
 	motionBlurDescriptorWrites.storageImageWrites = {
 		vkcv::StorageImageDescriptorWrite(5, m_renderTargets.outputColor) };
+	motionBlurDescriptorWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(6, m_fullPathWorkTileBuffer)};
 
 	m_core->writeDescriptorSet(m_motionBlurPass.descriptorSet, motionBlurDescriptorWrites);
 
@@ -133,6 +191,8 @@ vkcv::ImageHandle MotionBlur::render(
 		vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
 	colorCopyDescriptorWrites.storageImageWrites = {
 		vkcv::StorageImageDescriptorWrite(2, m_renderTargets.outputColor) };
+	colorCopyDescriptorWrites.storageBufferWrites = {
+		vkcv::BufferDescriptorWrite(3, m_copyPathWorkTileBuffer) };
 
 	m_core->writeDescriptorSet(m_colorCopyPass.descriptorSet, colorCopyDescriptorWrites);
 
@@ -164,30 +224,57 @@ vkcv::ImageHandle MotionBlur::render(
 		m_core->recordComputeIndirectDispatchToCmdStream(
 			cmdStream,
 			m_motionBlurPass.pipeline,
-			m_indirectArgumentBuffer,
+			m_fullPathWorkTileBuffer,
 			0,
 			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionBlurPass.descriptorSet).vulkanHandle) },
 			motionBlurPushConstants);
-	}
-	else if(mode == eMotionBlurMode::Disabled) {
+
 		m_core->recordComputeIndirectDispatchToCmdStream(
 			cmdStream,
 			m_colorCopyPass.pipeline,
-			m_indirectArgumentBuffer,
+			m_copyPathWorkTileBuffer,
 			0,
 			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_colorCopyPass.descriptorSet).vulkanHandle) },
 			vkcv::PushConstants(0));
 	}
-	else {
-		vkcv_log(vkcv::LogLevel::ERROR, "Unknown eMotionBlurMode enum option");
-		m_core->recordComputeIndirectDispatchToCmdStream(
+	else if(mode == eMotionBlurMode::Disabled) {
+		return colorBuffer;
+	}
+	else if (mode == eMotionBlurMode::TileVisualisation) {
+
+		vkcv::DescriptorWrites visualisationDescriptorWrites;
+		visualisationDescriptorWrites.sampledImageWrites = { 
+			vkcv::SampledImageDescriptorWrite(0, colorBuffer) };
+		visualisationDescriptorWrites.samplerWrites = {
+			vkcv::SamplerDescriptorWrite(1, m_nearestSampler) };
+		visualisationDescriptorWrites.storageImageWrites = {
+			vkcv::StorageImageDescriptorWrite(2, m_renderTargets.outputColor)};
+		visualisationDescriptorWrites.storageBufferWrites = {
+			vkcv::BufferDescriptorWrite(3, m_fullPathWorkTileBuffer),
+			vkcv::BufferDescriptorWrite(4, m_copyPathWorkTileBuffer)};
+
+		m_core->writeDescriptorSet(m_tileVisualisationPass.descriptorSet, visualisationDescriptorWrites);
+
+		const uint32_t tileCount = 
+			(m_core->getImageWidth(m_renderTargets.outputColor)  + MotionBlurConfig::maxMotionTileSize - 1) / MotionBlurConfig::maxMotionTileSize * 
+			(m_core->getImageHeight(m_renderTargets.outputColor) + MotionBlurConfig::maxMotionTileSize - 1) / MotionBlurConfig::maxMotionTileSize;
+
+		const uint32_t dispatchCounts[3] = {
+			tileCount,
+			1,
+			1 };
+
+		m_core->recordComputeDispatchToCmdStream(
 			cmdStream,
-			m_colorCopyPass.pipeline,
-			m_indirectArgumentBuffer,
-			0,
-			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_colorCopyPass.descriptorSet).vulkanHandle) },
+			m_tileVisualisationPass.pipeline,
+			dispatchCounts,
+			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileVisualisationPass.descriptorSet).vulkanHandle) },
 			vkcv::PushConstants(0));
 	}
+	else {
+		vkcv_log(vkcv::LogLevel::ERROR, "Unknown eMotionBlurMode enum option");
+		return colorBuffer;
+	}
 
 	return m_renderTargets.outputColor;
 }
diff --git a/projects/indirect_dispatch/src/MotionBlur.hpp b/projects/indirect_dispatch/src/MotionBlur.hpp
index 0158f328e82895d462d3dd87f82de60e5aee1052..13eff13b18ee186564e68f6a3790dceb2eaacae1 100644
--- a/projects/indirect_dispatch/src/MotionBlur.hpp
+++ b/projects/indirect_dispatch/src/MotionBlur.hpp
@@ -16,13 +16,15 @@ static const char* MotionVectorModeLabels[3] = {
 	"Tile neighbourhood max" };
 
 enum class eMotionBlurMode : int {
-	Default     = 0,
-	Disabled    = 1,
-	OptionCount = 2 };
+	Default             = 0,
+	Disabled            = 1,
+	TileVisualisation   = 2,
+	OptionCount         = 3 };
 
-static const char* MotionBlurModeLabels[2] = {
+static const char* MotionBlurModeLabels[3] = {
 	"Default",
-	"Disabled" };
+	"Disabled",
+	"Tile visualisation" };
 
 class MotionBlur {
 public:
@@ -64,8 +66,11 @@ private:
 	ComputePassHandles m_motionVectorMaxPass;
 	ComputePassHandles m_motionVectorMaxNeighbourhoodPass;
 	ComputePassHandles m_motionVectorVisualisationPass;
-	ComputePassHandles m_indirectArgumentPass;
 	ComputePassHandles m_colorCopyPass;
+	ComputePassHandles m_tileClassificationPass;
+	ComputePassHandles m_tileResetPass;
+	ComputePassHandles m_tileVisualisationPass;
 
-	vkcv::BufferHandle m_indirectArgumentBuffer;
+	vkcv::BufferHandle m_fullPathWorkTileBuffer;
+	vkcv::BufferHandle m_copyPathWorkTileBuffer;
 };
\ No newline at end of file
diff --git a/projects/indirect_dispatch/src/MotionBlurConfig.hpp b/projects/indirect_dispatch/src/MotionBlurConfig.hpp
index bdeb8acf988118fbc084595cc880f281594c62b1..c8201b256bae5169c1b80394e3067060eaacb00e 100644
--- a/projects/indirect_dispatch/src/MotionBlurConfig.hpp
+++ b/projects/indirect_dispatch/src/MotionBlurConfig.hpp
@@ -4,5 +4,7 @@
 namespace MotionBlurConfig {
 	const vk::Format    motionVectorTileFormat  = vk::Format::eR16G16Sfloat;
 	const vk::Format    outputColorFormat       = vk::Format::eB10G11R11UfloatPack32;
-	const uint32_t      maxMotionTileSize       = 20;	// must match "motionTileSize" in motionBlurConfig.inc
+	const uint32_t      maxMotionTileSize       = 24;	// must match "motionTileSize" in motionBlurConfig.inc
+	const uint32_t      maxWidth                = 3840;
+	const uint32_t      maxHeight               = 2160;
 }
\ No newline at end of file