Просмотр исходного кода

Merge branch 'batching'

* batching:
  Fix some broken test cases that were broken by batching.
  Add Unit Tests for injecting renderers with a string id.
  Use string id instead of enumeration.
  Update schemas...
  refactor(renderer_impl): Switch from driver enums to dependency-injection.
  Move rendering of glyphs into fps_counter class's ownership.
  Run clang-format because it got dropped from upgrading OS.
  Testing Mouse Release and Scene graphics.
  Test scene::in_bounds.
  Make all tests passing. TODO: Add tests for scene::in_bounds
  Fix naming conventions.
  Do a little cleanup on the screen size calcs.
  Make it so each batch renderer can have a different scale-factor.
  Push bounds check into scene.
  Make batch renderer able to live for more than one frame. TODO: Store normal and batch renderer code so that each 'scene' can use its own batch renderer.
Sam Jaffe 6 лет назад
Родитель
Сommit
b83865435b

+ 3 - 0
.gitmodules

@@ -17,3 +17,6 @@
 [submodule "include/scope_guard"]
 	path = include/scope_guard
 	url = freenas:utility/scope_guard
+[submodule "include/resource_factory"]
+	path = include/resource_factory
+	url = freenas:utility/resource_factory.git

+ 3 - 7
engine/engine.xcodeproj/xcshareddata/xcschemes/engine-test.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1030"
+   LastUpgradeVersion = "1110"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -10,9 +10,9 @@
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
       codeCoverageEnabled = "YES"
-      onlyGenerateCoverageForSpecifiedTargets = "YES"
-      shouldUseLaunchSchemeArgsEnv = "YES">
+      onlyGenerateCoverageForSpecifiedTargets = "YES">
       <CodeCoverageTargets>
          <BuildableReference
             BuildableIdentifier = "primary"
@@ -34,8 +34,6 @@
             </BuildableReference>
          </TestableReference>
       </Testables>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -47,8 +45,6 @@
       debugDocumentVersioning = "YES"
       debugServiceExtension = "internal"
       allowLocationSimulation = "YES">
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 1 - 0
engine/include/game/engine/fps_counter.hpp

@@ -23,6 +23,7 @@ namespace engine {
 
     void set_frame_step(env::clock::duration const & since);
 
+    void render(graphics::renderer & renderer) const;
     std::vector<graphics::object> const & glyphs() const { return glyphs_; }
 
   private:

+ 10 - 6
engine/include/game/engine/game_dispatch.hpp

@@ -39,21 +39,25 @@ namespace engine {
     void update();
     void render();
 
+    std::shared_ptr<graphics::renderer> make_renderer(math::vec2 const &) const;
     void register_scene(scene_t scn);
     void activate_scene(scene_id_t const & id);
     void set_current_timestamp();
+    void set_target_fps(env::clock::duration fps) {
+      minimum_frame_duration_ = fps;
+    }
 
   private:
     env::clock::tick next_frame();
 
   private:
-    std::shared_ptr<graphics::renderer> renderer;
-    math::vec2 screen_size;
-    env::clock::duration minimum_frame_duration;
+    math::vec2 screen_size_;
+    std::shared_ptr<graphics::renderer> renderer_, batch_renderer_;
+    env::clock::duration minimum_frame_duration_;
     std::shared_ptr<text_engine> text_engine_;
-    std::unique_ptr<fps_counter> counter;
+    std::unique_ptr<fps_counter> counter_;
 
-    std::unordered_map<scene_id_t, scene_t> scenes;
+    std::unordered_map<scene_id_t, scene_t> scenes_;
 
     bool running = true;
     env::clock::timestamp current_timestamp;
@@ -67,6 +71,6 @@ namespace engine {
       math::vec2 local_size;        // metadata
       bool is_mouse_event;
       bool is_keyboard_event;
-    } curr_scene;
+    } current_scene_;
   };
 }

+ 9 - 3
engine/include/game/engine/scene.hpp

