USB: Add Hori FlightStick controller emulation

This commit is contained in:
Matheus Fraguas 2025-11-29 21:48:10 -03:00
parent 0cbd884234
commit 640f55f99e
8 changed files with 916 additions and 1 deletions

View File

@ -1030,7 +1030,8 @@ QIcon USBDeviceWidget::getIcon() const
{"DJTurntable", "dj-hero-line"}, // DJ Hero TurnTable {"DJTurntable", "dj-hero-line"}, // DJ Hero TurnTable
{"Gametrak", "gametrak-line"}, // Gametrak Device {"Gametrak", "gametrak-line"}, // Gametrak Device
{"RealPlay", "realplay-sphere-line"}, // RealPlay Device {"RealPlay", "realplay-sphere-line"}, // RealPlay Device
{"TrainController", "train-line"} // Train Controller {"TrainController", "train-line"}, // Train Controller
{"FlightStickController", "controller-line"} // Hori FlightStick
}; };
for (size_t i = 0; i < std::size(icons); i++) for (size_t i = 0; i < std::size(icons); i++)

View File

@ -387,6 +387,7 @@ set(pcsx2USBSources
USB/usb-msd/usb-msd.cpp USB/usb-msd/usb-msd.cpp
USB/usb-pad/lg/lg_ff.cpp USB/usb-pad/lg/lg_ff.cpp
USB/usb-pad/usb-buzz.cpp USB/usb-pad/usb-buzz.cpp
USB/usb-pad/usb-flightstick.cpp
USB/usb-pad/usb-gametrak.cpp USB/usb-pad/usb-gametrak.cpp
USB/usb-pad/usb-realplay.cpp USB/usb-pad/usb-realplay.cpp
USB/usb-pad/usb-pad-ff.cpp USB/usb-pad/usb-pad-ff.cpp
@ -426,6 +427,7 @@ set(pcsx2USBHeaders
USB/usb-msd/usb-msd.h USB/usb-msd/usb-msd.h
USB/usb-pad/lg/lg_ff.h USB/usb-pad/lg/lg_ff.h
USB/usb-pad/usb-buzz.h USB/usb-pad/usb-buzz.h
USB/usb-pad/usb-flightstick.h
USB/usb-pad/usb-gametrak.h USB/usb-pad/usb-gametrak.h
USB/usb-pad/usb-realplay.h USB/usb-pad/usb-realplay.h
USB/usb-pad/usb-pad-sdl-ff.h USB/usb-pad/usb-pad-sdl-ff.h

View File

@ -4,6 +4,7 @@
#include "deviceproxy.h" #include "deviceproxy.h"
#include "usb-eyetoy/usb-eyetoy-webcam.h" #include "usb-eyetoy/usb-eyetoy-webcam.h"
#include "usb-pad/usb-buzz.h" #include "usb-pad/usb-buzz.h"
#include "usb-pad/usb-flightstick.h"
#include "usb-pad/usb-gametrak.h" #include "usb-pad/usb-gametrak.h"
#include "usb-pad/usb-realplay.h" #include "usb-pad/usb-realplay.h"
#include "usb-hid/usb-hid.h" #include "usb-hid/usb-hid.h"
@ -85,6 +86,7 @@ void RegisterDevice::Register()
inst.Add(DEVTYPE_GAMETRAK, new usb_pad::GametrakDevice()); inst.Add(DEVTYPE_GAMETRAK, new usb_pad::GametrakDevice());
inst.Add(DEVTYPE_REALPLAY, new usb_pad::RealPlayDevice()); inst.Add(DEVTYPE_REALPLAY, new usb_pad::RealPlayDevice());
inst.Add(DEVTYPE_TRAIN, new usb_pad::TrainDevice()); inst.Add(DEVTYPE_TRAIN, new usb_pad::TrainDevice());
inst.Add(DEVTYPE_FLIGHTSTICK, new usb_pad::FlightStickDevice());
} }
void RegisterDevice::Unregister() void RegisterDevice::Unregister()

View File

@ -42,6 +42,7 @@ enum DeviceType : s32
DEVTYPE_GAMETRAK, DEVTYPE_GAMETRAK,
DEVTYPE_REALPLAY, DEVTYPE_REALPLAY,
DEVTYPE_TRAIN, DEVTYPE_TRAIN,
DEVTYPE_FLIGHTSTICK,
}; };
class DeviceProxy class DeviceProxy

View File

