Bläddra i källkod

Merge branch 'test_catchup'

* test_catchup: (45 commits)
  Test quad collisions.
  Test all 6 ways that triangle intersection can return false using a simplified method (one triangle is a point) to avoid complication.
  Start moving towards 100% coverage of math - line::length - lines::parallel - intersects(triangle, triangle)
  Add tests for the entity class in engine.
  Update to new OS version...
  Change test to permit double-collision. Fix test failure with self-collision.
  Add test for objects with collide-able layers, but no actual intersects.
  Add tests for engine::scene for all collision_t enum rules. TODO: Test non-overlapping objects FAILING: SceneTest.ObjectCannotCollideSelf FAILING: SceneTest.DoubleCollisionDoesNotOccur
  Set up testing skeleton for engine::scene
  Add some ugly tests for handling Key remapping. Add tests for long pauses in frame rate.
  Start testing mouse events. FAILING: MouseEventDispatchTest.DispatchMouseTranslatesFrameOfRef
  Adding tests for update() and render() going from game_dispatch to scene.
  Fixing bug where we double-delete a scene ptr.
  Add skeleton to test game_dispatch.
  Write basic tests for FPS Counter.
  Test generating text graphic strings.
  Add tests for constructing a text_engine.
  Add tests to cover all parts of deserializing an object.
  reference enum to string correct implementation.
  Eliminate empty-throw. Remove redundant exception handling in texture_or_uniform's code.
  ...
Sam Jaffe 6 år sedan
förälder
incheckning
7f53c76f83
52 ändrade filer med 2331 tillägg och 435 borttagningar
  1. 47 4
      engine/engine.xcodeproj/project.pbxproj
  2. 67 0
      engine/engine.xcodeproj/xcshareddata/xcschemes/engine-test.xcscheme
  3. 1 1
      engine/include/game/engine/game_dispatch.hpp
  4. 4 4
      engine/src/game_dispatch.cpp
  5. 2 0
      engine/src/scene.cpp
  6. 1 0
      engine/src/serial.cxx
  7. BIN
      engine/test/data/images/fonts/font.bmp
  8. 17 0
      engine/test/data/images/fonts/font.json
  9. 105 0
      engine/test/entity_test.cxx
  10. 73 0
      engine/test/fps_counter_test.cxx
  11. 134 0
      engine/test/game_dispatch_test.cxx
  12. 54 0
      engine/test/mock_renderer.h
  13. 147 0
      engine/test/scene_test.cxx
  14. 139 0
      engine/test/serial_test.cxx
  15. 103 0
      engine/test/text_engine_test.cxx
  16. 22 0
      graphics/graphics-test/Info.plist
  17. 186 10
      graphics/graphics.xcodeproj/project.pbxproj
  18. 67 0
      graphics/graphics.xcodeproj/xcshareddata/xcschemes/graphics-test.xcscheme
  19. 42 0
      graphics/include/game/graphics/exception.h
  20. 6 0
      graphics/include/game/graphics/graphics_fwd.h
  21. 49 1
      graphics/include/game/graphics/manager.hpp
  22. 2 3
      graphics/include/game/graphics/material.hpp
  23. 1 0
      graphics/include/game/graphics/renderer.hpp
  24. 1 1
      graphics/include/game/graphics/shader.hpp
  25. 1 3
      graphics/include/game/graphics/shader_program.hpp
  26. 2 12
      graphics/include/game/graphics/texture.hpp
  27. 16 11
      graphics/src/helper.hpp
  28. 62 43
      graphics/src/manager.cxx
  29. 0 2
      graphics/src/material.cpp
  30. 0 265
      graphics/src/openGL/helper.cxx
  31. 213 0
      graphics/src/openGL/opengl_manager.cxx
  32. 18 26
      graphics/src/openGL/renderer_impl.cxx
  33. 66 0
      graphics/src/openGL/opengl_renderer.h
  34. 4 1
      graphics/src/renderer.cxx
  35. 2 4
      graphics/src/shader.cpp
  36. 4 6
      graphics/src/shader_program.cpp
  37. 16 29
      graphics/src/texture.cpp
  38. 271 0
      graphics/test/manager_test.cxx
  39. 157 0
      graphics/test/renderer_test.cxx
  40. BIN
      graphics/test/resources/black.bmp
  41. BIN
      graphics/test/resources/black2.bmp
  42. 2 0
      math/include/game/math/angle.hpp
  43. 9 2
      math/math.xcodeproj/project.pbxproj
  44. 1 1
      math/math.xcodeproj/xcshareddata/xcschemes/math-test.xcscheme
  45. 2 0
      math/src/angle.cpp
  46. 3 1
      math/src/common.cpp
  47. 94 0
      math/test/angle_test.cxx
  48. 84 0
      math/test/common_test.cxx
  49. 12 0
      math/test/shape_test.cxx
  50. 5 2
      util/gameutils.xcodeproj/project.pbxproj
  51. 3 0
      util/include/game/util/identity.hpp
  52. 14 3
      util/src/osx_env.mm

+ 47 - 4
engine/engine.xcodeproj/project.pbxproj

@@ -9,17 +9,28 @@
 /* Begin PBXBuildFile section */
 		CD1C83562298B55F00825C4E /* fps_counter.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1C83542298B55F00825C4E /* fps_counter.cxx */; };
 		CD1C8419229A095600825C4E /* text_engine.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1C8417229A095600825C4E /* text_engine.cxx */; };
+		CD39A88F22F521DB00FEC384 /* libjsoncpp.1.9.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD39A88D22F521A200FEC384 /* libjsoncpp.1.9.0.dylib */; };
+		CD39A89022F5225500FEC384 /* libjsoncpp.1.9.0.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD39A88D22F521A200FEC384 /* libjsoncpp.1.9.0.dylib */; };
+		CD39A89222F6052D00FEC384 /* entity_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD39A89122F6052D00FEC384 /* entity_test.cxx */; };
 		CD62D8462251A94C0023219A /* libgraphics.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62D8452251A94C0023219A /* libgraphics.dylib */; };
 		CD62D8482251A9500023219A /* libmath.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62D8472251A9500023219A /* libmath.dylib */; };
 		CD62FCCE22904A8900376440 /* libengine.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CDB1F8AE1D7A30CD00700C6B /* libengine.dylib */; };
 		CD62FCD622904A9B00376440 /* GoogleMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FCBE22904A7C00376440 /* GoogleMock.framework */; };
 		CD62FD35229364DB00376440 /* entity.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD33229364DB00376440 /* entity.cxx */; settings = {COMPILER_FLAGS = "-fvisibility=default"; }; };
-		CD62FD3C229370E200376440 /* libjsoncpp.1.8.4.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FD3922936E9C00376440 /* libjsoncpp.1.8.4.dylib */; };
 		CD62FD402293746900376440 /* serial.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD3E2293746900376440 /* serial.cxx */; settings = {COMPILER_FLAGS = "-fvisibility=default"; }; };
 		CD7E87792295FAB400D877FE /* libgameutils.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD7E87782295FAB400D877FE /* libgameutils.dylib */; };
+		CD8064EF22D21EB500B9B4E4 /* libgraphics.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62D8452251A94C0023219A /* libgraphics.dylib */; };
+		CD8064F422D2284500B9B4E4 /* data in Resources */ = {isa = PBXBuildFile; fileRef = CD8064F322D2284500B9B4E4 /* data */; };
+		CD8064F622D228D300B9B4E4 /* text_engine_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD8064F522D228D300B9B4E4 /* text_engine_test.cxx */; };
+		CD8064F722D22BF500B9B4E4 /* libgameutils.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD7E87782295FAB400D877FE /* libgameutils.dylib */; };
+		CD8064F922D2984400B9B4E4 /* fps_counter_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD8064F822D2984400B9B4E4 /* fps_counter_test.cxx */; };
+		CD8064FB22D69CAE00B9B4E4 /* game_dispatch_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD8064FA22D69CAE00B9B4E4 /* game_dispatch_test.cxx */; };
+		CD8064FD22D9456100B9B4E4 /* scene_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD8064FC22D9456100B9B4E4 /* scene_test.cxx */; };
+		CD8064FE22DA621200B9B4E4 /* libmath.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62D8472251A9500023219A /* libmath.dylib */; };
 		CDB1F8C81D7A312B00700C6B /* game_dispatch.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CDB1F8C61D7A312B00700C6B /* game_dispatch.cpp */; settings = {COMPILER_FLAGS = "-fvisibility=default"; }; };
 		CDB1F8CC1D7A319A00700C6B /* scene.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CDB1F8CA1D7A319A00700C6B /* scene.cpp */; settings = {COMPILER_FLAGS = "-fvisibility=default"; }; };
 		CDB1F8D21D7A32A300700C6B /* events.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CDB1F8D01D7A32A300700C6B /* events.cpp */; settings = {COMPILER_FLAGS = "-fvisibility=default"; }; };
+		CDED9C5122A4114F00AE5CE5 /* serial_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDED9C5022A4114F00AE5CE5 /* serial_test.cxx */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -70,6 +81,8 @@
 /* Begin PBXFileReference section */
 		CD1C83542298B55F00825C4E /* fps_counter.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = fps_counter.cxx; sourceTree = "<group>"; };
 		CD1C8417229A095600825C4E /* text_engine.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = text_engine.cxx; sourceTree = "<group>"; };
+		CD39A88D22F521A200FEC384 /* libjsoncpp.1.9.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libjsoncpp.1.9.0.dylib; path = ../../../../../../../opt/local/lib/libjsoncpp.1.9.0.dylib; sourceTree = "<group>"; };
+		CD39A89122F6052D00FEC384 /* entity_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = entity_test.cxx; sourceTree = "<group>"; };
 		CD62D8452251A94C0023219A /* libgraphics.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; path = libgraphics.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD62D8472251A9500023219A /* libmath.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; path = libmath.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD62FCB622904A7B00376440 /* GoogleMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GoogleMock.xcodeproj; path = "../../gmock-xcode-master/GoogleMock.xcodeproj"; sourceTree = "<group>"; };
@@ -79,11 +92,18 @@
 		CD62FD3922936E9C00376440 /* libjsoncpp.1.8.4.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libjsoncpp.1.8.4.dylib; path = ../../../../../../../opt/local/lib/libjsoncpp.1.8.4.dylib; sourceTree = "<group>"; };
 		CD62FD3E2293746900376440 /* serial.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = serial.cxx; sourceTree = "<group>"; };
 		CD7E87782295FAB400D877FE /* libgameutils.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; path = libgameutils.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
+		CD8064F322D2284500B9B4E4 /* data */ = {isa = PBXFileReference; lastKnownFileType = folder; path = data; sourceTree = "<group>"; };
+		CD8064F522D228D300B9B4E4 /* text_engine_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = text_engine_test.cxx; sourceTree = "<group>"; };
+		CD8064F822D2984400B9B4E4 /* fps_counter_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = fps_counter_test.cxx; sourceTree = "<group>"; };
+		CD8064FA22D69CAE00B9B4E4 /* game_dispatch_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = game_dispatch_test.cxx; sourceTree = "<group>"; };
+		CD8064FC22D9456100B9B4E4 /* scene_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = scene_test.cxx; sourceTree = "<group>"; };
 		CDA34D8422515C99008036A7 /* game */ = {isa = PBXFileReference; lastKnownFileType = folder; name = game; path = include/game; sourceTree = "<group>"; };
 		CDB1F8AE1D7A30CD00700C6B /* libengine.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libengine.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 		CDB1F8C61D7A312B00700C6B /* game_dispatch.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = game_dispatch.cpp; sourceTree = "<group>"; };
 		CDB1F8CA1D7A319A00700C6B /* scene.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = scene.cpp; sourceTree = "<group>"; };
 		CDB1F8D01D7A32A300700C6B /* events.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = events.cpp; sourceTree = "<group>"; };
+		CDED9C4C22A4112200AE5CE5 /* mock_renderer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = mock_renderer.h; sourceTree = "<group>"; };
+		CDED9C5022A4114F00AE5CE5 /* serial_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = serial_test.cxx; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -91,6 +111,10 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD39A88F22F521DB00FEC384 /* libjsoncpp.1.9.0.dylib in Frameworks */,
+				CD8064FE22DA621200B9B4E4 /* libmath.dylib in Frameworks */,
+				CD8064F722D22BF500B9B4E4 /* libgameutils.dylib in Frameworks */,
+				CD8064EF22D21EB500B9B4E4 /* libgraphics.dylib in Frameworks */,
 				CD62FCD622904A9B00376440 /* GoogleMock.framework in Frameworks */,
 				CD62FCCE22904A8900376440 /* libengine.dylib in Frameworks */,
 			);
@@ -100,8 +124,8 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD39A89022F5225500FEC384 /* libjsoncpp.1.9.0.dylib in Frameworks */,
 				CD7E87792295FAB400D877FE /* libgameutils.dylib in Frameworks */,
-				CD62FD3C229370E200376440 /* libjsoncpp.1.8.4.dylib in Frameworks */,
 				CD62D8482251A9500023219A /* libmath.dylib in Frameworks */,
 				CD62D8462251A94C0023219A /* libgraphics.dylib in Frameworks */,
 			);
@@ -113,6 +137,7 @@
 		CD62D8442251A94C0023219A /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				CD39A88D22F521A200FEC384 /* libjsoncpp.1.9.0.dylib */,
 				CD7E87782295FAB400D877FE /* libgameutils.dylib */,
 				CD62FD3922936E9C00376440 /* libjsoncpp.1.8.4.dylib */,
 				CD62D8452251A94C0023219A /* libgraphics.dylib */,
