// Copyright 2017 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include #include "common/math_util.h" #include "common/quaternion.h" #include "common/thread.h" #include "common/vector_math.h" #include "input_common/motion_emu.h" namespace InputCommon { // Implementation class of the motion emulation device class MotionEmuDevice { public: MotionEmuDevice(int update_millisecond, float sensitivity, float tilt_clamp) : update_millisecond(update_millisecond), update_duration(std::chrono::duration_cast( std::chrono::milliseconds(update_millisecond))), sensitivity(sensitivity), tilt_clamp(tilt_clamp), motion_emu_thread(&MotionEmuDevice::MotionEmuThread, this) {} ~MotionEmuDevice() { if (motion_emu_thread.joinable()) { shutdown_event.Set(); motion_emu_thread.join(); } } void BeginTilt(int x, int y) { mouse_origin = Math::MakeVec(x, y); is_tilting = true; } void Tilt(int x, int y) { 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 = std::clamp(tilt_direction.Normalize() * sensitivity, 0.0f, MathUtil::PI * this->tilt_clamp / 180.0f); } } } void EndTilt() { std::lock_guard guard(tilt_mutex); tilt_angle = 0; is_tilting = false; } std::tuple, Math::Vec3> GetStatus() { std::lock_guard guard(status_mutex); return status; } private: const int update_millisecond; const std::chrono::steady_clock::duration update_duration; const float sensitivity; Math::Vec2 mouse_origin; std::mutex tilt_mutex; Math::Vec2 tilt_direction; float tilt_angle = 0; float tilt_clamp = 90; bool is_tilting = false; Common::Event shutdown_event; std::tuple, Math::Vec3> status; std::mutex status_mutex; // Note: always keep the thread declaration at the end so that other objects are initialized // before this! std::thread motion_emu_thread; void MotionEmuThread() { 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 / 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 { std::lock_guard guard(status_mutex); status = std::make_tuple(gravity, angular_rate); } } } }; // Interface wrapper held by input receiver as a unique_ptr. It holds the implementation class as // a shared_ptr, which is also observed by the factory class as a weak_ptr. In this way the factory // can forward all the inputs to the implementation only when it is valid. class MotionEmuDeviceWrapper : public Input::MotionDevice { public: MotionEmuDeviceWrapper(int update_millisecond, float sensitivity, float tilt_clamp) { device = std::make_shared(update_millisecond, sensitivity, tilt_clamp); } std::tuple, Math::Vec3> GetStatus() const { return device->GetStatus(); } std::shared_ptr device; }; std::unique_ptr MotionEmu::Create(const Common::ParamPackage& params) { int update_period = params.Get("update_period", 100); float sensitivity = params.Get("sensitivity", 0.01f); float tilt_clamp = params.Get("tilt_clamp", 90.0f); auto device_wrapper = std::make_unique(update_period, sensitivity, tilt_clamp); // Previously created device is disconnected here. Having two motion devices for 3DS is not // expected. current_device = device_wrapper->device; return std::move(device_wrapper); } void MotionEmu::BeginTilt(int x, int y) { if (auto ptr = current_device.lock()) { ptr->BeginTilt(x, y); } } void MotionEmu::Tilt(int x, int y) { if (auto ptr = current_device.lock()) { ptr->Tilt(x, y); } } void MotionEmu::EndTilt() { if (auto ptr = current_device.lock()) { ptr->EndTilt(); } } } // namespace InputCommon