@ -0,0 +1,691 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "usb-flightstick.h"
#include "common/Console.h"
#include "Host.h"
#include "IconsFontAwesome6.h"
#include "IconsPromptFont.h"
#include "Input/InputManager.h"
#include "StateWrapper.h"
#include "USB/deviceproxy.h"
#include "USB/USB.h"
#include "USB/qemu-usb/USBinternal.h"
#include "USB/qemu-usb/desc.h"
namespace usb_pad
{
const char* FlightStickDevice::Name() const
{
return TRANSLATE_NOOP("USB", "Flight Stick Controller");
}
const char* FlightStickDevice::TypeName() const
{
return "FlightStickController";
}
const char* FlightStickDevice::IconName() const
{
return ICON_FA_GAMEPAD;
}
std::span<const char*> FlightStickDevice::SubTypes() const
{
static const char* subtypes[] = {
TRANSLATE_NOOP("USB", "HP2-13 (FS1)"),
TRANSLATE_NOOP("USB", "HP2-217 (FS2)"),
};
return subtypes;
}
enum FlightStickControlID
{
// analog data
CID_FS_STICK_L,
CID_FS_STICK_R,
CID_FS_STICK_U,
CID_FS_STICK_D,
CID_FS_RUDDER_L,
CID_FS_RUDDER_R,
CID_FS_THROTTLE_U,
CID_FS_THROTTLE_D,
CID_FS_ANALOG_HAT_L,
CID_FS_ANALOG_HAT_R,
CID_FS_ANALOG_HAT_U,
CID_FS_ANALOG_HAT_D,
CID_FS_TRIANGLE_A,
CID_FS_SQUARE_B,
// digital data
CID_FS_DPAD_1_L,
CID_FS_DPAD_1_R,
CID_FS_DPAD_1_U,
CID_FS_DPAD_1_D,
CID_FS_CROSS_TRIGGER,
CID_FS_CIRCLE_LAUNCH,
CID_FS_SELECT_C,
CID_FS_START,
CID_FS_ANALOG_HAT_CLICK,
// extra digital for FS2
CID_FS_D,
CID_FS_SW1,
CID_FS_DPAD_2_L,
CID_FS_DPAD_2_R,
CID_FS_DPAD_2_D,
CID_FS_DPAD_2_U,
CID_FS_DPAD_3_L,
CID_FS_DPAD_3_M,
CID_FS_DPAD_3_R,
BUTTONS_OFFSET = CID_FS_DPAD_1_L,
};
std::span<const InputBindingInfo> FlightStickDevice::Bindings(u32 subtype) const
{
//using macros for shared data
#define BINDINGS_FLIGHTSTICK_SHARED_ANALOG \
{"StickLeft", TRANSLATE_NOOP("USB", "Stick Left"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_STICK_L, GenericInputBinding::LeftStickLeft}, \
{"StickRight", TRANSLATE_NOOP("USB", "Stick Right"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_STICK_R, GenericInputBinding::LeftStickRight}, \
{"StickUp", TRANSLATE_NOOP("USB", "Stick Up"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_STICK_U, GenericInputBinding::LeftStickUp}, \
{"StickDown", TRANSLATE_NOOP("USB", "Stick Down"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_STICK_D, GenericInputBinding::LeftStickDown}, \
{"RudderLeft", TRANSLATE_NOOP("USB", "Rudder Left"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_RUDDER_L, GenericInputBinding::L1}, \
{"RudderRight", TRANSLATE_NOOP("USB", "Rudder Right"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_RUDDER_R, GenericInputBinding::R1}, \
{"ThrottleUp", TRANSLATE_NOOP("USB", "Throttle Up"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_THROTTLE_U, GenericInputBinding::R2}, \
{"ThrottleDown", TRANSLATE_NOOP("USB", "Throttle Down"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_THROTTLE_D, GenericInputBinding::L2}, \
{"HatLeft", TRANSLATE_NOOP("USB", "Stick Hat Left"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_ANALOG_HAT_L, GenericInputBinding::RightStickLeft}, \
{"HatRight", TRANSLATE_NOOP("USB", "Stick Hat Right"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_ANALOG_HAT_R, GenericInputBinding::RightStickRight}, \
{"HatkUp", TRANSLATE_NOOP("USB", "Stick Hat Up"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_ANALOG_HAT_U, GenericInputBinding::RightStickUp}, \
{"HatDown", TRANSLATE_NOOP("USB", "Stick Hat Down"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_ANALOG_HAT_D, GenericInputBinding::RightStickDown}, \
{"TriangleA", TRANSLATE_NOOP("USB", "Triangle (A)"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_TRIANGLE_A, GenericInputBinding::Triangle}, \
{"SquareB", TRANSLATE_NOOP("USB", "Square (B)"), nullptr, InputBindingInfo::Type::HalfAxis, CID_FS_SQUARE_B, GenericInputBinding::Square},
#define BINDINGS_FLIGHTSTICK_SHARED_DPAD \
{"Dpad1Left", TRANSLATE_NOOP("USB", "D-Pad Left"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_1_L, GenericInputBinding::DPadLeft}, \
{"Dpad1Right", TRANSLATE_NOOP("USB", "D-Pad Right"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_1_R, GenericInputBinding::DPadRight}, \
{"Dpad1Up", TRANSLATE_NOOP("USB", "D-Pad Up"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_1_U, GenericInputBinding::DPadUp}, \
{"Dpad1Down", TRANSLATE_NOOP("USB", "D-Pad Down"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_1_D, GenericInputBinding::DPadDown},
#define BINDINGS_FLIGHTSTICK_SHARED_BUTTONS \
{"CrossTrigger", TRANSLATE_NOOP("USB", "Cross (Trigger)"), nullptr, InputBindingInfo::Type::Button, CID_FS_CROSS_TRIGGER, GenericInputBinding::Cross}, \
{"CircleLaunch", TRANSLATE_NOOP("USB", "Circle (Launch)"), nullptr, InputBindingInfo::Type::Button, CID_FS_CIRCLE_LAUNCH, GenericInputBinding::Circle}, \
{"Select", TRANSLATE_NOOP("USB", "Select (Fire C)"), nullptr, InputBindingInfo::Type::Button, CID_FS_SELECT_C, GenericInputBinding::Select}, \
{"Start", TRANSLATE_NOOP("USB", "Start"), nullptr, InputBindingInfo::Type::Button, CID_FS_START, GenericInputBinding::Start}, \
{"HatClick", TRANSLATE_NOOP("USB", "Hat Click"), nullptr, InputBindingInfo::Type::Button, CID_FS_ANALOG_HAT_CLICK, GenericInputBinding::R3},
switch (subtype)
{
case FLIGHTSTICK_FS1:
{
static constexpr const InputBindingInfo bindings_fs1[] = {
BINDINGS_FLIGHTSTICK_SHARED_ANALOG
BINDINGS_FLIGHTSTICK_SHARED_DPAD
BINDINGS_FLIGHTSTICK_SHARED_BUTTONS
};
return bindings_fs1;
}
case FLIGHTSTICK_FS2:
{
static constexpr const InputBindingInfo bindings_fs2[] = {
BINDINGS_FLIGHTSTICK_SHARED_ANALOG
BINDINGS_FLIGHTSTICK_SHARED_DPAD
{"Dpad2Left", TRANSLATE_NOOP("USB", "D-Pad 2 Left"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_2_L, GenericInputBinding::Unknown},
{"Dpad2Right", TRANSLATE_NOOP("USB", "D-Pad 2 Right"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_2_R, GenericInputBinding::Unknown},
{"Dpad2Up", TRANSLATE_NOOP("USB", "D-Pad 2 Up"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_2_U, GenericInputBinding::Unknown},
{"Dpad2Down", TRANSLATE_NOOP("USB", "D-Pad 2 Down"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_2_D, GenericInputBinding::Unknown},
BINDINGS_FLIGHTSTICK_SHARED_BUTTONS
{"D", TRANSLATE_NOOP("USB", "D"), nullptr, InputBindingInfo::Type::Button, CID_FS_D, GenericInputBinding::L3},
{"SW1", TRANSLATE_NOOP("USB", "SW1 (Pinky Trigger)"), nullptr, InputBindingInfo::Type::Button, CID_FS_D, GenericInputBinding::Unknown},
{"Dpad3Left", TRANSLATE_NOOP("USB", "D-Pad 3 Left"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_3_L, GenericInputBinding::Unknown},
{"Dpad3Middle", TRANSLATE_NOOP("USB", "D-Pad 3 Middle"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_3_M, GenericInputBinding::Unknown},
{"Dpad3Right", TRANSLATE_NOOP("USB", "D-Pad 3 Right"), nullptr, InputBindingInfo::Type::Button, CID_FS_DPAD_3_R, GenericInputBinding::Unknown},
{"Motor", TRANSLATE_NOOP("USB", "Motor"), nullptr, InputBindingInfo::Type::Motor, 0, GenericInputBinding::LargeMotor},
};
return bindings_fs2;
}
default:
break;
}
return {};
//remove the macros
#undef BINDINGS_FLIGHTSTICK_SHARED_ANALOG
#undef BINDINGS_FLIGHTSTICK_SHARED_DPAD
#undef BINDINGS_FLIGHTSTICK_SHARED_BUTTONS
}
static constexpr u32 button_mask(u32 bind_index)
{
return (1u << (bind_index - FlightStickControlID::BUTTONS_OFFSET));
}
static constexpr u32 button_at(u32 value, u32 index)
{
return value & button_mask(index);
}
static void flightstick_handle_reset(USBDevice* dev)
{
FlightStickDeviceState* s = USB_CONTAINER_OF(dev, FlightStickDeviceState, dev);
s->Reset();
}
static void flightstick_handle_control(USBDevice* dev, USBPacket* p, int request, int value,
int index, int length, uint8_t* data)
{
const FlightStickDeviceState* s = USB_CONTAINER_OF(dev, const FlightStickDeviceState, dev);
int ret = 0;
switch (request)
{
case DeviceRequest | USB_REQ_GET_DESCRIPTOR:
{
ret = usb_desc_handle_control(dev, p, request, value, index, length, data);
if (ret < 0)
goto fail;
break;
}
case VendorDeviceRequest: // 0x00
{
FlightStickConData_VR00 vendordata_00{};
ret = sizeof(vendordata_00);
std::memset(&vendordata_00, 0xff, ret);
vendordata_00.fire_c = button_at(s->data.buttons, CID_FS_SELECT_C) ? 0 : 1;
//vendordata_00.button_d = 0x1;
vendordata_00.hat_btn = button_at(s->data.buttons, CID_FS_ANALOG_HAT_CLICK) ? 0 : 1;
vendordata_00.button_st = button_at(s->data.buttons, CID_FS_START) ? 0 : 1;
vendordata_00.hat1_u = button_at(s->data.buttons, CID_FS_DPAD_1_U) ? 0 : 1;
vendordata_00.hat1_r = button_at(s->data.buttons, CID_FS_DPAD_1_R) ? 0 : 1;
vendordata_00.hat1_d = button_at(s->data.buttons, CID_FS_DPAD_1_D) ? 0 : 1;
vendordata_00.hat1_l = button_at(s->data.buttons, CID_FS_DPAD_1_L) ? 0 : 1;
//vendordata_00.reserved1 = 0xf;
//vendordata_00.reserved2 : 0x1;
vendordata_00.launch = button_at(s->data.buttons, CID_FS_CIRCLE_LAUNCH) ? 0 : 1;
vendordata_00.trigger = button_at(s->data.buttons, CID_FS_CROSS_TRIGGER) ? 0 : 1;
//vendordata_00.reserved3 = 0x1;
if (s->type == FLIGHTSTICK_FS2)
{
vendordata_00.button_d = button_at(s->data.buttons, CID_FS_D) ? 0 : 1;
}
std::memcpy(data, &vendordata_00, ret);
p->actual_length = ret;
break;
}
case VendorDeviceRequest | 0x01:
{
FlightStickConData_VR01 vendordata_01{};
ret = sizeof(vendordata_01);
std::memset(&vendordata_01, 0xff, ret);
if (s->type == FLIGHTSTICK_FS2)
{
//vendordata_01.reserved4 = 0xf;
vendordata_01.hat3_r = button_at(s->data.buttons, CID_FS_DPAD_3_R) ? 0 : 1;
vendordata_01.hat3_m = button_at(s->data.buttons, CID_FS_DPAD_3_M) ? 0 : 1;
vendordata_01.hat3_l = button_at(s->data.buttons, CID_FS_DPAD_3_L) ? 0 : 1;
vendordata_01.reserved5 = 0x0;
vendordata_01.mode_select = s->mode; // stored on settings page
//vendordata_01.reserved6 = 0x1;
vendordata_01.button_sw1 = button_at(s->data.buttons, CID_FS_SW1) ? 0 : 1;
vendordata_01.hat2_u = button_at(s->data.buttons, CID_FS_DPAD_2_U) ? 0 : 1;
vendordata_01.hat2_r = button_at(s->data.buttons, CID_FS_DPAD_2_R) ? 0 : 1;
vendordata_01.hat2_d = button_at(s->data.buttons, CID_FS_DPAD_2_D) ? 0 : 1;
vendordata_01.hat2_l = button_at(s->data.buttons, CID_FS_DPAD_2_L) ? 0 : 1;
}
std::memcpy(data, &vendordata_01, ret);
p->actual_length = ret;
break;
}
case VendorDeviceOutRequest | 0x0C:
{
//rumble (only possible on FS2)
if (index == 0 && length == 1 && s->type == FLIGHTSTICK_FS2)
{
InputManager::SetUSBVibrationIntensity(s->port, std::min(static_cast<float>(data[0]) * (1.0f / 255.0f), 1.0f), 0);
}
ret = length;
p->actual_length = ret;
break;
}
default:
{
ret = usb_desc_handle_control(dev, p, request, value, index, length, data);
if (ret >= 0)
{
return;
}
}
fail:
p->status = USB_RET_STALL;
break;
}
//if (usb_desc_handle_control(dev, p, request, value, index, length, data) < 0)
// p->status = USB_RET_STALL;
}
static void flightstick_handle_destroy(USBDevice* dev) noexcept
{
FlightStickDeviceState* s = USB_CONTAINER_OF(dev, FlightStickDeviceState, dev);
delete s;
}
bool FlightStickDevice::Freeze(USBDevice* dev, StateWrapper& sw) const
{
FlightStickDeviceState* s = USB_CONTAINER_OF(dev, FlightStickDeviceState, dev);
if (!sw.DoMarker("FlightStickController"))
return false;
sw.Do(&s->data.stick_left);
sw.Do(&s->data.stick_right);
sw.Do(&s->data.stick_up);
sw.Do(&s->data.stick_down);
sw.Do(&s->data.rudder_left);
sw.Do(&s->data.rudder_right);
sw.Do(&s->data.throttle_up);
sw.Do(&s->data.throttle_down);
sw.Do(&s->data.stick_hat_left);
sw.Do(&s->data.stick_hat_right);
sw.Do(&s->data.stick_hat_up);
sw.Do(&s->data.stick_hat_down);
sw.Do(&s->data.stick_x);
sw.Do(&s->data.stick_y);
sw.Do(&s->data.rudder);
sw.Do(&s->data.throttle);
sw.Do(&s->data.hatstick_x);
sw.Do(&s->data.hatstick_y);
sw.Do(&s->data.button_a);
sw.Do(&s->data.button_b);
sw.DoBytes(&s->data.buttons, sizeof(u32));
return true;
}
void FlightStickDevice::UpdateSettings(USBDevice* dev, SettingsInterface& si) const
{
FlightStickDeviceState* s = USB_CONTAINER_OF(dev, FlightStickDeviceState, dev);
s->mode = USB::GetConfigInt(si, s->port, TypeName(), "Mode", 3);
if (s->type == FLIGHTSTICK_FS2)
{
Host::AddKeyedOSDMessage("USB", fmt::format("FlightStick Mode: {}", s->mode), Host::OSD_QUICK_DURATION);
}
}
std::span<const SettingInfo> FlightStickDevice::Settings(u32 subtype) const
{
static const char* s_mode_options[] = {
TRANSLATE_NOOP("USB", "1"),
TRANSLATE_NOOP("USB", "2"),
TRANSLATE_NOOP("USB", "3"),
nullptr};
static constexpr const SettingInfo mode = {
SettingInfo::Type::IntegerList, // type
"Mode", // name
TRANSLATE_NOOP("USB", "Mode switch"), // display name
TRANSLATE_NOOP("USB", "Set the stick mode switch position"), // description
"3", // default value
"1", // min value
"3", // max value
nullptr, // step value
nullptr, // format
s_mode_options, // options for integer lists
nullptr, // options for string lists
0.0f // multiplier
};
static constexpr const SettingInfo info[] = {mode};
if (subtype == FLIGHTSTICK_FS2)
return info;
else
return {};
}
float FlightStickDevice::GetBindingValue(const USBDevice* dev, u32 bind_index) const
{
const FlightStickDeviceState* s = USB_CONTAINER_OF(dev, const FlightStickDeviceState, dev);
switch (bind_index)
{
case CID_FS_STICK_L:
return (static_cast<float>(s->data.stick_left) / 255.0f);
case CID_FS_STICK_R:
return (static_cast<float>(s->data.stick_right) / 255.0f);
case CID_FS_STICK_U:
return (static_cast<float>(s->data.stick_up) / 255.0f);
case CID_FS_STICK_D:
return (static_cast<float>(s->data.stick_down) / 255.0f);
case CID_FS_RUDDER_L:
return (static_cast<float>(s->data.rudder_left) / 255.0f);
case CID_FS_RUDDER_R:
return (static_cast<float>(s->data.rudder_right) / 255.0f);
case CID_FS_THROTTLE_U:
return (static_cast<float>(s->data.throttle_up) / 255.0f);
case CID_FS_THROTTLE_D:
return (static_cast<float>(s->data.throttle_down) / 255.0f);
case CID_FS_ANALOG_HAT_L:
return (static_cast<float>(s->data.stick_hat_left) / 255.0f);
case CID_FS_ANALOG_HAT_R:
return (static_cast<float>(s->data.stick_hat_right) / 255.0f);
case CID_FS_ANALOG_HAT_U:
return (static_cast<float>(s->data.stick_hat_up) / 255.0f);
case CID_FS_ANALOG_HAT_D:
return (static_cast<float>(s->data.stick_hat_down) / 255.0f);
case CID_FS_TRIANGLE_A:
return (static_cast<float>(s->data.button_a) / 255.0f);
case CID_FS_SQUARE_B:
return (static_cast<float>(s->data.button_b) / 255.0f);
case CID_FS_DPAD_1_L:
case CID_FS_DPAD_1_R:
case CID_FS_DPAD_1_U:
case CID_FS_DPAD_1_D:
case CID_FS_CROSS_TRIGGER:
case CID_FS_CIRCLE_LAUNCH:
case CID_FS_SELECT_C:
case CID_FS_START:
case CID_FS_ANALOG_HAT_CLICK:
case CID_FS_D:
case CID_FS_SW1:
case CID_FS_DPAD_2_L:
case CID_FS_DPAD_2_R:
case CID_FS_DPAD_2_D:
case CID_FS_DPAD_2_U:
case CID_FS_DPAD_3_R:
case CID_FS_DPAD_3_M:
case CID_FS_DPAD_3_L:
{
return (button_at(s->data.buttons, bind_index) != 0u) ? 1.0f : 0.0f;
}
default:
return 0.0f;
}
}
void FlightStickDevice::SetBindingValue(USBDevice* dev, u32 bind_index, float value) const
{
FlightStickDeviceState* s = USB_CONTAINER_OF(dev, FlightStickDeviceState, dev);
switch (bind_index)
{
case CID_FS_STICK_L:
s->data.stick_left = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateStick();
break;
case CID_FS_STICK_R:
s->data.stick_right = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateStick();
break;
case CID_FS_STICK_U:
s->data.stick_up = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateStick();
break;
case CID_FS_STICK_D:
s->data.stick_down = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateStick();
break;
case CID_FS_RUDDER_L:
s->data.rudder_left = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateRudder();
break;
case CID_FS_RUDDER_R:
s->data.rudder_right = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateRudder();
break;
case CID_FS_THROTTLE_U:
s->data.throttle_up = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateThrottle();
break;
case CID_FS_THROTTLE_D:
s->data.throttle_down = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateThrottle();
break;
case CID_FS_ANALOG_HAT_L:
s->data.stick_hat_left = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateStickHat();
break;
case CID_FS_ANALOG_HAT_R:
s->data.stick_hat_right = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateStickHat();
break;
case CID_FS_ANALOG_HAT_U:
s->data.stick_hat_up = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateStickHat();
break;
case CID_FS_ANALOG_HAT_D:
s->data.stick_hat_down = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
s->UpdateStickHat();
break;
case CID_FS_TRIANGLE_A:
s->data.button_a = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
break;
case CID_FS_SQUARE_B:
s->data.button_b = static_cast<u8>(std::clamp<long>(std::lroundf(value * 255.0f), 0, 255));
break;
case CID_FS_DPAD_1_L:
case CID_FS_DPAD_1_R:
case CID_FS_DPAD_1_U:
case CID_FS_DPAD_1_D:
case CID_FS_CROSS_TRIGGER:
case CID_FS_CIRCLE_LAUNCH:
case CID_FS_SELECT_C:
case CID_FS_START:
case CID_FS_ANALOG_HAT_CLICK:
case CID_FS_D:
case CID_FS_SW1:
case CID_FS_DPAD_2_L:
case CID_FS_DPAD_2_R:
case CID_FS_DPAD_2_D:
case CID_FS_DPAD_2_U:
case CID_FS_DPAD_3_R:
case CID_FS_DPAD_3_M:
case CID_FS_DPAD_3_L:
{
const u32 mask = button_mask(bind_index);
if (value >= 0.5f)
s->data.buttons |= mask;
else
s->data.buttons &= ~mask;
}
break;
default:
break;
}
}
FlightStickDeviceState::FlightStickDeviceState(u32 port_, FlightStickDeviceTypes type_)
: port(port_)
, type(type_)
{
Reset();
}
FlightStickDeviceState::~FlightStickDeviceState() = default;
void FlightStickDeviceState::Reset()
{
data.stick_left = 0;
data.stick_right = 0;
data.stick_up = 0;
data.stick_down = 0;
data.rudder_left = 0;
data.rudder_right = 0;
data.throttle_up = 0;
data.throttle_down = 0;
data.stick_hat_left = 0;
data.stick_hat_right = 0;
data.stick_hat_up = 0;
data.stick_hat_down = 0;
data.stick_x = analog_center;
data.stick_y = analog_center;
data.rudder = analog_center;
data.throttle = analog_center;
data.hatstick_x = analog_center;
data.hatstick_y = analog_center;
data.button_a = 0x00;
data.button_b = 0x00;
data.buttons = 0;
}
void FlightStickDeviceState::UpdateStick() noexcept
{
if (data.stick_left > 0)
data.stick_x = static_cast<u8>(std::max<int>(analog_range - data.stick_left, 0));
else if (data.stick_right > 0)
data.stick_x = static_cast<u8>(std::min<int>(analog_range + data.stick_right, analog_range * 2));
else
data.stick_x = 0x80;
if (data.stick_up > 0)
data.stick_y = static_cast<u8>(std::max<int>(analog_range - data.stick_up, 0));
else if (data.stick_down > 0)
data.stick_y = static_cast<u8>(std::min<int>(analog_range + data.stick_down, analog_range * 2));
else
data.stick_y = 0x80;
}
void FlightStickDeviceState::UpdateRudder() noexcept
{
if (data.rudder_left > 0)
data.rudder = static_cast<u8>(std::max<int>(analog_range - data.rudder_left, 0));
else if (data.rudder_right > 0)
data.rudder = static_cast<u8>(std::min<int>(analog_range + data.rudder_right, analog_range * 2));
else
data.rudder = 0x80;
}
void FlightStickDeviceState::UpdateThrottle() noexcept
{
if (data.throttle_up > 0)
data.throttle = static_cast<u8>(std::min<int>(analog_range + data.throttle_up, analog_range * 2));
else if (data.throttle_down > 0)
data.throttle = static_cast<u8>(std::max<int>(analog_range - data.throttle_down, 0));
else
data.throttle = 0x80;
}
void FlightStickDeviceState::UpdateStickHat() noexcept
{
if (data.stick_hat_left > 0)
data.hatstick_x = static_cast<u8>(std::max<int>(analog_range - data.stick_hat_left, 0));
else if (data.stick_hat_right > 0)
data.hatstick_x = static_cast<u8>(std::min<int>(analog_range + data.stick_hat_right, analog_range * 2));
else
data.hatstick_x = 0x80;
if (data.stick_hat_up > 0)
data.hatstick_y = static_cast<u8>(std::max<int>(analog_range - data.stick_hat_up, 0));
else if (data.stick_hat_down > 0)
data.hatstick_y = static_cast<u8>(std::min<int>(analog_range + data.stick_hat_down, analog_range * 2));
else
data.hatstick_y = 0x80;
}
static void flightstick_handle_data(USBDevice* dev, USBPacket* p)
{
FlightStickDeviceState* s = USB_CONTAINER_OF(dev, FlightStickDeviceState, dev);
if (p->pid != USB_TOKEN_IN || p->ep->nr != 1)
{
Console.Error("Unhandled FlightStickController request pid=%d ep=%u", p->pid, p->ep->nr);
p->status = USB_RET_STALL;
return;
}
switch (s->type)
{
case FLIGHTSTICK_FS1:
case FLIGHTSTICK_FS2:
{
//interrupt input data
FlightStickConData out = {};
out.stick_x = s->data.stick_x;
out.stick_y = s->data.stick_y;
out.rudder = s->data.rudder;
out.throttle = s->data.throttle;
out.hat_x = s->data.hatstick_x;
out.hat_y = s->data.hatstick_y;
out.button_a = static_cast<u8>(~(s->data.button_a));
out.button_b = static_cast<u8>(~(s->data.button_b));
usb_packet_copy(p, &out, sizeof(out));
break;
}
default:
Console.Error("Unhandled FlightStickController USB_TOKEN_IN pid=%d ep=%u type=%u", p->pid, p->ep->nr, s->type);
p->status = USB_RET_IOERROR;
return;
}
}
USBDevice* FlightStickDevice::CreateDevice(SettingsInterface& si, u32 port, u32 subtype) const
{
FlightStickDeviceState* s = new FlightStickDeviceState(port, static_cast<FlightStickDeviceTypes>(subtype));
s->desc.full = &s->desc_dev;
switch (subtype)
{
case FLIGHTSTICK_FS1:
s->desc.str = fst01_desc_strings;
if (usb_desc_parse_dev(fst01_dev_descriptor, sizeof(fst01_dev_descriptor), s->desc, s->desc_dev) < 0)
goto fail;
break;
case FLIGHTSTICK_FS2:
s->desc.str = fst02_desc_strings;
if (usb_desc_parse_dev(fst02_dev_descriptor, sizeof(fst02_dev_descriptor), s->desc, s->desc_dev) < 0)
goto fail;
break;
default:
goto fail;
}
if (usb_desc_parse_config(flightstick_config_descriptor, sizeof(flightstick_config_descriptor), s->desc_dev) < 0)
goto fail;
s->dev.speed = USB_SPEED_FULL;
s->dev.klass.handle_attach = usb_desc_attach;
s->dev.klass.handle_reset = flightstick_handle_reset;
s->dev.klass.handle_control = flightstick_handle_control;
s->dev.klass.handle_data = flightstick_handle_data;
s->dev.klass.unrealize = flightstick_handle_destroy;
s->dev.klass.usb_desc = &s->desc;
s->dev.klass.product_desc = s->desc.str[2];
usb_desc_init(&s->dev);
usb_ep_init(&s->dev);
flightstick_handle_reset(&s->dev);
UpdateSettings(&s->dev, si);
return &s->dev;
fail:
flightstick_handle_destroy(&s->dev);
return nullptr;
}
} // namespace usb_pad

View File

@ -0,0 +1,210 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "USB/deviceproxy.h"
#include "USB/qemu-usb/qusb.h"
#include "USB/qemu-usb/desc.h"
namespace usb_pad
{
enum FlightStickDeviceTypes
{
FLIGHTSTICK_FS1, // HP2-13 (FlightStick)
FLIGHTSTICK_FS2, // HP2-217 (FlightStick 2)
FLIGHTSTICK_COUNT,
};
class FlightStickDevice final : public DeviceProxy
{
public:
USBDevice* CreateDevice(SettingsInterface& si, u32 port, u32 subtype) const override;
const char* Name() const override;
const char* TypeName() const override;
const char* IconName() const override;
std::span<const char*> SubTypes() const override;
void UpdateSettings(USBDevice* dev, SettingsInterface& si) const override;
std::span<const SettingInfo> Settings(u32 subtype) const override;
float GetBindingValue(const USBDevice* dev, u32 bind_index) const override;
void SetBindingValue(USBDevice* dev, u32 bind_index, float value) const override;
std::span<const InputBindingInfo> Bindings(u32 subtype) const override;
bool Freeze(USBDevice* dev, StateWrapper& sw) const override;
};
#pragma pack(push, 1)
struct FlightStickConData // interrupt input data
{ /* FlightStick 1 | FlightStick 2 */
u8 stick_x; /* stick (left=0x00, right=0xff) | identical */
u8 stick_y; /* stick (top=0x00, bottom=0xff) | identical */
u8 rudder; /* rudder (left=0x00, right=0xff) | identical */
u8 throttle; /* throttle (top=0xff, bottom=0x00) | (top=0x00, bottom=0xff) */
u8 hat_x; /* hat (left=0x00, right=0xff) | identical */
u8 hat_y; /* hat (top=0x00, bottom=0xff) | identical */
u8 button_a; /* triangle (press=0x00, release=0xff) | button A */
u8 button_b; /* square (press=0x00, release=0xff) | button B */
};
static_assert(sizeof(FlightStickConData) == 8);
struct FlightStickConData_VR00 // input data for vendor request 00
{ /* FlightStick 1 | FlightStick 2 */
bool fire_c : 1; /* button select | button fire-c */
bool button_d : 1; /* 0x1 | button D */
bool hat_btn : 1; /* hat press | hat press */
bool button_st : 1; /* button start | button ST */
bool hat1_u : 1; /* d-pad top | d-pad 1 top */
bool hat1_r : 1; /* d-pad right | d-pad 1 right */
bool hat1_d : 1; /* d-pad bottom | d-pad 1 bottom */
bool hat1_l : 1; /* d-pad left | d-pad 1 left */
u8 reserved1 : 4; /* 0xf | 0xf */
bool reserved2 : 1; /* 0x1 | 0x1 */
bool launch : 1; /* button launch | button launch */
bool trigger : 1; /* trigger | trigger */
bool reserved3 : 1; /* 0x1 | 0x1 */
};
static_assert(sizeof(FlightStickConData_VR00) == 2);
struct FlightStickConData_VR01 // input data for vendor request 01
{ /* FlightStick 1 | FlightStick 2 */
u8 reserved4 : 4; /* 0xf | 0xf */
bool hat3_r : 1; /* 0x1 | d-pad 3 right */
bool hat3_m : 1; /* 0x1 | d-pad 3 middle */
bool hat3_l : 1; /* 0x1 | d-pad 3 left */
bool reserved5 : 1; /* 0x1 | 0x0 */
u8 mode_select : 2; /* 0x3 | mode select (M1=2, M2=1, M3=3) */
bool reserved6 : 1; /* 0x1 | 0x1 */
bool button_sw1 : 1; /* 0x1 | button sw-1 */
bool hat2_u : 1; /* 0x1 | d-pad 2 top */
bool hat2_r : 1; /* 0x1 | d-pad 2 right */
bool hat2_d : 1; /* 0x1 | d-pad 2 bottom */
bool hat2_l : 1; /* 0x1 | d-pad 2 left */
};
static_assert(sizeof(FlightStickConData_VR01) == 2);
#pragma pack(pop)
struct FlightStickDeviceState
{
FlightStickDeviceState(u32 port_, FlightStickDeviceTypes type_);
~FlightStickDeviceState();
void Reset();
void UpdateStick() noexcept;
void UpdateRudder() noexcept;
void UpdateThrottle() noexcept;
void UpdateStickHat() noexcept;
USBDevice dev{};
USBDesc desc{};
USBDescDevice desc_dev{};
u32 port = 0;
FlightStickDeviceTypes type = FLIGHTSTICK_FS1;
u8 mode = 3;
const u8 analog_center = 0x80;
const u8 analog_range = 0xFF >> 1;
struct
{
// intermediate state, resolved at query time
u8 stick_left;
u8 stick_right;
u8 stick_up;
u8 stick_down;
u8 rudder_left;
u8 rudder_right;
u8 throttle_up;
u8 throttle_down;
u8 stick_hat_left;
u8 stick_hat_right;
u8 stick_hat_up;
u8 stick_hat_down;
u8 stick_x;
u8 stick_y;
u8 rudder;
u8 throttle;
u8 hatstick_x;
u8 hatstick_y;
u8 button_a;
u8 button_b;
u32 buttons; // dpads and buttons
} data = {};
};
#define DEFINE_DCTFS_DEV_DESCRIPTOR(prefix, bcdUSB, bcdDevice) \
static const uint8_t prefix##_dev_descriptor[] = { \
/* bLength */ USB_DEVICE_DESC_SIZE, \
/* bDescriptorType */ USB_DEVICE_DESCRIPTOR_TYPE, \
/* bcdUSB */ WBVAL(bcdUSB), /* FS1=0x0100, FS2=0x0110 */ \
/* bDeviceClass */ 0xFF, \
/* bDeviceSubClass */ 0x01, \
/* bDeviceProtocol */ 0xFF, \
/* bMaxPacketSize0 */ 0x08, \
/* idVendor */ WBVAL(0x06D3), \
/* idProduct */ WBVAL(0x0F10), \
/* bcdDevice */ WBVAL(bcdDevice), /* FS1=0x0001, FS2=0x0002 */ \
/* iManufacturer */ 0x00, \
/* iProduct */ 0x00, \
/* iSerialNumber */ 0x00, \
/* bNumConfigurations */ 0x01, \
}
// common for both models
static const uint8_t flightstick_config_descriptor[] = {
USB_CONFIGURATION_DESC_SIZE, // bLength
USB_CONFIGURATION_DESCRIPTOR_TYPE, // bDescriptorType
WBVAL(34), // wTotalLength
0x01, // bNumInterfaces
0x01, // bConfigurationValue
0x00, // iConfiguration (String Index)
0xA0, // bmAttributes
0x32, // bMaxPower 100mA
USB_INTERFACE_DESC_SIZE, // bLength
USB_INTERFACE_DESCRIPTOR_TYPE, // bDescriptorType
0x00, // bInterfaceNumber
0x00, // bAlternateSetting
0x01, // bNumEndpoints
0xFF, // bInterfaceClass
0x01, // bInterfaceSubClass
0x02, // bInterfaceProtocol
0x00, // iInterface (String Index)
// Unknown (looks to be HID. descriptor data is missing)
0x09, // bLength
0x21, // bDescriptorType (HID)
0x00, 0x01, // bcdHID 1.00
0x00, // bCountryCode
0x01, // bNumDescriptors
0x22, // bDescriptorType[0] (HID)
0x40, 0x00, // wDescriptorLength[0] 64
USB_ENDPOINT_DESC_SIZE, // bLength
USB_ENDPOINT_DESCRIPTOR_TYPE, // bDescriptorType
USB_ENDPOINT_IN(1), // bEndpointAddress (IN/D2H)
USB_ENDPOINT_TYPE_INTERRUPT, // bmAttributes (Interrupt)
WBVAL(8), // wMaxPacketSize
0x0A, // bInterval 10 (unit depends on device speed)
};
// ---- FlightStick "Type 1" ----
static const USBDescStrings fst01_desc_strings = {""};
// fst01_dev_descriptor
DEFINE_DCTFS_DEV_DESCRIPTOR(fst01, 0x0100, 0x0001);
// ---- FlightStick "Type 2" ----
static const USBDescStrings fst02_desc_strings = {""};
// fst02_dev_descriptor
DEFINE_DCTFS_DEV_DESCRIPTOR(fst02, 0x0110, 0x0002);
} // namespace usb_pad

View File

@ -400,6 +400,7 @@
<ClCompile Include="USB\usb-msd\usb-msd.cpp" /> <ClCompile Include="USB\usb-msd\usb-msd.cpp" />
<ClCompile Include="USB\usb-pad\lg\lg_ff.cpp" /> <ClCompile Include="USB\usb-pad\lg\lg_ff.cpp" />
<ClCompile Include="USB\usb-pad\usb-buzz.cpp" /> <ClCompile Include="USB\usb-pad\usb-buzz.cpp" />
<ClCompile Include="USB\usb-pad\usb-flightstick.cpp" />
<ClCompile Include="USB\usb-pad\usb-gametrak.cpp" /> <ClCompile Include="USB\usb-pad\usb-gametrak.cpp" />
<ClCompile Include="USB\usb-pad\usb-realplay.cpp" /> <ClCompile Include="USB\usb-pad\usb-realplay.cpp" />
<ClCompile Include="USB\usb-pad\usb-pad-ff.cpp" /> <ClCompile Include="USB\usb-pad\usb-pad-ff.cpp" />
@ -857,6 +858,7 @@
<ClInclude Include="USB\usb-msd\usb-msd.h" /> <ClInclude Include="USB\usb-msd\usb-msd.h" />
<ClInclude Include="USB\usb-pad\lg\lg_ff.h" /> <ClInclude Include="USB\usb-pad\lg\lg_ff.h" />
<ClInclude Include="USB\usb-pad\usb-buzz.h" /> <ClInclude Include="USB\usb-pad\usb-buzz.h" />
<ClInclude Include="USB\usb-pad\usb-flightstick.h" />
<ClInclude Include="USB\usb-pad\usb-gametrak.h" /> <ClInclude Include="USB\usb-pad\usb-gametrak.h" />
<ClInclude Include="USB\usb-pad\usb-realplay.h" /> <ClInclude Include="USB\usb-pad\usb-realplay.h" />
<ClInclude Include="USB\usb-pad\usb-pad-sdl-ff.h" /> <ClInclude Include="USB\usb-pad\usb-pad-sdl-ff.h" />

View File

@ -1449,6 +1449,9 @@
<ClCompile Include="GS\GSDrawingEnvironment.cpp"> <ClCompile Include="GS\GSDrawingEnvironment.cpp">
<Filter>System\Ps2\GS\Renderers\Software</Filter> <Filter>System\Ps2\GS\Renderers\Software</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="USB\usb-pad\usb-flightstick.cpp">
<Filter>System\Ps2\USB\usb-pad</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="Patch.h"> <ClInclude Include="Patch.h">
@ -2410,6 +2413,9 @@
<ClInclude Include="SIO\Pad\PadNegcon.h"> <ClInclude Include="SIO\Pad\PadNegcon.h">
<Filter>System\Ps2\Iop\SIO\PAD</Filter> <Filter>System\Ps2\Iop\SIO\PAD</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="USB\usb-pad\usb-flightstick.h">
<Filter>System\Ps2\USB\usb-pad</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<CustomBuildStep Include="rdebug\deci2.h"> <CustomBuildStep Include="rdebug\deci2.h">