@@ -143,6 +168,14 @@
 		CDA34D8522515CA9008036A7 /* test */ = {
 			isa = PBXGroup;
 			children = (
+				CD8064F322D2284500B9B4E4 /* data */,
+				CDED9C4C22A4112200AE5CE5 /* mock_renderer.h */,
+				CDED9C5022A4114F00AE5CE5 /* serial_test.cxx */,
+				CD8064FC22D9456100B9B4E4 /* scene_test.cxx */,
+				CD8064F522D228D300B9B4E4 /* text_engine_test.cxx */,
+				CD8064FA22D69CAE00B9B4E4 /* game_dispatch_test.cxx */,
+				CD39A89122F6052D00FEC384 /* entity_test.cxx */,
+				CD8064F822D2984400B9B4E4 /* fps_counter_test.cxx */,
 			);
 			path = test;
 			sourceTree = "<group>";
@@ -240,7 +273,7 @@
 			isa = PBXProject;
 			attributes = {
 				LastSwiftUpdateCheck = 1010;
-				LastUpgradeCheck = 1010;
+				LastUpgradeCheck = 1030;
 				ORGANIZATIONNAME = "Sam Jaffe";
 				TargetAttributes = {
 					CD62FCC822904A8900376440 = {
@@ -254,10 +287,11 @@
 			};
 			buildConfigurationList = CDB1F8A91D7A30CD00700C6B /* Build configuration list for PBXProject "engine" */;
 			compatibilityVersion = "Xcode 3.2";
-			developmentRegion = English;
+			developmentRegion = en;
 			hasScannedForEncodings = 0;
 			knownRegions = (
 				en,
+				Base,
 			);
 			mainGroup = CDB1F8A51D7A30CD00700C6B;
 			productRefGroup = CDB1F8AF1D7A30CD00700C6B /* Products */;
@@ -312,6 +346,7 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD8064F422D2284500B9B4E4 /* data in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -342,6 +377,12 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD8064F922D2984400B9B4E4 /* fps_counter_test.cxx in Sources */,
+				CD8064FD22D9456100B9B4E4 /* scene_test.cxx in Sources */,
+				CD8064F622D228D300B9B4E4 /* text_engine_test.cxx in Sources */,
+				CDED9C5122A4114F00AE5CE5 /* serial_test.cxx in Sources */,
+				CD39A89222F6052D00FEC384 /* entity_test.cxx in Sources */,
+				CD8064FB22D69CAE00B9B4E4 /* game_dispatch_test.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -436,6 +477,7 @@
 		CDB1F8B71D7A30CD00700C6B /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;
@@ -490,6 +532,7 @@
 		CDB1F8B81D7A30CD00700C6B /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;

+ 67 - 0
engine/engine.xcodeproj/xcshareddata/xcschemes/engine-test.xcscheme

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1030"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      codeCoverageEnabled = "YES"
+      onlyGenerateCoverageForSpecifiedTargets = "YES"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <CodeCoverageTargets>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "CDB1F8AD1D7A30CD00700C6B"
+            BuildableName = "libengine.dylib"
+            BlueprintName = "engine"
+            ReferencedContainer = "container:engine.xcodeproj">
+         </BuildableReference>
+      </CodeCoverageTargets>
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "CD62FCC822904A8900376440"
+               BuildableName = "engine-test.xctest"
+               BlueprintName = "engine-test"
+               ReferencedContainer = "container:engine.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 1 - 1
engine/include/game/engine/game_dispatch.hpp

@@ -59,7 +59,7 @@ namespace engine {
     env::clock::timestamp current_timestamp;
     struct current_scene_info {
       current_scene_info();
-      current_scene_info(scene *);
+      current_scene_info(scene_t);
 
       scene_t ptr;
 

+ 4 - 4
engine/src/game_dispatch.cpp

@@ -40,7 +40,7 @@ namespace engine {
 
   void game_dispatch::activate_scene(scene_id_t const & id) {
     // TODO: Cleanup
-    curr_scene = current_scene_info(scenes[id].get());
+    curr_scene = current_scene_info(scenes[id]);
   }
 
   void game_dispatch::set_current_timestamp() {
@@ -49,7 +49,7 @@ namespace engine {
 
   game_dispatch::current_scene_info::current_scene_info() {}
 
-  game_dispatch::current_scene_info::current_scene_info(scene * curr)
+  game_dispatch::current_scene_info::current_scene_info(scene_t curr)
       : ptr(curr), current_scene_id(ptr->id), local_size(ptr->size()) {}
 
   graphics::manager const & game_dispatch::graphics_manager() const {
@@ -57,7 +57,7 @@ namespace engine {
   }
 
   void game_dispatch::process_key_event(raw_key_t key, bool press) {
-    if (!curr_scene.is_keyboard_event) return;
+    //    if (!curr_scene.is_keyboard_event) return;
     auto const & binding = curr_scene.ptr->keys();
     auto it = binding.find(key);
     if (it == binding.end()) return;
@@ -67,7 +67,7 @@ namespace engine {
   }
 
   void game_dispatch::process_mouse_event(math::vec2 mouse_pos, bool press) {
-    if (!curr_scene.is_mouse_event) return;
+    //    if (!curr_scene.is_mouse_event) return;
     math::vec2 local_scene_position =
         mouse_pos * curr_scene.local_size / screen_size;
 

+ 2 - 0
engine/src/scene.cpp

@@ -24,6 +24,7 @@ void scene::check_collisions(std::vector<collidable *> const & from,
                              std::vector<collidable *> const & to) {
   for (auto & ent : from) {
     for (auto & hit : to) {
+      if (ent == hit) continue;
       if (math::intersects(ent->render_info().points,
                            hit->render_info().points)) {
         ent->collide(*hit);
@@ -56,6 +57,7 @@ graphics::manager const & scene::graphics_manager() const {
 }
 
 void scene::handle_key_event(event::key_event evt) {
+  // TODO (sjaffe): Handle quit binding correctly... (don't test this)
   if (evt.type & event::PRESSED_MASK && evt.key == keys::QUIT) {
     dispatch_.lock()->quit();
   }

+ 1 - 0
engine/src/serial.cxx

@@ -39,6 +39,7 @@ namespace engine {
 
   identity<graphics::shader_program> to_program(Json::Value const & json,
                                                 graphics::manager const & mgr) {
+    // TODO (sjaffe): This should be an error, not defaulting to OpenGL...
     if (json.empty()) {
       return mgr.get("data/shaders/BlankShader.fragment.glsl",
                      "data/shaders/BlankShader.vertex.glsl");

BIN
engine/test/data/images/fonts/font.bmp


+ 17 - 0
engine/test/data/images/fonts/font.json

@@ -0,0 +1,17 @@
+{
+  "type":"monospace",
+  "cells":[8, 8],
+  "glyph_size":[10, 20],
+  "file":"font.bmp",
+  "default":"?",
+  "glyphs":[
+    [" ", "!", "\"", "#", "$", "%", "&", "'"],
+    ["(", ")", "*", "+", ",", "-", ".", "\\"],
+    ["0", "1", "2", "3", "4", "5", "6", "7"],
+    ["8", "9", ":", ";", "<", "=", ">", "?"],
+    ["@", "A", "B", "C", "D", "E", "F", "G"],
+    ["H", "I", "J", "K", "L", "M", "N", "O"],
+    ["P", "Q", "R", "S", "T", "U", "V", "W"],
+    ["X", "Y", "Z", "[", "/", "]", "^", "_"]
+  ]
+}

+ 105 - 0
engine/test/entity_test.cxx

@@ -0,0 +1,105 @@
+//
+//  entity_test.cxx
+//  engine-test
+//
+//  Created by Sam Jaffe on 8/3/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <sstream>
+
+#include <gmock/gmock.h>
+#include <json/json.h>
+
+#include "game/engine/entity.hpp"
+
+#include "mock_renderer.h"
+
+using namespace engine;
+
+using testing::Eq;
+using testing::Ne;
+
+namespace math { namespace dim2 {
+  bool operator==(rectangle const & lhs, rectangle const & rhs) {
+    return lhs.origin == rhs.origin && lhs.size == rhs.size;
+  }
+  bool operator!=(rectangle const & lhs, rectangle const & rhs) {
+    return !(lhs == rhs);
+  }
+  bool operator==(quad const & lhs, quad const & rhs) {
+    return lhs.ll == rhs.ll && lhs.lr == rhs.lr && lhs.ul == rhs.ul
+        && lhs.ur == rhs.ur;
+  }
+  bool operator!=(quad const & lhs, quad const & rhs) {
+    return !(lhs == rhs);
+  }
+}}
+
+inline Json::Value to_json(std::string const & str) {
+  Json::Value json;
+  std::stringstream(str) >> json;
+  return json;
+}
+
+TEST(CollidableTest, ConstructsUsingGraphics) {
+  math::dim2::rectangle bounds{make_vector(0.f, 0.f), make_vector(1.f, 1.f)};
+  graphics::object obj{bounds, bounds, cast<graphics::material>(1), bounds};
+  
+  collidable collide{obj};
+  EXPECT_THAT(collide.render_info().location, Eq(obj.location));
+}
+
+std::string const data = R"(
+{
+  "velocity": [ 1.0, 2.0 ],
+  "size": 1.0
+}
+)";
+
+TEST(EntityTest, ConstructsFromJson) {
+  math::dim2::rectangle bounds{make_vector(0.f, 0.f), make_vector(1.f, 1.f)};
+  graphics::object obj{bounds, bounds, cast<graphics::material>(1), bounds};
+  
+  entity ent{to_json(data), obj};
+  EXPECT_THAT(ent.render_info().location, Eq(obj.location));
+}
+
+
+TEST(EntityTest, SizeParamAltersLocation) {
+  math::dim2::rectangle bounds{make_vector(0.f, 0.f), make_vector(1.f, 1.f)};
+  graphics::object obj{bounds, bounds, cast<graphics::material>(1), bounds};
+  
+  Json::Value json = to_json(data);
+  json["size"] = 2.f;
+  entity ent{json, obj};
+  math::dim2::rectangle expected{make_vector(0.f, 0.f), make_vector(2.f, 2.f)};
+  EXPECT_THAT(ent.render_info().location, Ne(obj.location));
+  EXPECT_THAT(ent.render_info().location, Eq(expected));
+}
+
+TEST(EntityTest, MoveWillAdjustPointsAndBounds) {
+  math::dim2::rectangle bounds{make_vector(0.f, 0.f), make_vector(1.f, 1.f)};
+  graphics::object obj{bounds, bounds, cast<graphics::material>(1), bounds};
+  
+  entity ent{to_json(data), obj};
+  ent.update(1.f);
+  
+  math::dim2::rectangle expected{make_vector(1.f, 2.f), make_vector(1.f, 1.f)};
+  EXPECT_THAT(ent.render_info().location, Eq(expected));
+  EXPECT_THAT(ent.render_info().points, Eq(math::dim2::quad(expected)));
+}
+
+TEST(EntityTest, MoveIsAFunctionOfVelocity) {
+  math::dim2::rectangle bounds{make_vector(0.f, 0.f), make_vector(1.f, 1.f)};
+  graphics::object obj{bounds, bounds, cast<graphics::material>(1), bounds};
+  
+  entity ent{to_json(data), obj};
+  ent.update(0.5f);
+  
+  math::dim2::rectangle expected{make_vector(0.5f, 1.f), make_vector(1.f, 1.f)};
+  EXPECT_THAT(ent.render_info().location, Eq(expected));
+  EXPECT_THAT(ent.render_info().points, Eq(math::dim2::quad(expected)));
+}
+
+// TODO: Test Acceleration and Angular-Velocity

+ 73 - 0
engine/test/fps_counter_test.cxx

@@ -0,0 +1,73 @@
+//
+//  fps_counter_test.cxx
+//  engine-test
+//
+//  Created by Sam Jaffe on 7/7/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <gmock/gmock.h>
+
+#include "game/engine/fps_counter.hpp"
+#include "game/engine/text_engine.hpp"
+
+#include "mock_renderer.h"
+
+#ifdef __APPLE__
+namespace env { namespace detail {
+  extern void bundle(std::string const &);
+}}
+#endif
+
+using testing::IsEmpty;
+using testing::SizeIs;
+
+struct FpsCounterTest : testing::Test {
+  void SetUp() override;
+  void TearDown() override;
+  std::shared_ptr<graphics::manager> manager;
+  std::shared_ptr<engine::text_engine> engine;
+};
+
+void FpsCounterTest::SetUp() {
+#ifdef __APPLE__
+  env::detail::bundle("leumasjaffe.engine-test");
+#endif
+  manager.reset(new stub_manager_impl);
+  engine.reset(new engine::text_engine("font", manager));
+}
+
+void FpsCounterTest::TearDown() {
+  engine.reset();
+  manager.reset();
+}
+
+TEST_F(FpsCounterTest, CanConstructCtrNoExcept) {
+  EXPECT_NO_THROW(engine::fps_counter(engine, 5));
+}
+
+TEST_F(FpsCounterTest, CanFrameNoExcept) {
+  engine::fps_counter counter(engine, 5);
+  env::clock::duration ms_100{100000000LL};
+  EXPECT_NO_THROW(counter.set_frame_step(ms_100));
+}
+
+// TODO (sjaffe): Give ability to specifiy change_after_ in ctor?
+TEST_F(FpsCounterTest, NoGlyphsUntilCrossesChangeThresh) {
+  engine::fps_counter counter(engine, 5);
+  env::clock::duration ms_100{100000000LL};
+  EXPECT_THAT(counter.glyphs(), IsEmpty());
+  counter.set_frame_step(ms_100);
+  EXPECT_THAT(counter.glyphs(), IsEmpty());
+}
+
+// TODO (sjaffe): Test different precisions
+// TODO (sjaffe): Test <10 fps and >=100 fps
+TEST_F(FpsCounterTest, ProducesEnoughDigitsForPrecision) {
+  engine::fps_counter counter(engine, 5);
+  env::clock::duration ms_100{100000000LL};
+  for (int i = 0; i < 10; ++i) {
+    counter.set_frame_step(ms_100);
+  }
+  EXPECT_THAT(counter.glyphs(), SizeIs(8));
+}

+ 134 - 0
engine/test/game_dispatch_test.cxx

@@ -0,0 +1,134 @@
+//
+//  game_dispatch_test.cxx
+//  engine-test
+//
+//  Created by Sam Jaffe on 7/10/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <memory>
+
+#include <gmock/gmock.h>
+
+#include "game/engine/events.hpp"
+#include "game/engine/game_dispatch.hpp"
+#include "game/engine/scene.hpp"
+#include "game/graphics/renderer.hpp"
+#include "game/util/env.hpp"
+
+#include "mock_renderer.h"
+
+struct mock_scene : engine::scene {
+  mock_scene(std::shared_ptr<engine::game_dispatch> game)
+      : engine::scene("mock", game) {}
+  MOCK_METHOD1(update, void(float));
+  MOCK_METHOD1(render, void(graphics::renderer &));
+  MOCK_METHOD1(handle_key_event, void(engine::event::key_event));
+  MOCK_METHOD1(handle_mouse_event, void(engine::event::mouse_event));
+};
+
+class GameDispatchTest : public testing::Test {
+protected:
+  void SetUp() override;
+  void TearDown() override;
+
+  mock_scene & scene() const { return *scene_; }
+  engine::game_dispatch & game() const { return *dispatch_; }
+
+  std::shared_ptr<stub_renderer> renderer_;
+  std::shared_ptr<engine::game_dispatch> dispatch_;
+  std::shared_ptr<mock_scene> scene_;
+};
+
+void GameDispatchTest::SetUp() {
+  env::resize_screen({{100, 100}});
+  renderer_.reset(new stub_renderer);
+  dispatch_.reset(new engine::game_dispatch(renderer_));
+  scene_.reset(new mock_scene(dispatch_));
+  game().register_scene(scene_);
+  game().activate_scene("mock");
+}
+
+void GameDispatchTest::TearDown() {
+  scene_.reset();
+  dispatch_.reset();
+  renderer_.reset();
+}
+
+using testing::AnyNumber;
+using testing::Eq;
+using testing::Field;
+using testing::FloatNear;
+using testing::Ge;
+using testing::Lt;
+using testing::_;
+
+TEST_F(GameDispatchTest, ManagerIsFetchedFromRenderer) {
+  EXPECT_THAT(&game().graphics_manager(), Eq(renderer_->manager().get()));
+}
+
+// TODO: Set FPS to a much smaller value for testing purposes (e.g. 512fps)
+TEST_F(GameDispatchTest, UpdateDispatchesToCurrentScene) {
+  EXPECT_CALL(scene(), update(_)).Times(1);
+  game().update();
+}
+
+TEST_F(GameDispatchTest, UpdateIsCappedInFrequencyByFPSParam) {
+  auto fps60 = 1.0 / 60.0;
+  EXPECT_CALL(scene(), update(Ge(fps60))).Times(AnyNumber());
+  EXPECT_CALL(scene(), update(Lt(fps60))).Times(0);
+  for (int i = 0; i < 10; i++) {
+    game().update();
+  }
+}
+
+TEST_F(GameDispatchTest, UpdateHasNoUpperBoundOnTime) {
+  EXPECT_CALL(scene(), update(Ge(0.03))).Times(1);
+  usleep(300000);
+  game().update();
+}
+
+TEST_F(GameDispatchTest, SetCurrentTimestampOverridesWaiting) {
+  auto fps60 = 1.0 / 60.0;
+  EXPECT_CALL(scene(), update(Ge(0.03))).Times(0);
+  EXPECT_CALL(scene(), update(FloatNear(fps60, 1E-2))).Times(1);
+  usleep(300000);
+  game().set_current_timestamp();
+  game().update();
+}
+
+TEST_F(GameDispatchTest, RenderDispatchesToCurrentScene) {
+  EXPECT_CALL(scene(), render(_)).Times(1);
+  game().render();
+}
+
+using MouseEventTest = GameDispatchTest;
+
+TEST_F(MouseEventTest, DispatchesToCurrentScene) {
+  EXPECT_CALL(scene(), handle_mouse_event(_)).Times(1);
+  game().process_mouse_event({{0.f, 0.f}}, true);
+}
+
+TEST_F(MouseEventTest, TranslatesFrameOfRef) {
+  // TODO: Actually set the local scene size element...
+  auto mouse_pos = &engine::event::mouse_event::current_mouse_position;
+  auto const where = make_vector(5.f, 5.f);
+  EXPECT_CALL(scene(), handle_mouse_event(Field(mouse_pos, where))).Times(1);
+  game().process_mouse_event({{50.f, 50.f}}, true);
+}
+
+using KeyEventTest = GameDispatchTest;
+
+TEST_F(KeyEventTest, DoesNotDispatchUnmappedKeys) {
+  EXPECT_CALL(scene(), handle_key_event(_)).Times(0);
+  game().process_key_event('A', true);
+}
+
+TEST_F(KeyEventTest, RemapsKeyWhenForwarding) {
+  // TODO: Properly set this instead of using this garbage
+  const_cast<engine::key_binding &>(scene().keys()).emplace('A', 'B');
+  auto key_value = &engine::event::key_event::key;
+  EXPECT_CALL(scene(), handle_key_event(Field(key_value, 'A'))).Times(0);
+  EXPECT_CALL(scene(), handle_key_event(Field(key_value, 'B'))).Times(1);
+  game().process_key_event('A', true);
+}

+ 54 - 0
engine/test/mock_renderer.h

@@ -0,0 +1,54 @@
+//
+//  mock_renderer.h
+//  engine
+//
+//  Created by Sam Jaffe on 6/2/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "game/graphics/manager.hpp"
+#include "game/graphics/renderer.hpp"
+#include "game/graphics/shader.hpp"
+#include "game/graphics/shader_program.hpp"
+#include "game/graphics/texture.hpp"
+#include "game/util/identity.hpp"
+
+template <typename T> inline identity<T> cast(unsigned int id) {
+  return *reinterpret_cast<identity<T> *>(&id);
+}
+
+struct stub_manager_impl : graphics::manager {
+  using shader = graphics::shader;
+  using shader_program = graphics::shader_program;
+  using texture = graphics::texture;
+
+  shader compile(graphics::shaders::type t,
+                 std::string const & s) const override {
+    return {0, t, s};
+  }
+  shader_program compile(identity<shader> f,
+                         identity<shader> v) const override {
+    return {0, f, v};
+  }
+  texture compile(graphics::textures::format, math::vec2i dims,
+                  void const *) const override {
+    return {0, dims};
+  }
+};
+
+class stub_renderer : public graphics::renderer {
+public:
+  std::shared_ptr<graphics::manager const> manager() const override {
+    return manager_;
+  }
+  void draw(graphics::object const &) override {}
+  void draw(identity<graphics::material>, math::matr4 const &,
+            std::vector<graphics::vertex> const &) override{};
+  void clear() override {}
+  void flush() override {}
+
+private:
+  std::shared_ptr<graphics::manager const> manager_{new stub_manager_impl};
+};

+ 147 - 0
engine/test/scene_test.cxx

@@ -0,0 +1,147 @@
+//
+//  scene_test.cxx
+//  engine-test
+//
+//  Created by Sam Jaffe on 7/12/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <gmock/gmock.h>
+
+#include "game/engine/entity.hpp"
+#include "game/engine/game_dispatch.hpp"
+#include "game/engine/scene.hpp"
+
+#include "mock_renderer.h"
+
+struct test_scene : engine::scene {
+  using engine::scene::scene;
+
+  void update(float) override { check_collisions(); }
+  void render(graphics::renderer &) override {}
+  void handle_key_event(engine::event::key_event) override {}
+  void handle_mouse_event(engine::event::mouse_event) override {}
+
+  void add_with(engine::collision_t t, engine::collidable & c) {
+    colliders[t].push_back(&c);
+  }
+  void add_as(engine::collision_t t, engine::collidable & c) {
+    collidables[t].push_back(&c);
+  }
+};
+
+struct mock_collidable : engine::collidable {
+  mock_collidable(math::dim2::rectangle bounds)
+      : engine::collidable({bounds, bounds, cast<graphics::material>(1), {}}) {}
+  MOCK_METHOD1(collide, void(engine::collidable const &));
+};
+
+class SceneTest : public testing::Test {
+protected:
+  void SetUp() override;
+  void TearDown() override;
+
+  test_scene & scene() const { return *scene_; }
+  engine::game_dispatch & game() const { return *dispatch_; }
+
+  std::shared_ptr<stub_renderer> renderer_;
+  std::shared_ptr<engine::game_dispatch> dispatch_;
+  std::shared_ptr<test_scene> scene_;
+};
+
+void SceneTest::SetUp() {
+  renderer_.reset(new stub_renderer);
+  dispatch_.reset(new engine::game_dispatch(renderer_));
+  scene_.reset(new test_scene("test", dispatch_));
+}
+
+void SceneTest::TearDown() {
+  scene_.reset();
+  dispatch_.reset();
+  renderer_.reset();
+}
+
+using testing::Ref;
+
+TEST_F(SceneTest, WillCollideOverlappingObjects) {
+  math::dim2::rectangle bnds{{{0.f, 0.f}}, {{1.f, 1.f}}};
+  mock_collidable one{bnds}, two{bnds};
+  scene().add_with(1, one);
+  scene().add_as(1, two);
+  EXPECT_CALL(one, collide(Ref(two))).Times(1);
+  scene().update(0.f);
+}
+
+TEST_F(SceneTest, NonOverlappingDontCollide) {
+  mock_collidable one{{{{0.f, 0.f}}, {{1.f, 1.f}}}};
+  mock_collidable two{{{{2.f, 2.f}}, {{1.f, 1.f}}}};
+  scene().add_with(1, one);
+  scene().add_as(1, two);
+  EXPECT_CALL(one, collide(Ref(two))).Times(0);
+  scene().update(0.f);
+}
+
+TEST_F(SceneTest, CollisionIsOneWay) {
+  math::dim2::rectangle bnds{{{0.f, 0.f}}, {{1.f, 1.f}}};
+  mock_collidable one{bnds}, two{bnds};
+  scene().add_with(1, one);
+  scene().add_as(1, two);
+  EXPECT_CALL(one, collide(Ref(two))).Times(1);
+  EXPECT_CALL(two, collide(Ref(one))).Times(0);
+  scene().update(0.f);
+}
+
+TEST_F(SceneTest, ObjectCannotCollideSelf) {
+  math::dim2::rectangle bnds{{{0.f, 0.f}}, {{1.f, 1.f}}};
+  mock_collidable one{bnds};
+  scene().add_with(1, one);
+  scene().add_as(1, one);
+  EXPECT_CALL(one, collide(Ref(one))).Times(0);
+  scene().update(0.f);
+}
+
+TEST_F(SceneTest, CollisionMatchesAsWithKeys) {
+  math::dim2::rectangle bnds{{{0.f, 0.f}}, {{1.f, 1.f}}};
+  mock_collidable one{bnds}, two{bnds}, three{bnds};
+  scene().add_with(2, one);
+  scene().add_as(1, two);
+  scene().add_as(2, three);
+  EXPECT_CALL(one, collide(Ref(two))).Times(0);
+  EXPECT_CALL(one, collide(Ref(three))).Times(1);
+  scene().update(0.f);
+}
+
+TEST_F(SceneTest, ObjectCanBeInMultipleCollidesWithGroups) {
+  math::dim2::rectangle bnds{{{0.f, 0.f}}, {{1.f, 1.f}}};
+  mock_collidable one{bnds}, two{bnds}, three{bnds};
+  scene().add_with(1, one);
+  scene().add_with(2, one);
+  scene().add_as(1, two);
+  scene().add_as(2, three);
+  EXPECT_CALL(one, collide(Ref(two))).Times(1);
+  EXPECT_CALL(one, collide(Ref(three))).Times(1);
+  scene().update(0.f);
+}
+
+TEST_F(SceneTest, ObjectCanBeInMultipleCollidableAsGroups) {
+  math::dim2::rectangle bnds{{{0.f, 0.f}}, {{1.f, 1.f}}};
+  mock_collidable one{bnds}, two{bnds}, three{bnds};
+  scene().add_with(1, one);
+  scene().add_with(2, three);
+  scene().add_as(1, two);
+  scene().add_as(2, two);
+  EXPECT_CALL(one, collide(Ref(two))).Times(1);
+  EXPECT_CALL(three, collide(Ref(two))).Times(1);
+  scene().update(0.f);
+}
+
+TEST_F(SceneTest, DoubleCollisionCanOccur) {
+  math::dim2::rectangle bnds{{{0.f, 0.f}}, {{1.f, 1.f}}};
+  mock_collidable one{bnds}, two{bnds};
+  scene().add_with(1, one);
+  scene().add_with(2, one);
+  scene().add_as(1, two);
+  scene().add_as(2, two);
+  EXPECT_CALL(one, collide(Ref(two))).Times(1);
+  scene().update(0.f);
+}

+ 139 - 0
engine/test/serial_test.cxx

@@ -0,0 +1,139 @@
+//
+//  serial_test.cxx
+//  engine-test
+//
+//  Created by Sam Jaffe on 6/2/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <sstream>
+
+#include "mock_renderer.h"
+#include <gmock/gmock.h>
+
+#include <json/json.h>
+#include <json/reader.h>
+
+#include "game/engine/serial.hpp"
+#include "game/graphics/object.hpp"
+
+Json::Value to_json(std::string const & str) {
+  Json::Value json;
+  std::stringstream(str) >> json;
+  return json;
+}
+
+struct SerialTest : testing::Test {};
+
+using testing::Eq;
+
+TEST_F(SerialTest, ReadsIntegerVector) {
+  math::vec2i const expected{{1, 5}};
+  std::string const data = "[1,5]";
+  EXPECT_THAT(engine::to_vec2i(to_json(data)), Eq(expected));
+}
+
+TEST_F(SerialTest, ReadsIntegerVectorFromFloats) {
+  math::vec2i const expected{{1, 5}};
+  std::string const data = "[1.5,5.01]";
+  EXPECT_THAT(engine::to_vec2i(to_json(data)), Eq(expected));
+}
+
+TEST_F(SerialTest, ReadsFloatVector) {
+  math::vec2 const expected{{1.5, 5.01}};
+  std::string const data = "[1.5,5.01]";
+  EXPECT_THAT(engine::to_vec2(to_json(data)), Eq(expected));
+}
+
+TEST_F(SerialTest, ReadsFloatVectorFromInts) {
+  math::vec2 const expected{{1, 5}};
+  std::string const data = "[1,5]";
+  EXPECT_THAT(engine::to_vec2(to_json(data)), Eq(expected));
+}
+
+TEST_F(SerialTest, MissingFloatDataIsZero) {
+  math::vec2 const expected{{1, 0}};
+  std::string const data = "[1]";
+  EXPECT_THAT(engine::to_vec2(to_json(data)), Eq(expected));
+}
+
+TEST_F(SerialTest, FallbackReaderDoesntInvokeIfArray) {
+  math::vec2 const expected{{0, 0}};
+  std::string const data = "[]";
+  EXPECT_THAT(engine::to_vec2(to_json(data), {{1, 5}}), Eq(expected));
+}
+
+TEST_F(SerialTest, FallbackReaderActivatesOnNull) {
+  math::vec2 const expected{{1, 5}};
+  std::string const data = "null";
+  EXPECT_THAT(engine::to_vec2(to_json(data), {{1, 5}}), Eq(expected));
+}
+
+std::string const object_data = R"(
+{
+  "material": {
+    "shaderProgram": {
+      "fragmentShader": "serial/fragment.test",
+      "vertexShader": "serial/vertex.test"
+    },
+    "texture": {
+      "uniform": "u_normalMap"
+    }
+  },
+  "position": [0, 0],
+  "scale": 1.0
+}
+)";
+
+struct ObjectSerialTest : testing::Test {
+  stub_manager_impl mgr;
+};
+
+TEST_F(ObjectSerialTest, CanConstructFromJson) {
+  EXPECT_NO_THROW(engine::to_object(to_json(object_data), mgr));
+}
+
+TEST_F(ObjectSerialTest, DoesNotRequireShaderProgramDefinition) {
+  Json::Value json = to_json(object_data);
+  json["material"].removeMember("shaderProgram");
+  EXPECT_NO_THROW(engine::to_object(json, mgr));
+}
+
+TEST_F(ObjectSerialTest, UsesPositionFromJsonForLocation) {
+  Json::Value json = to_json(object_data);
+  {
+    graphics::object const obj = engine::to_object(json, mgr);
+    EXPECT_THAT(obj.location.origin, Eq(make_vector(0.0f, 0.0f)));
+  }
+  {
+    json["position"][0] = 5.f;
+    graphics::object const obj = engine::to_object(json, mgr);
+    EXPECT_THAT(obj.location.origin, Eq(make_vector(5.0f, 0.0f)));
+  }
+}
+
+TEST_F(ObjectSerialTest, SizeIsTextureSizeTimesScale) {
+  Json::Value json = to_json(object_data);
+  {
+    graphics::object const obj = engine::to_object(json, mgr);
+    EXPECT_THAT(obj.location.size, Eq(make_vector(1.0f, 1.0f)));
+  }
+  {
+    json["scale"] = 0.5f;
+    graphics::object const obj = engine::to_object(json, mgr);
+    EXPECT_THAT(obj.location.size, Eq(make_vector(0.5f, 0.5f)));
+  }
+}
+
+TEST_F(ObjectSerialTest, DefaultFrameIsEntireTex) {
+  graphics::object const obj = engine::to_object(to_json(object_data), mgr);
+  EXPECT_THAT(obj.frame.size, Eq(make_vector(1.f, 1.f)));
+}
+
+TEST_F(ObjectSerialTest, OverridesFrameSize) {
+  Json::Value json = to_json(object_data);
+  json["frameSize"][0] = 0.125f;
+  json["frameSize"][1] = 0.125f;
+  graphics::object const obj = engine::to_object(json, mgr);
+  EXPECT_THAT(obj.frame.size, Eq(make_vector(0.125f, 0.125f)));
+}

+ 103 - 0
engine/test/text_engine_test.cxx

@@ -0,0 +1,103 @@
+//
+//  text_engine_test.cxx
+//  engine-test
+//
+//  Created by Sam Jaffe on 7/7/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <gmock/gmock.h>
+
+#include "game/engine/text_engine.hpp"
+
+#include "mock_renderer.h"
+
+#ifdef __APPLE__
+namespace env { namespace detail {
+  extern void bundle(std::string const &);
+}}
+#endif
+
+using testing::Eq;
+using testing::Le;
+using testing::SizeIs;
+
+struct TextEngineTest : testing::Test {
+  void SetUp() override;
+  void TearDown() override;
+  std::shared_ptr<graphics::manager> manager;
+};
+
+void TextEngineTest::SetUp() {
+#ifdef __APPLE__
+  env::detail::bundle("leumasjaffe.engine-test");
+#endif
+  manager.reset(new stub_manager_impl);
+}
+
+void TextEngineTest::TearDown() { manager.reset(); }
+
+TEST_F(TextEngineTest, CanBuildFromFont) {
+  EXPECT_NO_THROW(engine::text_engine("font", manager));
+}
+
+TEST_F(TextEngineTest, ThrowsOnInvalidFile) {
+  EXPECT_ANY_THROW(engine::text_engine("missing", manager));
+}
+
+struct CreateTextTest : TextEngineTest {
+  void SetUp() override;
+  void TearDown() override;
+
+  std::vector<graphics::object> out;
+  std::unique_ptr<engine::text_engine> engine;
+  engine::text_engine::cell const data{make_vector(0.f, 0.f),
+                                       make_vector(10.f, 20.f), "HELLO"};
+};
+
+void CreateTextTest::SetUp() {
+  TextEngineTest::SetUp();
+  engine.reset(new engine::text_engine("font", manager));
+}
+
+void CreateTextTest::TearDown() {
+  engine.reset();
+  TextEngineTest::TearDown();
+}
+
+TEST_F(CreateTextTest, TextGenIsNoThrow) {
+  EXPECT_NO_THROW(engine->create_text_cells(out, data));
+}
+
+TEST_F(CreateTextTest, GeneratesOneObjectPerChar) {
+  engine->create_text_cells(out, data);
+  EXPECT_THAT(out, SizeIs(5));
+}
+
+TEST_F(CreateTextTest, ShrinksOutVectorIfNeeded) {
+  out.resize(10, {{}, {}, cast<graphics::material>(1), {}});
+  engine->create_text_cells(out, data);
+  EXPECT_THAT(out, SizeIs(5));
+}
+
+TEST_F(CreateTextTest, ObjectsDoNotOverlap) {
+  engine->create_text_cells(out, data);
+  for (std::size_t i = 0; i < out.size() - 1; ++i) {
+    EXPECT_THAT(out[i].location.origin[0] + data.size[0],
+                Le(out[i + 1].location.origin[0]));
+  }
+}
+
+TEST_F(CreateTextTest, ObjectsAreOnSameHeight) {
+  engine->create_text_cells(out, data);
+  for (std::size_t i = 0; i < out.size(); ++i) {
+    EXPECT_THAT(out[i].location.origin[1], Eq(data.origin[1]));
+  }
+}
+
+TEST_F(CreateTextTest, EachObjectIsSizedByTheCell) {
+  engine->create_text_cells(out, data);
+  for (auto & obj : out) {
+    EXPECT_THAT(obj.location.size, Eq(data.size));
+  }
+}

+ 22 - 0
graphics/graphics-test/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 186 - 10
graphics/graphics.xcodeproj/project.pbxproj

@@ -16,13 +16,21 @@
 		CD3AC7191D2C0950002B4BB0 /* shader_program.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3AC7171D2C0950002B4BB0 /* shader_program.cpp */; };
 		CD3AC7261D2C0C63002B4BB0 /* object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3AC7241D2C0C63002B4BB0 /* object.cpp */; settings = {COMPILER_FLAGS = "-fvisibility=default"; }; };
 		CD62FCF72290DC9000376440 /* helper.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CD62FCF52290DC9000376440 /* helper.hpp */; };