@@ -21,11 +21,12 @@
 namespace engine {
   class scene : public identity<scene, std::string> {
   public:
-    scene(std::string const &, std::shared_ptr<game_dispatch>);
+    scene(std::string const &, math::vec2 const &,
+          std::shared_ptr<game_dispatch>);
     virtual ~scene();
 
     virtual void update(float delta) = 0;
-    virtual void render(graphics::renderer & renderer) = 0;
+    virtual void render() = 0;
     virtual void handle_key_event(event::key_event evt);
     virtual void handle_mouse_event(event::mouse_event evt);
 
@@ -35,11 +36,15 @@ namespace engine {
   protected:
     graphics::manager const & graphics_manager() const;
     void change_scene(std::string const & scene_id);
+    bool in_bounds(math::dim2::point const & c) const;
+    bool in_bounds(collidable * c) const;
     void check_collisions();
     static void check_collisions(std::vector<collidable *> const & from,
                                  std::vector<collidable *> const & to);
 
   protected:
+    graphics::renderer & renderer() { return *renderer_; }
+
     std::vector<entity> entities;
 
     // Map from entity::collides_with -> [entity*]
@@ -48,7 +53,8 @@ namespace engine {
     std::unordered_map<collision_t, std::vector<collidable *>> collidables;
 
   private:
-    math::vec2 local_scene_dimension_;
+    math::vec2 local_bounds_;
+    std::shared_ptr<graphics::renderer> renderer_;
     key_binding keys_;
     std::weak_ptr<game_dispatch> dispatch_;
   };

+ 8 - 1
engine/src/fps_counter.cxx

@@ -14,6 +14,7 @@
 #include "game/engine/serial.hpp"
 #include "game/engine/text_engine.hpp"
 #include "game/graphics/object.hpp"
+#include "game/graphics/renderer.hpp"
 
 using namespace engine;
 
@@ -24,6 +25,12 @@ fps_counter::fps_counter(std::shared_ptr<text_engine> text_engine,
 
 fps_counter::~fps_counter() {}
 
+void fps_counter::render(graphics::renderer & renderer) const {
+  for (auto & obj : glyphs()) {
+    renderer.draw(obj);
+  }
+}
+
 std::string fps_counter::fps(env::clock::duration const & since) const {
   auto fps = magnitude_ * env::clock::duration::period::den / since.count();
   std::string fps_str = std::to_string(fps);
@@ -35,5 +42,5 @@ void fps_counter::set_frame_step(env::clock::duration const & since) {
   if (++counter_ != change_after_) { return; }
   counter_ = 0;
   text_engine_->create_text_cells(
-      glyphs_, {make_vector(5.f, 680.f), make_vector(10.f, 20.f), fps(since)});
+      glyphs_, {make_vector(5.f, 780.f), make_vector(10.f, 20.f), fps(since)});
 }

+ 32 - 23
engine/src/game_dispatch.cpp

@@ -16,6 +16,7 @@
 #include "game/graphics/object.hpp"
 #include "game/graphics/renderer.hpp"
 #include "game/util/env.hpp"
+#include "matrix/matrix_helpers.hpp"
 
 namespace engine {
   namespace {
@@ -26,21 +27,22 @@ namespace engine {
   }
 
   game_dispatch::game_dispatch(std::shared_ptr<graphics::renderer> renderer)
-      : renderer(renderer), screen_size(env::screen_size()),
-        minimum_frame_duration(env::fps::v60),
+      : screen_size_(env::screen_size()), renderer_(renderer),
+        batch_renderer_(make_renderer(screen_size_)),
+        minimum_frame_duration_(0),
         text_engine_(new text_engine("font", renderer->manager())),
-        counter(new fps_counter(text_engine_)), scenes(),
-        current_timestamp(env::clock::now()), curr_scene() {}
+        counter_(new fps_counter(text_engine_)), scenes_(),
+        current_timestamp(env::clock::now()), current_scene_() {}
 
   game_dispatch::~game_dispatch() {}
 
   void game_dispatch::register_scene(scene_t scn) {
-    scenes.emplace(scn->id, scn);
+    scenes_.emplace(scn->id, scn);
   }
 
   void game_dispatch::activate_scene(scene_id_t const & id) {
     // TODO: Cleanup
-    curr_scene = current_scene_info(scenes[id]);
+    current_scene_ = current_scene_info(scenes_[id]);
   }
 
   void game_dispatch::set_current_timestamp() {
@@ -53,33 +55,33 @@ namespace engine {
       : ptr(curr), current_scene_id(ptr->id), local_size(ptr->size()) {}
 
   graphics::manager const & game_dispatch::graphics_manager() const {
-    return *renderer->manager();
+    return *renderer_->manager();
   }
 
   void game_dispatch::process_key_event(raw_key_t key, bool press) {
-    //    if (!curr_scene.is_keyboard_event) return;
-    auto const & binding = curr_scene.ptr->keys();
+    //    if (!current_scene_.is_keyboard_event) return;
+    auto const & binding = current_scene_.ptr->keys();
     auto it = binding.find(key);
     if (it == binding.end()) return;
 
-    curr_scene.ptr->handle_key_event(
+    current_scene_.ptr->handle_key_event(
         {it->second, mask(event::KEY_MASK, press)});
   }
 
   void game_dispatch::process_mouse_event(math::vec2 mouse_pos, bool press) {
-    //    if (!curr_scene.is_mouse_event) return;
+    //    if (!current_scene_.is_mouse_event) return;
     math::vec2 local_scene_position =
-        mouse_pos * curr_scene.local_size / screen_size;
+        mouse_pos * current_scene_.local_size / screen_size_;
 
-    curr_scene.ptr->handle_mouse_event(
+    current_scene_.ptr->handle_mouse_event(
         {local_scene_position, mask(event::MOUSE_MASK, press)});
   }
 
   env::clock::tick game_dispatch::next_frame() {
     env::clock::tick t(current_timestamp);
 
-    while (t.since < minimum_frame_duration) {
-      std::this_thread::sleep_for(minimum_frame_duration - t.since);
+    while (t.since < minimum_frame_duration_) {
+      std::this_thread::sleep_for(minimum_frame_duration_ - t.since);
       t = env::clock::tick(current_timestamp);
     }
 
@@ -89,16 +91,23 @@ namespace engine {
 
   void game_dispatch::update() {
     env::clock::tick t = next_frame();
-    counter->set_frame_step(t.since);
-    curr_scene.ptr->update(std::chrono::duration<float>(t.since).count());
+    counter_->set_frame_step(t.since);
+    current_scene_.ptr->update(std::chrono::duration<float>(t.since).count());
   }
 
   void game_dispatch::render() {
-    graphics::batch_renderer batch(renderer.get());
-    batch.clear();
-    for (auto & obj : counter->glyphs()) {
-      batch.draw(obj);
-    }
-    curr_scene.ptr->render(batch);
+    batch_renderer_->clear();
+    counter_->render(*batch_renderer_);
+    batch_renderer_->flush();
+    current_scene_.ptr->render();
+    renderer_->flush();
+  }
+
+  std::shared_ptr<graphics::renderer>
+  game_dispatch::make_renderer(math::vec2 const & bounds) const {
+    math::vec2 const factor = screen_size_ / bounds;
+    float const scale = std::min(factor[0], factor[1]);
+    return std::make_shared<graphics::batch_renderer>(
+        renderer_.get(), math::matrix::scalar(make_vector(scale, scale, 1.f)));
   }
 }

+ 21 - 3
engine/src/scene.cpp

@@ -12,14 +12,29 @@
 #include "game/engine/game_dispatch.hpp"
 #include "game/graphics/renderer.hpp"
 #include "game/math/common.hpp"
+#include "game/math/compare.hpp"
+#include "game/util/env.hpp"
 
 using namespace engine;
 
-scene::scene(std::string const & str, std::shared_ptr<game_dispatch> dispatch)
-    : identity<scene, std::string>(str), dispatch_(dispatch) {}
+scene::scene(std::string const & str, math::vec2 const & bounds,
+             std::shared_ptr<game_dispatch> dispatch)
+    : identity<scene, std::string>(str), local_bounds_(bounds),
+      renderer_(dispatch->make_renderer(size())), dispatch_(dispatch) {}
 
 scene::~scene() {}
 
+bool scene::in_bounds(math::dim2::point const & p) const {
+  return math::between(p[0], 0.f, local_bounds_[0]) &&
+         math::between(p[1], 0.f, local_bounds_[1]);
+}
+
+bool scene::in_bounds(collidable * c) const {
+  auto & quad = c->render_info().points;
+  return in_bounds(quad.ll) || in_bounds(quad.lr) || in_bounds(quad.ur) ||
+         in_bounds(quad.ul);
+}
+
 void scene::check_collisions(std::vector<collidable *> const & from,
                              std::vector<collidable *> const & to) {
   for (auto & ent : from) {
@@ -65,6 +80,9 @@ void scene::handle_key_event(event::key_event evt) {
 
 void scene::handle_mouse_event(event::mouse_event evt) {}
 
-math::vec2 scene::size() const { return local_scene_dimension_; }
+math::vec2 scene::size() const {
+  return (local_bounds_ == math::vec2()) ? math::vec2(env::screen_size())
+                                         : local_bounds_;
+}
 
 key_binding const & scene::keys() const { return keys_; }

+ 10 - 13
engine/test/entity_test.cxx

@@ -28,12 +28,10 @@ namespace math { namespace dim2 {
     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);
+    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) {
@@ -45,7 +43,7 @@ inline Json::Value to_json(std::string const & str) {
 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));
 }
@@ -60,16 +58,15 @@ std::string const data = R"(
 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};
@@ -81,10 +78,10 @@ TEST(EntityTest, SizeParamAltersLocation) {
 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)));
@@ -93,10 +90,10 @@ TEST(EntityTest, MoveWillAdjustPointsAndBounds) {
 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)));

+ 19 - 0
engine/test/fps_counter_test.cxx

@@ -71,3 +71,22 @@ TEST_F(FpsCounterTest, ProducesEnoughDigitsForPrecision) {
   }
   EXPECT_THAT(counter.glyphs(), SizeIs(8));
 }
+
+struct mock_renderer : public stub_renderer {
+  MOCK_METHOD1(draw, void(graphics::object const &));
+};
+
+TEST_F(FpsCounterTest, RenderCallInvokesOneDrawPerGlyph) {
+  // Set up expectations
+  using testing::_;
+  mock_renderer renderer;
+  EXPECT_CALL(renderer, draw(_)).Times(8);
+
+  // Set up counter
+  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);
+  }
+  counter.render(renderer);
+}

+ 33 - 5
engine/test/game_dispatch_test.cxx

@@ -20,9 +20,11 @@
 
 struct mock_scene : engine::scene {
   mock_scene(std::shared_ptr<engine::game_dispatch> game)
-      : engine::scene("mock", game) {}
+      : engine::scene("mock", make_vector(0.f, 0.f), game) {}
+  mock_scene(std::shared_ptr<engine::game_dispatch> game, math::vec2 dim)
+      : engine::scene("mock2", dim, game) {}
   MOCK_METHOD1(update, void(float));
-  MOCK_METHOD1(render, void(graphics::renderer &));
+  MOCK_METHOD0(render, void());
   MOCK_METHOD1(handle_key_event, void(engine::event::key_event));
   MOCK_METHOD1(handle_mouse_event, void(engine::event::mouse_event));
 };
@@ -44,6 +46,7 @@ void GameDispatchTest::SetUp() {
   env::resize_screen({{100, 100}});
   renderer_.reset(new stub_renderer);
   dispatch_.reset(new engine::game_dispatch(renderer_));
+  dispatch_->set_target_fps(env::fps::v60);
   scene_.reset(new mock_scene(dispatch_));
   game().register_scene(scene_);
   game().activate_scene("mock");
@@ -55,13 +58,14 @@ void GameDispatchTest::TearDown() {
   renderer_.reset();
 }
 
+using testing::_;
 using testing::AnyNumber;
 using testing::Eq;
 using testing::Field;
 using testing::FloatNear;
 using testing::Ge;
 using testing::Lt;
-using testing::_;
+using testing::Not;
 
 TEST_F(GameDispatchTest, ManagerIsFetchedFromRenderer) {
   EXPECT_THAT(&game().graphics_manager(), Eq(renderer_->manager().get()));
@@ -82,6 +86,16 @@ TEST_F(GameDispatchTest, UpdateIsCappedInFrequencyByFPSParam) {
   }
 }
 
+TEST_F(GameDispatchTest, FPSCanBeChanged) {
+  game().set_target_fps(env::fps::v120);
+  auto fps120 = 1.0 / 120.0;
+  EXPECT_CALL(scene(), update(Ge(fps120))).Times(AnyNumber());
+  EXPECT_CALL(scene(), update(Lt(fps120))).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);
@@ -98,7 +112,7 @@ TEST_F(GameDispatchTest, SetCurrentTimestampOverridesWaiting) {
 }
 
 TEST_F(GameDispatchTest, RenderDispatchesToCurrentScene) {
-  EXPECT_CALL(scene(), render(_)).Times(1);
+  EXPECT_CALL(scene(), render()).Times(1);
   game().render();
 }
 
@@ -109,8 +123,22 @@ TEST_F(MouseEventTest, DispatchesToCurrentScene) {
   game().process_mouse_event({{0.f, 0.f}}, true);
 }
 
+MATCHER(WasPressed, "") {
+  return (arg.type & engine::event::PRESSED_MASK) != 0;
+}
+
+TEST_F(MouseEventTest, CanPressAndRelease) {
+  EXPECT_CALL(scene(), handle_mouse_event(WasPressed())).Times(1);
+  game().process_mouse_event({}, true);
+  EXPECT_CALL(scene(), handle_mouse_event(Not(WasPressed()))).Times(1);
+  game().process_mouse_event({}, false);
+}
+
 TEST_F(MouseEventTest, TranslatesFrameOfRef) {
-  // TODO: Actually set the local scene size element...
+  // Resize the scene (not a supported operation)
+  scene_.reset(new mock_scene(dispatch_, make_vector(10.f, 10.f)));
+  game().register_scene(scene_);
+  game().activate_scene("mock2");
   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);

+ 61 - 3
engine/test/scene_test.cxx

@@ -14,13 +14,26 @@
 
 #include "mock_renderer.h"
 
+namespace env {
+  void resize_screen(math::vec2i const & size);
+}
+
 struct test_scene : engine::scene {
   using engine::scene::scene;
 
   void update(float) override { check_collisions(); }
-  void render(graphics::renderer &) override {}
+  void render() override {}
   void handle_key_event(engine::event::key_event) override {}
   void handle_mouse_event(engine::event::mouse_event) override {}
+  bool in_bounds(math::dim2::point c) const {
+    return engine::scene::in_bounds(c);
+  }
+  bool in_bounds(engine::collidable * c) const {
+    return engine::scene::in_bounds(c);
+  }
+  graphics::manager const & graphics_manager() const {
+    return engine::scene::graphics_manager();
+  }
 
   void add_with(engine::collision_t t, engine::collidable & c) {
     colliders[t].push_back(&c);
@@ -51,8 +64,10 @@ protected:
 
 void SceneTest::SetUp() {
   renderer_.reset(new stub_renderer);
+  math::vec2 screen{{1600.f, 900.f}};
+  env::resize_screen(math::vec2i(screen));
   dispatch_.reset(new engine::game_dispatch(renderer_));
-  scene_.reset(new test_scene("test", dispatch_));
+  scene_.reset(new test_scene("test", screen, dispatch_));
 }
 
 void SceneTest::TearDown() {
@@ -61,8 +76,13 @@ void SceneTest::TearDown() {
   renderer_.reset();
 }
 
+using testing::Eq;
 using testing::Ref;
 
+TEST_F(SceneTest, SharedGraphicsManagerWithDispatch) {
+  EXPECT_THAT(&scene().graphics_manager(), &game().graphics_manager());
+}
+
 TEST_F(SceneTest, WillCollideOverlappingObjects) {
   math::dim2::rectangle bnds{{{0.f, 0.f}}, {{1.f, 1.f}}};
   mock_collidable one{bnds}, two{bnds};
@@ -135,6 +155,7 @@ TEST_F(SceneTest, ObjectCanBeInMultipleCollidableAsGroups) {
   scene().update(0.f);
 }
 
+// TODO: Solve double-collision bug?
 TEST_F(SceneTest, DoubleCollisionCanOccur) {
   math::dim2::rectangle bnds{{{0.f, 0.f}}, {{1.f, 1.f}}};
   mock_collidable one{bnds}, two{bnds};
@@ -142,6 +163,43 @@ TEST_F(SceneTest, DoubleCollisionCanOccur) {
   scene().add_with(2, one);
   scene().add_as(1, two);
   scene().add_as(2, two);
-  EXPECT_CALL(one, collide(Ref(two))).Times(1);
+  EXPECT_CALL(one, collide(Ref(two))).Times(2);
   scene().update(0.f);
 }
+
+struct BoundsTest : SceneTest, testing::WithParamInterface<math::vec2> {};
+
+TEST_F(BoundsTest, BoundsCheckIsPointInScreenZone) {
+  // Confirm that the size is as expected...
+  EXPECT_THAT(scene().size(), Eq(make_vector(1600.f, 900.f)));
+
+  // Lower-Left Corner
+  EXPECT_FALSE(scene().in_bounds(make_vector(-1.f, 0.f)));
+  EXPECT_FALSE(scene().in_bounds(make_vector(0.f, -1.f)));
+  EXPECT_TRUE(scene().in_bounds(make_vector(0.f, 0.f)));
+  // Lower-Right Corner
+  EXPECT_FALSE(scene().in_bounds(make_vector(1600.f, -1.f)));
+  EXPECT_FALSE(scene().in_bounds(make_vector(1601.f, 0.f)));
+  EXPECT_TRUE(scene().in_bounds(make_vector(1600.f, 0.f)));
+  // Upper-Right Corner
+  EXPECT_FALSE(scene().in_bounds(make_vector(1601.f, 900.f)));
+  EXPECT_FALSE(scene().in_bounds(make_vector(1600.f, 901.f)));
+  EXPECT_TRUE(scene().in_bounds(make_vector(1600.f, 900.f)));
+  // Upper-Left Corner
+  EXPECT_FALSE(scene().in_bounds(make_vector(0.f, 901.f)));
+  EXPECT_FALSE(scene().in_bounds(make_vector(-1.f, 900.f)));
+  EXPECT_TRUE(scene().in_bounds(make_vector(0.f, 900.f)));
+}
+
+TEST_P(BoundsTest, BoundsCheckOnCollidableIsAnyPointInBounds) {
+  math::dim2::square bounds = {GetParam(), 2.f};
+  engine::collidable collide{{{}, bounds, cast<graphics::material>(1), {}}};
+
+  EXPECT_TRUE(scene().in_bounds(&collide));
+}
+
+INSTANTIATE_TEST_CASE_P(Collidable, BoundsTest,
+                        ::testing::Values(make_vector(-1.f, -1.f),
+                                          make_vector(1599.f, -1.f),
+                                          make_vector(1599.f, 899.f),
+                                          make_vector(-1.f, 899.f)));

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

@@ -9,19 +9,25 @@
 /* Begin PBXBuildFile section */
 		CD1C82BD22988EC700825C4E /* matrix.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1C82BC22988EC700825C4E /* matrix.cxx */; };
 		CD1C83E922998E2600825C4E /* manager.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1C83E722998E2600825C4E /* manager.cxx */; settings = {COMPILER_FLAGS = "-fvisibility=default"; }; };
-		CD1C840F2299B81500825C4E /* error_formatter.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1C840D2299B81500825C4E /* error_formatter.cxx */; };
 		CD3AC6F21D2C03B7002B4BB0 /* material.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3AC6F01D2C03B7002B4BB0 /* material.cpp */; };
 		CD3AC6F81D2C0518002B4BB0 /* texture.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3AC6F61D2C0518002B4BB0 /* texture.cpp */; };
 		CD3AC6FD1D2C06B5002B4BB0 /* shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD3AC6FB1D2C06B5002B4BB0 /* shader.cpp */; };
 		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 /* 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 /* opengl_renderer.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD212292C76B00376440 /* opengl_renderer.cxx */; };
+		CD6CDB68234EA31F00D76C1A /* opengl_renderer.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FD212292C76B00376440 /* opengl_renderer.cxx */; };
+		CD6CDB69234EA32300D76C1A /* opengl_manager.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD62FCF62290DC9000376440 /* opengl_manager.cxx */; };
+		CD6CDB6A234EA32300D76C1A /* error_formatter.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD1C840D2299B81500825C4E /* error_formatter.cxx */; };
+		CD6CDB6C234EA6CE00D76C1A /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FCF92290E2E500376440 /* OpenGL.framework */; };
+		CD6CDB6D234EA6DC00D76C1A /* libgameutils.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD62FD052291970F00376440 /* libgameutils.dylib */; };
+		CD6CDB6E234EA6DC00D76C1A /* libgameutils.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CD62FD052291970F00376440 /* libgameutils.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+		CD6CDB6F234EA6DC00D76C1A /* libmath.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CDA34D9922517A3D008036A7 /* libmath.dylib */; };
+		CD6CDB70234EA6DC00D76C1A /* libmath.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CDA34D9922517A3D008036A7 /* libmath.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+		CD6CDB72234EA70700D76C1A /* libgraphics.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CD3AC6E21D2C0364002B4BB0 /* libgraphics.dylib */; };
+		CD6CDB73234EA70700D76C1A /* libgraphics.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CD3AC6E21D2C0364002B4BB0 /* libgraphics.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		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 */; };
@@ -62,6 +68,13 @@
 			remoteGlobalIDString = 05818F901A685AEA0072A469;
 			remoteInfo = GoogleMockTests;
 		};
