diff --git a/duckstation.sln b/duckstation.sln
index 88f02df4e..79be65e9e 100644
--- a/duckstation.sln
+++ b/duckstation.sln
@@ -45,6 +45,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "xxhash", "dep\xxhash\xxhash
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "googletest", "dep\googletest\googletest.vcxproj", "{49953E1B-2EF7-46A4-B88B-1BF9E099093B}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "common-tests", "src\common-tests\common-tests.vcxproj", "{EA2B9C7A-B8CC-42F9-879B-191A98680C10}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -377,6 +379,22 @@ Global
{49953E1B-2EF7-46A4-B88B-1BF9E099093B}.ReleaseLTCG|x64.Build.0 = ReleaseLTCG|x64
{49953E1B-2EF7-46A4-B88B-1BF9E099093B}.ReleaseLTCG|x86.ActiveCfg = ReleaseLTCG|Win32
{49953E1B-2EF7-46A4-B88B-1BF9E099093B}.ReleaseLTCG|x86.Build.0 = ReleaseLTCG|Win32
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.Debug|x64.ActiveCfg = Debug|x64
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.Debug|x64.Build.0 = Debug|x64
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.Debug|x86.ActiveCfg = Debug|Win32
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.Debug|x86.Build.0 = Debug|Win32
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.DebugFast|x64.ActiveCfg = DebugFast|x64
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.DebugFast|x64.Build.0 = DebugFast|x64
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.DebugFast|x86.ActiveCfg = DebugFast|Win32
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.DebugFast|x86.Build.0 = DebugFast|Win32
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.Release|x64.ActiveCfg = Release|x64
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.Release|x64.Build.0 = Release|x64
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.Release|x86.ActiveCfg = Release|Win32
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.Release|x86.Build.0 = Release|Win32
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.ReleaseLTCG|x64.ActiveCfg = ReleaseLTCG|x64
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.ReleaseLTCG|x64.Build.0 = ReleaseLTCG|x64
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.ReleaseLTCG|x86.ActiveCfg = ReleaseLTCG|Win32
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}.ReleaseLTCG|x86.Build.0 = ReleaseLTCG|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 33527b7eb..89181315f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,4 +1,5 @@
add_subdirectory(common)
+add_subdirectory(common-tests)
add_subdirectory(core)
add_subdirectory(frontend-common)
diff --git a/src/common-tests/CMakeLists.txt b/src/common-tests/CMakeLists.txt
new file mode 100644
index 000000000..a7c50bba5
--- /dev/null
+++ b/src/common-tests/CMakeLists.txt
@@ -0,0 +1,5 @@
+add_executable(common-tests
+ rectangle_tests.cpp
+)
+
+target_link_libraries(common-tests PRIVATE common gtest gtest_main)
diff --git a/src/common-tests/common-tests.vcxproj b/src/common-tests/common-tests.vcxproj
new file mode 100644
index 000000000..1c261669e
--- /dev/null
+++ b/src/common-tests/common-tests.vcxproj
@@ -0,0 +1,373 @@
+
+
+
+
+ DebugFast
+ Win32
+
+
+ DebugFast
+ x64
+
+
+ Debug
+ Win32
+
+
+ Debug
+ x64
+
+
+ ReleaseLTCG
+ Win32
+
+
+ ReleaseLTCG
+ x64
+
+
+ Release
+ Win32
+
+
+ Release
+ x64
+
+
+
+
+ {49953e1b-2ef7-46a4-b88b-1bf9e099093b}
+
+
+ {ee054e08-3799-4a59-a422-18259c105ffd}
+
+
+
+
+
+
+
+ {EA2B9C7A-B8CC-42F9-879B-191A98680C10}
+ Win32Proj
+ common-tests
+ 10.0
+
+
+
+ Application
+ true
+ v142
+ NotSet
+
+
+ Application
+ true
+ v142
+ NotSet
+
+
+ Application
+ true
+ v142
+ NotSet
+
+
+ Application
+ true
+ v142
+ NotSet
+
+
+ Application
+ false
+ v142
+ true
+ NotSet
+ false
+
+
+ Application
+ false
+ v142
+ true
+ NotSet
+ false
+
+
+ Application
+ false
+ v142
+ true
+ NotSet
+ false
+
+
+ Application
+ false
+ v142
+ true
+ NotSet
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ $(SolutionDir)bin\$(Platform)\
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+
+
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+ true
+ $(SolutionDir)bin\$(Platform)\
+
+
+ true
+ $(SolutionDir)bin\$(Platform)\
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+
+
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+ true
+ $(SolutionDir)bin\$(Platform)\
+
+
+ $(SolutionDir)bin\$(Platform)\
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+
+
+ false
+ $(SolutionDir)bin\$(Platform)\
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+
+
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+ $(SolutionDir)bin\$(Platform)\
+
+
+ $(SolutionDir)build\$(ProjectName)-$(Platform)-$(Configuration)\
+ $(ProjectName)-$(Platform)-$(Configuration)
+ false
+ $(SolutionDir)bin\$(Platform)\
+
+
+
+
+
+ Level4
+ Disabled
+ _CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ true
+ ProgramDatabase
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\googletest\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ true
+ false
+ stdcpp17
+ true
+
+
+ Console
+ true
+ d3d11.lib;dxgi.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+ Level4
+ Disabled
+ _CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ true
+ ProgramDatabase
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\googletest\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ true
+ false
+ stdcpp17
+ true
+
+
+ Console
+ true
+ d3d11.lib;dxgi.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+ Level4
+ Disabled
+ _ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ true
+ ProgramDatabase
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\googletest\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ Default
+ true
+ false
+ stdcpp17
+ false
+ OnlyExplicitInline
+ true
+
+
+ Console
+ true
+ d3d11.lib;dxgi.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+ Level4
+ Disabled
+ _ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ true
+ ProgramDatabase
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\googletest\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ Default
+ true
+ false
+ stdcpp17
+ false
+ OnlyExplicitInline
+ true
+
+
+ Console
+ true
+ d3d11.lib;dxgi.lib;%(AdditionalDependencies)
+
+
+
+
+ Level4
+
+
+ MaxSpeed
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\googletest\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ true
+ false
+ stdcpp17
+ true
+
+
+ Console
+ true
+ true
+ true
+ d3d11.lib;dxgi.lib;%(AdditionalDependencies)
+ Default
+
+
+
+
+ Level4
+
+
+ MaxSpeed
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\googletest\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ true
+ true
+ stdcpp17
+ true
+ true
+
+
+ Console
+ true
+ true
+ true
+ d3d11.lib;dxgi.lib;%(AdditionalDependencies)
+ UseLinkTimeCodeGeneration
+
+
+
+
+ Level4
+
+
+ MaxSpeed
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\googletest\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ true
+ false
+ stdcpp17
+ true
+
+
+ Console
+ true
+ true
+ true
+ d3d11.lib;dxgi.lib;%(AdditionalDependencies)
+ Default
+
+
+
+
+ Level4
+
+
+ MaxSpeed
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\googletest\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ true
+ true
+ stdcpp17
+ true
+ true
+
+
+ Console
+ true
+ true
+ true
+ d3d11.lib;dxgi.lib;%(AdditionalDependencies)
+ UseLinkTimeCodeGeneration
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/common-tests/common-tests.vcxproj.filters b/src/common-tests/common-tests.vcxproj.filters
new file mode 100644
index 000000000..a10f34cc5
--- /dev/null
+++ b/src/common-tests/common-tests.vcxproj.filters
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/common-tests/rectangle_tests.cpp b/src/common-tests/rectangle_tests.cpp
new file mode 100644
index 000000000..3d4990a95
--- /dev/null
+++ b/src/common-tests/rectangle_tests.cpp
@@ -0,0 +1,83 @@
+#include "common/rectangle.h"
+#include
+
+using Common::Rectangle;
+using IntRectangle = Rectangle;
+
+TEST(Rectangle, DefaultConstructorIsInvalid)
+{
+ IntRectangle r;
+ ASSERT_FALSE(r.Valid());
+}
+
+TEST(Rectangle, AdjacentRectanglesNotIntersecting)
+{
+ IntRectangle r1(0, 0, 10, 10);
+ IntRectangle r2(10, 10, 20, 20);
+ ASSERT_FALSE(r1.Intersects(r2));
+}
+
+TEST(Rectangle, IntersectingRectanglesIntersecting)
+{
+ IntRectangle r1(0, 0, 10, 10);
+ IntRectangle r2(9, 9, 4, 4);
+ ASSERT_TRUE(r1.Intersects(r2));
+ ASSERT_TRUE(r2.Intersects(r1));
+}
+
+TEST(Rectangle, PointContainedInRectangle)
+{
+ IntRectangle r1(0, 0, 10, 10);
+ ASSERT_TRUE(r1.Contains(5, 5));
+}
+
+TEST(Rectangle, PointOutsideRectangleNotContained)
+{
+ IntRectangle r1(0, 0, 10, 10);
+ ASSERT_FALSE(r1.Contains(10, 10));
+}
+
+TEST(Rectangle, RectangleSize)
+{
+ IntRectangle r(0, 0, 10, 10);
+ ASSERT_EQ(r.GetWidth(), 10);
+ ASSERT_EQ(r.GetHeight(), 10);
+}
+
+TEST(Rectangle, IncludeAfterInvalid)
+{
+ IntRectangle r;
+ IntRectangle r2(0, 0, 10, 10);
+ ASSERT_FALSE(r.Valid());
+ ASSERT_TRUE(r2.Valid());
+ r.Include(r2);
+ ASSERT_EQ(r, r2);
+}
+
+TEST(Rectangle, EmptyRectangleHasNoExtents)
+{
+ IntRectangle r(0, 0, 0, 0);
+ ASSERT_FALSE(r.HasExtents());
+}
+
+TEST(Rectangle, NonEmptyRectangleHasExtents)
+{
+ IntRectangle r(0, 0, 1, 1);
+ ASSERT_TRUE(r.HasExtents());
+}
+
+TEST(Rectangle, RelationalOperators)
+{
+ IntRectangle r1(0, 0, 1, 1);
+ IntRectangle r2(1, 1, 2, 2);
+
+ ASSERT_EQ(r1, r1);
+ ASSERT_LE(r1, r1);
+ ASSERT_LE(r1, r2);
+ ASSERT_LT(r1, r2);
+ ASSERT_EQ(r2, r2);
+ ASSERT_GE(r2, r1);
+ ASSERT_GT(r2, r1);
+ ASSERT_NE(r1, r2);
+}
+