diff --git a/demos/viewer/src/viewer.cpp b/demos/viewer/src/viewer.cpp
index a5e5dc0390d4c6069bb079a3e661ad919d859a9b..bb293a384a638272b18eca2dd3b178a20fd135c8 100644
--- a/demos/viewer/src/viewer.cpp
+++ b/demos/viewer/src/viewer.cpp
@@ -52,57 +52,11 @@
 #include "phx/transform.hpp"
 #include "phx/window.hpp"
 
-#include "controller_behavior.hpp"
 #include "navigation_behavior.hpp"
 #include "phx/display_system_openvr.hpp"
 #include "rotation_behavior.hpp"
 #include "viewer_system.hpp"
 
-void AddControllerEntity(const std::shared_ptr<phx::Scene>& scene,
-                         phx::ResourcePointer<phx::Mesh> mesh,
-                         phx::ResourcePointer<phx::Material> material,
-                         ControllerBehavior::Side side) {
-  phx::Entity* controller = scene->CreateEntity();
-  controller->AddComponent<phx::MeshHandle>()->SetMesh(mesh);
-  controller->AddComponent<phx::Transform>();
-  controller->AddComponent<phx::MaterialHandle>()->SetMaterial(material);
-  controller->AddComponent<ControllerBehavior>()->SetSide(side);
-}
-
-void AddController(const std::shared_ptr<phx::Scene>& scene,
-                   ControllerBehavior::Side side) {
-  auto& resource_manager = phx::ResourceManager::instance();
-
-  phx::ResourceDeclaration mesh_declaration{
-      {"TYPE", "openVR"},
-      {"OpenVR_type", "mesh"},
-      {"side", (side == ControllerBehavior::RIGHT ? "right" : "left")}};
-  auto mesh_proxy =
-      phx::ResourceManager::instance().DeclareResource(mesh_declaration);
-  mesh_proxy->Load();
-
-  phx::ResourceDeclaration material_declaration{{"TYPE", "openVR"},
-                                                {"OpenVR_type", "material"}};
-  auto material_proxy = resource_manager.DeclareResource(material_declaration);
-  material_proxy->Load();
-
-  if (mesh_proxy->GetAs<phx::Mesh>() != nullptr) {
-    AddControllerEntity(scene, phx::ResourcePointer<phx::Mesh>(mesh_proxy),
-                        phx::ResourcePointer<phx::Material>(material_proxy),
-                        side);
-  }
-}
-
-void SetupOpenVRController(const std::shared_ptr<phx::Scene>& scene,
-                           phx::HMD* hmd) {
-  auto& resource_manager = phx::ResourceManager::instance();
-  auto openvr_loader = std::make_unique<phx::OpenVRResourceLoader>(hmd);
-  resource_manager.RegisterResourceType("openVR", std::move(openvr_loader));
-
-  AddController(scene, ControllerBehavior::RIGHT);
-  AddController(scene, ControllerBehavior::LEFT);
-}
-
 int main(int, char**) {
   std::unique_ptr<phx::Engine> engine = phx::Setup::CreateDefaultEngine();
   auto scene = engine->GetScene();
@@ -126,15 +80,8 @@ int main(int, char**) {
       viewer_system->SetShowFramerate(!viewer_system->GetShowFramerate());
   });
 