+		CD6CDB74234EA70700D76C1A /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD3AC6DA1D2C0364002B4BB0 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = CD3AC6E11D2C0364002B4BB0;
+			remoteInfo = graphics;
+		};
 		CDED9C1A22A2D6CE00AE5CE5 /* PBXContainerItemProxy */ = {
 			isa = PBXContainerItemProxy;
 			containerPortal = CD3AC6DA1D2C0364002B4BB0 /* Project object */;
@@ -78,6 +91,22 @@
 		};
 /* End PBXContainerItemProxy section */
 
+/* Begin PBXCopyFilesBuildPhase section */
+		CD6CDB71234EA6DC00D76C1A /* Embed Libraries */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+				CD6CDB73234EA70700D76C1A /* libgraphics.dylib in Embed Libraries */,
+				CD6CDB70234EA6DC00D76C1A /* libmath.dylib in Embed Libraries */,
+				CD6CDB6E234EA6DC00D76C1A /* libgameutils.dylib in Embed Libraries */,
+			);
+			name = "Embed Libraries";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
 /* Begin PBXFileReference section */
 		CD1C82B722988E4E00825C4E /* matrix.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = matrix.hpp; sourceTree = "<group>"; };
 		CD1C82BC22988EC700825C4E /* matrix.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = matrix.cxx; sourceTree = "<group>"; };
@@ -97,6 +126,7 @@
 		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 /* opengl_renderer.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = opengl_renderer.cxx; sourceTree = "<group>"; };
+		CD6CDB61234EA31500D76C1A /* libopengl_graphics.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libopengl_graphics.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
 		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; };
@@ -113,11 +143,21 @@
 			buildActionMask = 2147483647;
 			files = (
 				CD62FD062291970F00376440 /* libgameutils.dylib in Frameworks */,