-		CD62FCF82290DC9000376440 /* helper.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FCF62290DC9000376440 /* helper.cxx */; };
+		CD62FCF82290DC9000376440 /* opengl_manager.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FCF62290DC9000376440 /* opengl_manager.cxx */; };
 		CD62FCFA2290E2E500376440 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FCF92290E2E500376440 /* OpenGL.framework */; };
 		CD62FD062291970F00376440 /* libgameutils.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FD052291970F00376440 /* libgameutils.dylib */; };
 		CD62FD1E2292412900376440 /* renderer.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD1C2292412900376440 /* renderer.cxx */; settings = {COMPILER_FLAGS = "-fvisibility=default"; }; };
 		CD62FD222292C76B00376440 /* renderer_impl.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CD62FD202292C76B00376440 /* renderer_impl.hpp */; };
-		CD62FD232292C76B00376440 /* renderer_impl.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD212292C76B00376440 /* renderer_impl.cxx */; };
+		CD62FD232292C76B00376440 /* opengl_renderer.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD212292C76B00376440 /* opengl_renderer.cxx */; };
 		CDA34D9A22517A3D008036A7 /* libmath.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CDA34D9922517A3D008036A7 /* libmath.dylib */; };
+		CDED9C1922A2D6CE00AE5CE5 /* libgraphics.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD3AC6E21D2C0364002B4BB0 /* libgraphics.dylib */; };
+		CDED9C4322A2FACB00AE5CE5 /* renderer_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDED9C4222A2FACB00AE5CE5 /* renderer_test.cxx */; };
+		CDED9C4622A2FCA100AE5CE5 /* GoogleMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FCDF22904AD100376440 /* GoogleMock.framework */; };
+		CDED9C4722A308AE00AE5CE5 /* libmath.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CDA34D9922517A3D008036A7 /* libmath.dylib */; };
+		CDED9C5422A465DB00AE5CE5 /* opengl_renderer.h in Headers */ = {isa = PBXBuildFile; fileRef = CDED9C5322A465DB00AE5CE5 /* opengl_renderer.h */; settings = {ATTRIBUTES = (Private, ); }; };
+		CDED9C6322A961CE00AE5CE5 /* manager_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDED9C5E22A961CA00AE5CE5 /* manager_test.cxx */; };
+		CDED9C6822A9AD2300AE5CE5 /* resources in Resources */ = {isa = PBXBuildFile; fileRef = CDED9C6422A9AC7B00AE5CE5 /* resources */; };
+		CDED9C6922A9B26400AE5CE5 /* libgameutils.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FD052291970F00376440 /* libgameutils.dylib */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -54,6 +62,20 @@
 			remoteGlobalIDString = 05818F901A685AEA0072A469;
 			remoteInfo = GoogleMockTests;
 		};