-  auto display_system_hmd = engine->GetSystem<phx::DisplaySystemOpenVR>();
-  if (display_system_hmd != nullptr) {
-    auto hmd = display_system_hmd->GetHMD();
-    if (hmd != nullptr) {
-      // SetupOpenVRController(scene, hmd);
-    }
-  }
-
-  phx::SceneLoader::InsertModelIntoScene("models/bunny.obj", scene.get());
+  phx::SceneLoader::InsertModelIntoScene(
+      "models/UniversityScene/Univers20171013.obj", scene.get());
 
   std::vector<glm::quat> light_dirs{
       glm::quat(glm::angleAxis(-0.25f * glm::pi<float>(), glm::vec3(1, 0, 0))),
diff --git a/library/phx/hmd.cpp b/library/phx/hmd.cpp
index a4cbcbbda4bfa35d6c9973ebdd0133e3760242a7..4f8c5d3e7edd18133fe39a504e9924a9548f1e20 100644
--- a/library/phx/hmd.cpp
+++ b/library/phx/hmd.cpp
@@ -80,6 +80,11 @@ std::vector<vr::TrackedDeviceIndex_t> HMD::GetControllerIndices() {
   return std::move(indices);
 }
 
+vr::ETrackedControllerRole HMD::GetControllerRoleForTrackedDeviceIndex(
+    vr::TrackedDeviceIndex_t device_index) const {
+  return vr_system_->GetControllerRoleForTrackedDeviceIndex(device_index);
+}
+
 const glm::uvec2& HMD::GetViewportSize() const { return viewport_size_; }
 
 const glm::mat4& HMD::GetProjectionMatrix(Side side) const {
@@ -137,7 +142,9 @@ glm::mat4 HMD::GetRightControllerTransformation() {
 
 std::unique_ptr<phx::Mesh> HMD::GetControllerMesh(Controller controller) {
   auto model = GetControllerModel(controller);
-  if (model == nullptr) return nullptr;
+  if (model == nullptr) {
+    return nullptr;
+  }
   auto mesh = std::make_unique<phx::Mesh>();
   std::vector<glm::vec3> vertices;
   std::vector<glm::vec3> normals;
@@ -177,7 +184,7 @@ vr::RenderModel_t* HMD::GetControllerModel(Controller controller) {
     }
   }
 
-  vr::RenderModel_t* model;
+  vr::RenderModel_t* model = nullptr;
   while (vr::VRRenderModels()->LoadRenderModel_Async(
              &rendermodel_name[0], &model) == vr::VRRenderModelError_Loading) {
     std::this_thread::sleep_for(std::chrono::milliseconds(50));
@@ -225,9 +232,9 @@ std::unique_ptr<Image> HMD::GetControllerTexture(int id) {
             texture_map->rubTextureMapData + image_data.size(),
             image_data.begin());
   auto image = std::make_unique<phx::Image>(
-      image_data, std::array<std::size_t, 2>{
+      &image_data[0], std::array<std::size_t, 2>{
                       {static_cast<std::size_t>(texture_map->unWidth),
-                       static_cast<std::size_t>(texture_map->unHeight)}});
+                       static_cast<std::size_t>(texture_map->unHeight)}}, 32);
 
   return image;
 }
diff --git a/library/phx/hmd.hpp b/library/phx/hmd.hpp
index a3e0b7cfb165fc91f4e9622a6261cf803889fd59..82bedf2cfaf9e305a32b7126ec7415fd6635d45b 100644
--- a/library/phx/hmd.hpp
+++ b/library/phx/hmd.hpp
@@ -78,6 +78,8 @@ class PHOENIX_EXPORT HMD {
   void Submit(Side side, gl::texture_2d* texture);
 
   std::vector<vr::TrackedDeviceIndex_t> GetControllerIndices();
+  vr::ETrackedControllerRole GetControllerRoleForTrackedDeviceIndex(
+      vr::TrackedDeviceIndex_t device_index) const;
 
  private:
   glm::mat4 GetTransformationForRole(vr::ETrackedControllerRole role);
diff --git a/library/phx/image.hpp b/library/phx/image.hpp
index 5f686d332ed717a515565219c031645531368082..b9ccc8bcf7a0cb9f6cec1b2c406a9a6f15f1f941 100644
--- a/library/phx/image.hpp
+++ b/library/phx/image.hpp
@@ -71,7 +71,8 @@ class PHOENIX_EXPORT Image : public Resource, public Loggable {
         static_cast<std::int32_t>(dimensions[1]),
         static_cast<std::int32_t>((bits_per_pixel * dimensions[0] + 31) / 32 *
                                   4),
-        static_cast<std::int32_t>(bits_per_pixel), 0, 0, 0, false);
+        static_cast<std::int32_t>(bits_per_pixel), FI_RGBA_RED_MASK,
+        FI_RGBA_GREEN_MASK, FI_RGBA_BLUE_MASK, false);
     if (!native_) {
       throw std::runtime_error("FreeImage_ConvertFromRawBitsEx failed.");
     }
diff --git a/demos/viewer/src/controller_behavior.cpp b/library/phx/openvr_controller_behavior.cpp
similarity index 84%
rename from demos/viewer/src/controller_behavior.cpp
rename to library/phx/openvr_controller_behavior.cpp
index f4fc9a45a6bcdc867ff836d6a039a9039a924f09..51620fe1f74a172e3890a0207fa47f23c91a4383 100644
--- a/demos/viewer/src/controller_behavior.cpp
+++ b/library/phx/openvr_controller_behavior.cpp
@@ -20,14 +20,15 @@
 // limitations under the License.
 //------------------------------------------------------------------------------
 
-#include "controller_behavior.hpp"
+#include "openvr_controller_behavior.hpp"
 
 #include "phx/entity.hpp"
 #include "phx/runtime_component.hpp"
 #include "phx/scene.hpp"
 #include "phx/transform.hpp"
 
-void ControllerBehavior::OnUpdate() {
+namespace phx {
+void OpenVRControllerBehavior::OnUpdate() {
   phx::Scene* scene = GetEntity()->GetScene();
   phx::Entity* runtime_entity = nullptr;
   if (side_ == Side::LEFT) {
@@ -48,8 +49,14 @@ void ControllerBehavior::OnUpdate() {
       !(GetEntity()->GetFirstComponent<phx::Transform>()->GetParent() ==
         runtime_entity->GetFirstComponent<phx::Transform>())) {
     GetEntity()->GetFirstComponent<phx::Transform>()->SetParent(
-        runtime_entity->GetFirstComponent<phx::Transform>());
+        runtime_entity->GetFirstComponent<phx::Transform>(), false);
   }
 }
 
-void ControllerBehavior::SetSide(Side side) { side_ = side; }
+void OpenVRControllerBehavior::SetSide(Side side) { side_ = side; }
+
+OpenVRControllerBehavior::Side OpenVRControllerBehavior::GetSide() const {
+  return side_;
+}
+
+}  // namespace phx
diff --git a/demos/viewer/src/controller_behavior.hpp b/library/phx/openvr_controller_behavior.hpp
similarity index 79%
rename from demos/viewer/src/controller_behavior.hpp
rename to library/phx/openvr_controller_behavior.hpp
index 5f6855c2736df123f19220e24df2f6ccbd571a1a..c8fb9898d17f86bb238273837acb31dcc97df088 100644
--- a/demos/viewer/src/controller_behavior.hpp
+++ b/library/phx/openvr_controller_behavior.hpp
@@ -20,21 +20,26 @@
 // limitations under the License.
 //------------------------------------------------------------------------------
 
-#ifndef DEMOS_VIEWER_SRC_CONTROLLER_BEHAVIOR_HPP_
-#define DEMOS_VIEWER_SRC_CONTROLLER_BEHAVIOR_HPP_
+#ifndef LIBRARY_PHX_OPENVR_CONTROLLER_BEHAVIOR_HPP_
+#define LIBRARY_PHX_OPENVR_CONTROLLER_BEHAVIOR_HPP_
 
 #include "phx/behavior.hpp"
+#include "phx/export.hpp"
 
-class ControllerBehavior : public phx::Behavior {
+namespace phx {
+
+class PHOENIX_EXPORT OpenVRControllerBehavior : public Behavior {
  public:
   enum Side { LEFT, RIGHT };
 
   void OnUpdate() override;
 
   void SetSide(Side side);
+  Side GetSide() const;
 
  private:
   Side side_ = Side::LEFT;
 };
+}  // namespace phx
 
-#endif  // DEMOS_VIEWER_SRC_CONTROLLER_BEHAVIOR_HPP_
+#endif  // LIBRARY_PHX_OPENVR_CONTROLLER_BEHAVIOR_HPP_
diff --git a/library/phx/openvr_controller_system.cpp b/library/phx/openvr_controller_system.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..737f3be65a0201f4929455f29bf46e43456cccc9
--- /dev/null
+++ b/library/phx/openvr_controller_system.cpp
@@ -0,0 +1,146 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include "openvr_controller_system.hpp"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "display_system_openvr.hpp"
+#include "logger.hpp"
+#include "material_handle.hpp"
+#include "mesh_handle.hpp"
+#include "openvr_controller_behavior.hpp"
+#include "openvr_resource_loader.hpp"
+#include "resource_manager.hpp"
+#include "resource_pointer.hpp"
+#include "transform.hpp"
+
+namespace phx {
+
+OpenVRControllerSystem::OpenVRControllerSystem(Engine* engine,
+                                               DisplaySystem* display_system)
+    : System(engine) {
+  // find HMD
+  hmd_ = dynamic_cast<DisplaySystemOpenVR*>(display_system)->GetHMD();
+  if (hmd_ == nullptr) {
+    error("Cannot use OpenVRControllerSystem without an HMD!");
+    return;
+  }
+
+  auto& resource_manager = ResourceManager::instance();
+  auto openvr_loader = std::make_unique<OpenVRResourceLoader>(hmd_);
+  resource_manager.RegisterResourceType("openVR", std::move(openvr_loader));
+}
+
+Entity* AddControllerEntity(const std::shared_ptr<phx::Scene>& scene,
+                            ResourcePointer<Mesh> mesh,
+                            ResourcePointer<Material> material,
+                            OpenVRControllerBehavior::Side side) {
+  Entity* controller = scene->CreateEntity();
+  controller->AddComponent<MeshHandle>()->SetMesh(mesh);
+  controller->AddComponent<Transform>();
+  controller->AddComponent<MaterialHandle>()->SetMaterial(material);
+  controller->AddComponent<OpenVRControllerBehavior>()->SetSide(side);
+
+  return controller;
+}
+
+Entity* AddController(const std::shared_ptr<phx::Scene>& scene,
+                      OpenVRControllerBehavior::Side side) {
+  auto& resource_manager = ResourceManager::instance();
+  std::string side_string =
+      side == OpenVRControllerBehavior::LEFT ? "left" : "right";
+
+  ResourceDeclaration mesh_declaration{
+      {"TYPE", "openVR"}, {"OpenVR_type", "mesh"}, {"side", side_string}};
+  auto mesh_proxy =
+      ResourceManager::instance().DeclareResource(mesh_declaration);
+  mesh_proxy->Load();
+
+  ResourceDeclaration material_declaration{
+      {"TYPE", "openVR"}, {"OpenVR_type", "material"}, {"side", side_string}};
+  auto material_proxy = resource_manager.DeclareResource(material_declaration);
+  material_proxy->Load();
+
+  if (mesh_proxy->GetAs<Mesh>() != nullptr) {
+    return AddControllerEntity(scene, ResourcePointer<Mesh>(mesh_proxy),
+                               ResourcePointer<Material>(material_proxy), side);
+  }
+  return nullptr;
+}
+
+void OpenVRControllerSystem::Update(const FrameTimer::TimeInfo&) {
+  // check which controllers are active and update their scene representation,
+  // if necessary
+  if (!hmd_) return;
+  auto scene = GetEngine()->GetScene();
+  if (scene == nullptr) return;
+
+  // get controller entities in the scene
+  Entity* left_controller_entity = nullptr;
+  Entity* right_controller_entity = nullptr;
+  auto controller_entities =
+      GetEngine()->GetEntitiesWithComponents<OpenVRControllerBehavior>();
+  for (auto entity : controller_entities) {
+    if (entity->GetFirstComponent<OpenVRControllerBehavior>()->GetSide() ==
+        OpenVRControllerBehavior::LEFT) {
+      left_controller_entity = entity;
+    } else {
+      right_controller_entity = entity;
+    }
+  }
+
+  auto controller_indices = hmd_->GetControllerIndices();
+  bool left_controller_active = false;
+  bool right_controller_active = false;
+  for (auto idx : controller_indices) {
+    // is it a left controller?
+    auto role = hmd_->GetControllerRoleForTrackedDeviceIndex(idx);
+    if (role == vr::TrackedControllerRole_LeftHand) {
+      // do we have a left controller in the scene?
+      if (left_controller_entity == nullptr) {
+        // create that controller
+        left_controller_entity =
+            AddController(scene, OpenVRControllerBehavior::LEFT);
+      }
+      left_controller_active = true;
+    } else if (role == vr::TrackedControllerRole_RightHand) {
+      if (right_controller_entity == nullptr) {
+        right_controller_entity =
+            AddController(scene, OpenVRControllerBehavior::RIGHT);
+      }
+      right_controller_active = true;
+    }
+  }
+
+  // remove unnecessary entities
+  if (!left_controller_active && left_controller_entity != nullptr) {
+    scene->RemoveEntity(left_controller_entity);
+  }
+  if (!right_controller_active && right_controller_entity != nullptr) {
+    scene->RemoveEntity(right_controller_entity);
+  }
+}
+
+}  // namespace phx
diff --git a/library/phx/openvr_controller_system.hpp b/library/phx/openvr_controller_system.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..21a3f8b91e243cd6643f34ed9921a31980561ed2
--- /dev/null
+++ b/library/phx/openvr_controller_system.hpp
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#ifndef LIBRARY_PHX_OPENVR_CONTROLLER_SYSTEM_HPP_
+#define LIBRARY_PHX_OPENVR_CONTROLLER_SYSTEM_HPP_
+
+#include <memory>
+#include <vector>
+
+SUPPRESS_WARNINGS_BEGIN
+#include "glm/glm.hpp"
+SUPPRESS_WARNINGS_END
+
+#include "phx/display_system.hpp"
+#include "phx/engine.hpp"
+#include "phx/export.hpp"
+#include "phx/hmd.hpp"
+#include "phx/system.hpp"
+
+namespace phx {
+class PHOENIX_EXPORT OpenVRControllerSystem : public System {
+ public:
+  OpenVRControllerSystem() = delete;
+  OpenVRControllerSystem(const OpenVRControllerSystem&) = delete;
+  OpenVRControllerSystem(OpenVRControllerSystem&&) = default;
+  virtual ~OpenVRControllerSystem() = default;
+
+  OpenVRControllerSystem& operator=(const OpenVRControllerSystem&) = delete;
+  OpenVRControllerSystem& operator=(OpenVRControllerSystem&&) = default;
+
+  void Update(const FrameTimer::TimeInfo&) override;
+
+ protected:
+  template <typename SystemType, typename... SystemArguments>
+  friend SystemType* Engine::CreateSystem(SystemArguments&&... arguments);
+  OpenVRControllerSystem(Engine* engine, DisplaySystem* display_system);
+
+ private:
+  HMD* hmd_ = nullptr;
+};
+
+}  // namespace phx
+
+#endif  // LIBRARY_PHX_OPENVR_CONTROLLER_SYSTEM_HPP_
diff --git a/library/phx/setup.cpp b/library/phx/setup.cpp
index 05b837d3ae794d2c0ea33a6134acbaf19e8daaae..2b97bad44ba18b34ec81f4a458f3869c44af6318 100644
--- a/library/phx/setup.cpp
+++ b/library/phx/setup.cpp
@@ -35,6 +35,7 @@
 #include "hmd.hpp"
 #include "input_system.hpp"
 #include "logger.hpp"
+#include "openvr_controller_system.hpp"
 #include "render_target.hpp"
 #include "rendering_system.hpp"
 #include "tracking_system_openvr.hpp"
@@ -84,8 +85,11 @@ std::unique_ptr<Engine> Setup::CreateDefaultEngine() {
     SetupDefaultFrameGraphOpenVR(rendering_system);
     auto tracking_system =
         engine->CreateSystem<TrackingSystemOpenVR>(displaysys_hmd);
+    auto controller_system = engine->CreateSystem<OpenVRControllerSystem>(
+        engine->GetSystem<DisplaySystem>());
     engine->GetUpdateOrder().MoveBefore(tracking_system, rendering_system);
     engine->GetUpdateOrder().MoveAfter(behavior_system, tracking_system);
+    engine->GetUpdateOrder().MoveAfter(tracking_system, controller_system);
   } else {
     auto render_targets = displaysys_window->CreateRenderTargets();
     rendering_system->SetRenderTargets(&render_targets);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 0c516001fb8a2bb48d0353a54fac5d8469af3628..14e3a3d43d80b7ada144e5e8a3f81e07489abb90 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -226,6 +226,7 @@ add_mocked_test(test_shader MOCK_GLEW)
 add_mocked_test(test_display_system MOCK_SDL)
 add_mocked_test(test_engine MOCK_SDL MOCK_OPENVR MOCK_GLEW)
 add_mocked_test(test_tracking_system MOCK_SDL MOCK_OPENVR)
+add_mocked_test(test_openvr_controller_system MOCK_SDL MOCK_OPENVR MOCK_GLEW)
 
 add_mocked_test(integration_test_model_rendering MOCK_OPENVR)
 add_mocked_test(integration_test_opengl_buffer_data_download MOCK_OPENVR)
diff --git a/tests/src/mocks/openvr_mock.hpp b/tests/src/mocks/openvr_mock.hpp
index 89ecec4ee202166ee92d3cf35ac315b825a7f496..77881d7054b6d4a82e1777140ec2b34724e70a8c 100644
--- a/tests/src/mocks/openvr_mock.hpp
+++ b/tests/src/mocks/openvr_mock.hpp
@@ -39,11 +39,11 @@ using trompeloeil::_;
 class OPENVR_MOCK_EXPORT OpenVRMockInternal {
  public:
   MAKE_MOCK0(VR_IsHmdPresent, bool());
-  MAKE_MOCK2(VR_GetGenericInterface, void*(const char*, vr::EVRInitError*));
-  MAKE_MOCK3(VR_InitInternal2,
-             uint32_t(vr::EVRInitError*, vr::EVRApplicationType, const char*));
+  MAKE_MOCK2(VR_GetGenericInterface, void *(const char *, vr::EVRInitError *));
+  MAKE_MOCK3(VR_InitInternal2, uint32_t(vr::EVRInitError *,
+                                        vr::EVRApplicationType, const char *));
   MAKE_MOCK0(VR_ShutdownInternal, void());
-  MAKE_MOCK1(VR_IsInterfaceVersionValid, bool(const char*));
+  MAKE_MOCK1(VR_IsInterfaceVersionValid, bool(const char *));
   MAKE_MOCK0(VR_GetInitToken, uint32_t());
 };
 
@@ -52,30 +52,31 @@ namespace vr {
 class OPENVR_MOCK_EXPORT IVRSystemMock : public IVRSystem {
  public:
   virtual ~IVRSystemMock() = default;
-  MAKE_MOCK2(GetRecommendedRenderTargetSize, void(uint32_t*, uint32_t*));
-  MAKE_MOCK5(GetProjectionRaw, void(EVREye, float*, float*, float*, float*));
+  MAKE_MOCK2(GetRecommendedRenderTargetSize, void(uint32_t *, uint32_t *));
+  MAKE_MOCK5(GetProjectionRaw,
+             void(EVREye, float *, float *, float *, float *));
   MAKE_MOCK4(ComputeDistortion,
-             bool(EVREye, float, float, DistortionCoordinates_t*));
-  MAKE_MOCK2(GetTimeSinceLastVsync, bool(float*, uint64_t*));
+             bool(EVREye, float, float, DistortionCoordinates_t *));
+  MAKE_MOCK2(GetTimeSinceLastVsync, bool(float *, uint64_t *));
   MAKE_MOCK0(GetD3D9AdapterIndex, int32_t());
-  MAKE_MOCK1(GetDXGIOutputInfo, void(int32_t*));
-  MAKE_MOCK3(GetOutputDevice, void(uint64_t*, ETextureType, VkInstance_T*));
+  MAKE_MOCK1(GetDXGIOutputInfo, void(int32_t *));
+  MAKE_MOCK3(GetOutputDevice, void(uint64_t *, ETextureType, VkInstance_T *));
   MAKE_MOCK0(IsDisplayOnDesktop, bool());
   MAKE_MOCK1(SetDisplayVisibility, bool(bool));
   MAKE_MOCK4(GetDeviceToAbsoluteTrackingPose,
-             void(ETrackingUniverseOrigin, float, TrackedDevicePose_t*,
+             void(ETrackingUniverseOrigin, float, TrackedDevicePose_t *,
                   uint32_t));
   MAKE_MOCK0(ResetSeatedZeroPose, void());
   MAKE_MOCK0(GetSeatedZeroPoseToStandingAbsoluteTrackingPose, HmdMatrix34_t());
   MAKE_MOCK0(GetRawZeroPoseToStandingAbsoluteTrackingPose, HmdMatrix34_t());
   MAKE_MOCK4(GetSortedTrackedDeviceIndicesOfClass,
-             uint32_t(ETrackedDeviceClass, TrackedDeviceIndex_t*, uint32_t,
+             uint32_t(ETrackedDeviceClass, TrackedDeviceIndex_t *, uint32_t,
                       TrackedDeviceIndex_t));
   MAKE_MOCK1(GetTrackedDeviceActivityLevel,
              EDeviceActivityLevel(TrackedDeviceIndex_t));
   MAKE_MOCK3(ApplyTransform,
-             void(TrackedDevicePose_t*, const TrackedDevicePose_t*,
-                  const HmdMatrix34_t*));
+             void(TrackedDevicePose_t *, const TrackedDevicePose_t *,
+                  const HmdMatrix34_t *));
   MAKE_MOCK1(GetTrackedDeviceIndexForControllerRole,
              TrackedDeviceIndex_t(vr::ETrackedControllerRole));
   MAKE_MOCK1(GetControllerRoleForTrackedDeviceIndex,
@@ -85,43 +86,43 @@ class OPENVR_MOCK_EXPORT IVRSystemMock : public IVRSystem {
   MAKE_MOCK1(IsTrackedDeviceConnected, bool(vr::TrackedDeviceIndex_t));
   MAKE_MOCK3(GetBoolTrackedDeviceProperty,
              bool(vr::TrackedDeviceIndex_t, ETrackedDeviceProperty,
-                  ETrackedPropertyError*));
+                  ETrackedPropertyError *));
   MAKE_MOCK3(GetFloatTrackedDeviceProperty,
              float(vr::TrackedDeviceIndex_t, ETrackedDeviceProperty,
-                   ETrackedPropertyError*));
+                   ETrackedPropertyError *));
   MAKE_MOCK3(GetInt32TrackedDeviceProperty,
              int32_t(vr::TrackedDeviceIndex_t, ETrackedDeviceProperty,
-                     ETrackedPropertyError*));
+                     ETrackedPropertyError *));
   MAKE_MOCK3(GetUint64TrackedDeviceProperty,
              uint64_t(vr::TrackedDeviceIndex_t, ETrackedDeviceProperty,
-                      ETrackedPropertyError*));
+                      ETrackedPropertyError *));
   MAKE_MOCK3(GetMatrix34TrackedDeviceProperty,
              HmdMatrix34_t(vr::TrackedDeviceIndex_t, ETrackedDeviceProperty,
-                           ETrackedPropertyError*));
+                           ETrackedPropertyError *));
   MAKE_MOCK5(GetStringTrackedDeviceProperty,
-             uint32_t(vr::TrackedDeviceIndex_t, ETrackedDeviceProperty, char*,
-                      uint32_t, ETrackedPropertyError*));
-  MAKE_MOCK1(GetPropErrorNameFromEnum, const char*(ETrackedPropertyError));
-  MAKE_MOCK2(PollNextEvent, bool(VREvent_t*, uint32_t));
-  MAKE_MOCK4(PollNextEventWithPose, bool(ETrackingUniverseOrigin, VREvent_t*,
-                                         uint32_t, vr::TrackedDevicePose_t*));
-  MAKE_MOCK1(GetEventTypeNameFromEnum, const char*(EVREventType));
+             uint32_t(vr::TrackedDeviceIndex_t, ETrackedDeviceProperty, char *,
+                      uint32_t, ETrackedPropertyError *));
+  MAKE_MOCK1(GetPropErrorNameFromEnum, const char *(ETrackedPropertyError));
+  MAKE_MOCK2(PollNextEvent, bool(VREvent_t *, uint32_t));
+  MAKE_MOCK4(PollNextEventWithPose, bool(ETrackingUniverseOrigin, VREvent_t *,
+                                         uint32_t, vr::TrackedDevicePose_t *));
+  MAKE_MOCK1(GetEventTypeNameFromEnum, const char *(EVREventType));
   MAKE_MOCK2(GetHiddenAreaMesh, HiddenAreaMesh_t(EVREye, EHiddenAreaMeshType));
   MAKE_MOCK3(GetControllerState, bool(vr::TrackedDeviceIndex_t,
-                                      vr::VRControllerState_t*, uint32_t));
+                                      vr::VRControllerState_t *, uint32_t));
   MAKE_MOCK5(GetControllerStateWithPose,
              bool(ETrackingUniverseOrigin, vr::TrackedDeviceIndex_t,
-                  vr::VRControllerState_t*, uint32_t, TrackedDevicePose_t*));
+                  vr::VRControllerState_t *, uint32_t, TrackedDevicePose_t *));
   MAKE_MOCK3(TriggerHapticPulse,
              void(vr::TrackedDeviceIndex_t, uint32_t, unsigned short));