-				CD62FCFA2290E2E500376440 /* OpenGL.framework in Frameworks */,
 				CDA34D9A22517A3D008036A7 /* libmath.dylib in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		CD6CDB5F234EA31500D76C1A /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CD6CDB6D234EA6DC00D76C1A /* libgameutils.dylib in Frameworks */,
+				CD6CDB6C234EA6CE00D76C1A /* OpenGL.framework in Frameworks */,
+				CD6CDB6F234EA6DC00D76C1A /* libmath.dylib in Frameworks */,
+				CD6CDB72234EA70700D76C1A /* libgraphics.dylib in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		CDED9C1122A2D6CD00AE5CE5 /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
@@ -161,6 +201,7 @@
 			children = (
 				CD3AC6E21D2C0364002B4BB0 /* libgraphics.dylib */,
 				CDED9C1422A2D6CD00AE5CE5 /* graphics-test.xctest */,
+				CD6CDB61234EA31500D76C1A /* libopengl_graphics.dylib */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -252,6 +293,13 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		CD6CDB5D234EA31500D76C1A /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXHeadersBuildPhase section */
 
 /* Begin PBXNativeTarget section */
@@ -273,6 +321,25 @@
 			productReference = CD3AC6E21D2C0364002B4BB0 /* libgraphics.dylib */;
 			productType = "com.apple.product-type.library.dynamic";
 		};