+		CDED9C1A22A2D6CE00AE5CE5 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD3AC6DA1D2C0364002B4BB0 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = CD3AC6E11D2C0364002B4BB0;
+			remoteInfo = graphics;
+		};
+		CDED9C4422A2FC9C00AE5CE5 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD62FCD722904AD100376440 /* GoogleMock.xcodeproj */;
+			proxyType = 1;
+			remoteGlobalIDString = 05818F851A685AEA0072A469;
+			remoteInfo = GoogleMock;
+		};
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
@@ -69,14 +91,20 @@
 		CD3AC7241D2C0C63002B4BB0 /* object.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = object.cpp; sourceTree = "<group>"; };
 		CD62FCD722904AD100376440 /* GoogleMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GoogleMock.xcodeproj; path = "../../gmock-xcode-master/GoogleMock.xcodeproj"; sourceTree = "<group>"; };
 		CD62FCF52290DC9000376440 /* helper.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = helper.hpp; sourceTree = "<group>"; };
-		CD62FCF62290DC9000376440 /* helper.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = helper.cxx; sourceTree = "<group>"; };
+		CD62FCF62290DC9000376440 /* opengl_manager.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = opengl_manager.cxx; sourceTree = "<group>"; };
 		CD62FCF92290E2E500376440 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; };
 		CD62FD052291970F00376440 /* libgameutils.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; path = libgameutils.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 		CD62FD1C2292412900376440 /* renderer.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = renderer.cxx; sourceTree = "<group>"; };
 		CD62FD202292C76B00376440 /* renderer_impl.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = renderer_impl.hpp; sourceTree = "<group>"; };
-		CD62FD212292C76B00376440 /* renderer_impl.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = renderer_impl.cxx; sourceTree = "<group>"; };
+		CD62FD212292C76B00376440 /* opengl_renderer.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = opengl_renderer.cxx; sourceTree = "<group>"; };
 		CDA34D86225171AA008036A7 /* game */ = {isa = PBXFileReference; lastKnownFileType = folder; name = game; path = include/game; sourceTree = "<group>"; };
 		CDA34D9922517A3D008036A7 /* libmath.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; path = libmath.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
+		CDED9C1422A2D6CD00AE5CE5 /* graphics-test.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "graphics-test.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+		CDED9C1822A2D6CE00AE5CE5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		CDED9C4222A2FACB00AE5CE5 /* renderer_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = renderer_test.cxx; sourceTree = "<group>"; };
+		CDED9C5322A465DB00AE5CE5 /* opengl_renderer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opengl_renderer.h; sourceTree = "<group>"; };
+		CDED9C5E22A961CA00AE5CE5 /* manager_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = manager_test.cxx; sourceTree = "<group>"; };
+		CDED9C6422A9AC7B00AE5CE5 /* resources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = resources; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -90,14 +118,26 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		CDED9C1122A2D6CD00AE5CE5 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CDED9C6922A9B26400AE5CE5 /* libgameutils.dylib in Frameworks */,
+				CDED9C4722A308AE00AE5CE5 /* libmath.dylib in Frameworks */,
+				CDED9C4622A2FCA100AE5CE5 /* GoogleMock.framework in Frameworks */,
+				CDED9C1922A2D6CE00AE5CE5 /* libgraphics.dylib in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
 		CD1C84052299B72C00825C4E /* openGL */ = {
 			isa = PBXGroup;
 			children = (
-				CD62FD212292C76B00376440 /* renderer_impl.cxx */,
-				CD62FCF62290DC9000376440 /* helper.cxx */,
+				CDED9C5322A465DB00AE5CE5 /* opengl_renderer.h */,
+				CD62FD212292C76B00376440 /* opengl_renderer.cxx */,
+				CD62FCF62290DC9000376440 /* opengl_manager.cxx */,
 				CD1C840D2299B81500825C4E /* error_formatter.cxx */,
 			);
 			path = openGL;
@@ -109,6 +149,8 @@
 				CD62FCD722904AD100376440 /* GoogleMock.xcodeproj */,
 				CDA34D86225171AA008036A7 /* game */,
 				CD3AC6E41D2C0364002B4BB0 /* src */,
+				CDED9C3D22A2F52500AE5CE5 /* test */,
+				CDED9C1522A2D6CE00AE5CE5 /* graphics-test */,
 				CD3AC6E31D2C0364002B4BB0 /* Products */,
 				CDA34D9822517A3D008036A7 /* Frameworks */,
 			);
@@ -118,6 +160,7 @@
 			isa = PBXGroup;
 			children = (
 				CD3AC6E21D2C0364002B4BB0 /* libgraphics.dylib */,
+				CDED9C1422A2D6CD00AE5CE5 /* graphics-test.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -178,6 +221,24 @@
 			name = Frameworks;
 			sourceTree = "<group>";
 		};
+		CDED9C1522A2D6CE00AE5CE5 /* graphics-test */ = {
+			isa = PBXGroup;
+			children = (
+				CDED9C1822A2D6CE00AE5CE5 /* Info.plist */,
+			);
+			path = "graphics-test";
+			sourceTree = "<group>";
+		};
+		CDED9C3D22A2F52500AE5CE5 /* test */ = {
+			isa = PBXGroup;
+			children = (
+				CDED9C6422A9AC7B00AE5CE5 /* resources */,
+				CDED9C4222A2FACB00AE5CE5 /* renderer_test.cxx */,
+				CDED9C5E22A961CA00AE5CE5 /* manager_test.cxx */,
+			);
+			path = test;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXHeadersBuildPhase section */
@@ -186,6 +247,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				CD62FD222292C76B00376440 /* renderer_impl.hpp in Headers */,
+				CDED9C5422A465DB00AE5CE5 /* opengl_renderer.h in Headers */,
 				CD62FCF72290DC9000376440 /* helper.hpp in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -211,26 +273,50 @@
 			productReference = CD3AC6E21D2C0364002B4BB0 /* libgraphics.dylib */;
 			productType = "com.apple.product-type.library.dynamic";
 		};
+		CDED9C1322A2D6CD00AE5CE5 /* graphics-test */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = CDED9C2222A2D6CE00AE5CE5 /* Build configuration list for PBXNativeTarget "graphics-test" */;
+			buildPhases = (
+				CDED9C1022A2D6CD00AE5CE5 /* Sources */,
+				CDED9C1122A2D6CD00AE5CE5 /* Frameworks */,
+				CDED9C1222A2D6CD00AE5CE5 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				CDED9C4522A2FC9C00AE5CE5 /* PBXTargetDependency */,
+				CDED9C1B22A2D6CE00AE5CE5 /* PBXTargetDependency */,
+			);
+			name = "graphics-test";
+			productName = "graphics-test";
+			productReference = CDED9C1422A2D6CD00AE5CE5 /* graphics-test.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
 		CD3AC6DA1D2C0364002B4BB0 /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 1010;
+				LastUpgradeCheck = 1030;
 				ORGANIZATIONNAME = "Sam Jaffe";
 				TargetAttributes = {
 					CD3AC6E11D2C0364002B4BB0 = {
 						CreatedOnToolsVersion = 7.2.1;
 					};
+					CDED9C1322A2D6CD00AE5CE5 = {
+						CreatedOnToolsVersion = 10.1;
+						ProvisioningStyle = Automatic;
+					};
 				};
 			};
 			buildConfigurationList = CD3AC6DD1D2C0364002B4BB0 /* Build configuration list for PBXProject "graphics" */;
 			compatibilityVersion = "Xcode 3.2";
-			developmentRegion = English;
+			developmentRegion = en;
 			hasScannedForEncodings = 0;
 			knownRegions = (
 				en,
+				Base,
 			);
 			mainGroup = CD3AC6D91D2C0364002B4BB0;
 			productRefGroup = CD3AC6E31D2C0364002B4BB0 /* Products */;
@@ -244,6 +330,7 @@
 			projectRoot = "";
 			targets = (
 				CD3AC6E11D2C0364002B4BB0 /* graphics */,
+				CDED9C1322A2D6CD00AE5CE5 /* graphics-test */,
 			);
 		};
 /* End PBXProject section */
@@ -279,6 +366,17 @@
 		};
 /* End PBXReferenceProxy section */
 
+/* Begin PBXResourcesBuildPhase section */
+		CDED9C1222A2D6CD00AE5CE5 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CDED9C6822A9AD2300AE5CE5 /* resources in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
 /* Begin PBXShellScriptBuildPhase section */
 		CDA34DA222517B5E008036A7 /* ShellScript */ = {
 			isa = PBXShellScriptBuildPhase;
@@ -313,17 +411,40 @@
 				CD3AC6F81D2C0518002B4BB0 /* texture.cpp in Sources */,
 				CD3AC7261D2C0C63002B4BB0 /* object.cpp in Sources */,
 				CD1C83E922998E2600825C4E /* manager.cxx in Sources */,
-				CD62FCF82290DC9000376440 /* helper.cxx in Sources */,
-				CD62FD232292C76B00376440 /* renderer_impl.cxx in Sources */,
+				CD62FCF82290DC9000376440 /* opengl_manager.cxx in Sources */,
+				CD62FD232292C76B00376440 /* opengl_renderer.cxx in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		CDED9C1022A2D6CD00AE5CE5 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CDED9C4322A2FACB00AE5CE5 /* renderer_test.cxx in Sources */,
+				CDED9C6322A961CE00AE5CE5 /* manager_test.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXTargetDependency section */
+		CDED9C1B22A2D6CE00AE5CE5 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = CD3AC6E11D2C0364002B4BB0 /* graphics */;
+			targetProxy = CDED9C1A22A2D6CE00AE5CE5 /* PBXContainerItemProxy */;
+		};
+		CDED9C4522A2FC9C00AE5CE5 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			name = GoogleMock;
+			targetProxy = CDED9C4422A2FC9C00AE5CE5 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
 /* Begin XCBuildConfiguration section */
 		CD3AC6EB1D2C0364002B4BB0 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;
@@ -377,6 +498,7 @@
 		CD3AC6EC1D2C0364002B4BB0 /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;
@@ -446,6 +568,51 @@
 			};
 			name = Release;
 		};
+		CDED9C1C22A2D6CE00AE5CE5 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = "graphics-test/Info.plist";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 10.13;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.graphics-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		CDED9C1D22A2D6CE00AE5CE5 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				INFOPLIST_FILE = "graphics-test/Info.plist";
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 10.13;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.graphics-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
@@ -467,6 +634,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
+		CDED9C2222A2D6CE00AE5CE5 /* Build configuration list for PBXNativeTarget "graphics-test" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CDED9C1C22A2D6CE00AE5CE5 /* Debug */,
+				CDED9C1D22A2D6CE00AE5CE5 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 /* End XCConfigurationList section */
 	};
 	rootObject = CD3AC6DA1D2C0364002B4BB0 /* Project object */;

+ 67 - 0
graphics/graphics.xcodeproj/xcshareddata/xcschemes/graphics-test.xcscheme

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1030"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      codeCoverageEnabled = "YES"
+      onlyGenerateCoverageForSpecifiedTargets = "YES"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <CodeCoverageTargets>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "CD3AC6E11D2C0364002B4BB0"
+            BuildableName = "libgraphics.dylib"
+            BlueprintName = "graphics"
+            ReferencedContainer = "container:graphics.xcodeproj">
+         </BuildableReference>
+      </CodeCoverageTargets>
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "CDED9C1322A2D6CD00AE5CE5"
+               BuildableName = "graphics-test.xctest"
+               BlueprintName = "graphics-test"
+               ReferencedContainer = "container:graphics.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 42 - 0
graphics/include/game/graphics/exception.h

@@ -0,0 +1,42 @@
+//
+//  exception.h
+//  graphics
+//
+//  Created by Sam Jaffe on 7/6/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <stdexcept>
+#include <string>
+
+namespace graphics {
+  struct file_read_error : std::runtime_error {
+    file_read_error(std::string const & file)
+        : std::runtime_error("Unable to read file " + file) {}
+    file_read_error(std::string const & type, std::string const & file)
+        : std::runtime_error("Unable to read " + type + " file " + file) {}
+  };
+
+  struct unknown_texture_format : std::runtime_error {
+    unknown_texture_format(int components)
+        : std::runtime_error("Invalid number of components provided: " +
+                             std::to_string(components)) {}
+  };
+
+  template <typename E> struct unmapped_enum : std::logic_error {
+    // TODO (sjaffe): This is just an int cast, which isn't all that helpful.
+    // Instead, one might use  https://github.com/Neargye/magic_enum
+    // or wait for C++ to implement that sort of thing through metaclasses, etc.
+    unmapped_enum(E en)
+        : std::logic_error("Unknown " + type_name + std::to_string((int)en)) {}
+    unmapped_enum(std::string const & str)
+        : std::logic_error("Unknown " + type_name + str) {}
+
+    static std::string const type_name;
+  };
+
+  template <typename E>
+  std::string const unmapped_enum<E>::type_name{typeid(E).name()};
+}

+ 6 - 0
graphics/include/game/graphics/graphics_fwd.h

@@ -22,7 +22,13 @@ namespace graphics {
   struct shader;
   struct shader_program;
   class texture;
+  namespace materials {
+    enum class uniform : unsigned int;
+  }
   namespace shaders {
     enum class type : unsigned int;
   }
+  namespace textures {
+    enum class format : unsigned int;
+  }
 }

+ 49 - 1
graphics/include/game/graphics/manager.hpp

@@ -20,23 +20,71 @@ namespace graphics {
     manager();
     ~manager();
 
+    /**
+     * @brief Load a material - from either cache or by fetching data from the
+     * given arguments - and return its identifier.
+     * A material is a linkage of shader(s) and texture(s), that desribe the
+     * needed details to draw it on a page.
+     * @param program The shader program that should be used to paint this
+     * material.
+     * @param texture The path to an image file for this material
+     * @param uniform The name of the uniform to use as a fallback texture
+     */
     identity<material> get(identity<shader_program> program,
                            std::string const & texture,
                            std::string const & uniform) const;
+    /**
+     * @brief Load a shader - from either cache or by fetching data from the
+     * given arguments - and return its identifier.
+     * A shader is a type of program that is run on the GPU by a library like
+     * OpenGL.
+     * @param type A shader type describe what it draws...
+     * @param path The path to the shader's source code file.
+     */
     identity<shader> get(shaders::type type, std::string const & path) const;
+    /**
+     * @brief Load a shader_program - from either cache or by fetching data from
+     * the given arguments - and return its identifier.
+     * @param fragment The file path to the fragment shader code.
+     * @param vertex The file path to the vertex shader code.
+     */
     identity<shader_program> get(std::string const & fragment,
                                  std::string const & vertex) const;
+    /**
+     * @brief Load a texture - from either cache or by fetching data from the
+     * given arguments - and return its identifier.
+     * @param path The file path to an imagefile that contains one or more
+     * drawings of the object to be rendered.
+     */
     identity<texture> get(std::string const & path) const;
 
+    // TODO: This is kinda dumb...
     object create_object(identity<material> fromMaterial, math::vec2 atPosition,
                          math::vec2 frameWidth, float scale) const;
+
+    /**
+     * @brief Translate a material identity into an actual object.
+     * Used for internal linkage with the implementation code.
+     */
     material const & get(identity<material> identity) const;
+    /**
+     * @brief Translate a texture identity into an actual object.
+     * Used for internal linkage with the implementation code.
+     */
     texture const & get(identity<texture> identity) const;
 
   private:
+    void prepare_uniforms() const;
     identity<texture> texture_or_uniform(std::string const & path,
                                          std::string const & uniform) const;
 
-    std::unique_ptr<struct manager_impl> pimpl_;
+    virtual shader compile(shaders::type type,
+                           std::string const & path) const = 0;
+    virtual shader_program compile(identity<shader> fragment,
+                                   identity<shader> vertex) const = 0;
+    virtual texture compile(textures::format color, math::vec2i size,
+                            void const * buffer) const = 0;
+
+    std::unique_ptr<struct manager_cache> pcache_;
   };
 }

+ 2 - 3
graphics/include/game/graphics/material.hpp