-  MAKE_MOCK1(GetButtonIdNameFromEnum, const char*(EVRButtonId));
+  MAKE_MOCK1(GetButtonIdNameFromEnum, const char *(EVRButtonId));
   MAKE_MOCK1(GetControllerAxisTypeNameFromEnum,
-             const char*(EVRControllerAxisType));
+             const char *(EVRControllerAxisType));
   MAKE_MOCK0(CaptureInputFocus, bool());
   MAKE_MOCK0(ReleaseInputFocus, void());
   MAKE_MOCK0(IsInputFocusCapturedByAnotherProcess, bool());
-  MAKE_MOCK4(DriverDebugRequest,
-             uint32_t(vr::TrackedDeviceIndex_t, const char*, char*, uint32_t));
+  MAKE_MOCK4(DriverDebugRequest, uint32_t(vr::TrackedDeviceIndex_t,
+                                          const char *, char *, uint32_t));
   MAKE_MOCK1(PerformFirmwareUpdate,
              vr::EVRFirmwareError(vr::TrackedDeviceIndex_t));
   MAKE_MOCK0(AcknowledgeQuit_Exiting, void());
@@ -130,9 +131,9 @@ class OPENVR_MOCK_EXPORT IVRSystemMock : public IVRSystem {
   // somehow returning structs is a problem, so this workaround
   // TODO(WJ): should be reworked once this is solved in trompeloeil
   // https://github.com/rollbear/trompeloeil/issues/69
-  MAKE_MOCK3(GetProjectionMatrixArray, float*(vr::EVREye, float, float));
+  MAKE_MOCK3(GetProjectionMatrixArray, float *(vr::EVREye, float, float));
   vr::HmdMatrix44_t GetProjectionMatrix(vr::EVREye, float, float);
-  MAKE_MOCK1(GetEyeToHeadTransformArray, float*(EVREye));
+  MAKE_MOCK1(GetEyeToHeadTransformArray, float *(EVREye));
   HmdMatrix34_t GetEyeToHeadTransform(EVREye);
 };
 