+		CD6CDB60234EA31500D76C1A /* opengl_graphics */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = CD6CDB67234EA31500D76C1A /* Build configuration list for PBXNativeTarget "opengl_graphics" */;
+			buildPhases = (
+				CD6CDB5D234EA31500D76C1A /* Headers */,
+				CD6CDB5E234EA31500D76C1A /* Sources */,
+				CD6CDB5F234EA31500D76C1A /* Frameworks */,
+				CD6CDB71234EA6DC00D76C1A /* Embed Libraries */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				CD6CDB75234EA70700D76C1A /* PBXTargetDependency */,
+			);
+			name = opengl_graphics;
+			productName = opengl_graphics;
+			productReference = CD6CDB61234EA31500D76C1A /* libopengl_graphics.dylib */;
+			productType = "com.apple.product-type.library.dynamic";
+		};
 		CDED9C1322A2D6CD00AE5CE5 /* graphics-test */ = {
 			isa = PBXNativeTarget;
 			buildConfigurationList = CDED9C2222A2D6CE00AE5CE5 /* Build configuration list for PBXNativeTarget "graphics-test" */;
@@ -304,6 +371,10 @@
 					CD3AC6E11D2C0364002B4BB0 = {
 						CreatedOnToolsVersion = 7.2.1;
 					};
+					CD6CDB60234EA31500D76C1A = {
+						CreatedOnToolsVersion = 11.1;
+						ProvisioningStyle = Automatic;
+					};
 					CDED9C1322A2D6CD00AE5CE5 = {
 						CreatedOnToolsVersion = 10.1;
 						ProvisioningStyle = Automatic;
@@ -331,6 +402,7 @@
 			targets = (
 				CD3AC6E11D2C0364002B4BB0 /* graphics */,
 				CDED9C1322A2D6CD00AE5CE5 /* graphics-test */,
+				CD6CDB60234EA31500D76C1A /* opengl_graphics */,
 			);
 		};
 /* End PBXProject section */
@@ -406,13 +478,20 @@
 				CD3AC7191D2C0950002B4BB0 /* shader_program.cpp in Sources */,
 				CD1C82BD22988EC700825C4E /* matrix.cxx in Sources */,
 				CD3AC6F21D2C03B7002B4BB0 /* material.cpp in Sources */,
-				CD1C840F2299B81500825C4E /* error_formatter.cxx in Sources */,
 				CD62FD1E2292412900376440 /* renderer.cxx in Sources */,
 				CD3AC6F81D2C0518002B4BB0 /* texture.cpp in Sources */,
 				CD3AC7261D2C0C63002B4BB0 /* object.cpp in Sources */,
 				CD1C83E922998E2600825C4E /* manager.cxx in Sources */,
-				CD62FCF82290DC9000376440 /* opengl_manager.cxx in Sources */,
-				CD62FD232292C76B00376440 /* opengl_renderer.cxx in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		CD6CDB5E234EA31500D76C1A /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CD6CDB69234EA32300D76C1A /* opengl_manager.cxx in Sources */,
+				CD6CDB68234EA31F00D76C1A /* opengl_renderer.cxx in Sources */,
+				CD6CDB6A234EA32300D76C1A /* error_formatter.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -428,6 +507,11 @@
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXTargetDependency section */
+		CD6CDB75234EA70700D76C1A /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = CD3AC6E11D2C0364002B4BB0 /* graphics */;
+			targetProxy = CD6CDB74234EA70700D76C1A /* PBXContainerItemProxy */;
+		};
 		CDED9C1B22A2D6CE00AE5CE5 /* PBXTargetDependency */ = {
 			isa = PBXTargetDependency;
 			target = CD3AC6E11D2C0364002B4BB0 /* graphics */;
@@ -491,7 +575,7 @@
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
-				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include/ $(PROJECT_DIR)/../math/ $(PROJECT_DIR)/../include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../util/include";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include/ $(PROJECT_DIR)/../math/ $(PROJECT_DIR)/../include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../include/resource_factory/include/ $(PROJECT_DIR)/../util/include";
 			};
 			name = Debug;
 		};
