/* * synergy -- mouse and keyboard sharing utility * Copyright (C) 2012 Nick Bolton * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * found in the file COPYING that should have accompanied this file. * * This package is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If notsee . */ #include "CMSWindowsXInput.h" #include "XScreen.h" #include "CThread.h" #include "TMethodJob.h" #include "CLog.h" #include "XInputHook.h" #include "CMSWindowsScreen.h" #include "XInput.h" typedef DWORD (WINAPI *XInputGetStateFunc)(DWORD, XINPUT_STATE*); typedef DWORD (WINAPI *XInputSetStateFunc)(DWORD, XINPUT_VIBRATION*); CMSWindowsXInput::CMSWindowsXInput(CMSWindowsScreen* screen, const CGameDeviceInfo& gameDeviceInfo) : m_screen(screen), m_gameDeviceInfo(gameDeviceInfo), m_xInputPollThread(NULL), m_xInputTimingThread(NULL), m_xInputFeedbackThread(NULL), m_gameButtonsLast(0), m_gameLeftTriggerLast(0), m_gameRightTriggerLast(0), m_gameLeftStickXLast(0), m_gameLeftStickYLast(0), m_gameRightStickXLast(0), m_gameRightStickYLast(0), m_gameLastTimingSent(0), m_gameTimingWaiting(false), m_gameFakeLag(0), m_gamePollFreq(kGamePollFreqDefault), m_gamePollFreqAdjust(0), m_gameFakeLagMin(kGamePollFreqDefault), m_gameTimingStarted(false), m_gameTimingFirst(0), m_gameFakeLagLast(0), m_gameTimingCalibrated(false), m_xinputModule(NULL) { m_xinputModule = LoadLibrary("xinput1_3.dll"); if (m_xinputModule == NULL) { throw XScreenXInputFailure("could not load xinput library"); } if (screen->isPrimary()) { // only capture xinput on the server. m_xInputPollThread = new CThread(new TMethodJob( this, &CMSWindowsXInput::xInputPollThread)); } else { // check for queued timing requests on client. m_xInputTimingThread = new CThread(new TMethodJob( this, &CMSWindowsXInput::xInputTimingThread)); // check for waiting feedback state on client. m_xInputFeedbackThread = new CThread(new TMethodJob( this, &CMSWindowsXInput::xInputFeedbackThread)); } } CMSWindowsXInput::~CMSWindowsXInput() { } void CMSWindowsXInput::fakeGameDeviceButtons(GameDeviceID id, GameDeviceButton buttons) const { SetXInputButtons(id, buttons); } void CMSWindowsXInput::fakeGameDeviceSticks(GameDeviceID id, SInt16 x1, SInt16 y1, SInt16 x2, SInt16 y2) const { SetXInputSticks(id, x1, y1, x2, y2); } void CMSWindowsXInput::fakeGameDeviceTriggers(GameDeviceID id, UInt8 t1, UInt8 t2) const { SetXInputTriggers(id, t1, t2); } void CMSWindowsXInput::queueGameDeviceTimingReq() const { QueueXInputTimingReq(); } void CMSWindowsXInput::gameDeviceTimingResp(UInt16 freq) { if (!m_gameTimingStarted) { // record when timing started for calibration period. m_gameTimingFirst = (UInt16)(ARCH->time() * 1000); m_gameTimingStarted = true; } m_gameTimingWaiting = false; m_gameFakeLagLast = m_gameFakeLag; m_gameFakeLag = (UInt16)((ARCH->time() - m_gameLastTimingSent) * 1000); m_gameFakeLagRecord.push_back(m_gameFakeLag); if (m_gameFakeLag < m_gameFakeLagMin) { // record the lowest value so that the poll frequency // is adjusted to track. m_gameFakeLagMin = m_gameFakeLag; } else if (m_gameFakeLag > (m_gameFakeLagLast * 2)) { // if fake lag has increased significantly since the last // timing, then we must have reached the safe minimum. m_gameFakeLagMin = m_gameFakeLagLast; } // only change poll frequency if it's a sensible value. if (freq > kGamePollFreqMin && freq < kGamePollFreqMax) { m_gamePollFreq = freq; } UInt16 timeSinceStart = ((UInt16)(ARCH->time() * 1000) - m_gameTimingFirst); if (!m_gameTimingCalibrated && (timeSinceStart < kGameCalibrationPeriod)) { // during the calibration period, increase polling speed // to try and find the lowest lag value. m_gamePollFreqAdjust = 1; LOG((CLOG_DEBUG2 "calibrating game device poll frequency, start=%d, now=%d, since=%d", m_gameTimingFirst, (int)(ARCH->time() * 1000), timeSinceStart)); } else { // @bug - calibration seems to re-occur after a period of time, // though, this would actually be a nice feature (but could be // a bit risky -- could cause poor game play)... setting this // stops calibration from happening again. m_gameTimingCalibrated = true; // only adjust poll frequency if outside desired limits. m_gamePollFreqAdjust = 0; if (m_gameFakeLag > m_gameFakeLagMin * 3) { m_gamePollFreqAdjust = 1; } else if (m_gameFakeLag < m_gameFakeLagMin) { m_gamePollFreqAdjust = -1; } } LOG((CLOG_DEBUG3 "game device timing, lag=%dms, freq=%dms, adjust=%dms, min=%dms", m_gameFakeLag, m_gamePollFreq, m_gamePollFreqAdjust, m_gameFakeLagMin)); if (m_gameFakeLagRecord.size() >= kGameLagRecordMax) { UInt16 v, min = 65535, max = 0, total = 0; std::vector::iterator it; for (it = m_gameFakeLagRecord.begin(); it < m_gameFakeLagRecord.end(); ++it) { v = *it; if (v < min) { min = v; } if (v > max) { max = v; } total += v; } LOG((CLOG_INFO "game device timing, min=%dms, max=%dms, avg=%dms", min, max, (UInt16)(total / m_gameFakeLagRecord.size()))); m_gameFakeLagRecord.clear(); } } void CMSWindowsXInput::gameDeviceFeedback(GameDeviceID id, UInt16 m1, UInt16 m2) { XInputSetStateFunc xInputSetStateFunc = (XInputSetStateFunc)GetProcAddress(m_xinputModule, "XInputSetState"); if (xInputSetStateFunc == NULL) { throw XScreenXInputFailure("could not get function address: XInputSetState"); } XINPUT_VIBRATION vibration; ZeroMemory(&vibration, sizeof(XINPUT_VIBRATION)); vibration.wLeftMotorSpeed = m1; vibration.wRightMotorSpeed = m2; xInputSetStateFunc(id, &vibration); } void CMSWindowsXInput::xInputPollThread(void*) { LOG((CLOG_DEBUG "xinput poll thread started")); XInputGetStateFunc xInputGetStateFunc = (XInputGetStateFunc)GetProcAddress(m_xinputModule, "XInputGetState"); if (xInputGetStateFunc == NULL) { throw XScreenXInputFailure("could not get function address: XInputGetState"); } int index = 0; XINPUT_STATE state; bool end = false; while (!end) { ZeroMemory(&state, sizeof(XINPUT_STATE)); DWORD result = xInputGetStateFunc(index, &state); // timeout the timing request after 10 seconds if (m_gameTimingWaiting && (ARCH->time() - m_gameLastTimingSent > 2)) { m_gameTimingWaiting = false; LOG((CLOG_DEBUG "game device timing request timed out")); } // xinput controller is connected if (result == ERROR_SUCCESS) { // @todo game device state class bool buttonsChanged = state.Gamepad.wButtons != m_gameButtonsLast; bool leftTriggerChanged = state.Gamepad.bLeftTrigger != m_gameLeftTriggerLast; bool rightTriggerChanged = state.Gamepad.bRightTrigger != m_gameRightTriggerLast; bool leftStickXChanged = state.Gamepad.sThumbLX != m_gameLeftStickXLast; bool leftStickYChanged = state.Gamepad.sThumbLY != m_gameLeftStickYLast; bool rightStickXChanged = state.Gamepad.sThumbRX != m_gameRightStickXLast; bool rightStickYChanged = state.Gamepad.sThumbRY != m_gameRightStickYLast; m_gameButtonsLast = state.Gamepad.wButtons; m_gameLeftTriggerLast = state.Gamepad.bLeftTrigger; m_gameRightTriggerLast = state.Gamepad.bRightTrigger; m_gameLeftStickXLast = state.Gamepad.sThumbLX; m_gameLeftStickYLast = state.Gamepad.sThumbLY; m_gameRightStickXLast = state.Gamepad.sThumbRX; m_gameRightStickYLast = state.Gamepad.sThumbRY; bool eventSent = false; if (buttonsChanged) { LOG((CLOG_DEBUG "xinput buttons changed")); // xinput buttons convert exactly to synergy buttons m_screen->sendEvent(m_screen->getGameDeviceButtonsEvent(), new IPrimaryScreen::CGameDeviceButtonInfo(index, state.Gamepad.wButtons)); eventSent = true; } if (leftStickXChanged || leftStickYChanged || rightStickXChanged || rightStickYChanged) { LOG((CLOG_DEBUG "xinput sticks changed")); m_screen->sendEvent(m_screen->getGameDeviceSticksEvent(), new IPrimaryScreen::CGameDeviceStickInfo( index, m_gameLeftStickXLast, m_gameLeftStickYLast, m_gameRightStickXLast, m_gameRightStickYLast)); eventSent = true; } if (leftTriggerChanged || rightTriggerChanged) { LOG((CLOG_DEBUG "xinput triggers changed")); // @todo seems wrong re-using x/y for a single value... m_screen->sendEvent(m_screen->getGameDeviceTriggersEvent(), new IPrimaryScreen::CGameDeviceTriggerInfo( index, state.Gamepad.bLeftTrigger, state.Gamepad.bRightTrigger)); eventSent = true; } if (/*eventSent && */!m_gameTimingWaiting && (ARCH->time() - m_gameLastTimingSent > .5)) { m_screen->sendEvent(m_screen->getGameDeviceTimingReqEvent(), NULL); m_gameLastTimingSent = ARCH->time(); m_gameTimingWaiting = true; LOG((CLOG_DEBUG "game device timing request at %.4f", m_gameLastTimingSent)); } } UInt16 sleep = m_gamePollFreq + m_gamePollFreqAdjust; LOG((CLOG_DEBUG5 "xinput poll sleeping for %dms", sleep)); Sleep(sleep); } } void CMSWindowsXInput::xInputTimingThread(void*) { LOG((CLOG_DEBUG "xinput timing thread started")); bool end = false; while (!end) { // if timing request was queued, a timing response is queued // when the xinput status is faked; if it was faked, go tell // the server when this happened. if (DequeueXInputTimingResp()) { LOG((CLOG_DEBUG "dequeued game device timing response")); m_screen->sendEvent(m_screen->getGameDeviceTimingRespEvent(), new IPrimaryScreen::CGameDeviceTimingRespInfo(GetXInputFakeFreqMillis())); } // give the cpu a break. Sleep(1); } } void CMSWindowsXInput::xInputFeedbackThread(void*) { LOG((CLOG_DEBUG "xinput feedback thread started")); int index = 0; bool end = false; while (!end) { WORD leftMotor, rightMotor; if (DequeueXInputFeedback(&leftMotor, &rightMotor)) { LOG((CLOG_DEBUG "dequeued game device feedback")); m_screen->sendEvent(m_screen->getGameDeviceFeedbackEvent(), new IPrimaryScreen::CGameDeviceFeedbackInfo(index, leftMotor, rightMotor)); } Sleep(50); } }