#define _USE_MATH_DEFINES #include #include #include #include "Common/Math/math_util.h" #include "Common/Math/lin/vec3.h" #include "Common/Math/lin/matrix4x4.h" #include "Common/Log.h" #include "Common/System/Display.h" #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/HLE/sceCtrl.h" #include "Core/TiltEventProcessor.h" namespace TiltEventProcessor { static u32 tiltButtonsDown = 0; float rawTiltAnalogX; float rawTiltAnalogY; float g_currentYAngle = 0.0f; float GetCurrentYAngle() { return g_currentYAngle; } // These functions generate tilt events given the current Tilt amount, // and the deadzone radius. void GenerateAnalogStickEvent(float analogX, float analogY); void GenerateDPadEvent(int digitalX, int digitalY); void GenerateActionButtonEvent(int digitalX, int digitalY); void GenerateTriggerButtonEvent(int digitalX, int digitalY); } // deadzone is normalized - 0 to 1 // sensitivity controls how fast the deadzone reaches max value inline float ApplyDeadzoneAxis(float x, float deadzone) { if (deadzone >= 0.99f) { // Meaningless, and not reachable with normal controls. return x; } const float factor = 1.0f / (1.0f - deadzone); if (x > deadzone) { return (x - deadzone) * factor + deadzone; } else if (x < -deadzone) { return (x + deadzone) * factor - deadzone; } else { return 0.0f; } } inline void ApplyDeadzoneXY(float x, float y, float *adjustedX, float *adjustedY, float deadzone, bool circular) { if (circular) { if (x == 0.0f && y == 0.0f) { *adjustedX = 0.0f; *adjustedY = 0.0f; return; } float magnitude = sqrtf(x * x + y * y); if (magnitude <= deadzone + 0.00001f) { *adjustedX = 0.0f; *adjustedY = 0.0f; return; } float factor = 1.0f / (1.0f - deadzone); float newMagnitude = (magnitude - deadzone) * factor; *adjustedX = (x / magnitude) * newMagnitude; *adjustedY = (y / magnitude) * newMagnitude; } else { *adjustedX = ApplyDeadzoneAxis(x, deadzone); *adjustedY = ApplyDeadzoneAxis(y, deadzone); } } namespace TiltEventProcessor { // Also clamps to -1.0..1.0. // This applies a (circular if desired) inverse deadzone. inline void ApplyInverseDeadzone(float x, float y, float *outX, float *outY, float inverseDeadzone, bool circular) { if (inverseDeadzone == 0.0f) { *outX = Clamp(x, -1.0f, 1.0f); *outY = Clamp(y, -1.0f, 1.0f); return; } if (circular) { // If the regular deadzone centered it, let's leave it as-is. if (x == 0.0f && y == 0.0f) { *outX = x; *outY = y; return; } float magnitude = sqrtf(x * x + y * y); if (magnitude > 0.00001f) { magnitude = (magnitude + inverseDeadzone) / magnitude; } *outX = Clamp(x * magnitude, -1.0f, 1.0f); *outY = Clamp(y * magnitude, -1.0f, 1.0f); } else { // If the regular deadzone centered it, let's leave it as-is. *outX = x == 0.0f ? 0.0f : Clamp(x + copysignf(inverseDeadzone, x), -1.0f, 1.0f); *outY = y == 0.0f ? 0.0f : Clamp(y + copysignf(inverseDeadzone, y), -1.0f, 1.0f); } } void ProcessTilt(bool landscape, float calibrationAngle, float x, float y, float z, bool invertX, bool invertY, float xSensitivity, float ySensitivity) { if (g_Config.iTiltInputType == TILT_NULL) { // Turned off - nothing to do. return; } if (landscape) { std::swap(x, y); } else { x *= -1.0f; } Lin::Vec3 down = Lin::Vec3(x, y, z).normalized(); float angleAroundX = atan2(down.z, down.y); g_currentYAngle = angleAroundX; // TODO: Should smooth this out over time a bit. float yAngle = angleAroundX - calibrationAngle; float xAngle = asinf(down.x); _dbg_assert_(!my_isnanorinf(angleAroundX)); _dbg_assert_(!my_isnanorinf(yAngle)); _dbg_assert_(!my_isnanorinf(xAngle)); float tiltX = xAngle; float tiltY = yAngle; // invert x and y axes if requested. Can probably remove this. if (invertX) { tiltX = -tiltX; } if (invertY) { tiltY = -tiltY; } // It's not obvious what the factor for converting from tilt angle to value should be, // but there's nothing that says that 1 would make sense. The important thing is that // the sensitivity sliders get a range of values that makes sense. const float tiltFactor = 3.0f; tiltX *= xSensitivity * tiltFactor; tiltY *= ySensitivity * tiltFactor; if (g_Config.iTiltInputType == TILT_ANALOG) { // Only analog mappings use the deadzone. float adjustedTiltX; float adjustedTiltY; ApplyDeadzoneXY(tiltX, tiltY, &adjustedTiltX, &adjustedTiltY, g_Config.fTiltAnalogDeadzoneRadius, g_Config.bTiltCircularDeadzone); _dbg_assert_(!my_isnanorinf(adjustedTiltX)); _dbg_assert_(!my_isnanorinf(adjustedTiltY)); // Unlike regular deadzone, where per-axis is okay, inverse deadzone (to compensate for game deadzones) really needs to be // applied on the two axes together. // TODO: Share this code with the joystick code. For now though, we keep it separate. ApplyInverseDeadzone(adjustedTiltX, adjustedTiltY, &adjustedTiltX, &adjustedTiltY, g_Config.fTiltInverseDeadzone, g_Config.bTiltCircularDeadzone); _dbg_assert_(!my_isnanorinf(adjustedTiltX)); _dbg_assert_(!my_isnanorinf(adjustedTiltY)); rawTiltAnalogX = adjustedTiltX; rawTiltAnalogY = adjustedTiltY; GenerateAnalogStickEvent(adjustedTiltX, adjustedTiltY); return; } // Remaining are digital now so do the digital check here. // We use a fixed 0.3 threshold instead of a deadzone since you can simply use sensitivity to set it - // these parameters were never independent. It should feel similar to analog that way. int digitalX = 0; int digitalY = 0; const float threshold = 0.5f; if (tiltX < -threshold) { digitalX = -1; } else if (tiltX > threshold) { digitalX = 1; } if (tiltY < -threshold) { digitalY = -1; } else if (tiltY > threshold) { digitalY = 1; } switch (g_Config.iTiltInputType) { case TILT_DPAD: GenerateDPadEvent(digitalX, digitalY); break; case TILT_ACTION_BUTTON: GenerateActionButtonEvent(digitalX, digitalY); break; case TILT_TRIGGER_BUTTONS: GenerateTriggerButtonEvent(digitalX, digitalY); break; default: break; } } inline float clamp(float f) { if (f > 1.0f) return 1.0f; if (f < -1.0f) return -1.0f; return f; } // TODO: Instead of __Ctrl, route data into the ControlMapper. void GenerateAnalogStickEvent(float tiltX, float tiltY) { __CtrlSetAnalogXY(CTRL_STICK_LEFT, clamp(tiltX), clamp(tiltY)); } void GenerateDPadEvent(int digitalX, int digitalY) { static const int dir[4] = { CTRL_RIGHT, CTRL_DOWN, CTRL_LEFT, CTRL_UP }; if (digitalX == 0) { __CtrlUpdateButtons(0, tiltButtonsDown & (CTRL_RIGHT | CTRL_LEFT)); tiltButtonsDown &= ~(CTRL_LEFT | CTRL_RIGHT); } if (digitalY == 0) { __CtrlUpdateButtons(0, tiltButtonsDown & (CTRL_UP | CTRL_DOWN)); tiltButtonsDown &= ~(CTRL_UP | CTRL_DOWN); } if (digitalX == 0 && digitalY == 0) { return; } int ctrlMask = 0; if (digitalX == -1) ctrlMask |= CTRL_LEFT; if (digitalX == 1) ctrlMask |= CTRL_RIGHT; if (digitalY == -1) ctrlMask |= CTRL_DOWN; if (digitalY == 1) ctrlMask |= CTRL_UP; ctrlMask &= ~__CtrlPeekButtons(); __CtrlUpdateButtons(ctrlMask, 0); tiltButtonsDown |= ctrlMask; } void GenerateActionButtonEvent(int digitalX, int digitalY) { static const int buttons[4] = { CTRL_CIRCLE, CTRL_CROSS, CTRL_SQUARE, CTRL_TRIANGLE }; if (digitalX == 0) { __CtrlUpdateButtons(0, tiltButtonsDown & (CTRL_SQUARE | CTRL_CIRCLE)); tiltButtonsDown &= ~(CTRL_SQUARE | CTRL_CIRCLE); } if (digitalY == 0) { __CtrlUpdateButtons(0, tiltButtonsDown & (CTRL_TRIANGLE | CTRL_CROSS)); tiltButtonsDown &= ~(CTRL_TRIANGLE | CTRL_CROSS); } if (digitalX == 0 && digitalY == 0) { return; } int ctrlMask = 0; if (digitalX == -1) ctrlMask |= CTRL_SQUARE; if (digitalX == 1) ctrlMask |= CTRL_CIRCLE; if (digitalY == -1) ctrlMask |= CTRL_CROSS; if (digitalY == 1) ctrlMask |= CTRL_TRIANGLE; ctrlMask &= ~__CtrlPeekButtons(); __CtrlUpdateButtons(ctrlMask, 0); tiltButtonsDown |= ctrlMask; } void GenerateTriggerButtonEvent(int digitalX, int digitalY) { u32 upButtons = 0; u32 downButtons = 0; // Y axis up for both if (digitalY == 1) { downButtons = CTRL_LTRIGGER | CTRL_RTRIGGER; } else if (digitalX == 0) { upButtons = CTRL_LTRIGGER | CTRL_RTRIGGER; } else if (digitalX == -1) { downButtons = CTRL_LTRIGGER; upButtons = CTRL_RTRIGGER; } else if (digitalX == 1) { downButtons = CTRL_RTRIGGER; upButtons = CTRL_LTRIGGER; } downButtons &= ~__CtrlPeekButtons(); __CtrlUpdateButtons(downButtons, tiltButtonsDown & upButtons); tiltButtonsDown = (tiltButtonsDown & ~upButtons) | downButtons; } void ResetTiltEvents() { // Reset the buttons we have marked pressed. __CtrlUpdateButtons(0, tiltButtonsDown); tiltButtonsDown = 0; __CtrlSetAnalogXY(CTRL_STICK_LEFT, 0.0f, 0.0f); } } // namespace TiltEventProcessor namespace MouseEventProcessor { // Technically, we may be OK without a mutex here. // But, the cost isn't high. std::mutex g_mouseMutex; float g_mouseDeltaXAccum = 0; float g_mouseDeltaYAccum = 0; float g_mouseDeltaX; float g_mouseDeltaY; void DecayMouse(double now) { g_mouseDeltaX = g_mouseDeltaXAccum; g_mouseDeltaY = g_mouseDeltaYAccum; const float decay = g_Config.fMouseSmoothing; static double lastTime = 0.0f; if (lastTime == 0.0) { lastTime = now; return; } double dt = now - lastTime; lastTime = now; // Decay the mouse deltas. We do an approximation of the old polling. // Should be able to use a smooth exponential here, when I get around to doing // the math. static double accumDt = 0.0; accumDt += dt; const double oldPollInterval = 1.0 / 250.0; // See Windows "PollControllers". while (accumDt > oldPollInterval) { accumDt -= oldPollInterval; g_mouseDeltaXAccum *= decay; g_mouseDeltaYAccum *= decay; } } void ProcessDelta(double now, float dx, float dy) { std::unique_lock lock(g_mouseMutex); // Accumulate mouse deltas, for some kind of smoothing. g_mouseDeltaXAccum += dx; g_mouseDeltaYAccum += dy; DecayMouse(now); } void MouseDeltaToAxes(double now, float *mx, float *my) { std::unique_lock lock(g_mouseMutex); float scaleFactor_x = g_display.dpi_scale_x * 0.1 * g_Config.fMouseSensitivity; float scaleFactor_y = g_display.dpi_scale_y * 0.1 * g_Config.fMouseSensitivity; DecayMouse(now); // TODO: Make configurable. float mouseDeadZone = 0.1f; float outX = clamp_value(g_mouseDeltaX * scaleFactor_x, -1.0f, 1.0f); float outY = clamp_value(g_mouseDeltaY * scaleFactor_y, -1.0f, 1.0f); ApplyDeadzoneXY(outX, outY, mx, my, mouseDeadZone, true); } } // namespace