@@ -538,7 +622,7 @@
 				MACOSX_DEPLOYMENT_TARGET = 10.10;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = macosx;
-				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include/ $(PROJECT_DIR)/../math/ $(PROJECT_DIR)/../include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../util/include";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include/ $(PROJECT_DIR)/../include/expect/include/ $(PROJECT_DIR)/../math/ $(PROJECT_DIR)/../include/ $(PROJECT_DIR)/../math/include/ $(PROJECT_DIR)/../include/resource_factory/include/ $(PROJECT_DIR)/../util/include";
 			};
 			name = Release;
 		};
@@ -568,6 +652,51 @@
 			};
 			name = Release;
 		};
+		CD6CDB62234EA31500D76C1A /* 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;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				EXECUTABLE_PREFIX = lib;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				MACOSX_DEPLOYMENT_TARGET = 10.10;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+			};
+			name = Debug;
+		};
+		CD6CDB63234EA31500D76C1A /* 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;
+				DYLIB_COMPATIBILITY_VERSION = 1;
+				DYLIB_CURRENT_VERSION = 1;
+				EXECUTABLE_PREFIX = lib;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				MACOSX_DEPLOYMENT_TARGET = 10.10;
+				MTL_FAST_MATH = YES;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+			};
+			name = Release;
+		};
 		CDED9C1C22A2D6CE00AE5CE5 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -634,6 +763,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
+		CD6CDB67234EA31500D76C1A /* Build configuration list for PBXNativeTarget "opengl_graphics" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CD6CDB62234EA31500D76C1A /* Debug */,
+				CD6CDB63234EA31500D76C1A /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		CDED9C2222A2D6CE00AE5CE5 /* Build configuration list for PBXNativeTarget "graphics-test" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (

+ 3 - 7
graphics/graphics.xcodeproj/xcshareddata/xcschemes/graphics-test.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1030"
+   LastUpgradeVersion = "1110"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -10,9 +10,9 @@
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
       codeCoverageEnabled = "YES"
-      onlyGenerateCoverageForSpecifiedTargets = "YES"
-      shouldUseLaunchSchemeArgsEnv = "YES">
+      onlyGenerateCoverageForSpecifiedTargets = "YES">
       <CodeCoverageTargets>
          <BuildableReference
             BuildableIdentifier = "primary"
@@ -34,8 +34,6 @@
             </BuildableReference>
          </TestableReference>
       </Testables>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -47,8 +45,6 @@
       debugDocumentVersioning = "YES"
       debugServiceExtension = "internal"
       allowLocationSimulation = "YES">
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 6 - 2
graphics/include/game/graphics/renderer.hpp

@@ -13,6 +13,7 @@
 #include "game/graphics/graphics_fwd.h"
 #include "game/math/math_fwd.hpp"
 #include "game/util/hash.hpp"
+#include "matrix/matrix.hpp"
 #include "vector/vector.hpp"
 
 namespace graphics {
@@ -26,11 +27,11 @@ namespace graphics {
     virtual void flush() = 0;
   };
 
-  enum class driver { openGL };
+  enum class driver {}; // Dummy class...
 
   class direct_renderer : public renderer {
   public:
-    direct_renderer(driver d);
+    direct_renderer(std::string const & driver_id);
     direct_renderer(class renderer_impl * pi);
     std::shared_ptr<class manager const> manager() const override;
     void draw(object const & obj) override;
@@ -46,6 +47,8 @@ namespace graphics {
   class batch_renderer : public renderer {
   public:
     batch_renderer(renderer * impl, std::size_t batch_size = 0);
+    batch_renderer(renderer * impl, math::matr4 const &,
+                   std::size_t batch_size = 0);
     ~batch_renderer();
     std::shared_ptr<class manager const> manager() const override {
       return impl_->manager();
@@ -63,6 +66,7 @@ namespace graphics {
   private:
     renderer * impl_;
     std::unordered_map<identity<material>, std::vector<vertex>> batches_;
+    math::matr4 object_to_world_;
     std::size_t batch_size_;
     std::size_t elements_;
   };

+ 6 - 1
graphics/src/openGL/opengl_renderer.cxx

@@ -114,7 +114,12 @@ void opengl_renderer::activate(material const & mat) {
   glActiveTexture(GL_TEXTURE0);
 }
 
-template <> renderer_impl * graphics::get_renderer_impl<driver::openGL>() {
+renderer_impl * get_openGL() {
   static opengl_renderer impl;
   return &impl;
 }
+
+namespace {
+  bool _ =
+      graphics::renderer_impl_factory::instance().bind("openGL", &get_openGL);
+}

+ 1 - 1
graphics/src/openGL/opengl_renderer.h

@@ -9,9 +9,9 @@
 #pragma once
 
 #include "../helper.hpp"
+#include "../renderer_impl.hpp"
 #include "game/graphics/manager.hpp"
 #include "game/util/identity.hpp"
-#include "renderer_impl.hpp"
 
 #include "matrix/matrix.hpp"
 #include "matrix/matrix_helpers.hpp"

+ 21 - 13
graphics/src/renderer.cxx

@@ -18,16 +18,19 @@
 
 using namespace graphics;
 
-renderer_impl * get_renderer_impl(driver d) {
-  switch (d) {
-  case driver::openGL:
-    return get_renderer_impl<driver::openGL>();
-  default:
-    throw unmapped_enum<driver>(d);
-  }
+INSTANTIATE_PROTOTYPE_FACTORY(renderer_impl *);
+renderer_impl * get_renderer_impl(std::string const & id) {
+  renderer_impl * rval = renderer_impl_factory::instance().get(id);
+  if (rval == nullptr) throw unmapped_enum<driver>(id);
+  return rval;
+}
+
+namespace {
+  const math::matr4 identity4 = math::matrix::identity<float, 4>();
 }
 
-direct_renderer::direct_renderer(driver d) : pimpl(::get_renderer_impl(d)) {}
+direct_renderer::direct_renderer(std::string const & driver_id)
+    : direct_renderer(::get_renderer_impl(driver_id)) {}
 
 direct_renderer::direct_renderer(renderer_impl * pi) : pimpl(pi) {}
 
@@ -38,7 +41,7 @@ std::shared_ptr<class manager const> direct_renderer::manager() const {
 void direct_renderer::draw(object const & obj) {
   std::vector<vertex> verts;
   vertices(verts, obj);
-  draw(obj.material, math::matr4(), verts);
+  draw(obj.material, identity4, verts);
 }
 
 void direct_renderer::draw(identity<material> material,
@@ -51,7 +54,12 @@ void direct_renderer::clear() { pimpl->clear(); }
 void direct_renderer::flush() { pimpl->flush(); }
 
 batch_renderer::batch_renderer(renderer * impl, std::size_t batch_size)
-    : impl_(impl), batches_(), batch_size_(batch_size), elements_(0) {}
+    : batch_renderer(impl, identity4, batch_size) {}
+
+batch_renderer::batch_renderer(renderer * impl, math::matr4 const & to_world,
+                               std::size_t batch_size)
+    : impl_(impl), batches_(), object_to_world_(to_world),
+      batch_size_(batch_size), elements_(0) {}
 
 batch_renderer::~batch_renderer() { flush(); }
 
@@ -74,7 +82,7 @@ void batch_renderer::draw(identity<material> material, math::matr4 const &,
 
 void batch_renderer::flush() {
   write();
-  impl_->flush();
+  //  impl_->flush();
 }
 
 void batch_renderer::check() {
@@ -83,8 +91,8 @@ void batch_renderer::check() {
 
 void batch_renderer::write() {
   for (auto & pair : batches_) {
-    impl_->draw(pair.first, math::matrix::identity<float, 4>(), pair.second);
+    impl_->draw(pair.first, object_to_world_, pair.second);
+    pair.second.clear();
   }
-  batches_.clear();
   elements_ = 0;
 }

+ 3 - 1
graphics/src/renderer_impl.hpp

@@ -12,8 +12,10 @@
 #include "game/graphics/renderer.hpp"
 #include "game/math/math_fwd.hpp"
 
+#include "resource_factory/prototype_factory.hpp"
+
 namespace graphics {
-  template <driver> renderer_impl * get_renderer_impl();
+  using renderer_impl_factory = objects::prototype::factory<renderer_impl *>;
 
   struct renderer_impl {
     virtual ~renderer_impl() {}

+ 1 - 1
graphics/test/manager_test.cxx

@@ -21,6 +21,7 @@ namespace env { namespace detail {
 }}
 #endif
 
+using testing::_;
 using testing::AllOf;
 using testing::AnyNumber;
 using testing::Eq;
@@ -28,7 +29,6 @@ using testing::Ge;
 using testing::Le;
 using testing::Ne;
 using testing::SizeIs;
-using testing::_;
 
 struct mock_manager_impl : graphics::manager {
   using shader = graphics::shader;

+ 50 - 9
graphics/test/renderer_test.cxx

@@ -9,16 +9,18 @@
 #include <gmock/gmock.h>
 
 #include "../src/renderer_impl.hpp"
+#include "game/graphics/exception.h"
 #include "game/graphics/object.hpp"
 #include "game/graphics/renderer.hpp"
 #include "game/graphics/vertex.h"
 #include "game/math/shape.hpp"
 #include "matrix/matrix.hpp"
+#include "matrix/matrix_helpers.hpp"
 
+using testing::_;
 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>());
@@ -61,8 +63,9 @@ TEST_F(DirectRendererTest, DrawPassesToDraw) {
   renderer->draw(cast(1), math::matr4(), {});
 }
 
-TEST_F(DirectRendererTest, DrawObjectHasEmptyTranslation) {
-  EXPECT_CALL(*mock, draw(cast(1), math::matr4(), _)).Times(1);
+TEST_F(DirectRendererTest, DrawObjectHasIdenTranslation) {
+  auto identity = math::matrix::identity<float, 4>();
+  EXPECT_CALL(*mock, draw(cast(1), identity, _)).Times(1);
   renderer->draw(DemoObject());
 }
 
@@ -108,8 +111,8 @@ void BatchRendererTest::TearDown() {
   delete mock;
 }
 
-TEST_F(BatchRendererTest, CallsFlushOnDestructor) {
-  EXPECT_CALL(*mock, flush()).Times(1);
+TEST_F(BatchRendererTest, DoesNotCallFlushOnDestructor) {
+  EXPECT_CALL(*mock, flush()).Times(0);
   renderer.reset();
   testing::Mock::VerifyAndClearExpectations(mock);
 }
@@ -120,10 +123,8 @@ TEST_F(BatchRendererTest, ClearPassesToClear) {
   renderer->clear();
 }
 
-TEST_F(BatchRendererTest, FlushPassesToFlush) {
-  EXPECT_CALL(*mock, flush()).Times(AnyNumber());
-  // This will end up changing
-  EXPECT_CALL(*mock, flush()).Times(1).RetiresOnSaturation();
+TEST_F(BatchRendererTest, FlushDoesNotPassToFlush) {
+  EXPECT_CALL(*mock, flush()).Times(0);
   renderer->flush();
 }
 
@@ -148,6 +149,8 @@ TEST_F(BatchRendererTest, GroupsDataTogetherByMaterial) {
 TEST_F(BatchRendererTest, BatchLimitingCanSplitTheWrites) {
   EXPECT_CALL(*mock, flush()).Times(AnyNumber());
   renderer.reset(new graphics::batch_renderer(drenderer.get(), 15));
+  // This sometimes occurs...
+  EXPECT_CALL(*mock, draw(_, _, SizeIs(0))).Times(AnyNumber());
   EXPECT_CALL(*mock, draw(cast(1), _, SizeIs(12))).Times(1);
   EXPECT_CALL(*mock, draw(cast(2), _, SizeIs(6))).Times(2);
   renderer->draw(DemoObject());
@@ -155,3 +158,41 @@ TEST_F(BatchRendererTest, BatchLimitingCanSplitTheWrites) {
   renderer->draw(DemoObject(2));
   renderer->draw(DemoObject(2));
 }
+
+struct InjectRendererTest : testing::Test {
+protected:
+  void SetUp() override;
+  void TearDown() override;
+  mock_renderer_impl * mock;
+
+private:
+  graphics::renderer_impl_factory::scoped_binding scope_;
+};
+
+void InjectRendererTest::SetUp() {
+  mock = new mock_renderer_impl;
+  auto & factory = graphics::renderer_impl_factory::instance();
+  scope_ = factory.bind_scoped("mock", [this]() { return mock; });
+}
+
+void InjectRendererTest::TearDown() { delete mock; }
+
+TEST_F(InjectRendererTest, ThrowsOnRequestOfBlank) {
+  EXPECT_THROW(graphics::direct_renderer(""),
+               graphics::unmapped_enum<graphics::driver>);
+}
+
+TEST(UnboundRendererTest, ThrowsOnRequestOfUnboundDriver) {
+  EXPECT_THROW(graphics::direct_renderer("mock"),
+               graphics::unmapped_enum<graphics::driver>);
+}
+
+TEST_F(InjectRendererTest, SuccessfulConstructionWithBoundImpl) {
+  EXPECT_NO_THROW(graphics::direct_renderer("mock"));
+}
+
+TEST_F(InjectRendererTest, BoundImplIsSamePointed) {
+  graphics::direct_renderer renderer("mock");
+  EXPECT_CALL(*mock, draw(cast(1), math::matr4(), IsEmpty())).Times(1);
+  renderer.draw(cast(1), math::matr4(), {});
+}

+ 1 - 0
include/resource_factory

@@ -0,0 +1 @@
+Subproject commit 5131e8ab6f8ec488d9e9fc9d82aac6f5ba09c620

+ 3 - 7
math/math.xcodeproj/xcshareddata/xcschemes/math-test.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1030"
+   LastUpgradeVersion = "1110"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"
@@ -10,9 +10,9 @@
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
       codeCoverageEnabled = "YES"
-      onlyGenerateCoverageForSpecifiedTargets = "YES"
-      shouldUseLaunchSchemeArgsEnv = "YES">
+      onlyGenerateCoverageForSpecifiedTargets = "YES">
       <CodeCoverageTargets>
          <BuildableReference
             BuildableIdentifier = "primary"
@@ -34,8 +34,6 @@
             </BuildableReference>
          </TestableReference>
       </Testables>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -47,8 +45,6 @@
       debugDocumentVersioning = "YES"
       debugServiceExtension = "internal"
       allowLocationSimulation = "YES">
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 1 - 1
math/src/common.cpp

@@ -129,7 +129,7 @@ namespace math {
 
   // 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) {
+  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};

+ 4 - 6
math/test/common_test.cxx

@@ -254,9 +254,7 @@ TEST(TriangleTest, TriangleIntersectsSelf) {
 
 TEST(TriangleTest, DoesNotIntersectOffset) {
   triangle lhs{{{0, 0}}, {{0, 1}}, {{1, 0}}};
-  auto tri = [](math::dim2::point const & pt) {
-    return triangle{pt, pt, pt};
-  };
+  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}};
@@ -285,19 +283,19 @@ TEST(QuadTest, IntersectsContain) {
 
 TEST(QuadTest, NoIntersectionAtEdge) {
   quad const lhs = square{{{-0.5f, -0.5f}}, 1};
-  quad const rhs = square{{{ 0.0f,  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};
+  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};
+  quad const rhs = square{{{0.5f, 0.5f}}, 1};
   EXPECT_FALSE(math::intersects(lhs, rhs));
 }
 

+ 0 - 1
math/test/shape_test.cxx

@@ -49,7 +49,6 @@ TEST(LineTest, UnitLineHasLengthOne) {
   EXPECT_THAT(unit.length(), Eq(1));
 }
 
-
 TEST(LineTest, ParallelLinesHaveSameSlope) {
   line const lhs{{{0, 0}}, {{1, 0}}};
   line const rhs{{{-1, 0}}, {{-2, 0}}};