@@ -140,27 +141,28 @@ class OPENVR_MOCK_EXPORT IVRCompositorMock : public IVRCompositor {
  public:
   MAKE_MOCK1(SetTrackingSpace, void(ETrackingUniverseOrigin));
   MAKE_MOCK0(GetTrackingSpace, ETrackingUniverseOrigin());
-  MAKE_MOCK4(WaitGetPoses, EVRCompositorError(TrackedDevicePose_t*, uint32_t,
-                                              TrackedDevicePose_t*, uint32_t));
-  MAKE_MOCK4(GetLastPoses, EVRCompositorError(TrackedDevicePose_t*, uint32_t,
-                                              TrackedDevicePose_t*, uint32_t));
+  MAKE_MOCK4(WaitGetPoses, EVRCompositorError(TrackedDevicePose_t *, uint32_t,
+                                              TrackedDevicePose_t *, uint32_t));
+  MAKE_MOCK4(GetLastPoses, EVRCompositorError(TrackedDevicePose_t *, uint32_t,
+                                              TrackedDevicePose_t *, uint32_t));
   MAKE_MOCK3(GetLastPoseForTrackedDeviceIndex,
-             EVRCompositorError(TrackedDeviceIndex_t, TrackedDevicePose_t*,
-                                TrackedDevicePose_t*));
+             EVRCompositorError(TrackedDeviceIndex_t, TrackedDevicePose_t *,
+                                TrackedDevicePose_t *));
   MAKE_MOCK4(Submit,
-             EVRCompositorError(EVREye, const Texture_t*,
-                                const VRTextureBounds_t*, EVRSubmitFlags));
+             EVRCompositorError(EVREye, const Texture_t *,
+                                const VRTextureBounds_t *, EVRSubmitFlags));
   MAKE_MOCK0(ClearLastSubmittedFrame, void());
   MAKE_MOCK0(PostPresentHandoff, void());
