Browse Source

Visual tests: Highlight differences when comparing to previous capture by holding shift key

Michael Ragazzon 2 years ago
parent
commit
81ae0c80e8

+ 2 - 2
Tests/Data/visual_tests_help.rml

@@ -19,7 +19,7 @@
 		font-size: 17dp;
 		width: 700dp;
 		margin: 0 auto;
-		padding-top: 30dp;
+		padding-top: 20dp;
 	}
 	p {
 		clear: both;
@@ -64,7 +64,7 @@
 <key>Ctrl+R</key> <value>Reload test</value>
 <key>Ctrl+S</key> <value>View source of test</value>
 <key>Ctrl+Shift+S</key> <value>View source of reference</value>
-<key>Ctrl+Q</key> <value>Show previous capture of test</value>
+<key>Ctrl+Q</key> <value>Show previous capture of test<br/>(hold Shift to highlight differences)</value>
 <key>Left Mouse Button on Link</key> <value>Copy link to clipboard (on supported platforms)</value>
 <key>F1</key> <value>Show / hide help</value>
 

+ 44 - 35
Tests/Source/VisualTests/CaptureScreen.cpp

@@ -92,7 +92,8 @@ struct DeferFree {
 	~DeferFree() { free(ptr); }
 };
 
-ComparisonResult CompareScreenToPreviousCapture(Rml::RenderInterface* render_interface, const Rml::String& filename, TextureGeometry* out_geometry)
+ComparisonResult CompareScreenToPreviousCapture(Rml::RenderInterface* render_interface, const Rml::String& filename, TextureGeometry* out_reference,
+	TextureGeometry* out_highlight)
 {
 	using Image = RendererExtensions::Image;
 
@@ -114,25 +115,6 @@ ComparisonResult CompareScreenToPreviousCapture(Rml::RenderInterface* render_int
 	}
 	RMLUI_ASSERT(w_ref > 0 && h_ref > 0 && data_ref);
 
-	// Optionally render the previous capture to a texture.
-	if (out_geometry)
-	{
-		if (!render_interface->GenerateTexture(out_geometry->texture_handle, data_ref, Rml::Vector2i((int)w_ref, (int)h_ref)))
-		{
-			ComparisonResult result;
-			result.success = false;
-			result.error_msg = Rml::CreateString(1024, "Could not generate texture from file %s", input_path.c_str());
-			return result;
-		}
-
-		const Rml::Colourb colour = {255, 255, 255, 255};
-		const Rml::Vector2f uv_top_left = {0, 0};
-		const Rml::Vector2f uv_bottom_right = {1, 1};
-
-		Rml::GeometryUtilities::GenerateQuad(out_geometry->vertices, out_geometry->indices, Rml::Vector2f(0, 0),
-			Rml::Vector2f((float)w_ref, (float)h_ref), colour, uv_top_left, uv_bottom_right, 0);
-	}
-
 	Image screen = RendererExtensions::CaptureScreen();
 	if (!screen.data)
 	{
@@ -146,7 +128,7 @@ ComparisonResult CompareScreenToPreviousCapture(Rml::RenderInterface* render_int
 	Image diff;
 	diff.width = w_ref;
 	diff.height = h_ref;
-	diff.num_components = 3;
+	diff.num_components = 4;
 	diff.data = Rml::UniquePtr<Rml::byte[]>(new Rml::byte[diff.width * diff.height * diff.num_components]);
 
 	// So we have both images now, compare them! Also create a diff image.
@@ -162,24 +144,30 @@ ComparisonResult CompareScreenToPreviousCapture(Rml::RenderInterface* render_int
 		return result;
 	}
 
+	const Rml::Colourb highlight_color(255, 0, 255, 255);
 	size_t sum_diff = 0;
-
-	constexpr int c = 3;
 	for (int y = 0; y < (int)h_ref; y++)
 	{
 		const int y_flipped_screen = screen.height - y - 1;
-		const int yb_screen = y_flipped_screen * screen.width * c;
-
-		const int wb_ref = w_ref * c;
-		const int yb_ref = y * w_ref * c;
-
-		for (int xb = 0; xb < wb_ref; xb++)
+		for (int x = 0; x < (int)w_ref; x++)
 		{
-			const int i_ref = yb_ref + xb;
-			Rml::byte pix_ref = data_ref[(i_ref * 4) / 3];
-			Rml::byte pix_screen = screen.data[yb_screen + xb];
-			diff.data[i_ref] = (Rml::byte)std::abs((int)pix_ref - (int)pix_screen);
-			sum_diff += (size_t)diff.data[i_ref];
+			const int i0_screen = (y_flipped_screen * screen.width + x) * 3;
+			const int i0_ref = (y * w_ref + x) * 4;
+			const int i0_diff = (y * diff.width + x) * 4;
+
+			int pixel_diff = 0;
+			for (int z = 0; z < 3; z++)
+			{
+				const Rml::byte pix_ref = data_ref[i0_ref + z];
+				const Rml::byte pix_screen = screen.data[i0_screen + z];
+				pixel_diff += Rml::Math::Absolute((int)pix_ref - (int)pix_screen);
+			}
+
+			diff.data[i0_diff + 0] = (pixel_diff ? highlight_color[0] : screen.data[i0_screen + 0]);
+			diff.data[i0_diff + 1] = (pixel_diff ? highlight_color[1] : screen.data[i0_screen + 1]);
+			diff.data[i0_diff + 2] = (pixel_diff ? highlight_color[2] : screen.data[i0_screen + 2]);
+			diff.data[i0_diff + 3] = highlight_color[3];
+			sum_diff += (size_t)pixel_diff;
 		}
 	}
 
@@ -189,9 +177,30 @@ ComparisonResult CompareScreenToPreviousCapture(Rml::RenderInterface* render_int
 	result.is_equal = (sum_diff == 0);
 	result.absolute_difference_sum = sum_diff;
 
-	const size_t max_diff = size_t(c * 255) * size_t(w_ref) * size_t(h_ref);
+	const size_t max_diff = size_t(3 * 255) * size_t(w_ref) * size_t(h_ref);
 	result.similarity_score = (sum_diff == 0 ? 1.0 : 1.0 - std::log(double(sum_diff)) / std::log(double(max_diff)));
 
+	// Optionally render the screen capture or diff to a texture.
+	auto GenerateGeometry = [&](TextureGeometry& geometry, Rml::byte* data, Rml::Vector2i dimensions) -> bool {
+		if (!render_interface->GenerateTexture(geometry.texture_handle, data, dimensions))
+			return false;
+		const Rml::Colourb colour = {255, 255, 255, 255};
+		const Rml::Vector2f uv_top_left = {0, 0};
+		const Rml::Vector2f uv_bottom_right = {1, 1};
+		Rml::GeometryUtilities::GenerateQuad(geometry.vertices, geometry.indices, Rml::Vector2f(0, 0), Rml::Vector2f((float)w_ref, (float)h_ref),
+			colour, uv_top_left, uv_bottom_right, 0);
+		return true;
+	};
+
+	if (out_reference && result.success)
+		result.success = GenerateGeometry(*out_reference, data_ref, {(int)w_ref, (int)h_ref});
+
+	if (out_highlight && result.success)
+		result.success = GenerateGeometry(*out_highlight, diff.data.get(), {diff.width, diff.height});
+
+	if (!result.success)
+		result.error_msg = Rml::CreateString(1024, "Could not generate texture from file %s", input_path.c_str());
+
 	return result;
 }
 

+ 2 - 1
Tests/Source/VisualTests/CaptureScreen.h

@@ -50,7 +50,8 @@ struct TextureGeometry {
 
 bool CaptureScreenshot(const Rml::String& filename, int clip_width);
 
-ComparisonResult CompareScreenToPreviousCapture(Rml::RenderInterface* render_interface, const Rml::String& filename, TextureGeometry* out_geometry);
+ComparisonResult CompareScreenToPreviousCapture(Rml::RenderInterface* render_interface, const Rml::String& filename, TextureGeometry* out_reference,
+	TextureGeometry* out_highlight);
 
 void RenderTextureGeometry(Rml::RenderInterface* render_interface, TextureGeometry& geometry);
 

+ 56 - 19
Tests/Source/VisualTests/TestNavigator.cpp

@@ -49,6 +49,7 @@ TestNavigator::TestNavigator(Rml::RenderInterface* render_interface, Rml::Contex
 	RMLUI_ASSERT(context);
 	RMLUI_ASSERTMSG(!test_suites.empty(), "At least one test suite is required.");
 	context->GetRootElement()->AddEventListener(Rml::EventId::Keydown, this, true);
+	context->GetRootElement()->AddEventListener(Rml::EventId::Keyup, this, true);
 	context->GetRootElement()->AddEventListener(Rml::EventId::Keydown, this);
 	context->GetRootElement()->AddEventListener(Rml::EventId::Textinput, this);
 	context->GetRootElement()->AddEventListener(Rml::EventId::Change, this);
@@ -63,10 +64,12 @@ TestNavigator::TestNavigator(Rml::RenderInterface* render_interface, Rml::Contex
 TestNavigator::~TestNavigator()
 {
 	context->GetRootElement()->RemoveEventListener(Rml::EventId::Keydown, this, true);
+	context->GetRootElement()->RemoveEventListener(Rml::EventId::Keyup, this, true);
 	context->GetRootElement()->RemoveEventListener(Rml::EventId::Keydown, this);
 	context->GetRootElement()->RemoveEventListener(Rml::EventId::Textinput, this);
 	context->GetRootElement()->RemoveEventListener(Rml::EventId::Change, this);
 	ReleaseTextureGeometry(render_interface, reference_geometry);
+	ReleaseTextureGeometry(render_interface, reference_highlight_geometry);
 }
 
 void TestNavigator::Update()
@@ -112,10 +115,10 @@ void TestNavigator::Update()
 
 void TestNavigator::Render()
 {
-	if (show_reference && reference_geometry.texture_handle)
+	if (reference_state != ReferenceState::None && source_state == SourceType::None && !viewer->IsHelpVisible())
 	{
-		render_interface->RenderGeometry(reference_geometry.vertices, 4, reference_geometry.indices, 6, reference_geometry.texture_handle,
-			Rml::Vector2f(0, 0));
+		TextureGeometry& geometry = (reference_state == ReferenceState::ShowReferenceHighlight ? reference_highlight_geometry : reference_geometry);
+		render_interface->RenderGeometry(geometry.vertices, 4, geometry.indices, 6, geometry.texture_handle, Rml::Vector2f(0, 0));
 	}
 }
 
@@ -179,6 +182,7 @@ void TestNavigator::ProcessEvent(Rml::Event& event)
 		}
 		else if (key_identifier == Rml::Input::KI_F1)
 		{
+			ShowReference(ReferenceState::None);
 			viewer->ShowHelp(!viewer->IsHelpVisible());
 		}
 		else if (key_identifier == Rml::Input::KI_F && key_ctrl)
@@ -205,11 +209,19 @@ void TestNavigator::ProcessEvent(Rml::Event& event)
 					source_state = SourceType::None;
 			}
 			viewer->ShowSource(source_state);
-			ShowReference(false, false);
+			ShowReference(ReferenceState::None);
 		}
 		else if (key_identifier == Rml::Input::KI_Q && key_ctrl)
 		{
-			ShowReference(!show_reference, false);
+			if (reference_state != ReferenceState::None)
+				ShowReference(ReferenceState::None);
+			else
+				ShowReference(key_shift ? ReferenceState::ShowReferenceHighlight : ReferenceState::ShowReference);
+		}
+		else if (key_identifier == Rml::Input::KI_LSHIFT || key_identifier == Rml::Input::KI_RSHIFT)
+		{
+			if (reference_state == ReferenceState::ShowReference)
+				ShowReference(ReferenceState::ShowReferenceHighlight);
 		}
 		else if (key_identifier == Rml::Input::KI_ESCAPE)
 		{
@@ -226,6 +238,10 @@ void TestNavigator::ProcessEvent(Rml::Event& event)
 				source_state = SourceType::None;
 				viewer->ShowSource(source_state);
 			}
+			else if (reference_state != ReferenceState::None)
+			{
+				ShowReference(ReferenceState::None);
+			}
 			else if (element_filter_input->IsPseudoClassSet("focus"))
 			{
 				element_filter_input->Blur();
@@ -251,6 +267,16 @@ void TestNavigator::ProcessEvent(Rml::Event& event)
 		}
 	}
 
+	if (event == Rml::EventId::Keyup && event.GetPhase() == Rml::EventPhase::Capture)
+	{
+		const auto key_identifier = (Rml::Input::KeyIdentifier)event.GetParameter<int>("key_identifier", 0);
+		if (key_identifier == Rml::Input::KI_LSHIFT || key_identifier == Rml::Input::KI_RSHIFT)
+		{
+			if (reference_state == ReferenceState::ShowReferenceHighlight)
+				ShowReference(ReferenceState::ShowReference);
+		}
+	}
+
 	// Keydown events in target/bubble phase ignored when focusing on input.
 	if (event == Rml::EventId::Keydown && event.GetPhase() != Rml::EventPhase::Capture)
 	{
@@ -359,7 +385,7 @@ void TestNavigator::LoadActiveTest(bool keep_scroll_position)
 	viewer->LoadTest(suite.GetDirectory(), suite.GetFilename(), suite.GetIndex(), suite.GetNumTests(), suite.GetFilterIndex(),
 		suite.GetNumFilteredTests(), suite_index, (int)test_suites.size(), keep_scroll_position);
 	viewer->ShowSource(source_state);
-	ShowReference(false, true);
+	ShowReference(ReferenceState::None);
 	UpdateGoToText();
 }
 
@@ -373,7 +399,7 @@ ComparisonResult TestNavigator::CompareCurrentView()
 {
 	const Rml::String filename = GetImageFilenameFromCurrentTest();
 
-	ComparisonResult result = CompareScreenToPreviousCapture(render_interface, filename, nullptr);
+	ComparisonResult result = CompareScreenToPreviousCapture(render_interface, filename, nullptr, nullptr);
 
 	return result;
 }
@@ -562,31 +588,42 @@ void TestNavigator::UpdateGoToText(bool out_of_bounds)
 		viewer->SetGoToText("Capturing all tests");
 	else if (iteration_state == IterationState::Comparison)
 		viewer->SetGoToText("Comparing all tests");
-	else if (show_reference)
+	else if (reference_state == ReferenceState::ShowReference)
 		viewer->SetGoToText(Rml::CreateString(100, "Showing reference capture (%.1f%% similar)", reference_comparison.similarity_score * 100.));
+	else if (reference_state == ReferenceState::ShowReferenceHighlight)
+		viewer->SetGoToText("Showing reference capture (highlight differences)");
 	else
 		viewer->SetGoToText("Press 'F1' for keyboard shortcuts.");
 }
 
-void TestNavigator::ShowReference(bool show, bool clear)
+void TestNavigator::ShowReference(ReferenceState new_reference_state)
 {
-	if (clear)
-	{
-		ReleaseTextureGeometry(render_interface, reference_geometry);
-		reference_comparison = {};
-	}
+	if (new_reference_state == reference_state || viewer->IsHelpVisible())
+		return;
 
 	Rml::String error_msg;
-	if (show && !reference_geometry.texture_handle)
+
+	if (reference_state == ReferenceState::None)
 	{
-		reference_comparison = CompareScreenToPreviousCapture(render_interface, GetImageFilenameFromCurrentTest(), &reference_geometry);
+		reference_comparison =
+			CompareScreenToPreviousCapture(render_interface, GetImageFilenameFromCurrentTest(), &reference_geometry, &reference_highlight_geometry);
 
-		if (!reference_comparison.success)
+		if (!reference_comparison.success || !reference_geometry.texture_handle || !reference_highlight_geometry.texture_handle)
+		{
 			error_msg = reference_comparison.error_msg;
+			new_reference_state = ReferenceState::None;
+		}
+	}
+
+	if (new_reference_state == ReferenceState::None)
+	{
+		ReleaseTextureGeometry(render_interface, reference_geometry);
+		ReleaseTextureGeometry(render_interface, reference_highlight_geometry);
+		reference_comparison = {};
 	}
 
-	show_reference = (show && reference_comparison.success && !reference_comparison.is_equal);
-	viewer->SetAttention(show_reference);
+	reference_state = new_reference_state;
+	viewer->SetAttention(reference_state != ReferenceState::None);
 
 	if (!error_msg.empty())
 		viewer->SetGoToText(error_msg);

+ 4 - 2
Tests/Source/VisualTests/TestNavigator.h

@@ -50,6 +50,7 @@ protected:
 
 private:
 	enum class IterationState { None, Capture, Comparison };
+	enum class ReferenceState { None, ShowReference, ShowReferenceHighlight };
 
 	TestSuite& CurrentSuite() { return test_suites[suite_index]; }
 
@@ -64,7 +65,7 @@ private:
 
 	void UpdateGoToText(bool out_of_bounds = false);
 
-	void ShowReference(bool show, bool clear);
+	void ShowReference(ReferenceState new_reference_state);
 
 	Rml::String GetImageFilenameFromCurrentTest();
 
@@ -79,9 +80,10 @@ private:
 	int goto_index = -1;
 	SourceType source_state = SourceType::None;
 
-	bool show_reference = false;
+	ReferenceState reference_state = ReferenceState::None;
 	ComparisonResult reference_comparison;
 	TextureGeometry reference_geometry;
+	TextureGeometry reference_highlight_geometry;
 
 	IterationState iteration_state = IterationState::None;