deskflow/src/lib/platform/CMSWindowsXInput.cpp
2012-06-10 16:50:54 +00:00

362 lines
10 KiB
C++

/*
* 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 <http://www.gnu.org/licenses/>.
*/
#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<CMSWindowsXInput>(
this, &CMSWindowsXInput::xInputPollThread));
}
else
{
// check for queued timing requests on client.
m_xInputTimingThread = new CThread(new TMethodJob<CMSWindowsXInput>(
this, &CMSWindowsXInput::xInputTimingThread));
// check for waiting feedback state on client.
m_xInputFeedbackThread = new CThread(new TMethodJob<CMSWindowsXInput>(
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<UInt16>::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);
}
}