-  MAKE_MOCK2(GetFrameTiming, bool(Compositor_FrameTiming*, uint32_t));
-  MAKE_MOCK2(GetFrameTimings, uint32_t(Compositor_FrameTiming*, uint32_t));
+  MAKE_MOCK2(GetFrameTiming, bool(Compositor_FrameTiming *, uint32_t));
+  MAKE_MOCK2(GetFrameTimings, uint32_t(Compositor_FrameTiming *, uint32_t));
   MAKE_MOCK0(GetFrameTimeRemaining, float());
-  MAKE_MOCK2(GetCumulativeStats, void(Compositor_CumulativeStats*, uint32_t));
+  MAKE_MOCK2(GetCumulativeStats, void(Compositor_CumulativeStats *, uint32_t));
   MAKE_MOCK6(FadeToColor, void(float, float, float, float, float, bool));
   MAKE_MOCK1(GetCurrentFadeColor, HmdColor_t(bool));
   MAKE_MOCK2(FadeGrid, void(float, bool));
   MAKE_MOCK0(GetCurrentGridAlpha, float());
-  MAKE_MOCK2(SetSkyboxOverride, EVRCompositorError(const Texture_t*, uint32_t));
+  MAKE_MOCK2(SetSkyboxOverride,
+             EVRCompositorError(const Texture_t *, uint32_t));
   MAKE_MOCK0(ClearSkyboxOverride, void());
   MAKE_MOCK0(CompositorBringToFront, void());
   MAKE_MOCK0(CompositorGoToBack, void());
