diff --git a/pcsx2/CMakeLists.txt b/pcsx2/CMakeLists.txt index db54f785f2..be628e2be7 100644 --- a/pcsx2/CMakeLists.txt +++ b/pcsx2/CMakeLists.txt @@ -389,6 +389,7 @@ set(pcsx2USBSources USB/usb-pad/usb-pad-sdl-ff.cpp USB/usb-pad/usb-pad.cpp USB/usb-pad/usb-seamic.cpp + USB/usb-pad/usb-train.cpp USB/usb-pad/usb-trance-vibrator.cpp USB/usb-pad/usb-turntable.cpp USB/usb-printer/usb-printer.cpp @@ -425,6 +426,7 @@ set(pcsx2USBHeaders USB/usb-pad/usb-realplay.h USB/usb-pad/usb-pad-sdl-ff.h USB/usb-pad/usb-pad.h + USB/usb-pad/usb-train.h USB/usb-pad/usb-trance-vibrator.h USB/usb-printer/usb-printer.h ) diff --git a/pcsx2/USB/deviceproxy.cpp b/pcsx2/USB/deviceproxy.cpp index dec230b5f5..2515d95dd4 100644 --- a/pcsx2/USB/deviceproxy.cpp +++ b/pcsx2/USB/deviceproxy.cpp @@ -11,6 +11,7 @@ #include "usb-mic/usb-mic.h" #include "usb-msd/usb-msd.h" #include "usb-pad/usb-pad.h" +#include "usb-pad/usb-train.h" #include "usb-pad/usb-trance-vibrator.h" #include "usb-pad/usb-turntable.h" #include "usb-printer/usb-printer.h" @@ -83,6 +84,7 @@ void RegisterDevice::Register() inst.Add(DEVTYPE_GUNCON2, new usb_lightgun::GunCon2Device()); inst.Add(DEVTYPE_GAMETRAK, new usb_pad::GametrakDevice()); inst.Add(DEVTYPE_REALPLAY, new usb_pad::RealPlayDevice()); + inst.Add(DEVTYPE_TRAIN, new usb_pad::TrainDevice()); } void RegisterDevice::Unregister() diff --git a/pcsx2/USB/deviceproxy.h b/pcsx2/USB/deviceproxy.h index 9103094f51..b03e93952c 100644 --- a/pcsx2/USB/deviceproxy.h +++ b/pcsx2/USB/deviceproxy.h @@ -41,6 +41,7 @@ enum DeviceType : s32 DEVTYPE_DJ, DEVTYPE_GAMETRAK, DEVTYPE_REALPLAY, + DEVTYPE_TRAIN, }; class DeviceProxy diff --git a/pcsx2/USB/usb-pad/usb-train.cpp b/pcsx2/USB/usb-pad/usb-train.cpp new file mode 100644 index 0000000000..ca2bb8cb1d --- /dev/null +++ b/pcsx2/USB/usb-pad/usb-train.cpp @@ -0,0 +1,333 @@ +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "usb-train.h" + +#include "common/Console.h" +#include "Host.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* TrainDevice::Name() const + { + return TRANSLATE_NOOP("USB", "Train Controller"); + } + + const char* TrainDevice::TypeName() const + { + return "TrainController"; + } + + enum TrainControlID + { + CID_TC_POWER, + CID_TC_BRAKE, + CID_TC_UP, + CID_TC_RIGHT, + CID_TC_DOWN, + CID_TC_LEFT, + + // TCPP20009 sends the buttons in this order in the relevant byte + CID_TC_B, + CID_TC_A, + CID_TC_C, + CID_TC_D, + CID_TC_SELECT, + CID_TC_START, + + BUTTONS_OFFSET = CID_TC_B, + }; + + std::span TrainDevice::Bindings(u32 subtype) const + { + static constexpr const InputBindingInfo bindings[] = { + {"Power", TRANSLATE_NOOP("USB", "Power"), ICON_PF_LEFT_ANALOG_DOWN, InputBindingInfo::Type::Axis, CID_TC_POWER, GenericInputBinding::LeftStickDown}, + {"Brake", TRANSLATE_NOOP("USB", "Brake"), ICON_PF_LEFT_ANALOG_UP, InputBindingInfo::Type::Axis, CID_TC_BRAKE, GenericInputBinding::LeftStickUp}, + {"Up", TRANSLATE_NOOP("USB", "D-Pad Up"), ICON_PF_DPAD_UP, InputBindingInfo::Type::Button, CID_TC_UP, GenericInputBinding::DPadUp}, + {"Down", TRANSLATE_NOOP("USB", "D-Pad Down"), ICON_PF_DPAD_DOWN, InputBindingInfo::Type::Button, CID_TC_DOWN, GenericInputBinding::DPadDown}, + {"Left", TRANSLATE_NOOP("USB", "D-Pad Left"), ICON_PF_DPAD_LEFT, InputBindingInfo::Type::Button, CID_TC_LEFT, GenericInputBinding::DPadLeft}, + {"Right", TRANSLATE_NOOP("USB", "D-Pad Right"), ICON_PF_DPAD_RIGHT, InputBindingInfo::Type::Button, CID_TC_RIGHT, GenericInputBinding::DPadRight}, + {"A", TRANSLATE_NOOP("USB", "A Button"), ICON_PF_KEY_A, InputBindingInfo::Type::Button, CID_TC_A, GenericInputBinding::Square}, + {"B", TRANSLATE_NOOP("USB", "B Button"), ICON_PF_KEY_B, InputBindingInfo::Type::Button, CID_TC_B, GenericInputBinding::Cross}, + {"C", TRANSLATE_NOOP("USB", "C Button"), ICON_PF_KEY_C, InputBindingInfo::Type::Button, CID_TC_C, GenericInputBinding::Circle}, + {"D", TRANSLATE_NOOP("USB", "D Button"), ICON_PF_KEY_D, InputBindingInfo::Type::Button, CID_TC_D, GenericInputBinding::Triangle}, + {"Select", TRANSLATE_NOOP("USB", "Select"), ICON_PF_SELECT_SHARE, InputBindingInfo::Type::Button, CID_TC_SELECT, GenericInputBinding::Select}, + {"Start", TRANSLATE_NOOP("USB", "Start"), ICON_PF_START, InputBindingInfo::Type::Button, CID_TC_START, GenericInputBinding::Start}, + }; + + return bindings; + } + + static void train_handle_reset(USBDevice* dev) + { + TrainDeviceState* s = USB_CONTAINER_OF(dev, TrainDeviceState, dev); + s->Reset(); + } + + static void train_handle_control(USBDevice* dev, USBPacket* p, int request, int value, + int index, int length, uint8_t* data) + { + if (usb_desc_handle_control(dev, p, request, value, index, length, data) < 0) + p->status = USB_RET_STALL; + } + + static void train_handle_destroy(USBDevice* dev) noexcept + { + TrainDeviceState* s = USB_CONTAINER_OF(dev, TrainDeviceState, dev); + delete s; + } + + bool TrainDevice::Freeze(USBDevice* dev, StateWrapper& sw) const + { + TrainDeviceState* s = USB_CONTAINER_OF(dev, TrainDeviceState, dev); + + if (!sw.DoMarker("TrainController")) + return false; + + sw.Do(&s->data.power); + sw.Do(&s->data.brake); + return true; + } + + static constexpr u32 button_mask(u32 bind_index) + { + return (1u << (bind_index - TrainControlID::BUTTONS_OFFSET)); + } + + static constexpr u8 button_at(u8 value, u32 index) + { + return value & button_mask(index); + } + + float TrainDevice::GetBindingValue(const USBDevice* dev, u32 bind_index) const + { + const TrainDeviceState* s = USB_CONTAINER_OF(dev, const TrainDeviceState, dev); + + switch (bind_index) + { + case CID_TC_POWER: + return (static_cast(s->data.power) / 255.0f); + case CID_TC_BRAKE: + return (static_cast(s->data.brake) / 255.0f); + + case CID_TC_UP: + return static_cast(s->data.hat_up); + case CID_TC_DOWN: + return static_cast(s->data.hat_down); + case CID_TC_LEFT: + return static_cast(s->data.hat_left); + case CID_TC_RIGHT: + return static_cast(s->data.hat_right); + + case CID_TC_A: + case CID_TC_B: + case CID_TC_C: + case CID_TC_D: + case CID_TC_SELECT: + case CID_TC_START: + { + return (button_at(s->data.buttons, bind_index) != 0u) ? 1.0f : 0.0f; + } + + default: + return 0.0f; + } + } + + void TrainDevice::SetBindingValue(USBDevice* dev, u32 bind_index, float value) const + { + TrainDeviceState* s = USB_CONTAINER_OF(dev, TrainDeviceState, dev); + + switch (bind_index) + { + case CID_TC_POWER: + s->data.power = static_cast(std::clamp(std::lroundf(value * 255.0f), 0, 255)); + break; + case CID_TC_BRAKE: + s->data.brake = static_cast(std::clamp(std::lroundf(value * 255.0f), 0, 255)); + break; + + case CID_TC_UP: + s->data.hat_up = static_cast(std::clamp(std::lroundf(value * 255.0f), 0, 255)); + s->UpdateHatSwitch(); + break; + case CID_TC_DOWN: + s->data.hat_down = static_cast(std::clamp(std::lroundf(value * 255.0f), 0, 255)); + s->UpdateHatSwitch(); + break; + case CID_TC_LEFT: + s->data.hat_left = static_cast(std::clamp(std::lroundf(value * 255.0f), 0, 255)); + s->UpdateHatSwitch(); + break; + case CID_TC_RIGHT: + s->data.hat_right = static_cast(std::clamp(std::lroundf(value * 255.0f), 0, 255)); + s->UpdateHatSwitch(); + break; + + case CID_TC_A: + case CID_TC_B: + case CID_TC_C: + case CID_TC_D: + case CID_TC_SELECT: + case CID_TC_START: + { + const u32 mask = button_mask(bind_index); + if (value >= 0.5f) + s->data.buttons |= mask; + else + s->data.buttons &= ~mask; + } + break; + + default: + break; + } + } + + TrainDeviceState::TrainDeviceState(u32 port_) + : port(port_) + { + Reset(); + } + + TrainDeviceState::~TrainDeviceState() = default; + + void TrainDeviceState::Reset() + { + data.power = 0x00; + data.brake = 0x00; + } + + void TrainDeviceState::UpdateHatSwitch() noexcept + { + if (data.hat_up && data.hat_right) + data.hatswitch = 1; + else if (data.hat_right && data.hat_down) + data.hatswitch = 3; + else if (data.hat_down && data.hat_left) + data.hatswitch = 5; + else if (data.hat_left && data.hat_up) + data.hatswitch = 7; + else if (data.hat_up) + data.hatswitch = 0; + else if (data.hat_right) + data.hatswitch = 2; + else if (data.hat_down) + data.hatswitch = 4; + else if (data.hat_left) + data.hatswitch = 6; + else + data.hatswitch = 8; + } + + static u8 dct01_power(u8 value) + { + // (N) 0x81 0x6D 0x54 0x3F 0x21 0x00 (P5) + static std::pair const notches[] = { + // { control_in, emulated_out }, + {0xF8, 0x00}, + {0xC8, 0x21}, + {0x98, 0x3F}, + {0x58, 0x54}, + {0x28, 0x6D}, + {0x00, 0x81}, + }; + + for (const auto& x : notches) + { + if (value >= x.first) + return x.second; + } + return notches[std::size(notches) - 1].second; + } + + static u8 dct01_brake(u8 value) + { + // (NB) 0x79 0x8A 0x94 0x9A 0xA2 0xA8 0xAF 0xB2 0xB5 0xB9 (EB) + static std::pair const notches[] = { + // { control_in, emulated_out }, + {0xF8, 0xB9}, + {0xE6, 0xB5}, + {0xCA, 0xB2}, + {0xAE, 0xAF}, + {0x92, 0xA8}, + {0x76, 0xA2}, + {0x5A, 0x9A}, + {0x3E, 0x94}, + {0x22, 0x8A}, + {0x00, 0x79}, + }; + + for (const auto& x : notches) + { + if (value >= x.first) + return x.second; + } + return notches[std::size(notches) - 1].second; + } + +// TrainControlID buttons are laid out in Type 2 ordering, no need to remap. +#define dct01_buttons(buttons) (buttons) + + static void train_handle_data(USBDevice* dev, USBPacket* p) + { + TrainDeviceState* s = USB_CONTAINER_OF(dev, TrainDeviceState, dev); + + if (p->pid != USB_TOKEN_IN || p->ep->nr != 1) + { + Console.Error("Unhandled TrainController request pid=%d ep=%u", p->pid, p->ep->nr); + p->status = USB_RET_STALL; + return; + } + + s->UpdateHatSwitch(); + + TrainConData_Type2 out = {}; + out.control = 0x1; + out.brake = dct01_brake(s->data.brake); + out.power = dct01_power(s->data.power); + out.horn = 0xFF; // Button C doubles as horn. + out.hat = s->data.hatswitch; + out.buttons = dct01_buttons(s->data.buttons); + usb_packet_copy(p, &out, sizeof(out)); + } + + USBDevice* TrainDevice::CreateDevice(SettingsInterface& si, u32 port, u32 subtype) const + { + TrainDeviceState* s = new TrainDeviceState(port); + + s->desc.full = &s->desc_dev; + s->desc.str = dct01_desc_strings; + + if (usb_desc_parse_dev(dct01_dev_descriptor, sizeof(dct01_dev_descriptor), s->desc, s->desc_dev) < 0) + goto fail; + if (usb_desc_parse_config(taito_denshacon_config_descriptor, sizeof(taito_denshacon_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 = train_handle_reset; + s->dev.klass.handle_control = train_handle_control; + s->dev.klass.handle_data = train_handle_data; + s->dev.klass.unrealize = train_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); + train_handle_reset(&s->dev); + + return &s->dev; + + fail: + train_handle_destroy(&s->dev); + return nullptr; + } +} // namespace usb_pad diff --git a/pcsx2/USB/usb-pad/usb-train.h b/pcsx2/USB/usb-pad/usb-train.h new file mode 100644 index 0000000000..9bb345dfa2 --- /dev/null +++ b/pcsx2/USB/usb-pad/usb-train.h @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2002-2024 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 +{ + + class TrainDevice final : public DeviceProxy + { + public: + USBDevice* CreateDevice(SettingsInterface& si, u32 port, u32 subtype) const override; + const char* Name() const override; + const char* TypeName() 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 Bindings(u32 subtype) const override; + bool Freeze(USBDevice* dev, StateWrapper& sw) const override; + }; + +#pragma pack(push, 1) + struct TrainConData_Type2 + { + u8 control; + u8 brake; + u8 power; + u8 horn; + u8 hat; + u8 buttons; + }; + static_assert(sizeof(TrainConData_Type2) == 6); +#pragma pack(pop) + + struct TrainDeviceState + { + TrainDeviceState(u32 port_); + ~TrainDeviceState(); + + void Reset(); + void UpdateHatSwitch() noexcept; + + USBDevice dev{}; + USBDesc desc{}; + USBDescDevice desc_dev{}; + + u32 port = 0; + + struct + { + // intermediate state, resolved at query time + bool hat_left : 1; + bool hat_right : 1; + bool hat_up : 1; + bool hat_down : 1; + + u8 power; // 255 is fully applied + u8 brake; // 255 is fully applied + u8 hatswitch; // direction + u8 buttons; // active high + } data = {}; + }; + + // Taito Densha Controllers as described at: + // https://marcriera.github.io/ddgo-controller-docs/controllers/usb/ +#define DEFINE_DCT_DEV_DESCRIPTOR(prefix, subclass, product) \ + static const uint8_t prefix##_dev_descriptor[] = { \ + /* bLength */ USB_DEVICE_DESC_SIZE, \ + /* bDescriptorType */ USB_DEVICE_DESCRIPTOR_TYPE, \ + /* bcdUSB */ WBVAL(0x0110), /* USB 1.1 */ \ + /* bDeviceClass */ 0xFF, \ + /* bDeviceSubClass */ subclass, \ + /* bDeviceProtocol */ 0x00, \ + /* bMaxPacketSize0 */ 0x08, \ + /* idVendor */ WBVAL(0x0ae4), \ + /* idProduct */ WBVAL(product), \ + /* bcdDevice */ WBVAL(0x0102), /* 1.02 */ \ + /* iManufacturer */ 0x01, \ + /* iProduct */ 0x02, \ + /* iSerialNumber */ 0x03, \ + /* bNumConfigurations */ 0x01, \ + } + + // These settings are common across multiple models. + static const uint8_t taito_denshacon_config_descriptor[] = { + USB_CONFIGURATION_DESC_SIZE, // bLength + USB_CONFIGURATION_DESCRIPTOR_TYPE, // bDescriptorType + WBVAL(25), // wTotalLength + 0x01, // bNumInterfaces + 0x01, // bConfigurationValue + 0x00, // iConfiguration (String Index) + 0xA0, // bmAttributes + 0xFA, // bMaxPower 500mA + + USB_INTERFACE_DESC_SIZE, // bLength + USB_INTERFACE_DESCRIPTOR_TYPE, // bDescriptorType + 0x00, // bInterfaceNumber + 0x00, // bAlternateSetting + 0x01, // bNumEndpoints + USB_CLASS_HID, // bInterfaceClass + 0x00, // bInterfaceSubClass + 0x00, // bInterfaceProtocol + 0x00, // iInterface (String Index) + + 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 + 0x14, // bInterval 20 (unit depends on device speed) + // 25 bytes (43 total with dev descriptor) + }; + + // ---- Two handle controller "Type 2" ---- + + static const USBDescStrings dct01_desc_strings = { + "", + "TAITO", + "TAITO_DENSYA_CON_T01", + "TCPP20009", + }; + + // dct01_dev_descriptor + DEFINE_DCT_DEV_DESCRIPTOR(dct01, 0x04, 0x0004); + +} // namespace usb_pad diff --git a/pcsx2/pcsx2.vcxproj b/pcsx2/pcsx2.vcxproj index edb71d9892..a99413cf08 100644 --- a/pcsx2/pcsx2.vcxproj +++ b/pcsx2/pcsx2.vcxproj @@ -402,6 +402,7 @@ + @@ -853,6 +854,7 @@ + diff --git a/pcsx2/pcsx2.vcxproj.filters b/pcsx2/pcsx2.vcxproj.filters index 25e5a352e3..bbb34a6b10 100644 --- a/pcsx2/pcsx2.vcxproj.filters +++ b/pcsx2/pcsx2.vcxproj.filters @@ -1199,6 +1199,9 @@ System\Ps2\USB\usb-pad + + System\Ps2\USB\usb-pad + System\Ps2\USB\usb-pad @@ -2145,6 +2148,9 @@ System\Ps2\USB\usb-pad + + System\Ps2\USB\usb-pad + System\Ps2\USB\usb-pad