From bcf9d20d5713e7003792bb8fa740b19a7204920e Mon Sep 17 00:00:00 2001
From: wwylele <wwylele@gmail.com>
Date: Sun, 11 Dec 2016 23:32:41 +0200
Subject: [PATCH] Frontend: emulate motion sensor

---
 src/citra/emu_window/emu_window_sdl2.cpp | 22 ++++--
 src/citra/emu_window/emu_window_sdl2.h   |  5 ++
 src/citra_qt/bootmanager.cpp             | 10 ++-
 src/citra_qt/bootmanager.h               |  4 ++
 src/core/CMakeLists.txt                  |  2 +
 src/core/frontend/emu_window.cpp         | 22 ++++++
 src/core/frontend/emu_window.h           | 49 ++++++++++---
 src/core/frontend/motion_emu.cpp         | 89 ++++++++++++++++++++++++
 src/core/frontend/motion_emu.h           | 52 ++++++++++++++
 9 files changed, 239 insertions(+), 16 deletions(-)
 create mode 100644 src/core/frontend/motion_emu.cpp
 create mode 100644 src/core/frontend/motion_emu.h

diff --git a/src/citra/emu_window/emu_window_sdl2.cpp b/src/citra/emu_window/emu_window_sdl2.cpp
index b0d82b6707..81a3abe3f0 100644
--- a/src/citra/emu_window/emu_window_sdl2.cpp
+++ b/src/citra/emu_window/emu_window_sdl2.cpp
@@ -19,16 +19,22 @@
 
 void EmuWindow_SDL2::OnMouseMotion(s32 x, s32 y) {
     TouchMoved((unsigned)std::max(x, 0), (unsigned)std::max(y, 0));
+    motion_emu->Tilt(x, y);
 }
 
 void EmuWindow_SDL2::OnMouseButton(u32 button, u8 state, s32 x, s32 y) {
-    if (button != SDL_BUTTON_LEFT)
-        return;
-
-    if (state == SDL_PRESSED) {
-        TouchPressed((unsigned)std::max(x, 0), (unsigned)std::max(y, 0));
-    } else {
-        TouchReleased();
+    if (button == SDL_BUTTON_LEFT) {
+        if (state == SDL_PRESSED) {
+            TouchPressed((unsigned)std::max(x, 0), (unsigned)std::max(y, 0));
+        } else {
+            TouchReleased();
+        }
+    } else if (button == SDL_BUTTON_RIGHT) {
+        if (state == SDL_PRESSED) {
+            motion_emu->BeginTilt(x, y);
+        } else {
+            motion_emu->EndTilt();
+        }
     }
 }
 
@@ -54,6 +60,7 @@ EmuWindow_SDL2::EmuWindow_SDL2() {
     keyboard_id = KeyMap::NewDeviceId();
 
     ReloadSetKeymaps();
+    motion_emu = std::make_unique<Motion::MotionEmu>(*this);
 
     SDL_SetMainReady();
 
@@ -109,6 +116,7 @@ EmuWindow_SDL2::EmuWindow_SDL2() {
 EmuWindow_SDL2::~EmuWindow_SDL2() {
     SDL_GL_DeleteContext(gl_context);
     SDL_Quit();
+    motion_emu = nullptr;
 }
 
 void EmuWindow_SDL2::SwapBuffers() {
diff --git a/src/citra/emu_window/emu_window_sdl2.h b/src/citra/emu_window/emu_window_sdl2.h
index c8cd919c66..b1cbf16d7d 100644
--- a/src/citra/emu_window/emu_window_sdl2.h
+++ b/src/citra/emu_window/emu_window_sdl2.h
@@ -4,8 +4,10 @@
 
 #pragma once
 
+#include <memory>
 #include <utility>
 #include "core/frontend/emu_window.h"
+#include "core/frontend/motion_emu.h"
 
 struct SDL_Window;
 
@@ -61,4 +63,7 @@ private:
 
     /// Device id of keyboard for use with KeyMap
     int keyboard_id;
+
+    /// Motion sensors emulation
+    std::unique_ptr<Motion::MotionEmu> motion_emu;
 };
diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp
index 57fde6caa7..4ea332a9fc 100644
--- a/src/citra_qt/bootmanager.cpp
+++ b/src/citra_qt/bootmanager.cpp
@@ -191,6 +191,7 @@ qreal GRenderWindow::windowPixelRatio() {
 }
 
 void GRenderWindow::closeEvent(QCloseEvent* event) {
+    motion_emu = nullptr;
     emit Closed();
     QWidget::closeEvent(event);
 }
@@ -204,11 +205,13 @@ void GRenderWindow::keyReleaseEvent(QKeyEvent* event) {
 }
 
 void GRenderWindow::mousePressEvent(QMouseEvent* event) {
+    auto pos = event->pos();
     if (event->button() == Qt::LeftButton) {
-        auto pos = event->pos();
         qreal pixelRatio = windowPixelRatio();
         this->TouchPressed(static_cast<unsigned>(pos.x() * pixelRatio),
                            static_cast<unsigned>(pos.y() * pixelRatio));
+    } else if (event->button() == Qt::RightButton) {
+        motion_emu->BeginTilt(pos.x(), pos.y());
     }
 }
 
@@ -217,11 +220,14 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
     qreal pixelRatio = windowPixelRatio();
     this->TouchMoved(std::max(static_cast<unsigned>(pos.x() * pixelRatio), 0u),
                      std::max(static_cast<unsigned>(pos.y() * pixelRatio), 0u));
+    motion_emu->Tilt(pos.x(), pos.y());
 }
 
 void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
     if (event->button() == Qt::LeftButton)
         this->TouchReleased();
+    else if (event->button() == Qt::RightButton)
+        motion_emu->EndTilt();
 }
 
 void GRenderWindow::ReloadSetKeymaps() {
@@ -279,11 +285,13 @@ void GRenderWindow::OnMinimalClientAreaChangeRequest(
 }
 
 void GRenderWindow::OnEmulationStarting(EmuThread* emu_thread) {
+    motion_emu = std::make_unique<Motion::MotionEmu>(*this);
     this->emu_thread = emu_thread;
     child->DisablePainting();
 }
 
 void GRenderWindow::OnEmulationStopping() {
+    motion_emu = nullptr;
     emu_thread = nullptr;
     child->EnablePainting();
 }
diff --git a/src/citra_qt/bootmanager.h b/src/citra_qt/bootmanager.h
index 43015390b2..7dac1c4806 100644
--- a/src/citra_qt/bootmanager.h
+++ b/src/citra_qt/bootmanager.h
@@ -11,6 +11,7 @@
 #include <QThread>
 #include "common/thread.h"
 #include "core/frontend/emu_window.h"
+#include "core/frontend/motion_emu.h"
 
 class QKeyEvent;
 class QScreen;
@@ -156,6 +157,9 @@ private:
 
     EmuThread* emu_thread;
 
+    /// Motion sensors emulation
+    std::unique_ptr<Motion::MotionEmu> motion_emu;
+
 protected:
     void showEvent(QShowEvent* event) override;
 };
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 3621449b3a..4c5b633e0e 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -31,6 +31,7 @@ set(SRCS
             file_sys/savedata_archive.cpp
             frontend/emu_window.cpp
             frontend/key_map.cpp
+            frontend/motion_emu.cpp
             gdbstub/gdbstub.cpp
             hle/config_mem.cpp
             hle/applets/applet.cpp
@@ -202,6 +203,7 @@ set(HEADERS
             file_sys/savedata_archive.h
             frontend/emu_window.h
             frontend/key_map.h
+            frontend/motion_emu.h
             gdbstub/gdbstub.h
             hle/config_mem.h
             hle/function_wrappers.h
diff --git a/src/core/frontend/emu_window.cpp b/src/core/frontend/emu_window.cpp
index f6f90f9e1c..13c7f3de22 100644
--- a/src/core/frontend/emu_window.cpp
+++ b/src/core/frontend/emu_window.cpp
@@ -5,6 +5,7 @@
 #include <algorithm>
 #include <cmath>
 #include "common/assert.h"
+#include "common/profiler_reporting.h"
 #include "core/frontend/emu_window.h"
 #include "core/frontend/key_map.h"
 #include "video_core/video_core.h"
@@ -89,6 +90,27 @@ void EmuWindow::TouchMoved(unsigned framebuffer_x, unsigned framebuffer_y) {
     TouchPressed(framebuffer_x, framebuffer_y);
 }
 
+void EmuWindow::AccelerometerChanged(float x, float y, float z) {
+    constexpr float coef = 512;
+
+    // TODO(wwylele): do a time stretch as it in GyroscopeChanged
+    // The time stretch formula should be like
+    // stretched_vector = (raw_vector - gravity) * stretch_ratio + gravity
+    accel_x = x * coef;
+    accel_y = y * coef;
+    accel_z = z * coef;
+}
+
+void EmuWindow::GyroscopeChanged(float x, float y, float z) {
+    constexpr float FULL_FPS = 60;
+    float coef = GetGyroscopeRawToDpsCoefficient();
+    float stretch =
+        FULL_FPS / Common::Profiling::GetTimingResultsAggregator()->GetAggregatedResults().fps;
+    gyro_x = x * coef * stretch;
+    gyro_y = y * coef * stretch;
+    gyro_z = z * coef * stretch;
+}
+
 void EmuWindow::UpdateCurrentFramebufferLayout(unsigned width, unsigned height) {
     Layout::FramebufferLayout layout;
     switch (Settings::values.layout_option) {
diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h
index 835c4d5003..2cdbf17422 100644
--- a/src/core/frontend/emu_window.h
+++ b/src/core/frontend/emu_window.h
@@ -92,6 +92,27 @@ public:
      */
     void TouchMoved(unsigned framebuffer_x, unsigned framebuffer_y);
 
+    /**
+     * Signal accelerometer state has changed.
+     * @param x X-axis accelerometer value
+     * @param y Y-axis accelerometer value
+     * @param z Z-axis accelerometer value
+     * @note all values are in unit of g (gravitational acceleration).
+     *    e.g. x = 1.0 means 9.8m/s^2 in x direction.
+     * @see GetAccelerometerState for axis explanation.
+     */
+    void AccelerometerChanged(float x, float y, float z);
+
+    /**
+     * Signal gyroscope state has changed.
+     * @param x X-axis accelerometer value
+     * @param y Y-axis accelerometer value
+     * @param z Z-axis accelerometer value
+     * @note all values are in deg/sec.
+     * @see GetGyroscopeState for axis explanation.
+     */
+    void GyroscopeChanged(float x, float y, float z);
+
     /**
      * Gets the current pad state (which buttons are pressed).
      * @note This should be called by the core emu thread to get a state set by the window thread.
@@ -134,12 +155,11 @@ public:
      *   1 unit of return value = 1/512 g (measured by hw test),
      *   where g is the gravitational acceleration (9.8 m/sec2).
      * @note This should be called by the core emu thread to get a state set by the window thread.
-     * @todo Implement accelerometer input in front-end.
+     * @todo Fix this function to be thread-safe.
      * @return std::tuple of (x, y, z)
      */
-    std::tuple<s16, s16, s16> GetAccelerometerState() const {
-        // stubbed
-        return std::make_tuple(0, -512, 0);
+    std::tuple<s16, s16, s16> GetAccelerometerState() {
+        return std::make_tuple(accel_x, accel_y, accel_z);
     }
 
     /**
@@ -153,12 +173,11 @@ public:
      *   1 unit of return value = (1/coef) deg/sec,
      *   where coef is the return value of GetGyroscopeRawToDpsCoefficient().
      * @note This should be called by the core emu thread to get a state set by the window thread.
-     * @todo Implement gyroscope input in front-end.
+     * @todo Fix this function to be thread-safe.
      * @return std::tuple of (x, y, z)
      */
-    std::tuple<s16, s16, s16> GetGyroscopeState() const {
-        // stubbed
-        return std::make_tuple(0, 0, 0);
+    std::tuple<s16, s16, s16> GetGyroscopeState() {
+        return std::make_tuple(gyro_x, gyro_y, gyro_z);
     }
 
     /**
@@ -216,6 +235,12 @@ protected:
         circle_pad_x = 0;
         circle_pad_y = 0;
         touch_pressed = false;
+        accel_x = 0;
+        accel_y = -512;
+        accel_z = 0;
+        gyro_x = 0;
+        gyro_y = 0;
+        gyro_z = 0;
     }
     virtual ~EmuWindow() {}
 
@@ -281,6 +306,14 @@ private:
     s16 circle_pad_x; ///< Circle pad X-position in native 3DS pixel coordinates (-156 - 156)
     s16 circle_pad_y; ///< Circle pad Y-position in native 3DS pixel coordinates (-156 - 156)
 
+    s16 accel_x; ///< Accelerometer X-axis value in native 3DS units
+    s16 accel_y; ///< Accelerometer Y-axis value in native 3DS units
+    s16 accel_z; ///< Accelerometer Z-axis value in native 3DS units
+
+    s16 gyro_x; ///< Gyroscope X-axis value in native 3DS units
+    s16 gyro_y; ///< Gyroscope Y-axis value in native 3DS units
+    s16 gyro_z; ///< Gyroscope Z-axis value in native 3DS units
+
     /**
      * Clip the provided coordinates to be inside the touchscreen area.
      */
diff --git a/src/core/frontend/motion_emu.cpp b/src/core/frontend/motion_emu.cpp
new file mode 100644
index 0000000000..9a5b3185d2
--- /dev/null
+++ b/src/core/frontend/motion_emu.cpp
@@ -0,0 +1,89 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/math_util.h"
+#include "common/quaternion.h"
+#include "core/frontend/emu_window.h"
+#include "core/frontend/motion_emu.h"
+
+namespace Motion {
+
+static constexpr int update_millisecond = 100;
+static constexpr auto update_duration =
+    std::chrono::duration_cast<std::chrono::steady_clock::duration>(
+        std::chrono::milliseconds(update_millisecond));
+
+MotionEmu::MotionEmu(EmuWindow& emu_window)
+    : motion_emu_thread(&MotionEmu::MotionEmuThread, this, std::ref(emu_window)) {}
+
+MotionEmu::~MotionEmu() {
+    if (motion_emu_thread.joinable()) {
+        shutdown_event.Set();
+        motion_emu_thread.join();
+    }
+}
+
+void MotionEmu::MotionEmuThread(EmuWindow& emu_window) {
+    auto update_time = std::chrono::steady_clock::now();
+    Math::Quaternion<float> q = MakeQuaternion(Math::Vec3<float>(), 0);
+    Math::Quaternion<float> old_q;
+
+    while (!shutdown_event.WaitUntil(update_time)) {
+        update_time += update_duration;
+        old_q = q;
+
+        {
+            std::lock_guard<std::mutex> guard(tilt_mutex);
+
+            // Find the quaternion describing current 3DS tilting
+            q = MakeQuaternion(Math::MakeVec(-tilt_direction.y, 0.0f, tilt_direction.x),
+                               tilt_angle);
+        }
+
+        auto inv_q = q.Inverse();
+
+        // Set the gravity vector in world space
+        auto gravity = Math::MakeVec(0.0f, -1.0f, 0.0f);
+
+        // Find the angular rate vector in world space
+        auto angular_rate = ((q - old_q) * inv_q).xyz * 2;
+        angular_rate *= 1000 / update_millisecond / MathUtil::PI * 180;
+
+        // Transform the two vectors from world space to 3DS space
+        gravity = QuaternionRotate(inv_q, gravity);
+        angular_rate = QuaternionRotate(inv_q, angular_rate);
+
+        // Update the sensor state
+        emu_window.AccelerometerChanged(gravity.x, gravity.y, gravity.z);
+        emu_window.GyroscopeChanged(angular_rate.x, angular_rate.y, angular_rate.z);
+    }
+}
+
+void MotionEmu::BeginTilt(int x, int y) {
+    mouse_origin = Math::MakeVec(x, y);
+    is_tilting = true;
+}
+
+void MotionEmu::Tilt(int x, int y) {
+    constexpr float SENSITIVITY = 0.01f;
+    auto mouse_move = Math::MakeVec(x, y) - mouse_origin;
+    if (is_tilting) {
+        std::lock_guard<std::mutex> guard(tilt_mutex);
+        if (mouse_move.x == 0 && mouse_move.y == 0) {
+            tilt_angle = 0;
+        } else {
+            tilt_direction = mouse_move.Cast<float>();
+            tilt_angle = MathUtil::Clamp(tilt_direction.Normalize() * SENSITIVITY, 0.0f,
+                                         MathUtil::PI * 0.5f);
+        }
+    }
+}
+
+void MotionEmu::EndTilt() {
+    std::lock_guard<std::mutex> guard(tilt_mutex);
+    tilt_angle = 0;
+    is_tilting = false;
+}
+
+} // namespace Motion
diff --git a/src/core/frontend/motion_emu.h b/src/core/frontend/motion_emu.h
new file mode 100644
index 0000000000..99d41a7264
--- /dev/null
+++ b/src/core/frontend/motion_emu.h
@@ -0,0 +1,52 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+#include "common/thread.h"
+#include "common/vector_math.h"
+
+class EmuWindow;
+
+namespace Motion {
+
+class MotionEmu final {
+public:
+    MotionEmu(EmuWindow& emu_window);
+    ~MotionEmu();
+
+    /**
+     * Signals that a motion sensor tilt has begun.
+     * @param x the x-coordinate of the cursor
+     * @param y the y-coordinate of the cursor
+     */
+    void BeginTilt(int x, int y);
+
+    /**
+     * Signals that a motion sensor tilt is occurring.
+     * @param x the x-coordinate of the cursor
+     * @param y the y-coordinate of the cursor
+     */
+    void Tilt(int x, int y);
+
+    /**
+     * Signals that a motion sensor tilt has ended.
+     */
+    void EndTilt();
+
+private:
+    Math::Vec2<int> mouse_origin;
+
+    std::mutex tilt_mutex;
+    Math::Vec2<float> tilt_direction;
+    float tilt_angle = 0;
+
+    bool is_tilting = false;
+
+    Common::Event shutdown_event;
+    std::thread motion_emu_thread;
+
+    void MotionEmuThread(EmuWindow& emu_window);
+};
+
+} // namespace Motion