@@ -178,22 +180,62 @@ class OPENVR_MOCK_EXPORT IVRCompositorMock : public IVRCompositor {
   MAKE_MOCK0(ForceReconnectProcess, void());
   MAKE_MOCK1(SuspendRendering, void(bool));
   MAKE_MOCK3(GetMirrorTextureD3D11,
-             EVRCompositorError(vr::EVREye, void*, void**));
-  MAKE_MOCK1(ReleaseMirrorTextureD3D11, void(void*));
+             EVRCompositorError(vr::EVREye, void *, void **));
+  MAKE_MOCK1(ReleaseMirrorTextureD3D11, void(void *));
   MAKE_MOCK3(GetMirrorTextureGL,
-             vr::EVRCompositorError(vr::EVREye, vr::glUInt_t*,
-                                    vr::glSharedTextureHandle_t*));
+             vr::EVRCompositorError(vr::EVREye, vr::glUInt_t *,
+                                    vr::glSharedTextureHandle_t *));
   MAKE_MOCK2(ReleaseSharedGLTexture,
              bool(vr::glUInt_t, vr::glSharedTextureHandle_t));
   MAKE_MOCK1(LockGLSharedTextureForAccess, void(vr::glSharedTextureHandle_t));
   MAKE_MOCK1(UnlockGLSharedTextureForAccess, void(vr::glSharedTextureHandle_t));
-  MAKE_MOCK2(GetVulkanInstanceExtensionsRequired, uint32_t(char*, uint32_t));
+  MAKE_MOCK2(GetVulkanInstanceExtensionsRequired, uint32_t(char *, uint32_t));
   MAKE_MOCK3(GetVulkanDeviceExtensionsRequired,
-             uint32_t(VkPhysicalDevice_T*, char*, uint32_t));
+             uint32_t(VkPhysicalDevice_T *, char *, uint32_t));
   MAKE_MOCK1(SetExplicitTimingMode, void(bool));
   MAKE_MOCK0(SubmitExplicitTimingData, EVRCompositorError());
 };
 
