diff --git a/src/citra/emu_window/emu_window_sdl2.cpp b/src/citra/emu_window/emu_window_sdl2.cpp index 591f68aa4..8e0ea9fb4 100644 --- a/src/citra/emu_window/emu_window_sdl2.cpp +++ b/src/citra/emu_window/emu_window_sdl2.cpp @@ -25,16 +25,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(); + } } } @@ -62,6 +68,7 @@ EmuWindow_SDL2::EmuWindow_SDL2() { keyboard_id = KeyMap::NewDeviceId(); ReloadSetKeymaps(); + motion_emu = std::make_unique(*this); SDL_SetMainReady(); @@ -115,6 +122,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 77279f022..9be484f22 100644 --- a/src/citra/emu_window/emu_window_sdl2.h +++ b/src/citra/emu_window/emu_window_sdl2.h @@ -4,9 +4,11 @@ #pragma once +#include #include #include "common/emu_window.h" +#include "common/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_emu; }; diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index 414b2f8af..6b0d708b0 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -229,6 +229,7 @@ qreal GRenderWindow::windowPixelRatio() } void GRenderWindow::closeEvent(QCloseEvent* event) { + motion_emu = nullptr; emit Closed(); QWidget::closeEvent(event); } @@ -245,12 +246,13 @@ void GRenderWindow::keyReleaseEvent(QKeyEvent* event) void GRenderWindow::mousePressEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton) - { - auto pos = event->pos(); + auto pos = event->pos(); + if (event->button() == Qt::LeftButton) { qreal pixelRatio = windowPixelRatio(); this->TouchPressed(static_cast(pos.x() * pixelRatio), static_cast(pos.y() * pixelRatio)); + } else if (event->button() == Qt::RightButton) { + motion_emu->BeginTilt(pos.x(), pos.y()); } } @@ -260,12 +262,15 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent *event) qreal pixelRatio = windowPixelRatio(); this->TouchMoved(std::max(static_cast(pos.x() * pixelRatio), 0u), std::max(static_cast(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() @@ -286,11 +291,13 @@ void GRenderWindow::OnMinimalClientAreaChangeRequest(const std::pair(*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 0dcf3e5eb..6bc1ea330 100644 --- a/src/citra_qt/bootmanager.h +++ b/src/citra_qt/bootmanager.h @@ -10,6 +10,7 @@ #include #include "common/emu_window.h" +#include "common/motion_emu.h" #include "common/thread.h" class QKeyEvent; @@ -149,6 +150,9 @@ private: EmuThread* emu_thread; + /// Motion sensors emulation + std::unique_ptr motion_emu; + protected: void showEvent(QShowEvent* event) override; }; diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index aa6eee2a3..7c8f48122 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -13,6 +13,7 @@ set(SRCS memory_util.cpp microprofile.cpp misc.cpp + motion_emu.cpp profiler.cpp scm_rev.cpp string_util.cpp @@ -46,8 +47,10 @@ set(HEADERS memory_util.h microprofile.h microprofileui.h + motion_emu.h platform.h profiler_reporting.h + quaternion.h scm_rev.h scope_exit.h string_util.h diff --git a/src/common/emu_window.cpp b/src/common/emu_window.cpp index fd728c109..5420fef18 100644 --- a/src/common/emu_window.cpp +++ b/src/common/emu_window.cpp @@ -7,6 +7,7 @@ #include "common/assert.h" #include "common/key_map.h" +#include "common/profiler_reporting.h" #include "emu_window.h" #include "video_core/video_core.h" @@ -90,6 +91,26 @@ 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; +} + EmuWindow::FramebufferLayout EmuWindow::FramebufferLayout::DefaultScreenLayout(unsigned width, unsigned height) { // When hiding the widget, the function receives a size of 0 if (width == 0) width = 1; diff --git a/src/common/emu_window.h b/src/common/emu_window.h index 57e303b6d..f2c7b30af 100644 --- a/src/common/emu_window.h +++ b/src/common/emu_window.h @@ -111,6 +111,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. @@ -153,12 +174,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 GetAccelerometerState() const { - // stubbed - return std::make_tuple(0, -512, 0); + std::tuple GetAccelerometerState() { + return std::make_tuple(accel_x, accel_y, accel_z); } /** @@ -172,12 +192,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 GetGyroscopeState() const { - // stubbed - return std::make_tuple(0, 0, 0); + std::tuple GetGyroscopeState() { + return std::make_tuple(gyro_x, gyro_y, gyro_z); } /** @@ -226,6 +245,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() {} @@ -288,6 +313,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/common/motion_emu.cpp b/src/common/motion_emu.cpp new file mode 100644 index 000000000..185388446 --- /dev/null +++ b/src/common/motion_emu.cpp @@ -0,0 +1,88 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/emu_window.h" +#include "common/math_util.h" +#include "common/motion_emu.h" +#include "common/quaternion.h" + +namespace Motion { + +static constexpr int update_millisecond = 100; +static constexpr auto update_duration = + std::chrono::duration_cast(std::chrono::milliseconds(update_millisecond)); +static constexpr float PI = 3.14159265f; + +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 q = MakeQuaternion(Math::Vec3(), 0); + Math::Quaternion old_q; + + while (!shutdown_event.WaitUntil(update_time)) { + update_time += update_duration; + old_q = q; + + { + std::lock_guard 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 / 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 guard(tilt_mutex); + if (mouse_move.x == 0 && mouse_move.y == 0) { + tilt_angle = 0; + } else { + tilt_direction = mouse_move.Cast(); + tilt_angle = MathUtil::Clamp(tilt_direction.Normalize() * SENSITIVITY, 0.0f, PI * 0.5f); + } + } +} + +void MotionEmu::EndTilt() { + std::lock_guard guard(tilt_mutex); + tilt_angle = 0; + is_tilting = false; +} + +} // namespace Motion diff --git a/src/common/motion_emu.h b/src/common/motion_emu.h new file mode 100644 index 000000000..99d41a726 --- /dev/null +++ b/src/common/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 mouse_origin; + + std::mutex tilt_mutex; + Math::Vec2 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 diff --git a/src/common/quaternion.h b/src/common/quaternion.h new file mode 100644 index 000000000..1bd1a8396 --- /dev/null +++ b/src/common/quaternion.h @@ -0,0 +1,49 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/vector_math.h" + +namespace Math { + +template +class Quaternion { +public: + Math::Vec3 xyz; + T w; + + Quaternion Inverse() const { + return { -xyz, w }; + } + + Quaternion operator+ (const Quaternion& other) const { + return { xyz + other.xyz, w + other.w }; + } + + Quaternion operator- (const Quaternion& other) const { + return { xyz - other.xyz, w - other.w }; + } + + Quaternion operator* (const Quaternion& other) const { + return { + xyz * other.w + other.xyz * w + Cross(xyz, other.xyz), + w * other.w - Dot(xyz, other.xyz) + }; + } +}; + +template +auto QuaternionRotate(const Quaternion& q, const Math::Vec3& v) { + return v + 2 * Cross(q.xyz, Cross(q.xyz, v) + v * q.w); +} + +inline Quaternion MakeQuaternion(const Math::Vec3& axis, float angle) { + return { + axis * std::sin(angle / 2), + std::cos(angle / 2) + }; +} + +} // namspace Math diff --git a/src/common/thread.h b/src/common/thread.h index bbfa8befa..fbd865c4f 100644 --- a/src/common/thread.h +++ b/src/common/thread.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -55,6 +56,15 @@ public: is_set = false; } + template + bool WaitUntil(const std::chrono::time_point& time) { + std::unique_lock lk(mutex); + if (!condvar.wait_until(lk, time, [this]{ return is_set; })) + return false; + is_set = false; + return true; + } + void Reset() { std::unique_lock lk(mutex); // no other action required, since wait loops on the predicate and any lingering signal will get cleared on the first iteration diff --git a/src/common/vector_math.h b/src/common/vector_math.h index cfb9481b6..aacee4e68 100644 --- a/src/common/vector_math.h +++ b/src/common/vector_math.h @@ -173,6 +173,18 @@ Vec2 operator * (const V& f, const Vec2& vec) typedef Vec2 Vec2f; +template<> +inline float Vec2::Length() const { + return std::sqrt(x * x + y * y); +} + +template<> +inline float Vec2::Normalize() { + float length = Length(); + *this /= length; + return length; +} + template class Vec3 { @@ -341,6 +353,12 @@ inline Vec3 Vec3::Normalized() const { return *this / Length(); } +template<> +inline float Vec3::Normalize() { + float length = Length(); + *this /= length; + return length; +} typedef Vec3 Vec3f;