@@ -16,15 +16,14 @@
 namespace graphics {
   struct uniform {
     identity<texture> texture;
-    int uniform_id; // TODO (sjaffe): use an enum and hide remapping?
+    materials::uniform
+        uniform_id; // TODO (sjaffe): use an enum and hide remapping?
   };
 
   struct material : public identity<material> {
     material(identity<shader_program> const & sp, math::vec2i size,
              std::vector<uniform> const & uniforms);
 
-    void activate() const;
-
     identity<shader_program> program;
     math::vec2i size;
     std::vector<uniform> uniforms;

+ 1 - 0
graphics/include/game/graphics/renderer.hpp

@@ -31,6 +31,7 @@ namespace graphics {
   class direct_renderer : public renderer {
   public:
     direct_renderer(driver d);
+    direct_renderer(class renderer_impl * pi);
     std::shared_ptr<class manager const> manager() const override;
     void draw(object const & obj) override;
     void draw(identity<material>, math::matr4 const &,

+ 1 - 1
graphics/include/game/graphics/shader.hpp

@@ -13,7 +13,7 @@
 
 namespace graphics {
   struct shader : public identity<shader> {
-    shader(shaders::type, std::string const &);
+    shader(unsigned int id, shaders::type, std::string const &);
 
     shaders::type type;
     std::string path;

+ 1 - 3
graphics/include/game/graphics/shader_program.hpp

@@ -13,9 +13,7 @@
 
 namespace graphics {
   struct shader_program : public identity<shader_program> {
-    shader_program(identity<shader>, identity<shader>);
-
-    void activate() const;
+    shader_program(unsigned int id, identity<shader>, identity<shader>);
 
     identity<shader> fragment_shader;
     identity<shader> vertex_shader;

+ 2 - 12
graphics/include/game/graphics/texture.hpp

@@ -12,19 +12,9 @@
 #include "vector/vector.hpp"
 
 namespace graphics {
-  class texture : public identity<texture> {
-  public:
-    texture(std::string const & str);
+  struct texture : public identity<texture> {
+    texture(unsigned int id, math::vec2i const & size);
 
-    static texture const & WHITE();
-    static texture const & DARK_YELLOW();
-    static texture const & LIGHT_BLUE();
-
-  private:
-    texture(std::pair<unsigned int, math::vec2i>);
-    texture(char const *, math::vec2i);
-
-  public:
     math::vec2i const size;
   };
 }

+ 16 - 11
graphics/src/helper.hpp

@@ -13,25 +13,24 @@
 
 #include "game/graphics/graphics_fwd.h"
 #include "game/math/math_fwd.hpp"
+#include "vector/vector.hpp"
 
 namespace graphics {
-  struct uniform;
   namespace textures {
-    enum class format { RGB, RGBA };
-    unsigned int init(format, math::vec2i, void const *);
+    enum class format : unsigned int { RGB, RGBA };
+    struct external_data {
+      external_data(std::string const & abs_path);
+      ~external_data();
+      format color;
+      math::vec2i size;
+      void * buffer;
+    };
   }
   namespace shaders {
     enum class type : unsigned int { FRAGMENT, VERTEX };
-    unsigned int init(type, std::string const &);
-    unsigned int init(identity<shader> const & fragmentShader,
-                      identity<shader> const & vertexShader);
-    void activate(identity<shader_program> id);
-    int uniform_location(identity<shader_program> id,
-                         std::string const & uniform);
   }
   namespace materials {
-    void activate(identity<shader_program> program,
-                  std::vector<uniform> const & uniforms);
+    enum class uniform : unsigned int { NORMAL, DIFFUSE, SPECULAR };
   }
 }
 
@@ -41,4 +40,10 @@ namespace std {
       return std::hash<unsigned int>()(static_cast<unsigned int>(tp));
     }
   };
+
+  template <> struct hash<graphics::materials::uniform> {
+    std::size_t operator()(graphics::materials::uniform uf) const {
+      return std::hash<unsigned int>()(static_cast<unsigned int>(uf));
+    }
+  };
 }

+ 62 - 43
graphics/src/manager.cxx

@@ -8,6 +8,7 @@
 
 #include "game/graphics/manager.hpp"
 
+#include "game/graphics/exception.h"
 #include "game/graphics/material.hpp"
 #include "game/graphics/object.hpp"
 #include "game/graphics/shader.hpp"
@@ -45,90 +46,70 @@ identity<T> cache<T>::emplace(key_t<T> const & key, T && value) {
   return values.emplace(key, std::forward<T>(value)).first->second;
 }
 
-struct graphics::manager_impl {
+struct graphics::manager_cache {
   cache<material> materials;
   cache<shader> shaders;
   cache<shader_program> programs;
   cache<texture> textures;
 };
 
-manager::manager() : pimpl_(new manager_impl) {}
+manager::manager() : pcache_(new manager_cache) {}
 manager::~manager() {}
 
+materials::uniform uniform_id(std::string const & uniform) {
+  if (uniform == "u_normalMap") {
+    return materials::uniform::NORMAL;
+  } else if (uniform == "u_specularMap") {
+    return materials::uniform::SPECULAR;
+  } else if (uniform == "u_diffuseMap") {
+    return materials::uniform::DIFFUSE;
+  }
+  throw unmapped_enum<materials::uniform>(uniform);
+}
+
 identity<material> manager::get(identity<shader_program> program,
                                 std::string const & texture,
                                 std::string const & uniform) const {
   auto key = std::make_tuple(program, texture, uniform);
-  auto & cache = pimpl_->materials;
+  auto & cache = pcache_->materials;
   auto found = cache.values.find(key);
   if (found != cache.values.end()) { return found->second; }
 
   std::vector<struct uniform> uniforms;
-  uniforms.push_back({texture_or_uniform(texture, uniform),
-                      shaders::uniform_location(program, uniform)});
+  uniforms.push_back(
+      {texture_or_uniform(texture, uniform), uniform_id(uniform)});
   math::vec2i size = get(uniforms[0].texture).size;
   //  if (!uniforms.empty()) { size = ...; }
   return cache.emplace(key, material(program, size, uniforms));
 }
 
-material const & manager::get(identity<material> identity) const {
-  auto & cache = pimpl_->materials;
-  return cache.values.find(cache.keys.find(identity)->second)->second;
-}
-
-texture const & manager::get(identity<texture> identity) const {
-  auto & cache = pimpl_->textures;
-  auto it = cache.keys.find(identity);
-  if (it == cache.keys.end()) { return texture::WHITE(); }
-  return cache.values.find(it->second)->second;
-}
-
 identity<shader> manager::get(shaders::type type,
                               std::string const & path) const {
   auto key = std::make_pair(type, path);
-  auto & cache = pimpl_->shaders;
+  auto & cache = pcache_->shaders;
   auto found = cache.values.find(key);
   if (found != cache.values.end()) { return found->second; }
-  return cache.emplace(key, shader(type, path));
-}
-
-identity<texture>
-manager::texture_or_uniform(std::string const & path,
-                            std::string const & uniform) const {
-  if (!path.empty()) {
-    try {
-      return get(path);
-    } catch (std::exception const & e) {
-      // TODO: Logging
-    }
-  }
-  if (uniform == "u_normalMap") {
-    return texture::LIGHT_BLUE();
-  } else if (uniform == "u_specularMap") {
-    return texture::DARK_YELLOW();
-  } else if (uniform == "u_diffuseMap") {
-    return texture::WHITE();
-  }
-  throw;
+  return cache.emplace(key, compile(type, path));
 }
 
 identity<shader_program> manager::get(std::string const & fragment,
                                       std::string const & vertex) const {
   auto key = std::make_pair(fragment, vertex);
-  auto & cache = pimpl_->programs;
+  auto & cache = pcache_->programs;
   auto found = cache.values.find(key);
   if (found != cache.values.end()) { return found->second; }
   auto fragment_shader = get(shaders::type::FRAGMENT, fragment);
   auto vertex_shader = get(shaders::type::VERTEX, vertex);
 
-  return cache.emplace(key, shader_program(fragment_shader, vertex_shader));
+  return cache.emplace(key, compile(fragment_shader, vertex_shader));
 }
 
 identity<texture> manager::get(std::string const & path) const {
-  auto & cache = pimpl_->textures;
+  auto & cache = pcache_->textures;
   auto found = cache.values.find(path);
   if (found != cache.values.end()) { return found->second; }
-  return cache.emplace(path, texture(path));
+  textures::external_data data(path);
+  return cache.emplace(path, compile(data.color, data.size, data.buffer));
 }
 
 object manager::create_object(identity<material> fromMaterial,
@@ -138,3 +119,41 @@ object manager::create_object(identity<material> fromMaterial,
                                                (scale ? scale : 1.f)};
   return {bounds, bounds, fromMaterial, {make_vector(0.f, 0.f), frameWidth}};
 }
+
+material const & manager::get(identity<material> identity) const {
+  auto & cache = pcache_->materials;
+  return cache.values.find(cache.keys.find(identity)->second)->second;
+}
+
+texture const & manager::get(identity<texture> identity) const {
+  auto & cache = pcache_->textures;
+  auto it = cache.keys.find(identity);
+  //  if (it == cache.keys.end()) { return texture::WHITE(); }
+  return cache.values.find(it->second)->second;
+}
+
+void manager::prepare_uniforms() const {
+  // Initialize the three default uniform-textures immediately
+  auto & cache = pcache_->textures;
+  if (cache.values.count("u_normalMap") != 0) return;
+  auto RGBA = textures::format::RGBA;
+  cache.emplace("u_normalMap", compile(RGBA, {{1, 1}}, "\x80\x80\xFF\xFF"));
+  cache.emplace("u_specularMap", compile(RGBA, {{1, 1}}, "\x80\x80\x00\xFF"));
+  cache.emplace("u_diffuseMap", compile(RGBA, {{1, 1}}, "\xFF\xFF\xFF\xFF"));
+}
+
+identity<texture>
+manager::texture_or_uniform(std::string const & path,
+                            std::string const & uniform) const {
+  if (!path.empty()) {
+    try {
+      return get(path);
+    } catch (std::exception const & e) {
+      // TODO: Logging
+    }
+  }
+  prepare_uniforms();
+  // The uniform is primed into the cache already.
+  auto & cache = pcache_->textures;
+  return cache.values.find(uniform)->second;
+}

+ 0 - 2
graphics/src/material.cpp

@@ -24,5 +24,3 @@ material::material(identity<shader_program> const & sp, math::vec2i size,
                    std::vector<uniform> const & uniforms)
     : identity<material>(next_id()), program(sp), size(size),
       uniforms(uniforms) {}
-
-void material::activate() const { materials::activate(program, uniforms); }

+ 0 - 265
graphics/src/openGL/helper.cxx

@@ -1,265 +0,0 @@
-//
-//  helper.cxx
-//  graphics
-//
-//  Created by Sam Jaffe on 5/18/19.
-//  Copyright © 2019 Sam Jaffe. All rights reserved.
-//
-
-#include "helper.hpp"
-
-#include <iostream>
-#include <memory>
-#include <sstream>
-#ifdef __APPLE__
-#include <OpenGL/gl3.h>
-#endif
-
-#include "scope_guard/scope_guard.hpp"
-#include "vector/vector.hpp"
-
-#include "game/graphics/material.hpp"
-#include "game/graphics/shader.hpp"
-#include "game/util/env.hpp"
-#include "game/util/files.hpp"
-#include "game/util/time.hpp"
-
-namespace graphics {
-  extern void print_shader_error(GLuint, std::string const &);
-  extern void print_shader_program_error(GLuint, std::string const &,
-                                         std::string const &);
-}
-
-namespace graphics { namespace textures {
-  static int glfmt(format color_fmt) {
-    switch (color_fmt) {
-    case format::RGB:
-      return GL_RGB;
-    case format::RGBA:
-      return GL_RGBA;
-    }
-  }
-
-  unsigned int init(format color_fmt, math::vec2i dimension,
-                    void const * data) {
-    unsigned int id;
-    // Enable texturings
-    //    glEnable( GL_TEXTURE_2D );
-
-    // Tell OpenGL that our pixel data is single-byte aligned
-    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
-
-    // Ask OpenGL for an unused texName (ID number) to use for this texture
-    glGenTextures(1, &id);
-
-    // Tell OpenGL to bind (set) this as the currently active texture
-    glBindTexture(GL_TEXTURE_2D, id);
-    // Set texture clamp vs. wrap (repeat)
-
-    // one of: GL_CLAMP_TO_EDGE, GL_REPEAT, GL_MIRRORED_REPEAT,
-    // GL_MIRROR_CLAMP_TO_EDGE, ...
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-
-    // the format our source pixel data is currently in; any of: GL_RGB,
-    // GL_RGBA, GL_LUMINANCE, GL_LUMINANCE_ALPHA, ...
-    int bufferFormat = glfmt(color_fmt);
-
-    // the format we want the texture to me on the card; allows us to translate
-    // into a different texture format as we upload to OpenGL
-    int internalFormat = bufferFormat;
-
-    /* glTexImage2D: Load a 2d texture image
-     * target: Creating this as a 2d texture.
-     * level: Which mipmap level to use as the "root" (0 = the highest-quality,
-     *  full-res image), if mipmaps are enabled.
-     * internalFormat: Type of texel format we want OpenGL to use for this
-     *  texture internally on the video card.
-     * width: Texel-width of image; for maximum compatibility, use 2^N + 2^B,
-     *  where N is some integer in the range [3,10], and B is the border
-     *  thickness [0,1]
-     * height: Texel-height of image; for maximum compatibility, use 2^M + 2^B,
-     *  where M is some integer in the range [3,10], and B is the border
-     *  thickness [0,1]
-     * border: Border size, in texels (must be 0 or 1)
-     * format: Pixel format describing the composition of the pixel data in
-     *  buffer
-     * type: Pixel color components are unsigned bytes (one byte per color/alpha
-     *  channel)
-     * pixels: Location of the actual pixel data bytes/buffer
-     */
-    glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, dimension.x(), dimension.y(),
-                 0, bufferFormat, GL_UNSIGNED_BYTE, data);
-
-    glGenerateMipmap(GL_TEXTURE_2D);
-
-    // Set magnification (texel > pixel) and minification (texel < pixel)
-    // filters one of: GL_NEAREST, GL_LINEAR
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-    // one of: GL_NEAREST, GL_LINEAR, GL_NEAREST_MIPMAP_NEAREST,
-    // GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST,
-    // GL_LINEAR_MIPMAP_LINEAR
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
-                    GL_NEAREST_MIPMAP_LINEAR);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 5);
-
-    return id;
-  }
-}}
-
-namespace graphics { namespace shaders {
-  struct file_read_error : std::runtime_error {
-    using std::runtime_error::runtime_error;
-  };
-
-  struct compilation_error : std::runtime_error {
-    using std::runtime_error::runtime_error;
-  };
-
-  struct linker_error : std::runtime_error {
-    using std::runtime_error::runtime_error;
-  };
-
-  static int gltype(type tp) {
-    switch (tp) {
-    case type::FRAGMENT:
-      return GL_FRAGMENT_SHADER;
-    case type::VERTEX:
-      return GL_VERTEX_SHADER;
-    }
-  }
-
-  unsigned int init(type tp, std::string const & path) {
-    std::unique_ptr<char const[]> buffer;
-
-    // 1. Load the vertex shader code (text file) to a new memory buffer
-    std::string const abs_path = env::resource_file(path);
-    if (!(buffer = files::load(abs_path))) {
-      throw file_read_error("Could not load shader file " + abs_path);
-    }
-
-    // 2. Create a new shader ID
-    // GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
-    unsigned int id = glCreateShader(gltype(tp));
-
-    // 3. Associate the shader code with the new shader ID
-    char const * buffer_ptr = buffer.get();
-    glShaderSource(id, 1, &buffer_ptr, NULL);
-
-    // 4. Compile the shader (the shader compiler is built in to your graphics
-    //    card driver)
-    glCompileShader(id);
-
-    // 5. Check for compile errors
-    int return_code;
-    glGetShaderiv(id, GL_COMPILE_STATUS, &return_code);
-
-    if (return_code != GL_TRUE) {
-      print_shader_error(id, abs_path);
-      throw compilation_error("Could not compile the shader file");
-    }
-    return id;
-  }
-
-  unsigned int init(identity<shader> const & fragmentShader,
-                    identity<shader> const & vertexShader) {
-    // 9. Create a new shader program ID
-    unsigned int id = glCreateProgram();
-
-    // 10. Attach the vertex and fragment shaders to the new shader program
-    glAttachShader(id, vertexShader.id);
-    glAttachShader(id, fragmentShader.id);
-
-    // 11. Do some other advanced stuff we'll do later on (like setting generic
-    // vertex attrib locations)
-    glBindAttribLocation(id, 0, "a_position");
-    glBindAttribLocation(id, 1, "a_color");
-    glBindAttribLocation(id, 2, "a_texCoords");
-    glBindAttribLocation(id, 3, "a_normal");
-
-    // 12. Link the program
-    glLinkProgram(id);
-
-    // 13. Check for link errors
-    int return_code;
-    glGetProgramiv(id, GL_LINK_STATUS, &return_code);
-
-    if (return_code != GL_TRUE) {
-      //      print_shader_program_error(id, fragmentShader.actual().path,
-      //                                 vertexShader.actual().path);
-      throw linker_error("Could not link shader program");
-    }
-    return id;
-  }
-
-  void activate(identity<shader_program> program) {
-    // 100. Use the shader program ID to "turn it on" for all subsequent drawing
-    glUseProgram(program.id);
-
-    /*
-     // 101. Enable texturing and Bind texture(s) to GPU texture units
-     glActiveTexture(GL_TEXTURE3);
-     glEnable(GL_TEXTURE_2D);
-     glBindTexture(GL_TEXTURE_2D, emissiveTextureID);
-
-     glActiveTexture(GL_TEXTURE2);
-     glEnable(GL_TEXTURE_2D);
-     glBindTexture(GL_TEXTURE_2D, specularTextureID);
-
-     glActiveTexture(GL_TEXTURE1);
-     glEnable(GL_TEXTURE_2D);
-     glBindTexture(GL_TEXTURE_2D, normalTextureID);
-
-     glActiveTexture(GL_TEXTURE0);
-     glEnable(GL_TEXTURE_2D);
-     glBindTexture(GL_TEXTURE_2D, diffuseTextureID);
-     */
-
-    // 102. Get the location # of each named uniform you wish to pass in to the
-    // shader
-    int timeUniformLocation = glGetUniformLocation(program.id, "u_time");
-    int scale = glGetUniformLocation(program.id, "Scale");
-    int diffuseMap = glGetUniformLocation(program.id, "u_diffuseMap");
-    int normalMap = glGetUniformLocation(program.id, "u_normalMap");
-    int specularMap = glGetUniformLocation(program.id, "u_specularMap");
-    int emissiveMap = glGetUniformLocation(program.id, "u_emissiveMap");
-    int debugWave = glGetUniformLocation(program.id, "g_debugWave");
-
-    // 103. Set the uniform values, including the texture unit numbers for
-    // texture (sampler) uniforms
-    // Env::GetCurrentTimeSeconds()
-    glUniform1f(timeUniformLocation, env::clock::current_time<float>());
-    glUniform1f(scale, 2.f);
-    glUniform1i(debugWave, 1); // TODO: m_waveEffectOn in ShaderProgram??
-    // for GL_TEXTURE0, texture unit 0
-    glUniform1i(diffuseMap, 0);
-    // for GL_TEXTURE1, texture unit 1
-    glUniform1i(normalMap, 1);
-    // for GL_TEXTURE2, texture unit 2
-    glUniform1i(specularMap, 2);
-    // for GL_TEXTURE3, texture unit 3
-    glUniform1i(emissiveMap, 3);
-  }
-
-  int uniform_location(identity<shader_program> program,
-                       std::string const & uniform) {
-    return glGetUniformLocation(program.id, uniform.c_str());
-  }
-}}
-
-namespace graphics { namespace materials {
-  void activate(identity<shader_program> program,
-                std::vector<uniform> const & uniforms) {
-    glUseProgram(program.id);
-
-    for (unsigned int i = 0; i < uniforms.size(); i++) {
-      const uniform & uniform = uniforms[i];
-      glActiveTexture(i + GL_TEXTURE0);
-      //      glEnable(GL_TEXTURE_2D);
-      glBindTexture(GL_TEXTURE_2D, uniform.texture.id);
-      glUniform1i(uniform.uniform_id, i);
-    }
-
-    glActiveTexture(GL_TEXTURE0);
-  }
-}}

+ 213 - 0
graphics/src/openGL/opengl_manager.cxx

@@ -0,0 +1,213 @@
+//
+//  helper.cxx
+//  graphics
+//
+//  Created by Sam Jaffe on 5/18/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "helper.hpp"
+#include "opengl_renderer.h"
+
+#include <iostream>
+#include <memory>
+#include <sstream>
+#ifdef __APPLE__
+#include <OpenGL/gl3.h>
+#endif
+
+#include "scope_guard/scope_guard.hpp"
+#include "vector/vector.hpp"
+
+#include "game/graphics/exception.h"
+#include "game/graphics/material.hpp"
+#include "game/graphics/shader.hpp"
+#include "game/graphics/shader_program.hpp"
+#include "game/graphics/texture.hpp"
+#include "game/util/env.hpp"
+#include "game/util/files.hpp"
+#include "game/util/time.hpp"
+
+namespace graphics {
+  extern void print_shader_error(GLuint, std::string const &);
+  extern void print_shader_program_error(GLuint, std::string const &,
+                                         std::string const &);
+
+  struct compilation_error : std::runtime_error {
+    using std::runtime_error::runtime_error;
+  };
+
+  struct linker_error : std::runtime_error {
+    using std::runtime_error::runtime_error;
+  };
+}
+
+using namespace graphics;
+
+namespace {
+  int glfmt(textures::format color_fmt) {
+    switch (color_fmt) {
+    case textures::format::RGB:
+      return GL_RGB;
+    case textures::format::RGBA:
+      return GL_RGBA;
+    }
+  }
+
+  int gltype(shaders::type tp) {
+    switch (tp) {
+    case shaders::type::FRAGMENT:
+      return GL_FRAGMENT_SHADER;
+    case shaders::type::VERTEX:
+      return GL_VERTEX_SHADER;
+    }
+  }
+}
+
+texture opengl_manager::compile(textures::format color, math::vec2i size,
+                                void const * buffer) const {
+  //  textures::external_data data(env::resource_file(path));
+  //  unsigned int init(format color_fmt, math::vec2i dimension,
+  //                    void const * data) {
+  unsigned int id;
+  // Enable texturings
+  //    glEnable( GL_TEXTURE_2D );
+
+  // Tell OpenGL that our pixel data is single-byte aligned
+  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
+
+  // Ask OpenGL for an unused texName (ID number) to use for this texture
+  glGenTextures(1, &id);
+
+  // Tell OpenGL to bind (set) this as the currently active texture
+  glBindTexture(GL_TEXTURE_2D, id);
+  // Set texture clamp vs. wrap (repeat)
+
+  // one of: GL_CLAMP_TO_EDGE, GL_REPEAT, GL_MIRRORED_REPEAT,
+  // GL_MIRROR_CLAMP_TO_EDGE, ...
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+  // the format our source pixel data is currently in; any of: GL_RGB,
+  // GL_RGBA, GL_LUMINANCE, GL_LUMINANCE_ALPHA, ...
+  int bufferFormat = glfmt(color);
+
+  // the format we want the texture to me on the card; allows us to translate
+  // into a different texture format as we upload to OpenGL
+  int internalFormat = bufferFormat;
+
+  /* glTexImage2D: Load a 2d texture image
+   * target: Creating this as a 2d texture.
+   * level: Which mipmap level to use as the "root" (0 = the highest-quality,
+   *  full-res image), if mipmaps are enabled.
+   * internalFormat: Type of texel format we want OpenGL to use for this
+   *  texture internally on the video card.
+   * width: Texel-width of image; for maximum compatibility, use 2^N + 2^B,
+   *  where N is some integer in the range [3,10], and B is the border
+   *  thickness [0,1]
+   * height: Texel-height of image; for maximum compatibility, use 2^M + 2^B,
+   *  where M is some integer in the range [3,10], and B is the border
+   *  thickness [0,1]
+   * border: Border size, in texels (must be 0 or 1)
+   * format: Pixel format describing the composition of the pixel data in
+   *  buffer
+   * type: Pixel color components are unsigned bytes (one byte per color/alpha
+   *  channel)
+   * pixels: Location of the actual pixel data bytes/buffer
+   */
+  glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, size.x(), size.y(), 0,
+               bufferFormat, GL_UNSIGNED_BYTE, buffer);
+
+  glGenerateMipmap(GL_TEXTURE_2D);
+
+  // Set magnification (texel > pixel) and minification (texel < pixel)
+  // filters one of: GL_NEAREST, GL_LINEAR
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+  // one of: GL_NEAREST, GL_LINEAR, GL_NEAREST_MIPMAP_NEAREST,
+  // GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST,
+  // GL_LINEAR_MIPMAP_LINEAR
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
+                  GL_NEAREST_MIPMAP_LINEAR);
+  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 5);
+
+  return texture{id, size};
+}
+
+shader opengl_manager::compile(shaders::type tp,
+                               std::string const & path) const {
+  std::unique_ptr<char const[]> buffer;
+
+  // 1. Load the vertex shader code (text file) to a new memory buffer
+  std::string const abs_path = env::resource_file(path);
+  if (!(buffer = files::load(abs_path))) {
+    throw file_read_error("shader", abs_path);
+  }
+
+  // 2. Create a new shader ID
+  // GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
+  unsigned int id = glCreateShader(gltype(tp));
+
+  // 3. Associate the shader code with the new shader ID
+  char const * buffer_ptr = buffer.get();
+  glShaderSource(id, 1, &buffer_ptr, NULL);
+
+  // 4. Compile the shader (the shader compiler is built in to your graphics
+  //    card driver)
+  glCompileShader(id);
+
+  // 5. Check for compile errors
+  int return_code;
+  glGetShaderiv(id, GL_COMPILE_STATUS, &return_code);
+
+  if (return_code != GL_TRUE) {
+    print_shader_error(id, abs_path);
+    throw compilation_error("Could not compile the shader file");
+  }
+
+  return shader{id, tp, path};
+}
+
+shader_program opengl_manager::compile(identity<shader> fragmentShader,
+                                       identity<shader> vertexShader) const {
+  // 9. Create a new shader program ID
+  unsigned int id = glCreateProgram();
+
+  // 10. Attach the vertex and fragment shaders to the new shader program
+  glAttachShader(id, vertexShader.id);
+  glAttachShader(id, fragmentShader.id);
+
+  // 11. Do some other advanced stuff we'll do later on (like setting generic
+  // vertex attrib locations)
+  glBindAttribLocation(id, 0, "a_position");
+  glBindAttribLocation(id, 1, "a_color");
+  glBindAttribLocation(id, 2, "a_texCoords");
+  glBindAttribLocation(id, 3, "a_normal");
+
+  // 12. Link the program
+  glLinkProgram(id);
+
+  // 13. Check for link errors
+  int return_code;
+  glGetProgramiv(id, GL_LINK_STATUS, &return_code);
+
+  if (return_code != GL_TRUE) {
+    //      print_shader_program_error(id, fragmentShader.actual().path,
+    //                                 vertexShader.actual().path);
+    throw linker_error("Could not link shader program");
+  }
+  return shader_program{id, fragmentShader, vertexShader};
+}
+
+opengl_uniform_data & opengl_manager::data(identity<shader_program> program) {
+  auto & data = data_[program];
+  // TODO: perform this in compile()
+  if (data.uniform_id.empty()) {
+    glUseProgram(program.id);
+    using materials::uniform;
+    data.uniform_id = {
+        {uniform::NORMAL, glGetUniformLocation(program.id, "u_normalMap")},
+        {uniform::DIFFUSE, glGetUniformLocation(program.id, "u_diffuseMap")},
+        {uniform::SPECULAR, glGetUniformLocation(program.id, "u_specularMap")}};
+  }
+  return data;
+}

+ 18 - 26
graphics/src/openGL/renderer_impl.cxx

@@ -6,7 +6,7 @@
 //  Copyright © 2019 Sam Jaffe. All rights reserved.
 //
 
-#include "renderer_impl.hpp"
+#include "opengl_renderer.h"
 
 #ifdef __APPLE__
 #include <OpenGL/gl3.h>
@@ -23,31 +23,8 @@
 
 using namespace graphics;
 
-class opengl_renderer : public renderer_impl {
-public:
-  opengl_renderer();
-  ~opengl_renderer();
-
-  void draw(identity<material>, math::matr4 const &,
-            std::vector<vertex> const &) override;
-  void clear() override;
-  void flush() override;
-
-  std::shared_ptr<class manager const> manager() const override { return mgr; }
-
-private:
-  const math::matr4 identity{math::matrix::identity<float, 4>()};
-
-  std::shared_ptr<class manager> mgr;
-  unsigned int active_material;
-
-  math::matr4 world_to_clip{identity};
-  double current_time{0.0};
-  unsigned int vertex_array_object{0}, vertex_buffer_object{0};
-};
-
 opengl_renderer::opengl_renderer()
-    : mgr(new class manager), active_material(0) {
+    : manager_(new opengl_manager), active_material(0) {
   glGenVertexArrays(1, &vertex_array_object);
   glBindVertexArray(vertex_array_object);
   glGenBuffers(1, &vertex_buffer_object);
@@ -85,7 +62,7 @@ void opengl_renderer::draw(::identity<material> material_id,
                            math::matr4 const & object_to_world,
                            std::vector<vertex> const & vertices) {
   material const & mat = manager()->get(material_id);
-  if (material_id != active_material) { mat.activate(); }
+  activate(mat);
   // TODO: Attatch shader-id to material-id
   unsigned int const id = mat.program.id;
 
@@ -122,6 +99,21 @@ void opengl_renderer::draw(::identity<material> material_id,
   glDisableVertexAttribArray(texCoordsLocation);
 }
 
+void opengl_renderer::activate(material const & mat) {
+  if (mat.id == active_material) return;
+  glUseProgram(mat.program.id);
+
+  for (unsigned int i = 0; i < mat.uniforms.size(); i++) {
+    const uniform & uniform = mat.uniforms[i];
+    glActiveTexture(i + GL_TEXTURE0);
+    //      glEnable(GL_TEXTURE_2D);
+    glBindTexture(GL_TEXTURE_2D, uniform.texture.id);
+    glUniform1i(manager_->data(mat.program)[uniform.uniform_id], i);
+  }
+
+  glActiveTexture(GL_TEXTURE0);
+}
+
 template <> renderer_impl * graphics::get_renderer_impl<driver::openGL>() {
   static opengl_renderer impl;
   return &impl;

+ 66 - 0
graphics/src/openGL/opengl_renderer.h

@@ -0,0 +1,66 @@
+//
+//  opengl_renderer.h
+//  graphics
+//
+//  Created by Sam Jaffe on 6/2/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include "../helper.hpp"
+#include "game/graphics/manager.hpp"
+#include "game/util/identity.hpp"
+#include "renderer_impl.hpp"
+
+#include "matrix/matrix.hpp"
+#include "matrix/matrix_helpers.hpp"
+
+namespace graphics {
+  struct opengl_uniform_data {
+    unsigned int operator[](materials::uniform id) { return uniform_id[id]; }
+    std::unordered_map<materials::uniform, unsigned int> uniform_id;
+  };
+
+  class opengl_manager : public manager {
+  public:
+    shader compile(shaders::type type, std::string const & path) const override;
+    shader_program compile(identity<shader> fragment,
+                           identity<shader> vertex) const override;
+    texture compile(textures::format color, math::vec2i size,
+                    void const * buffer) const override;
+
+    opengl_uniform_data & data(identity<shader_program> id);
+
+  private:
+    std::unordered_map<identity<shader_program>, opengl_uniform_data> data_;
+  };
+
+  class opengl_renderer : public renderer_impl {
+  public:
+    opengl_renderer();
+    ~opengl_renderer();
+
+    void draw(identity<material>, math::matr4 const &,
+              std::vector<vertex> const &) override;
+    void clear() override;
+    void flush() override;
+
+    std::shared_ptr<class manager const> manager() const override {
+      return manager_;
+    }
+
+  private:
+    void activate(material const & mat);
+
+  private:
+    const math::matr4 identity{math::matrix::identity<float, 4>()};
+
+    std::shared_ptr<opengl_manager> manager_;
+    unsigned int active_material;
+
+    math::matr4 world_to_clip{identity};
+    double current_time{0.0};
+    unsigned int vertex_array_object{0}, vertex_buffer_object{0};
+  };
+}

+ 4 - 1
graphics/src/renderer.cxx

@@ -9,6 +9,7 @@
 #include "game/graphics/renderer.hpp"
 #include <vector>
 
+#include "game/graphics/exception.h"
 #include "game/graphics/object.hpp"
 #include "game/graphics/vertex.h"
 #include "matrix/matrix.hpp"
@@ -22,12 +23,14 @@ renderer_impl * get_renderer_impl(driver d) {
   case driver::openGL:
     return get_renderer_impl<driver::openGL>();
   default:
-    throw;
+    throw unmapped_enum<driver>(d);
   }
 }
 
 direct_renderer::direct_renderer(driver d) : pimpl(::get_renderer_impl(d)) {}
 
+direct_renderer::direct_renderer(renderer_impl * pi) : pimpl(pi) {}
+
 std::shared_ptr<class manager const> direct_renderer::manager() const {
   return pimpl->manager();
 }

+ 2 - 4
graphics/src/shader.cpp

@@ -7,9 +7,7 @@
 
 #include "game/graphics/shader.hpp"
 
-#include "helper.hpp"
-
 using namespace graphics;
 
-shader::shader(shaders::type type, std::string const & path)
-    : identity<shader>(shaders::init(type, path)), type(type), path(path) {}
+shader::shader(unsigned int id, shaders::type type, std::string const & path)
+    : identity<shader>(id), type(type), path(path) {}

+ 4 - 6
graphics/src/shader_program.cpp

@@ -7,13 +7,11 @@
 
 #include "game/graphics/shader_program.hpp"
 
-#include "game/graphics/shader.hpp"
 #include "helper.hpp"
 
 using namespace graphics;
 
-shader_program::shader_program(identity<shader> frag, identity<shader> vert)
-    : identity<shader_program>(shaders::init(frag, vert)),
-      fragment_shader(frag), vertex_shader(vert) {}
-
-void shader_program::activate() const { shaders::activate(id); }
+shader_program::shader_program(unsigned int id, identity<shader> frag,
+                               identity<shader> vert)
+    : identity<shader_program>(id), fragment_shader(frag), vertex_shader(vert) {
+}

+ 16 - 29
graphics/src/texture.cpp

@@ -17,6 +17,7 @@
 #include "stb/stb_image.h"
 #pragma clang diagnostic pop
 
+#include "game/graphics/exception.h"
 #include "game/util/env.hpp"
 #include "game/util/hash.hpp"
 #include "helper.hpp"
@@ -25,44 +26,30 @@ unsigned char * stbi_load(char const *, int *, int *, int *, int);
 void stbi_image_free(void *);
 
 using namespace graphics;
+using namespace textures;
 
-static textures::format format(int comps) {
+static format as_format(int comps) {
   switch (comps) {
   case 3:
-    return textures::format::RGB;
+    return format::RGB;
   case 4:
-    return textures::format::RGBA;
+    return format::RGBA;
   default:
-    throw;
+    throw unknown_texture_format(comps);
   }
 }
 
-std::pair<unsigned int, math::vec2i> create(std::string const & path) {
+external_data::external_data(std::string const & rel_path) {
+  std::string abs_path = env::resource_file(rel_path);
   int components = 0;
-  math::vec2i size;
-  std::string file = env::resource_file(path);
-  auto data = stbi_load(file.c_str(), &size.x(), &size.y(), &components, 0);
-  scope(exit) { stbi_image_free(data); };
-  return {textures::init(format(components), size, data), size};
+  buffer = stbi_load(abs_path.c_str(), &size.x(), &size.y(), &components, 0);
+  if (!buffer) { throw file_read_error("texture", rel_path); }
+  color = as_format(components);
 }
 
-texture::texture(std::string const & path) : texture(create(path)) {}
-
-texture::texture(std::pair<unsigned int, math::vec2i> pair)
-    : identity<texture>(pair.first), size(pair.second) {}
-
-texture::texture(char const * data, math::vec2i size)
-    : identity<texture>(textures::init(format(4), size, data)), size(size) {}
-
-texture const & texture::WHITE() {
-  static auto t = texture("\xFF\xFF\xFF\xFF", {{1, 1}});
-  return t;
-}
-texture const & texture::DARK_YELLOW() {
-  static auto t = texture("\x80\x80\x00\xFF", {{1, 1}});
-  return t;
-}
-texture const & texture::LIGHT_BLUE() {
-  static auto t = texture("\x80\x80\xFF\xFF", {{1, 1}});
-  return t;
+external_data::~external_data() {
+  if (buffer) { stbi_image_free(buffer); }
 }
+
+texture::texture(unsigned int id, math::vec2i const & size)
+    : identity<texture>(id), size(size) {}

+ 271 - 0
graphics/test/manager_test.cxx

@@ -0,0 +1,271 @@
+//
+//  manager_test.cxx
+//  graphics
+//
+//  Created by Sam Jaffe on 6/6/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <gmock/gmock.h>
+
+#include "../src/helper.hpp"
+#include "game/graphics/manager.hpp"
+#include "game/graphics/material.hpp"
+#include "game/graphics/shader.hpp"
+#include "game/graphics/shader_program.hpp"
+#include "game/graphics/texture.hpp"
+
+#ifdef __APPLE__
+namespace env { namespace detail {
+  extern void bundle(std::string const &);
+}}
+#endif
+
+using testing::AllOf;
+using testing::AnyNumber;
+using testing::Eq;
+using testing::Ge;
+using testing::Le;
+using testing::Ne;
+using testing::SizeIs;
+using testing::_;
+
+struct mock_manager_impl : graphics::manager {
+  using shader = graphics::shader;
+  using shader_program = graphics::shader_program;
+  using texture = graphics::texture;
+
+  MOCK_CONST_METHOD2(compile_shader,
+                     shader(graphics::shaders::type, std::string const &));
+  MOCK_CONST_METHOD2(compile_program,
+                     shader_program(identity<shader>, identity<shader>));
+  shader compile(graphics::shaders::type t, std::string const & s) const {
+    return compile_shader(t, s);
+  }
+  shader_program compile(identity<shader> f, identity<shader> v) const {
+    return compile_program(f, v);
+  }
+  MOCK_CONST_METHOD3(compile, texture(graphics::textures::format, math::vec2i,
+                                      void const *));
+};
+
+template <typename T> identity<T> cast(unsigned int id) {
+  return *reinterpret_cast<identity<T> *>(&id);
+}
+
+auto cast_p(unsigned int id) { return cast<graphics::shader_program>(id); }
+auto cast_s(unsigned int id) { return cast<graphics::shader>(id); }
+
+class ManagerTest : public testing::Test {
+public:
+  void SetUp() override;
+
+  mock_manager_impl mock;
+
+private:
+  struct {
+    graphics::texture operator()(math::vec2i const & sz) { return {++i, sz}; }
+    unsigned int i{0};
+  } next_tex;
+  struct {
+    graphics::shader operator()(graphics::shaders::type tp,
+                                std::string const & path) {
+      return {++i, tp, path};
+    }
+    unsigned int i{0};
+  } next_shader;
+  struct {
+    graphics::shader_program operator()(identity<graphics::shader> f,
+                                        identity<graphics::shader> v) {
+      return {++i, f, v};
+    }
+    unsigned int i{0};
+  } next_program;
+};
+
+void ManagerTest::SetUp() {
+#ifdef __APPLE__
+  env::detail::bundle("leumasjaffe.graphics-test");
+#endif
+  using testing::Invoke;
+  using testing::WithArg;
+  // Start with a new set of IDs
+  ON_CALL(mock, compile(_, _, _))
+      .WillByDefault(WithArg<1>(Invoke(std::ref(next_tex))));
+  ON_CALL(mock, compile_shader(_, _))
+      .WillByDefault(Invoke(std::ref(next_shader)));
+  ON_CALL(mock, compile_program(_, _))
+      .WillByDefault(Invoke(std::ref(next_program)));
+}
+
+TEST_F(ManagerTest, DoesNotGreedilyCompile) {
+  EXPECT_CALL(mock, compile(_, _, _)).Times(0);
+  EXPECT_CALL(mock, compile_shader(_, _)).Times(0);
+  EXPECT_CALL(mock, compile_program(_, _)).Times(0);
+}
+
+using TextureTest = ManagerTest;
+
+TEST_F(TextureTest, ThrowsOnNonExistantFile) {
+  EXPECT_THROW(mock.get("resources/missing.png"), std::runtime_error);
+}
+
+TEST_F(TextureTest, NoExceptIfFileExists) {
+  EXPECT_CALL(mock, compile(_, make_vector(1, 1), _)).Times(1);
+  EXPECT_NO_THROW(mock.get("resources/black.bmp"));
+}
+
+TEST_F(TextureTest, CreatedTextureCanBeRefetched) {
+  EXPECT_CALL(mock, compile(_, make_vector(1, 1), _)).Times(1);
+  auto id_1 = mock.get("resources/black.bmp");
+  auto id_2 = mock.get("resources/black.bmp");
+  EXPECT_THAT(id_1, Eq(id_2));
+}
+
+TEST_F(TextureTest, CanGetTextureFromId) {
+  EXPECT_CALL(mock, compile(_, make_vector(1, 1), _)).Times(1);
+  auto texture_id = mock.get("resources/black.bmp");
+  auto texture = mock.get(texture_id);
+  EXPECT_THAT(texture, Eq(texture_id));
+}
+
+TEST_F(TextureTest, SizeOfTexturePassedIntoCompile) {
+  EXPECT_CALL(mock, compile(_, make_vector(1, 1), _)).Times(0);
+  EXPECT_CALL(mock, compile(_, make_vector(2, 2), _)).Times(1);
+  mock.get("resources/black2.bmp");
+}
+
+using MaterialTest = ManagerTest;
+
+// TEST_F(MaterialTest, ThrowsExceptionIfNoTexOrUniform) {
+//  EXPECT_CALL(mock, compile(_, _, _)).Times(AnyNumber());
+//  // TODO (sjaffe): Throw a specific exception here, since throw; kills us
+//  EXPECT_ANY_THROW(mock.get(cast_p(1), "", ""));
+//}
+
+TEST_F(MaterialTest, GeneratesUniformTexturesIfNoTexFile) {
+  using graphics::materials::uniform;
+  EXPECT_CALL(mock, compile(_, make_vector(1, 1), _)).Times(3);
+  EXPECT_NO_THROW(mock.get(cast_p(1), "", "u_normalMap"));
+}
+
+TEST_F(MaterialTest, CreatedMaterialCanBeRefetched) {
+  // Three times and no more
+  EXPECT_CALL(mock, compile(_, make_vector(1, 1), _)).Times(3);
+  auto id_1 = mock.get(cast_p(1), "", "u_normalMap");
+  auto id_2 = mock.get(cast_p(1), "", "u_normalMap");
+  EXPECT_THAT(id_1, Eq(id_2));
+}
+
+TEST_F(MaterialTest, CanGetMaterialFromId) {
+  auto material_id = mock.get(cast_p(1), "", "u_normalMap");
+  auto material = mock.get(material_id);
+  // Ensure that the material is the 'same one' i.e.
+  // mock.get(mock.get(id)) == mock.get(id)
+  EXPECT_THAT(material, Eq(material_id));
+}
+
+TEST_F(MaterialTest, UniformMaterialIsOneByOne) {
+  auto material = mock.get(mock.get(cast_p(1), "", "u_normalMap"));
+  // Uniforms are always sized 1x1
+  EXPECT_THAT(material.size, Eq(make_vector(1, 1)));
+}
+
+TEST_F(MaterialTest, SizeCapturesTextureSize) {
+  auto material =
+      mock.get(mock.get(cast_p(1), "resources/black2.bmp", "u_normalMap"));
+  EXPECT_THAT(material.size, Eq(make_vector(2, 2)));
+}
+
+TEST_F(MaterialTest, UniformMaterialHasDataBindingToNormalTex) {
+  using graphics::materials::uniform;
+  auto material = mock.get(mock.get(cast_p(1), "", "u_normalMap"));
+  EXPECT_THAT(material.uniforms, SizeIs(1));
+  // Because we never initialize any textures, we are within the first three
+  // texture ID units.
+  EXPECT_THAT(material.uniforms[0].texture.id, AllOf(Ge(1), Le(3)));
+  // Test the mapping from "u_normalMap" to uniform::NORMAL
+  EXPECT_THAT(material.uniforms[0].uniform_id, Eq(uniform::NORMAL));
+}
+
+TEST_F(MaterialTest, DifferentProgramMakesDifferentMaterial) {
+  EXPECT_CALL(mock, compile(_, make_vector(1, 1), _)).Times(3);
+  auto id_1 = mock.get(cast_p(1), "", "u_normalMap");
+  auto id_2 = mock.get(cast_p(2), "", "u_normalMap");
+  EXPECT_THAT(id_1, Ne(id_2));
+}
+
+TEST_F(MaterialTest, DifferentProgramDoesntChangeUniformId) {
+  EXPECT_CALL(mock, compile(_, make_vector(1, 1), _)).Times(3);
+  auto mat_1 = mock.get(mock.get(cast_p(1), "", "u_normalMap"));
+  auto mat_2 = mock.get(mock.get(cast_p(2), "", "u_normalMap"));
+  EXPECT_THAT(mat_1.uniforms[0].texture, Eq(mat_2.uniforms[0].texture));
+}
+
+TEST_F(MaterialTest, TexFileSkipsUniformLoad) {
+  using graphics::materials::uniform;
+  EXPECT_CALL(mock, compile(_, make_vector(1, 1), _)).Times(1);
+  auto material =
+      mock.get(mock.get(cast_p(1), "resources/black.bmp", "u_normalMap"));
+  EXPECT_THAT(material.uniforms, SizeIs(1));
+  // Because we never initialize any textures, we are within the first three
+  // texture ID units.
+  EXPECT_THAT(material.uniforms[0].texture.id, Eq(1));
+  // Test the mapping from "u_normalMap" to uniform::NORMAL
+  EXPECT_THAT(material.uniforms[0].uniform_id, Eq(uniform::NORMAL));
+}
+
+TEST_F(MaterialTest, MissingFileNoExcept) {
+  EXPECT_NO_THROW(
+      mock.get(mock.get(cast_p(1), "resources/missing.png", "u_normalMap")));
+}
+
+TEST_F(MaterialTest, MissingFileFallsBackOnUniform) {
+  using graphics::materials::uniform;
+  auto material =
+      mock.get(mock.get(cast_p(1), "resources/missing.png", "u_normalMap"));
+  EXPECT_THAT(material.uniforms, SizeIs(1));
+  // Because we never initialize any textures, we are within the first three
+  // texture ID units.
+  EXPECT_THAT(material.uniforms[0].texture.id, AllOf(Ge(1), Le(3)));
+  // Test the mapping from "u_normalMap" to uniform::NORMAL
+  EXPECT_THAT(material.uniforms[0].uniform_id, Eq(uniform::NORMAL));
+}
+
+using ShaderTest = ManagerTest;
+TEST_F(ShaderTest, CanCreateShaders) {
+  using graphics::shaders::type;
+  EXPECT_CALL(mock, compile_shader(_, _)).Times(1);
+  EXPECT_NO_THROW(mock.get(type::FRAGMENT, "A"));
+}
+
+TEST_F(ShaderTest, CreatedShaderCanBeRefetched) {
+  using graphics::shaders::type;
+  EXPECT_CALL(mock, compile_shader(_, _)).Times(1);
+  auto id_1 = mock.get(type::FRAGMENT, "A");
+  auto id_2 = mock.get(type::FRAGMENT, "A");
+  EXPECT_THAT(id_1, Eq(id_2));
+}
+
+TEST_F(ShaderTest, DifferentTypeMakesDifferentShader) {
+  using graphics::shaders::type;
+  EXPECT_CALL(mock, compile_shader(_, _)).Times(2);
+  auto id_1 = mock.get(type::FRAGMENT, "A");
+  auto id_2 = mock.get(type::VERTEX, "A");
+  EXPECT_THAT(id_1, Ne(id_2));
+}
+
+using ShaderProgramTest = ManagerTest;
+TEST_F(ShaderProgramTest, CreatingProgramCreatesTwoShaders) {
+  EXPECT_CALL(mock, compile_shader(_, _)).Times(2);
+  EXPECT_CALL(mock, compile_program(cast_s(1), cast_s(2))).Times(1);
+  EXPECT_NO_THROW(mock.get("A", "B"));
+}
+
+TEST_F(ShaderProgramTest, CreatedProgramCanBeRefetched) {
+  EXPECT_CALL(mock, compile_shader(_, _)).Times(2);
+  EXPECT_CALL(mock, compile_program(cast_s(1), cast_s(2))).Times(1);
+  auto id_1 = mock.get("A", "B");
+  auto id_2 = mock.get("A", "B");
+  EXPECT_THAT(id_1, Eq(id_2));
+}

+ 157 - 0
graphics/test/renderer_test.cxx

@@ -0,0 +1,157 @@
+//
+//  direct_renderer_test.cxx
+//  graphics-test
+//
+//  Created by Sam Jaffe on 6/1/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include <gmock/gmock.h>
+
+#include "../src/renderer_impl.hpp"
+#include "game/graphics/object.hpp"
+#include "game/graphics/renderer.hpp"
+#include "game/graphics/vertex.h"
+#include "game/math/shape.hpp"
+#include "matrix/matrix.hpp"
+
+using testing::AnyNumber;
+using testing::IsEmpty;
+using testing::SizeIs;
+using testing::_;
+
+struct mock_renderer_impl : graphics::renderer_impl {
+  MOCK_CONST_METHOD0(manager, std::shared_ptr<graphics::manager const>());
+  MOCK_METHOD3(draw, void(identity<graphics::material>, math::matr4 const &,
+                          std::vector<graphics::vertex> const &));
+  MOCK_METHOD0(clear, void());
+  MOCK_METHOD0(flush, void());
+};
+
+identity<graphics::material> cast(unsigned int id) {
+  return *reinterpret_cast<identity<graphics::material> *>(&id);
+}
+
+struct DirectRendererTest : testing::Test {
+  void SetUp() override;
+  void TearDown() override;
+
+  std::unique_ptr<graphics::direct_renderer> renderer;
+  mock_renderer_impl * mock;
+};
+
+void DirectRendererTest::SetUp() {
+  mock = new mock_renderer_impl;
+  renderer.reset(new graphics::direct_renderer(mock));
+}
+
+void DirectRendererTest::TearDown() {
+  renderer.reset();
+  delete mock;
+}
+
+graphics::object DemoObject(unsigned int id = 1) {
+  math::dim2::rectangle const size{{{-1.f, -1.f}}, {{2.f, 2.f}}};
+  math::dim2::rectangle const tex{{{0.f, 0.f}}, {{1.f, 1.f}}};
+  return {size, size, cast(id), tex};
+}
+
+TEST_F(DirectRendererTest, DrawPassesToDraw) {
+  EXPECT_CALL(*mock, draw(cast(1), math::matr4(), IsEmpty())).Times(1);
+  renderer->draw(cast(1), math::matr4(), {});
+}
+
+TEST_F(DirectRendererTest, DrawObjectHasEmptyTranslation) {
+  EXPECT_CALL(*mock, draw(cast(1), math::matr4(), _)).Times(1);
+  renderer->draw(DemoObject());
+}
+
+TEST_F(DirectRendererTest, DrawObjectHasSixVertices) {
+  EXPECT_CALL(*mock, draw(_, _, SizeIs(6))).Times(1);
+  renderer->draw(DemoObject());
+}
+
+TEST_F(DirectRendererTest, MultipleDrawCallsDispatchMultipleDraws) {
+  EXPECT_CALL(*mock, draw(_, _, SizeIs(6))).Times(2);
+  renderer->draw(DemoObject());
+  renderer->draw(DemoObject());
+}
+
+TEST_F(DirectRendererTest, ClearPassesToClear) {
+  EXPECT_CALL(*mock, clear()).Times(1);
+  renderer->clear();
+}
+
+TEST_F(DirectRendererTest, FlushPassesToFlush) {
+  EXPECT_CALL(*mock, flush()).Times(1);
+  renderer->flush();
+}
+
+struct BatchRendererTest : testing::Test {
+  void SetUp() override;
+  void TearDown() override;
+
+  std::unique_ptr<graphics::batch_renderer> renderer;
+  std::unique_ptr<graphics::direct_renderer> drenderer;
+  mock_renderer_impl * mock;
+};
+
+void BatchRendererTest::SetUp() {
+  mock = new mock_renderer_impl;
+  drenderer.reset(new graphics::direct_renderer(mock));
+  renderer.reset(new graphics::batch_renderer(drenderer.get()));
+}
+
+void BatchRendererTest::TearDown() {
+  renderer.reset();
+  drenderer.reset();
+  delete mock;
+}
+
+TEST_F(BatchRendererTest, CallsFlushOnDestructor) {
+  EXPECT_CALL(*mock, flush()).Times(1);
+  renderer.reset();
+  testing::Mock::VerifyAndClearExpectations(mock);
+}
+
+TEST_F(BatchRendererTest, ClearPassesToClear) {
+  EXPECT_CALL(*mock, flush()).Times(AnyNumber());
+  EXPECT_CALL(*mock, clear()).Times(1);
+  renderer->clear();
+}
+
+TEST_F(BatchRendererTest, FlushPassesToFlush) {
+  EXPECT_CALL(*mock, flush()).Times(AnyNumber());
+  // This will end up changing
+  EXPECT_CALL(*mock, flush()).Times(1).RetiresOnSaturation();
+  renderer->flush();
+}
+
+TEST_F(BatchRendererTest, DoesNotWriteImmediately) {
+  EXPECT_CALL(*mock, draw(_, _, _)).Times(0);
+  renderer->draw(cast(1), math::matr4(), {});
+  testing::Mock::VerifyAndClearExpectations(mock);
+  // We need to re-enact this expectation
+  EXPECT_CALL(*mock, flush()).Times(AnyNumber());
+  EXPECT_CALL(*mock, draw(_, _, _)).Times(1);
+}
+
+TEST_F(BatchRendererTest, GroupsDataTogetherByMaterial) {
+  EXPECT_CALL(*mock, flush()).Times(AnyNumber());
+  EXPECT_CALL(*mock, draw(cast(1), _, SizeIs(12))).Times(1);
+  EXPECT_CALL(*mock, draw(cast(2), _, SizeIs(6))).Times(1);
+  renderer->draw(DemoObject());
+  renderer->draw(DemoObject());
+  renderer->draw(DemoObject(2));
+}
+
+TEST_F(BatchRendererTest, BatchLimitingCanSplitTheWrites) {
+  EXPECT_CALL(*mock, flush()).Times(AnyNumber());
+  renderer.reset(new graphics::batch_renderer(drenderer.get(), 15));
+  EXPECT_CALL(*mock, draw(cast(1), _, SizeIs(12))).Times(1);
+  EXPECT_CALL(*mock, draw(cast(2), _, SizeIs(6))).Times(2);
+  renderer->draw(DemoObject());
+  renderer->draw(DemoObject());
+  renderer->draw(DemoObject(2));
+  renderer->draw(DemoObject(2));
+}

BIN
graphics/test/resources/black.bmp


BIN
graphics/test/resources/black2.bmp


+ 2 - 0
math/include/game/math/angle.hpp

@@ -10,12 +10,14 @@
 namespace math {
   struct degree {
     degree(double v);
+    degree operator-() const;
     double value;
   };
 
   struct radian {
     radian(double v);
     radian(degree d);
+    radian operator-() const;
     operator degree() const;
     double value;
   };

+ 9 - 2
math/math.xcodeproj/project.pbxproj

@@ -16,6 +16,7 @@
 		CDA34D9522517967008036A7 /* matrix_helpers.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CDA34D8C22517680008036A7 /* matrix_helpers.hpp */; };
 		CDA34D9622517969008036A7 /* matrix.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CDA34D8D22517680008036A7 /* matrix.hpp */; };
 		CDA34D972251796B008036A7 /* vector.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CDA34D9022517689008036A7 /* vector.hpp */; };
+		CDED9C2422A2D71600AE5CE5 /* angle_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDED9C2322A2D71600AE5CE5 /* angle_test.cxx */; };
 		CDEDC5B9227F2D38003A2E45 /* common_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDEDC5B8227F2D38003A2E45 /* common_test.cxx */; };
 /* End PBXBuildFile section */
 
@@ -77,6 +78,7 @@
 		CDA34D8C22517680008036A7 /* matrix_helpers.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = matrix_helpers.hpp; path = matrix/matrix_helpers.hpp; sourceTree = SOURCE_ROOT; };
 		CDA34D8D22517680008036A7 /* matrix.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = matrix.hpp; path = matrix/matrix.hpp; sourceTree = SOURCE_ROOT; };
 		CDA34D9022517689008036A7 /* vector.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = vector.hpp; path = vector/vector.hpp; sourceTree = SOURCE_ROOT; };
+		CDED9C2322A2D71600AE5CE5 /* angle_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = angle_test.cxx; sourceTree = "<group>"; };
 		CDEDC5B8227F2D38003A2E45 /* common_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = common_test.cxx; sourceTree = "<group>"; };
 		CDEDC5BD227F2DB2003A2E45 /* test_printers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = test_printers.h; sourceTree = "<group>"; };
 /* End PBXFileReference section */
@@ -166,6 +168,7 @@
 				CD1FCFC8227E193000F9BF93 /* shape_test.cxx */,
 				CDEDC5B8227F2D38003A2E45 /* common_test.cxx */,
 				CDEDC5BD227F2DB2003A2E45 /* test_printers.h */,
+				CDED9C2322A2D71600AE5CE5 /* angle_test.cxx */,
 			);
 			path = test;
 			sourceTree = "<group>";
@@ -240,7 +243,7 @@
 			isa = PBXProject;
 			attributes = {
 				LastSwiftUpdateCheck = 1010;
-				LastUpgradeCheck = 1010;
+				LastUpgradeCheck = 1030;
 				ORGANIZATIONNAME = "Sam Jaffe";
 				TargetAttributes = {
 					CD1FCFCC227E194D00F9BF93 = {
@@ -254,10 +257,11 @@
 			};
 			buildConfigurationList = CD3786131CF9F61100BE89B2 /* Build configuration list for PBXProject "math" */;
 			compatibilityVersion = "Xcode 3.2";
-			developmentRegion = English;
+			developmentRegion = en;
 			hasScannedForEncodings = 0;
 			knownRegions = (
 				en,
+				Base,
 			);
 			mainGroup = CD37860F1CF9F61100BE89B2;
 			productRefGroup = CD3786191CF9F61100BE89B2 /* Products */;
@@ -344,6 +348,7 @@
 			files = (
 				CDEDC5B9227F2D38003A2E45 /* common_test.cxx in Sources */,
 				CD1FCFD8227E195B00F9BF93 /* shape_test.cxx in Sources */,
+				CDED9C2422A2D71600AE5CE5 /* angle_test.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -426,6 +431,7 @@
 		CD3786211CF9F61100BE89B2 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;
@@ -479,6 +485,7 @@
 		CD3786221CF9F61100BE89B2 /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;

+ 1 - 1
math/math.xcodeproj/xcshareddata/xcschemes/math-test.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1010"
+   LastUpgradeVersion = "1030"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"

+ 2 - 0
math/src/angle.cpp

@@ -11,9 +11,11 @@
 
 namespace math {
   degree::degree(double v) : value(v) {}
+  degree degree::operator-() const { return degree(-value); }
 
   radian::radian(double v) : value(v) {}
   radian::radian(degree d) : value(d.value * M_PI / 180.f) {}
+  radian radian::operator-() const { return radian(-value); }
   radian::operator degree() const { return {value * M_1_PI * 180.f}; }
 
   double sin(radian r) { return std::sin(r.value); }

+ 3 - 1
math/src/common.cpp

@@ -127,7 +127,9 @@ namespace math {
     return true;
   }
 
-  bool intersects(dim2::quad const & lhs, dim2::quad const & rhs) {
+  // TODO (sjaffe): This does not cause intersection when the edges brush
+  //                against one another, is this good?
+ bool intersects(dim2::quad const & lhs, dim2::quad const & rhs) {
     dim2::triangle l1{lhs.ll, lhs.lr, lhs.ul};
     dim2::triangle l2{lhs.ul, lhs.ur, lhs.ll};
     dim2::triangle r1{rhs.ll, rhs.lr, rhs.ul};

+ 94 - 0
math/test/angle_test.cxx

@@ -0,0 +1,94 @@
+//
+//  angle_test.cxx
+//  math-test
+//
+//  Created by Sam Jaffe on 6/1/19.
+//  Copyright © 2019 Sam Jaffe. All rights reserved.
+//
+
+#include "game/math/angle.hpp"
+#include <gmock/gmock.h>
+
+using testing::DoubleNear;
+using testing::Eq;
+
+TEST(RadianTest, Negation) {
+  math::radian radians{M_PI};
+  EXPECT_THAT((-radians).value, -M_PI);
+  EXPECT_THAT((-(-radians)).value, radians.value);
+}
+
+TEST(DegreeTest, Negation) {
+  math::degree degrees{180.0};
+  EXPECT_THAT((-degrees).value, -180.0);
+  EXPECT_THAT((-(-degrees)).value, degrees.value);
+}
+
+// Degree -> Radian
+TEST(RadianTest, ZeroDegreeIsZeroRadian) {
+  math::degree degrees{0};
+  EXPECT_THAT(math::radian{degrees}.value, Eq(0.0));
+}
+
+TEST(RadianTest, OneRadianIsAbout57Degrees) {
+  math::degree degrees{57.2957795131};
+  EXPECT_THAT(math::radian{degrees}.value, DoubleNear(1, 1E-10));
+}
+
+TEST(RadianTest, OneEightyDegreeIsPiRadians) {
+  math::degree degrees{180};
+  EXPECT_THAT(math::radian{degrees}.value, DoubleNear(M_PI, 1E-12));
+}
+
+TEST(RadianTest, NinetyDegreeIsHalfPiRadians) {
+  math::degree degrees{90};
+  EXPECT_THAT(math::radian{degrees}.value, DoubleNear(M_PI_2, 1E-12));
+}
+
+// Radian -> Degree
+TEST(DegreeTest, ZeroDegreeIsZeroRadian) {
+  math::radian radians{0};
+  EXPECT_THAT(math::degree{radians}.value, Eq(0.0));
+}
+
+TEST(DegreeTest, OneRadianIsAbout57Degrees) {
+  math::radian radians{1.0};
+  // 1E-10 because that's how many digits I wrote down
+  EXPECT_THAT(math::degree{radians}.value, DoubleNear(57.2957795131, 1E-10));
+}
+
+TEST(DegreeTest, OneEightyDegreeIsPiRadians) {
+  math::radian radians{M_PI};
+  EXPECT_THAT(math::degree{radians}.value, DoubleNear(180.0, 1E-12));
+}
+
+TEST(DegreeTest, NinetyDegreeIsHalfPiRadians) {
+  math::radian radians{M_PI_2};
+  EXPECT_THAT(math::degree{radians}.value, DoubleNear(90, 1E-12));
+}
+
+// Trigonometry
+struct TrigonometryTest : testing::TestWithParam<double> {};
+
+TEST_P(TrigonometryTest, SinRadianIsSinDouble) {
+  double const angle = GetParam();
+  math::radian rad{angle};
+  EXPECT_THAT(sin(rad), sin(angle));
+}
+
+TEST_P(TrigonometryTest, CosRadianIsCosDouble) {
+  double const angle = GetParam();
+  math::radian rad{angle};
+  EXPECT_THAT(cos(rad), cos(angle));
+}
+
+TEST_P(TrigonometryTest, TanRadianIsTanDouble) {
+  double const angle = GetParam();
+  math::radian rad{angle};
+  EXPECT_THAT(tan(rad), tan(angle));
+}
+
+using testing::Values;
+INSTANTIATE_TEST_CASE_P(MapsToRawValue, TrigonometryTest,
+                        Values(0.0, M_PI_4, M_PI_2, 3 * M_PI_4, M_PI,
+                               5 * M_PI_4, 3 * M_PI_2, 7 * M_PI_4, 2 * M_PI));

+ 84 - 0
math/test/common_test.cxx

@@ -18,6 +18,10 @@
 using namespace math::dim2;
 using namespace testing;
 
+namespace math {
+  bool intersects(dim2::triangle const &, dim2::triangle const &);
+}
+
 template <typename T, typename G>
 decltype(auto) generate(T min, T max, T step, G && generator) {
   std::vector<decltype(generator(min))> out;
@@ -242,3 +246,83 @@ TEST(CircleTest, NonIntersecting) {
   square const sq{{{1.5, 0.5}}, 0.5};
   EXPECT_FALSE(math::intersects(c1, sq));
 }
+
+TEST(TriangleTest, TriangleIntersectsSelf) {
+  triangle tri{{{0, 0}}, {{0, 1}}, {{1, 0}}};
+  EXPECT_TRUE(math::intersects(tri, tri));
+}
+
+TEST(TriangleTest, DoesNotIntersectOffset) {
+  triangle lhs{{{0, 0}}, {{0, 1}}, {{1, 0}}};
+  auto tri = [](math::dim2::point const & pt) {
+    return triangle{pt, pt, pt};
+  };
+  math::dim2::point const clock_ab{{1, 1}};
+  math::dim2::point const clock_bc{{-1, -1}};
+  math::dim2::point const clock_ca{{-1, 2}};
+
+  // Each of these expectations fails one of the check-edges calls
+  // in the intersection algorithm, which proves that each point of
+  // the triangle is not within (or on) the bounds of the other triangle
+  // Each of these tests indicates one edge of the triangle 'lhs', and
+  // tests for whether it orients around any of the points of the other
+  // 'triangle'.
+  // Tests are done for each edge vs. the the opposite triangle,
+  // starting with edges for the lhs, and then the edges on the rhs.
+  EXPECT_FALSE(math::intersects(lhs, tri(clock_ab)));
+  EXPECT_FALSE(math::intersects(lhs, tri(clock_bc)));
+  EXPECT_FALSE(math::intersects(lhs, tri(clock_ca)));
+  EXPECT_FALSE(math::intersects(tri(clock_ab), lhs));
+  EXPECT_FALSE(math::intersects(tri(clock_bc), lhs));
+  EXPECT_FALSE(math::intersects(tri(clock_ca), lhs));
+}
+
+TEST(QuadTest, IntersectsContain) {
+  quad const lhs = square{{{-0.5f, -0.5f}}, 1};
+  quad const rhs = square{{{-.25f, -.25f}}, 0.5};
+  EXPECT_TRUE(math::intersects(lhs, rhs));
+}
+
+TEST(QuadTest, NoIntersectionAtEdge) {
+  quad const lhs = square{{{-0.5f, -0.5f}}, 1};
+  quad const rhs = square{{{ 0.0f,  0.5f}}, 1};
+  EXPECT_FALSE(math::intersects(lhs, rhs));
+}
+
+TEST(QuadTest, NoIntersectionAtCorner) {
+  quad const lhs = square{{{-0.5f, -0.5f}}, 1};
+  quad const rhs = square{{{ 0.5f,  0.5f}}, 1};
+  EXPECT_FALSE(math::intersects(lhs, rhs));
+}
+
+TEST(QuadTest, NoIntersection) {
+  quad const lhs = square{{{-1.0f, -0.5f}}, 1};
+  quad const rhs = square{{{ 0.5f,  0.5f}}, 1};
+  EXPECT_FALSE(math::intersects(lhs, rhs));
+}
+
+TEST(RotateTest, RotatingSquareAroundOrigin) {
+  math::degree degrees{90.0};
+  // A square with a side-length of 2 and a center at the origin
+  math::vec2 const origin = make_vector(0.f, 0.f);
+  quad const object = square{{{-1, -1}}, 2};
+  quad const rotated = math::rotate(origin, object, degrees);
+  EXPECT_THAT(rotated.ll, Eq(make_vector(1.f, -1.f)));
+  EXPECT_THAT(rotated.lr, Eq(make_vector(1.f, 1.f)));
+  EXPECT_THAT(rotated.ur, Eq(make_vector(-1.f, 1.f)));
+  EXPECT_THAT(rotated.ul, Eq(make_vector(-1.f, -1.f)));
+  EXPECT_THAT(math::rotate(origin, rotated, -degrees), Eq(object));
+}
+
+TEST(RotateTest, RotatingSquareAroundOwnPoint) {
+  math::degree degrees{90.0};
+  // A square with a side-length of 2 and a center at the origin
+  math::vec2 const axis = make_vector(-1.f, -1.f);
+  quad const object = square{{{-1, -1}}, 2};
+  quad const rotated = math::rotate(axis, object, degrees);
+  EXPECT_THAT(rotated.ll, Eq(make_vector(-1.f, -1.f)));
+  EXPECT_THAT(rotated.lr, Eq(make_vector(-1.f, 1.f)));
+  EXPECT_THAT(rotated.ur, Eq(make_vector(-3.f, 1.f)));
+  EXPECT_THAT(rotated.ul, Eq(make_vector(-3.f, -1.f)));
+  EXPECT_THAT(math::rotate(axis, rotated, -degrees), Eq(object));
+}

+ 12 - 0
math/test/shape_test.cxx

@@ -44,6 +44,18 @@ INSTANTIATE_TEST_CASE_P(LineIntersection, FromOriginTest,
 
 struct UnitLineTest : TestWithParam<point> {};
 
+TEST(LineTest, UnitLineHasLengthOne) {
+  line const unit{{{0, 0}}, {{1, 0}}};
+  EXPECT_THAT(unit.length(), Eq(1));
+}
+
+
+TEST(LineTest, ParallelLinesHaveSameSlope) {
+  line const lhs{{{0, 0}}, {{1, 0}}};
+  line const rhs{{{-1, 0}}, {{-2, 0}}};
+  EXPECT_TRUE(math::lines::parallel(lhs, rhs));
+}
+
 TEST_P(UnitLineTest, OrthoOnIntersection) {
   line const ln{{{0, 0}}, {{1, 0}}};
   point const pt = GetParam();

+ 5 - 2
util/gameutils.xcodeproj/project.pbxproj

@@ -153,7 +153,7 @@
 		CD3AC7001D2C0726002B4BB0 /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 1010;
+				LastUpgradeCheck = 1030;
 				ORGANIZATIONNAME = "Sam Jaffe";
 				TargetAttributes = {
 					CD3AC7071D2C0726002B4BB0 = {
@@ -163,10 +163,11 @@
 			};
 			buildConfigurationList = CD3AC7031D2C0726002B4BB0 /* Build configuration list for PBXProject "gameutils" */;
 			compatibilityVersion = "Xcode 3.2";
-			developmentRegion = English;
+			developmentRegion = en;
 			hasScannedForEncodings = 0;
 			knownRegions = (
 				en,
+				Base,
 			);
 			mainGroup = CD3AC6FF1D2C0726002B4BB0;
 			productRefGroup = CD3AC7091D2C0726002B4BB0 /* Products */;
@@ -253,6 +254,7 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;
@@ -307,6 +309,7 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;

+ 3 - 0
util/include/game/util/identity.hpp

@@ -21,6 +21,9 @@ private:
   friend bool operator==(identity const & lhs, identity const & rhs) {
     return lhs.id == rhs.id;
   }
+  friend bool operator!=(identity const & lhs, identity const & rhs) {
+    return lhs.id != rhs.id;
+  }
   friend bool operator!=(identity const & lhs, ID const & rhs) {
     return lhs.id != rhs;
   }

+ 14 - 3
util/src/osx_env.mm

@@ -20,6 +20,17 @@ namespace {
 }
 
 namespace env {
+  namespace detail {
+    NSString * bundle_name = nil;
+    void bundle(std::string const & str) {
+      bundle_name = str.empty() ? nil : translate(str);
+    }
+    NSBundle * bundle() {
+      return bundle_name == nil ? [NSBundle mainBundle] :
+          [NSBundle bundleWithIdentifier:bundle_name];
+    }
+  }
+  
   std::string resource_file(std::string const& path) {
     size_t dir_idx = path.find_last_of("/");
     size_t ext_idx = path.find_first_of(".");
@@ -27,9 +38,9 @@ namespace env {
     std::string name = path.substr(dir_idx + 1, ext_idx - (dir_idx + 1));
     std::string type = path.substr(ext_idx + 1);
     
-    NSString* url = [[NSBundle mainBundle] pathForResource:translate(name)
-                                                    ofType:translate(type)
-                                               inDirectory:translate(base)];
+    NSString* url = [detail::bundle() pathForResource:translate(name)
+                                               ofType:translate(type)
+                                          inDirectory:translate(base)];
     
     char const* abs_path = [url cStringUsingEncoding:encoding];
     return abs_path ? std::string(abs_path) : std::string();