362 lines
10 KiB
C++
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);
|
|
}
|
|
}
|