+class OPENVR_MOCK_EXPORT IVRRenderModelsMock : public IVRRenderModels {
+ public:
+  MAKE_MOCK2(LoadRenderModel_Async,
+             EVRRenderModelError(const char *, RenderModel_t **));
+  MAKE_MOCK1(FreeRenderModel, void(RenderModel_t *));
+  MAKE_MOCK2(LoadTexture_Async,
+             EVRRenderModelError(TextureID_t, RenderModel_TextureMap_t **));
+  MAKE_MOCK1(FreeTexture, void(RenderModel_TextureMap_t *));
+  MAKE_MOCK3(LoadTextureD3D11_Async,
+             EVRRenderModelError(TextureID_t, void *, void **));
+  MAKE_MOCK2(LoadIntoTextureD3D11_Async,
+             EVRRenderModelError(TextureID_t, void *));
+  MAKE_MOCK1(FreeTextureD3D11, void(void *));
+  MAKE_MOCK3(GetRenderModelName,
+             uint32_t(uint32_t, VR_OUT_STRING() char *, uint32_t));
+  MAKE_MOCK0(GetRenderModelCount, uint32_t());
+  MAKE_MOCK1(GetComponentCount, uint32_t(const char *));
+  MAKE_MOCK4(GetComponentName, uint32_t(const char *, uint32_t,
+                                        VR_OUT_STRING() char *, uint32_t));
+
+  MAKE_MOCK2(GetComponentButtonMask, uint64_t(const char *, const char *));
+  MAKE_MOCK4(GetComponentRenderModelName,
+             uint32_t(const char *, const char *, VR_OUT_STRING() char *,
+                      uint32_t));
+  MAKE_MOCK5(GetComponentState,
+             bool(const char *, const char *, const vr::VRControllerState_t *,
+                  const RenderModel_ControllerMode_State_t *,
+                  RenderModel_ComponentState_t *));
+
+  MAKE_MOCK2(RenderModelHasComponent, bool(const char *, const char *));
+  MAKE_MOCK4(GetRenderModelThumbnailURL,
+             uint32_t(const char *, VR_OUT_STRING() char *, uint32_t,
+                      vr::EVRRenderModelError *));
+  MAKE_MOCK4(GetRenderModelOriginalPath,
+             uint32_t(const char *, VR_OUT_STRING() char *, uint32_t,
+                      vr::EVRRenderModelError *));
+  MAKE_MOCK1(GetRenderModelErrorNameFromEnum,
+             const char *(vr::EVRRenderModelError));
+};
+
 }  // namespace vr
 
 class OPENVR_MOCK_EXPORT OpenVRMock {
@@ -201,7 +243,8 @@ class OPENVR_MOCK_EXPORT OpenVRMock {
   OpenVRMock()
       : mock_(new OpenVRMockInternal),
         ivr_system_mock_(new vr::IVRSystemMock),
-        ivr_compositor_mock_(new vr::IVRCompositorMock) {}
+        ivr_compositor_mock_(new vr::IVRCompositorMock),
+        ivr_render_models_mock_(new vr::IVRRenderModelsMock) {}
   ~OpenVRMock() {
     // We have to leak the mock_ pointer for the time being.
     // see
@@ -209,9 +252,35 @@ class OPENVR_MOCK_EXPORT OpenVRMock {
     // TODO(@tvierjahn) regularly check progress on this issue
   }
 
-  OpenVRMockInternal& Get() { return *mock_; }
-  vr::IVRSystemMock& GetSystem() const { return *ivr_system_mock_; }
-  vr::IVRCompositorMock& GetCompositor() { return *ivr_compositor_mock_; }
+  OpenVRMockInternal &Get() { return *mock_; }
+  vr::IVRSystemMock &GetSystem() const { return *ivr_system_mock_; }
+  vr::IVRCompositorMock &GetCompositor() { return *ivr_compositor_mock_; }
+  vr::IVRRenderModelsMock &GetRenderModels() {
+    return *ivr_render_models_mock_;
+  }
+
+  static vr::RenderModel_t *CreateDummyTriangleModel() {
+    vr::RenderModel_t *model = new vr::RenderModel_t();
+    model->rIndexData = new uint16_t[3]{0, 1, 2};
+    model->unVertexCount = 3;
+    model->unTriangleCount = 1;
+    model->diffuseTextureId = -1;
+
+    vr::RenderModel_Vertex_t vertex1;
+    vertex1.vPosition = {{-0.1f, -0.1f, 0.f}};
+    vertex1.vNormal = {{0.f, 0.f, 1.f}};
+    vertex1.rfTextureCoord[0] = 0.f;
+    vertex1.rfTextureCoord[1] = 0.f;
+    vr::RenderModel_Vertex_t vertex2 = vertex1;
+    vertex2.vPosition = {{+0.1f, -0.1f, 0.f}};
+    vr::RenderModel_Vertex_t vertex3 = vertex1;
+    vertex2.vPosition = {{0.f, +0.1f, 0.f}};
+
+    model->rVertexData =
+        new vr::RenderModel_Vertex_t[3]{vertex1, vertex2, vertex3};
+
+    return model;
+  }
 
   static vr::HmdMatrix44_t toHMDMatrix44_t(float mat[16]);
   static vr::HmdMatrix34_t toHMDMatrix34_t(float mat[12]);
@@ -229,9 +298,10 @@ class OPENVR_MOCK_EXPORT OpenVRMock {
                                     0.0f, 1.8f, 0.0f, 0.0f, 1.0f, 0.0f};
 
  private:
-  OpenVRMockInternal* mock_;
-  vr::IVRSystemMock* ivr_system_mock_;
-  vr::IVRCompositorMock* ivr_compositor_mock_;
+  OpenVRMockInternal *mock_;
+  vr::IVRSystemMock *ivr_system_mock_;
+  vr::IVRCompositorMock *ivr_compositor_mock_;
+  vr::IVRRenderModelsMock *ivr_render_models_mock_;
 };
 
 extern OPENVR_MOCK_EXPORT OpenVRMock openvr_mock;
@@ -251,12 +321,14 @@ extern OPENVR_MOCK_EXPORT OpenVRMock openvr_mock;
       .RETURN(openvr_mock.eye_to_head_right_);                               \
   ALLOW_CALL(openvr_mock.GetSystem(),                                        \
              GetSortedTrackedDeviceIndicesOfClass(_, _, _, _))               \
-      .RETURN(0u);                                                           \
+      .RETURN(2u)                                                            \
+      .SIDE_EFFECT(*_2 = 0u)                                                 \
+      .SIDE_EFFECT(*(_2 + sizeof(vr::TrackedDeviceIndex_t)) = 1u);           \
   ALLOW_CALL(openvr_mock.GetSystem(),                                        \
-             GetControllerRoleForTrackedDeviceIndex(_))                      \
+             GetControllerRoleForTrackedDeviceIndex(0u))                     \
       .RETURN(vr::TrackedControllerRole_LeftHand);                           \
   ALLOW_CALL(openvr_mock.GetSystem(),                                        \
-             GetControllerRoleForTrackedDeviceIndex(_))                      \
+             GetControllerRoleForTrackedDeviceIndex(1u))                     \
       .RETURN(vr::TrackedControllerRole_RightHand);                          \
   ALLOW_CALL(openvr_mock.GetCompositor(), WaitGetPoses(_, _, _, _))          \
       .RETURN(vr::EVRCompositorError::VRCompositorError_None)                \
@@ -268,16 +340,28 @@ extern OPENVR_MOCK_EXPORT OpenVRMock openvr_mock;
       .RETURN(vr::EVRCompositorError::VRCompositorError_None);               \
   ALLOW_CALL(openvr_mock.Get(), VR_GetGenericInterface(_, _))                \
       .WITH(std::string(vr::IVRSystem_Version) == std::string(_1))           \
-      .RETURN(static_cast<void*>(&openvr_mock.GetSystem()));                 \
+      .RETURN(static_cast<void *>(&openvr_mock.GetSystem()));                \
+  ALLOW_CALL(openvr_mock.Get(), VR_GetGenericInterface(_, _))                \
+      .WITH(std::string(vr::IVRRenderModels_Version) == std::string(_1))     \
+      .RETURN(static_cast<void *>(&openvr_mock.GetRenderModels()));          \
   ALLOW_CALL(openvr_mock.Get(), VR_GetGenericInterface(_, _))                \
       .WITH(std::string(vr::IVRCompositor_Version) == std::string(_1))       \
-      .RETURN(static_cast<void*>(&openvr_mock.GetCompositor()));             \
+      .RETURN(static_cast<void *>(&openvr_mock.GetCompositor()));            \
   ALLOW_CALL(openvr_mock.Get(),                                              \
              VR_InitInternal2(_, vr::VRApplication_Scene, _))                \
       .RETURN(static_cast<uint32_t>(1337))                                   \
       .SIDE_EFFECT(*_1 = vr::VRInitError_None);                              \
   ALLOW_CALL(openvr_mock.Get(), VR_ShutdownInternal());                      \
   ALLOW_CALL(openvr_mock.Get(), VR_IsInterfaceVersionValid(_)).RETURN(true); \
