SpirvShaderDebugger: Improve stepping for inlined functions

Makes stepping around inlined functions less jumpy.

Fixes: b/170650010
Change-Id: Ie7b003722e3dee523a39bedf4dc42f1c0b05a041
Reviewed-on: https://swiftshader-review.googlesource.com/c/SwiftShader/+/49228
Kokoro-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Alexis Hétu <sugoi@google.com>
Tested-by: Ben Clayton <bclayton@google.com>
diff --git a/src/Pipeline/SpirvShaderDebugger.cpp b/src/Pipeline/SpirvShaderDebugger.cpp
index 45c01ba..2280fa0 100644
--- a/src/Pipeline/SpirvShaderDebugger.cpp
+++ b/src/Pipeline/SpirvShaderDebugger.cpp
@@ -2200,7 +2200,7 @@
 		auto shrink = [&](size_t maxLen) {
 			while(stack.size() > maxLen)
 			{
-				thread->exit();
+				thread->exit(true);
 				stack.pop_back();
 			}
 		};
@@ -2216,10 +2216,10 @@
 		{
 			if(stack[i] != newStack[i])
 			{
-				bool isTopMostFrame = i == (newStack.size() - 1);
+				bool wasTopMostFrame = i == (stack.size() - 1);
 				auto oldFunction = debug::find<debug::Function>(stack[i].block);
 				auto newFunction = debug::find<debug::Function>(newStack[i].block);
-				if(isTopMostFrame && oldFunction == newFunction)
+				if(wasTopMostFrame && oldFunction == newFunction)
 				{
 					// Deviation is just a movement in the top most frame's
 					// function.
@@ -2227,8 +2227,20 @@
 					// be treated as a step out and step in, breaking stepping
 					// commands. Instead, just update the frame variables for
 					// the new scope.
-					stack[i].block = newStack[i].block;
+					stack[i] = newStack[i];
 					thread->update(true, [&](vk::dbg::Frame &frame) {
+						// Update the frame location if we're entering a
+						// function. This allows the debugger to pause at the
+						// line (which may not have any instructions or OpLines)
+						// of a inlined function call. This is less jarring
+						// than magically appearing in another function before
+						// you've reached the line of the call site.
+						// See b/170650010 for more context.
+						if(stack.size() < newStack.size())
+						{
+							auto function = debug::find<debug::Function>(stack[i].block);
+							frame.location = vk::dbg::Location{ function->source->dbgFile, (int)stack[i].line };
+						}
 						updateFrameLocals(state, frame, stack[i].block);
 					});
 				}
@@ -2240,17 +2252,34 @@
 			}
 		}
 
-		// Now rebuild the parts of stack frames that are new
+		// Now rebuild the parts of stack frames that are new.
+		//
+		// This is done in two stages:
+		// (1) thread->enter() is called to construct the new stack frame with
+		//     the opening scope line. The frames locals and hovers are built
+		//     and assigned.
+		// (2) thread->update() is called to adjust the frame's location to
+		//     entry.line. This may be different to the function entry in the
+		//     case of multiple nested inline functions. If its the same, then
+		//     this is a no-op.
+		//
+		// This two-stage approach allows the debugger to step through chains of
+		// inlined function calls without having a jarring jump from the outer
+		// function to the first statement within the function.
+		// See b/170650010 for more context.
 		for(size_t i = stack.size(); i < newStack.size(); i++)
 		{
 			auto entry = newStack[i];
 			stack.emplace_back(entry);
 			auto function = debug::find<debug::Function>(entry.block);
 			thread->enter(entry.block->source->dbgFile, function->name, [&](vk::dbg::Frame &frame) {
-				frame.location = vk::dbg::Location{ function->source->dbgFile, (int)entry.line };
+				frame.location = vk::dbg::Location{ function->source->dbgFile, (int)function->line };
 				frame.hovers->variables->extend(std::make_shared<HoversFromLocals>(frame.locals->variables));
 				updateFrameLocals(state, frame, entry.block);
 			});
+			thread->update(true, [&](vk::dbg::Frame &frame) {
+				frame.location.line = (int)entry.line;
+			});
 		}
 	}
 
diff --git a/src/Vulkan/Debug/Thread.cpp b/src/Vulkan/Debug/Thread.cpp
index 2f4dcc0..dd9e22d 100644
--- a/src/Vulkan/Debug/Thread.cpp
+++ b/src/Vulkan/Debug/Thread.cpp
@@ -113,10 +113,14 @@
 	}
 }
 
-void Thread::exit()
+void Thread::exit(bool isStep /* = false */)
 {
 	marl::lock lock(mutex);
 	frames.pop_back();
+	if(isStep)
+	{
+		onLocationUpdate(lock);
+	}
 }
 
 void Thread::update(bool isStep, const UpdateFrame &f)
diff --git a/src/Vulkan/Debug/Thread.hpp b/src/Vulkan/Debug/Thread.hpp
index d9ba32d..1730146 100644
--- a/src/Vulkan/Debug/Thread.hpp
+++ b/src/Vulkan/Debug/Thread.hpp
@@ -130,7 +130,10 @@
 	void enter(const std::shared_ptr<File> &file, const std::string &function, const UpdateFrame &f = nullptr);
 
 	// exit() pops the thread's stack frame.
-	void exit();
+	// If isStep is true, then this will be considered a line-steppable event,
+	// and the debugger may pause at the function call at the new top most stack
+	// frame.
+	void exit(bool isStep = false);
 
 	// frame() returns a copy of the thread's top most stack frame.
 	Frame frame() const;