-  ALLOW_CALL(openvr_mock.Get(), VR_GetInitToken()).RETURN(1337);
+  ALLOW_CALL(openvr_mock.Get(), VR_GetInitToken()).RETURN(1337);             \
+  ALLOW_CALL(openvr_mock.GetRenderModels(), LoadRenderModel_Async(_, _))     \
+      .RETURN(vr::VRRenderModelError_None)                                   \
+      .SIDE_EFFECT(*_2 = openvr_mock.CreateDummyTriangleModel());            \
+  ALLOW_CALL(openvr_mock.GetSystem(),                                        \
+             GetStringTrackedDeviceProperty(_, _, _, _, _))                  \
+      .RETURN(0);                                                            \
+  ALLOW_CALL(openvr_mock.GetRenderModels(), LoadTexture_Async(_, _))         \
+      .RETURN(vr::VRRenderModelError_None)                                   \
+      .SIDE_EFFECT(*_2 = nullptr);
 
 #endif  // TESTS_SRC_MOCKS_OPENVR_MOCK_HPP_
diff --git a/tests/src/test_engine.cpp b/tests/src/test_engine.cpp
index 9ebc1171eebd2256c1f87887f8381b3c8790ad22..f8a7baf5c5e313fd405ed9ca22895433af8bfaad 100644
--- a/tests/src/test_engine.cpp
+++ b/tests/src/test_engine.cpp
@@ -41,9 +41,7 @@
 
 #include "trompeloeil.hpp"
 
-SUPPRESS_WARNINGS_BEGIN
 #include "mocks/openvr_mock.hpp"
-SUPPRESS_WARNINGS_END
 #include "mocks/opengl_mock.hpp"
 #include "mocks/sdl_mock.hpp"
 
diff --git a/tests/src/test_openvr_controller_system.cpp b/tests/src/test_openvr_controller_system.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..225ea9b498109211629d506451deb94b6834fde4
--- /dev/null
+++ b/tests/src/test_openvr_controller_system.cpp
@@ -0,0 +1,118 @@
+//------------------------------------------------------------------------------
+// Project Phoenix
+//
+// Copyright (c) 2017-2018 RWTH Aachen University, Germany,
+// Virtual Reality & Immersive Visualization Group.
+//------------------------------------------------------------------------------
+//                                 License
+//
+// Licensed under the 3-Clause BSD License (the "License");
+// you may not use this file except in compliance with the License.
+// See the file LICENSE for the full text.
+// You may obtain a copy of the License at
+//
+//     https://opensource.org/licenses/BSD-3-Clause
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//------------------------------------------------------------------------------
+
+#include <memory>
+
+#include "catch/catch.hpp"
+
+SUPPRESS_WARNINGS_BEGIN
+#include "GL/glew.h"
+SUPPRESS_WARNINGS_END
+
+#include "trompeloeil.hpp"
+
+#include "phx/display_system_openvr.hpp"
+#include "phx/entity.hpp"
+#include "phx/material_handle.hpp"
+#include "phx/mesh_handle.hpp"
+#include "phx/openvr_controller_behavior.hpp"
+#include "phx/openvr_controller_system.hpp"
+#include "phx/rendering_system.hpp"
+#include "phx/scene.hpp"
+
+#include "mocks/opengl_mock.hpp"
+#include "mocks/openvr_mock.hpp"
+#include "mocks/sdl_mock.hpp"
+
+using trompeloeil::_;
+using trompeloeil::ne;
+
+extern template struct trompeloeil::reporter<trompeloeil::specialized>;
+
+class EngineStopTestSystem : public phx::System {
+ public:
+  explicit EngineStopTestSystem(phx::Engine* engine, int stop_in_frame = 1)
+      : System(engine), stop_on_update_(stop_in_frame) {}
+
+  void Update(const phx::FrameTimer::TimeInfo& time_info) override {
+    update_counter_++;
+    if (update_counter_ >= stop_on_update_) {
+      GetEngine()->Stop();
+    }
+    time_info_ = &time_info;
+  }
+
+  const phx::FrameTimer::TimeInfo* time_info_ = nullptr;
+
+ private:
+  int update_counter_ = 0;
+  int stop_on_update_ = 1;
+};
+
+SCENARIO(
+    "The OpenVRControllerSystem automatically inserts controller entities into "
+    "a scene if controllers are connected",
+    "[phx][phx::OpenVRControllerSystem]") {
+  OPENGL_MOCK_ALLOW_ANY_CALL;
+  OPENVR_MOCK_ALLOW_ANY_CALL;
+  SDL_MOCK_ALLOW_ANY_CALL;
+
+  phx::Engine engine;
+  auto display_system = engine.CreateSystem<phx::DisplaySystemOpenVR>();
+
+  GIVEN(
+      "The display system has an HMD and the engine has a scene. Also, there's "
+      "an OpenVRControllerSystem.") {
+    display_system->CreateHMD();
+    auto scene = std::make_shared<phx::Scene>();
+    engine.SetScene(scene);
+    engine.CreateSystem<phx::OpenVRControllerSystem>(display_system);
+    engine.CreateSystem<EngineStopTestSystem>();
+    engine.CreateSystem<phx::RenderingSystem>(display_system);
+
+    WHEN("We run the engine for once frame (updating each system once)") {
+      engine.Run();
+
+      THEN(
+          "There are controller entities in the scene with controller "
+          "behaviors attached to them, as well as mesh and material handles, "
+          "and the controllers are left and right") {
+        auto controller_entities =
+            scene->GetEntitiesWithComponents<phx::OpenVRControllerBehavior>();
+        REQUIRE(controller_entities.size() == 2);
+
+        for (auto entity : controller_entities) {
+          REQUIRE(entity->GetFirstComponent<phx::MeshHandle>() != nullptr);
+          REQUIRE(entity->GetFirstComponent<phx::MaterialHandle>() != nullptr);
+        }
+
+        auto side0 = controller_entities[0]
+                         ->GetFirstComponent<phx::OpenVRControllerBehavior>()
+                         ->GetSide();
+        auto side1 = controller_entities[1]
+                         ->GetFirstComponent<phx::OpenVRControllerBehavior>()
+                         ->GetSide();
+        REQUIRE(side0 != side1);
+      }
+    }
+  }
+}