From 5ec42165b7c9740041447f1e632f2a125e5377d2 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Mon, 23 Mar 2026 00:51:36 -0500 Subject: [PATCH] Triforce: Rewrite JVS IO board emulation. Moved JVS IO emulation from SI_DeviceAMBaseboard into new JVSIOBoard class. Sega/Namco board-specific functionality is handled by derived JVSIOBoard classes. Game input is now sourced from IOPorts rather than being hard coded into JVS IO handlers. SI_DeviceAMBaseboard: Use IOPorts for status switch input. --- Source/Core/Core/CMakeLists.txt | 6 + .../Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp | 1244 ++--------------- Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h | 126 +- Source/Core/Core/HW/Triforce/JVSIO.cpp | 767 ++++++++++ Source/Core/Core/HW/Triforce/JVSIO.h | 93 ++ Source/Core/DolphinLib.props | 6 + 6 files changed, 988 insertions(+), 1254 deletions(-) create mode 100644 Source/Core/Core/HW/Triforce/JVSIO.cpp create mode 100644 Source/Core/Core/HW/Triforce/JVSIO.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index eb11771fa0..61ae899f00 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -296,10 +296,16 @@ add_library(core HW/SystemTimers.h HW/Triforce/DeckReader.cpp HW/Triforce/DeckReader.h + HW/Triforce/FZeroAX.cpp + HW/Triforce/FZeroAX.h HW/Triforce/ICCardReader.cpp HW/Triforce/ICCardReader.h HW/Triforce/IOPorts.cpp HW/Triforce/IOPorts.h + HW/Triforce/JVSIO.cpp + HW/Triforce/JVSIO.h + HW/Triforce/MarioKartGP.cpp + HW/Triforce/MarioKartGP.h HW/Triforce/SerialDevice.cpp HW/Triforce/SerialDevice.h HW/Triforce/Touchscreen.cpp diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp index 2bd7455b8a..2da22e37b3 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp @@ -13,13 +13,11 @@ #include "Common/FileUtil.h" #include "Common/Logging/Log.h" #include "Common/MsgHandler.h" -#include "Common/Swap.h" #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/CoreTiming.h" #include "Core/HW/DVD/AMMediaboard.h" -#include "Core/HW/GCPad.h" #include "Core/HW/MagCard/C1231BR.h" #include "Core/HW/MagCard/C1231LR.h" #include "Core/HW/Memmap.h" @@ -28,91 +26,49 @@ #include "Core/HW/SI/SI_Device.h" #include "Core/HW/SystemTimers.h" #include "Core/HW/Triforce/DeckReader.h" +#include "Core/HW/Triforce/FZeroAX.h" #include "Core/HW/Triforce/ICCardReader.h" +#include "Core/HW/Triforce/JVSIO.h" +#include "Core/HW/Triforce/MarioKartGP.h" #include "Core/HW/Triforce/Touchscreen.h" #include "Core/Movie.h" #include "Core/System.h" -#include "InputCommon/GCPadStatus.h" +namespace +{ +enum class GCAMCommand : u8 +{ + StatusSwitches = 0x10, + SerialNumber = 0x11, + Unknown_12 = 0x12, + Unknown_14 = 0x14, + FirmVersion = 0x15, + FPGAVersion = 0x16, + RegionSettings = 0x1F, + + Unknown_21 = 0x21, + Unknown_22 = 0x22, + Unknown_23 = 0x23, + Unknown_24 = 0x24, + + SerialA = 0x31, + SerialB = 0x32, + + JVSIOA = 0x40, + JVSIOB = 0x41, + + Unknown_60 = 0x60, +}; + +// This value prevents F-Zero AX mag card breakage. +// It's now used for serial port reads in general. +// TODO: Verify how the hardware actually works. +constexpr u32 SERIAL_PORT_MAX_READ_SIZE = 0x1f; + +} // namespace namespace SerialInterface { -using namespace AMMediaboard; - -void JVSIOMessage::Start(int node) -{ - m_last_start = m_pointer; - const u8 header[3] = {0xE0, (u8)node, 0}; - m_checksum = 0; - AddData(header, 3, 1); -} - -void JVSIOMessage::AddData(const u8* dst, std::size_t len, int sync = 0) -{ - if (m_pointer + len >= sizeof(m_message)) - { - PanicAlertFmt("JVSIOMessage overrun!"); - return; - } - - while (len--) - { - const u8 c = *dst++; - if (!sync && ((c == 0xE0) || (c == 0xD0))) - { - if (m_pointer + 2 > sizeof(m_message)) - { - PanicAlertFmt("JVSIOMessage overrun!"); - break; - } - m_message[m_pointer++] = 0xD0; - m_message[m_pointer++] = c - 1; - } - else - { - if (m_pointer >= sizeof(m_message)) - { - PanicAlertFmt("JVSIOMessage overrun!"); - break; - } - m_message[m_pointer++] = c; - } - - if (!sync) - m_checksum += c; - sync = 0; - } -} - -void JVSIOMessage::AddData(const void* data, std::size_t len) -{ - AddData(static_cast(data), len); -} - -void JVSIOMessage::AddData(const char* data) -{ - AddData(data, strlen(data)); -} - -void JVSIOMessage::AddData(u32 n) -{ - const u8 cs = n; - AddData(&cs, 1); -} - -void JVSIOMessage::End() -{ - const u32 len = m_pointer - m_last_start; - if (m_last_start + 2 < sizeof(m_message) && len >= 3) - { - m_message[m_last_start + 2] = len - 2; // assuming len <0xD0 - AddData(m_checksum + len - 2); - } - else - { - PanicAlertFmt("JVSIOMessage: Not enough space for checksum!"); - } -} const constexpr u8 s_region_flags[] = "\x00\x00\x30\x00" // "\x01\xfe\x00\x00" // JAPAN @@ -128,18 +84,47 @@ CSIDevice_AMBaseboard::CSIDevice_AMBaseboard(Core::System& system, SIDevices dev m_mag_card_settings.card_path = File::GetUserPath(D_TRIUSER_IDX); m_mag_card_settings.card_name = fmt::format("tricard_{}.bin", SConfig::GetInstance().GetGameID()); + m_io_ports.AddIOAdapter(std::make_unique(m_system)); + switch (AMMediaboard::GetGameType()) { + using namespace AMMediaboard; + case FZeroAX: + { + // This board enables DX mode on AX machines. + // All this does is enable the chair motion. + m_jvs_io_board = std::make_unique(&m_io_ports); + auto slot_a = std::make_unique(); + m_io_ports.AddIOAdapter(std::make_unique(slot_a.get())); + m_io_ports.AddIOAdapter(std::make_unique()); + m_serial_device_a = std::move(slot_a); m_serial_device_b = std::make_unique(&m_mag_card_settings); break; - + } + case FZeroAXMonster: + { + auto slot_a = std::make_unique(); + m_io_ports.AddIOAdapter(std::make_unique(slot_a.get())); + m_io_ports.AddIOAdapter(std::make_unique()); + m_serial_device_a = std::move(slot_a); + break; + } case MarioKartGP: case MarioKartGP2: - m_io_ports.AddIOAdapter(std::make_unique()); + { + m_jvs_io_board = std::make_unique(&m_io_ports); + auto slot_a = std::make_unique(); + m_io_ports.AddIOAdapter(std::make_unique(slot_a.get())); + m_serial_device_a = std::move(slot_a); m_serial_device_b = std::make_unique(&m_mag_card_settings); break; - + } + case VirtuaStriker3: + { + m_io_ports.AddIOAdapter(std::make_unique()); + break; + } case VirtuaStriker4: { auto slot_a = std::make_unique(0); @@ -160,7 +145,6 @@ CSIDevice_AMBaseboard::CSIDevice_AMBaseboard(Core::System& system, SIDevices dev std::make_unique(slot_a.get(), slot_b.get())); m_serial_device_a = std::move(slot_a); m_serial_device_b = std::move(slot_b); - break; } case GekitouProYakyuu: @@ -171,7 +155,6 @@ CSIDevice_AMBaseboard::CSIDevice_AMBaseboard(Core::System& system, SIDevices dev std::make_unique(slot_a.get(), slot_b.get())); m_serial_device_a = std::move(slot_a); m_serial_device_b = std::move(slot_b); - break; } case KeyOfAvalon: @@ -183,18 +166,24 @@ CSIDevice_AMBaseboard::CSIDevice_AMBaseboard(Core::System& system, SIDevices dev m_serial_device_b = std::make_unique(); break; } - default: break; } + + // Fallback to a Sega board. + if (m_jvs_io_board == nullptr) + m_jvs_io_board = std::make_unique(&m_io_ports); } +CSIDevice_AMBaseboard::~CSIDevice_AMBaseboard() = default; + int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) { // Debug logging ISIDevice::RunBuffer(buffer, request_length); - const auto& serial_interface = m_system.GetSerialInterface(); + // Update user input. + m_io_ports.Update(); const auto bb_command = EBufferCommands(buffer[0]); switch (bb_command) @@ -255,7 +244,10 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) { case GCAMCommand::StatusSwitches: { - if (!validate_data_in_out(1, 4, "StatusSwitches")) + const auto status_switches = m_io_ports.GetStatusSwitches(); + const auto out_length = u8(status_switches.size()); + + if (!validate_data_in_out(1, 2 + out_length, "StatusSwitches")) break; const u8 status = *data_in++; @@ -263,33 +255,11 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) status); data_out[data_offset++] = gcam_command; - data_out[data_offset++] = 0x2; + data_out[data_offset++] = out_length; - // We read Test/Service from the JVS-I/O SwitchesInput instead - // - // const GCPadStatus pad_status = Pad::GetStatus(ISIDevice::m_device_number); - // baseboard test/service switches - // if (pad_status.button & PAD_BUTTON_Y) // Test - // dip_switch_0 &= ~0x80; - // if (pad_status.button & PAD_BUTTON_X) // Service - // dip_switch_0 &= ~0x40; + std::ranges::copy(status_switches, data_out.data() + data_offset); - // Horizontal Scanning Frequency switch - // Required for F-Zero AX booting via Sega Boot - if (AMMediaboard::GetGameType() == FZeroAX || AMMediaboard::GetGameType() == FZeroAXMonster) - { - m_dip_switch_0 &= ~0x20; - } - - // Disable camera in MKGP1/2 - if (AMMediaboard::GetGameType() == MarioKartGP || - AMMediaboard::GetGameType() == MarioKartGP2) - { - m_dip_switch_0 &= ~0x10; - } - - data_out[data_offset++] = m_dip_switch_0; - data_out[data_offset++] = m_dip_switch_1; + data_offset += out_length; break; } case GCAMCommand::SerialNumber: @@ -438,234 +408,17 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) { if (!validate_data_in_out(1, 0, "SerialA")) break; + const u32 in_length = *data_in++; - const u32 length = *data_in++; - if (length) - { - if (!validate_data_in_out(length, 0, "SerialA")) - break; - - if (m_serial_device_a != nullptr) - { - m_serial_device_a->WriteRxBytes({data_in, length}); - data_in += length; - break; - } - - INFO_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x31, length=0x{:02x}, hexdump:\n{}", - length, HexDump(data_in, length)); - - // Serial - Wheel - if (AMMediaboard::GetGameType() == MarioKartGP || - AMMediaboard::GetGameType() == MarioKartGP2) - { - if (!validate_data_in_out(10, 2, "SerialA (Wheel)")) - break; - - INFO_LOG_FMT(SERIALINTERFACE_AMBB, - "GC-AM: Command 0x31, (WHEEL) {:02x}{:02x} {:02x}{:02x} {:02x} {:02x} " - "{:02x} {:02x} {:02x} {:02x}", - data_in[0], data_in[1], data_in[2], data_in[3], data_in[4], data_in[5], - data_in[6], data_in[7], data_in[8], data_in[9]); - - data_out[data_offset++] = gcam_command; - data_out[data_offset++] = 0x03; - - switch (m_wheel_init) - { - case 0: - if (!validate_data_in_out(0, 3, "SerialA (Wheel)")) - break; - data_out[data_offset++] = 'E'; // Error - data_out[data_offset++] = '0'; - data_out[data_offset++] = '0'; - m_wheel_init++; - break; - case 1: - if (!validate_data_in_out(0, 3, "SerialA (Wheel)")) - break; - data_out[data_offset++] = 'C'; // Power Off - data_out[data_offset++] = '0'; - data_out[data_offset++] = '6'; - // Only turn on when a wheel is connected - if (serial_interface.GetDeviceType(1) == SerialInterface::SIDEVICE_GC_STEERING) - { - m_wheel_init++; - } - break; - case 2: - if (!validate_data_in_out(0, 3, "SerialA (Wheel)")) - break; - data_out[data_offset++] = 'C'; // Power On - data_out[data_offset++] = '0'; - data_out[data_offset++] = '1'; - break; - default: - break; - } - - // u16 CenteringForce= ptr(6); - // u16 FrictionForce = ptr(8); - // u16 Roll = ptr(10); - if (!validate_data_in_out(length, 0, "SerialA (Wheel)")) - break; - data_in += length; - break; - } - } - - u32 command_offset = 0; - while (command_offset < length) - { - // All commands are OR'd with 0x80 - // Last byte is checksum which we don't care about - if (!validate_data_in_out(command_offset + 4, 0, "SerialA")) - break; - const u32 serial_command = Common::swap32(data_in + command_offset) ^ 0x80000000; - - if (AMMediaboard::GetGameType() == FZeroAX || - AMMediaboard::GetGameType() == FZeroAXMonster) - { - INFO_LOG_FMT(SERIALINTERFACE_AMBB, - "GC-AM: Command 0x31 (MOTOR) Length:{:02x} Command:{:04x}({:02x})", length, - (serial_command >> 8) & 0xFFFF, serial_command >> 24); - } - else - { - INFO_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x31 (SERIAL) Command:{:06x}", - serial_command); - - if (serial_command == 0x801000) - { - if (!validate_data_in_out(0, 4, "SerialA")) - break; - data_out[data_offset++] = 0x31; - data_out[data_offset++] = 0x02; - data_out[data_offset++] = 0xFF; - data_out[data_offset++] = 0x01; - } - } - - command_offset += sizeof(u32); - - if (AMMediaboard::GetGameType() == FZeroAX || - AMMediaboard::GetGameType() == FZeroAXMonster) - { - if (command_offset + 5 >= std::size(m_motor_reply)) - { - ERROR_LOG_FMT( - SERIALINTERFACE_AMBB, - "GC-AM: Command 0x31 (MOTOR) overflow: offset={} >= motor_reply_size={}", - command_offset + 5, std::size(m_motor_reply)); - data_in = data_in_end; - break; - } - - // Status - m_motor_reply[command_offset + 2] = 0; - m_motor_reply[command_offset + 3] = 0; - - // Error - m_motor_reply[command_offset + 4] = 0; - - switch (serial_command >> 24) - { - case 0: - case 1: // Set Maximum? - case 2: - break; - - // 0x00-0x40: left - // 0x40-0x80: right - case 4: // Move Steering Wheel - // Left - if (serial_command & 0x010000) - { - m_motor_force_y = -((s16)serial_command & 0xFF00); - } - else // Right - { - m_motor_force_y = (serial_command - 0x4000) & 0xFF00; - } - - m_motor_force_y *= 2; - - // FFB - if (m_motor_init == 2) - { - if (serial_interface.GetDeviceType(1) == SerialInterface::SIDEVICE_GC_STEERING) - { - const GCPadStatus pad_status = Pad::GetStatus(1); - if (pad_status.isConnected) - { - const ControlState mapped_strength = (double)(m_motor_force_y >> 8) / 127.f; - - Pad::Rumble(1, mapped_strength); - INFO_LOG_FMT(SERIALINTERFACE_AMBB, - "GC-AM: Command 0x31 (MOTOR) mapped_strength:{}", mapped_strength); - } - } - } - break; - case 6: // nice - case 9: - default: - break; - // Switch back to normal controls - case 7: - m_motor_init = 2; - break; - // Reset - case 0x7F: - m_motor_init = 1; - memset(m_motor_reply, 0, sizeof(m_motor_reply)); - break; - } - - // Checksum - m_motor_reply[command_offset + 5] = m_motor_reply[command_offset + 2] ^ - m_motor_reply[command_offset + 3] ^ - m_motor_reply[command_offset + 4]; - } - } - - if (length == 0) - { - if (!validate_data_in_out(length, 2, "SerialA")) - break; - data_out[data_offset++] = gcam_command; - data_out[data_offset++] = 0x00; - } - else - { - if (m_motor_init) - { - // Motor - m_motor_reply[0] = gcam_command; - m_motor_reply[1] = length; // Same out as in size - - const u32 reply_size = m_motor_reply[1] + 2; - if (!validate_data_in_out(length, reply_size, "SerialA")) - break; - - if (reply_size > sizeof(m_motor_reply)) - { - ERROR_LOG_FMT(SERIALINTERFACE_AMBB, - "GC-AM: Command SerialA, reply_size={} too big for m_motor_reply", - reply_size); - data_in = data_in_end; - break; - } - memcpy(data_out.data() + data_offset, m_motor_reply, reply_size); - data_offset += reply_size; - } - } - - if (!validate_data_in_out(length, 0, "SerialA")) - { + if (!validate_data_in_out(in_length, 0, "SerialA")) break; + + if (m_serial_device_a != nullptr) + { + m_serial_device_a->WriteRxBytes({data_in, in_length}); } - data_in += length; + + data_in += in_length; break; } case GCAMCommand::SerialB: @@ -683,788 +436,41 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) } data_in += in_length; - break; } case GCAMCommand::JVSIOA: case GCAMCommand::JVSIOB: { - if (!validate_data_in_out(4, 0, "JVSIO")) + if (!validate_data_in_out(1, 0, "JVSIO")) + break; + const u32 in_length = *data_in++; + + if (!validate_data_in_out(in_length, 0, "JVSIO")) break; - JVSIOMessage message; + m_jvs_io_board->WriteRxBytes({data_in, in_length}); + data_in += in_length; - const u8* const frame = &data_in[0]; - const u8 nr_bytes = frame[3]; // Byte after E0 xx - u32 frame_len = nr_bytes + 3; // Header(2) + length byte + payload + checksum + m_jvs_io_board->Update(); - u8 jvs_buf[0x80]; + const auto out_length = u32(m_jvs_io_board->GetTxByteCount()); - frame_len = std::min(frame_len, sizeof(jvs_buf)); - - if (!validate_data_in_out(frame_len, 0, "JVSIO")) + if (out_length == 0) break; - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "GC-AM: Command {:02x} (JVS IO), hexdump:\n{}", - gcam_command, HexDump(data_in, frame_len)); - memcpy(jvs_buf, frame, frame_len); - - // Extract node and payload pointers - u8 node = jvs_buf[2]; - u8* jvs_io = jvs_buf + 4; // First payload byte - u8* const jvs_end = jvs_buf + frame_len; // One byte before checksum - u8* const jvs_begin = jvs_io; - - message.Start(0); - message.AddData(1); - - // Helper to check that iterating over jvs_io n times is safe, - // i.e. *jvs_io++ at most lead to jvs_end - auto validate_jvs_io = [&](u32 n, std::string_view command) -> bool { - if (jvs_io + n > jvs_end) - ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: overflow in {}", command); - else - return true; - ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, - "Overflow details:\n" - " - jvs_io(begin={}, current={}, end={}, n={})\n" - " - delay={}, node={}\n" - " - frame(begin={}, len={})", - fmt::ptr(jvs_begin), fmt::ptr(jvs_io), fmt::ptr(jvs_end), n, m_delay, node, - fmt::ptr(frame), frame_len); - jvs_io = jvs_end; - return false; - }; - - // Now iterate over the payload - while (jvs_io < jvs_end) - { - const u8 jvsio_command = *jvs_io++; - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO:node={}, command={:02x}", node, - jvsio_command); - - switch (JVSIOCommand(jvsio_command)) - { - case JVSIOCommand::IOID: - message.AddData(StatusOkay); - switch (AMMediaboard::GetGameType()) - { - case FZeroAX: - // Specific version that enables DX mode on AX machines, all this does is enable the - // motion of a chair - message.AddData("SEGA ENTERPRISES,LTD.;837-13844-01 I/O CNTL BD2 ;"); - break; - case FZeroAXMonster: - case MarioKartGP: - case MarioKartGP2: - default: - message.AddData("namco ltd.;FCA-1;Ver1.01;JPN,Multipurpose + Rotary Encoder"); - break; - case VirtuaStriker3: - case VirtuaStriker4: - case VirtuaStriker4_2006: - message.AddData("SEGA ENTERPRISES,LTD.;I/O BD JVS;837-13551;Ver1.00"); - break; - } - NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x10, BoardID"); - message.AddData((u32)0); - break; - case JVSIOCommand::CommandRevision: - message.AddData(StatusOkay); - message.AddData(0x11); - NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x11, CommandRevision"); - break; - case JVSIOCommand::JVRevision: - message.AddData(StatusOkay); - message.AddData(0x20); - NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x12, JVRevision"); - break; - case JVSIOCommand::CommunicationVersion: - message.AddData(StatusOkay); - message.AddData(0x10); - NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x13, CommunicationVersion"); - break; - - // Slave features: - // - // Inputs: - // 0x01: Switch input: players, buttons - // 0x02: Coin input: slots - // 0x03: Analog input: channels, bits - // 0x04: Rotary input: channels - // 0x05: Keycode input: 0,0,0 ? - // 0x06: Screen position input: X bits, Y bits, channels - // - // Outputs: - // 0x10: Card system: slots - // 0x11: Medal hopper: channels - // 0x12: GPO-out: slots - // 0x13: Analog output: channels - // 0x14: Character output: width, height, type - // 0x15: Backup - case JVSIOCommand::CheckFunctionality: - message.AddData(StatusOkay); - switch (AMMediaboard::GetGameType()) - { - case FZeroAX: - case FZeroAXMonster: - // 2 Player (12bit) (p2=paddles), 1 Coin slot, 6 Analog-in - // message.AddData((void *)"\x01\x02\x0C\x00", 4); - // message.AddData((void *)"\x02\x01\x00\x00", 4); - // message.AddData((void *)"\x03\x06\x00\x00", 4); - // message.AddData((void *)"\x00\x00\x00\x00", 4); - // - // DX Version: 2 Player (22bit) (p2=paddles), 2 Coin slot, 8 Analog-in, - // 22 Driver-out - message.AddData("\x01\x02\x12\x00", 4); - message.AddData("\x02\x02\x00\x00", 4); - message.AddData("\x03\x08\x0A\x00", 4); - message.AddData("\x12\x16\x00\x00", 4); - message.AddData("\x00\x00\x00\x00", 4); - break; - case VirtuaStriker3: - // 2 Player (13bit), 2 Coin slot, 4 Analog-in, 8 Driver-out - message.AddData("\x01\x02\x0D\x00", 4); - message.AddData("\x02\x02\x00\x00", 4); - message.AddData("\x12\x08\x00\x00", 4); - message.AddData("\x00\x00\x00\x00", 4); - break; - case GekitouProYakyuu: - // 2 Player (13bit), 2 Coin slot, 4 Analog-in, 8 Driver-out - message.AddData("\x01\x02\x0D\x00", 4); - message.AddData("\x02\x02\x00\x00", 4); - message.AddData("\x03\x04\x00\x00", 4); - message.AddData("\x12\x08\x00\x00", 4); - message.AddData("\x00\x00\x00\x00", 4); - break; - case VirtuaStriker4: - case VirtuaStriker4_2006: - // 2 Player (13bit), 1 Coin slot, 4 Analog-in, 22 Driver-out - message.AddData("\x01\x02\x0D\x00", 4); - message.AddData("\x02\x01\x00\x00", 4); - message.AddData("\x03\x04\x00\x00", 4); - message.AddData("\x12\x16\x00\x00", 4); - message.AddData("\x00\x00\x00\x00", 4); - break; - case KeyOfAvalon: - // 1 Player (15bit), 1 Coin slot, 3 Analog-in, Touch, 1 Driver-out - // (Unconfirmed) - message.AddData("\x01\x01\x0F\x00", 4); - message.AddData("\x02\x01\x00\x00", 4); - message.AddData("\x03\x03\x00\x00", 4); - message.AddData("\x06\x10\x10\x01", 4); - message.AddData("\x12\x01\x00\x00", 4); - message.AddData("\x00\x00\x00\x00", 4); - break; - case MarioKartGP: - case MarioKartGP2: - default: - // 1 Player (15bit), 1 Coin slot, 3 Analog-in, 1 Driver-out - message.AddData("\x01\x01\x0F\x00", 4); - message.AddData("\x02\x01\x00\x00", 4); - message.AddData("\x03\x03\x00\x00", 4); - message.AddData("\x12\x01\x00\x00", 4); - message.AddData("\x00\x00\x00\x00", 4); - break; - } - NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x14, CheckFunctionality"); - break; - case JVSIOCommand::MainID: - { - const u8* const main_id = jvs_io; - while (jvs_io < jvs_end && *jvs_io++) - { - } - if (main_id < jvs_io) - { - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command MainId:\n{}", - HexDump(main_id, jvs_io - main_id)); - } - message.AddData(StatusOkay); - break; - } - case JVSIOCommand::SwitchesInput: - { - if (!validate_jvs_io(2, "SwitchesInput")) - break; - const u32 player_count = *jvs_io++; - const u32 player_byte_count = *jvs_io++; - - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x20, SwitchInputs: {} {}", - player_count, player_byte_count); - - message.AddData(StatusOkay); - - GCPadStatus pad_status = Pad::GetStatus(0); - - // Test button - if (pad_status.switches & SWITCH_TEST) - { - // Trying to access the test menu without SegaBoot present will cause a crash - if (AMMediaboard::GetTestMenu()) - { - message.AddData(0x80); - } - else - { - PanicAlertFmt("Test menu is disabled due to missing SegaBoot"); - } - } - else - { - message.AddData((u32)0x00); - } - - for (u32 i = 0; i < player_count; ++i) - { - std::array player_data{}; - - const auto io_ports_player_data = m_io_ports.GetSwitchInputs(i); - std::copy_n(io_ports_player_data.data(), - std::min(io_ports_player_data.size(), player_data.size()), - player_data.data()); - - // Service button - if (pad_status.switches & SWITCH_SERVICE) - player_data[0] |= 0x40; - - switch (AMMediaboard::GetGameType()) - { - // Controller configuration for F-Zero AX (DX) - case FZeroAX: - if (i == 0) - { - if (m_fzdx_seatbelt) - { - player_data[0] |= 0x01; - } - - // Start - if (pad_status.button & PAD_BUTTON_START) - player_data[0] |= 0x80; - // Boost - if (pad_status.button & PAD_BUTTON_A) - player_data[0] |= 0x02; - // View Change 1 - if (pad_status.button & PAD_BUTTON_RIGHT) - player_data[0] |= 0x20; - // View Change 2 - if (pad_status.button & PAD_BUTTON_LEFT) - player_data[0] |= 0x10; - // View Change 3 - if (pad_status.button & PAD_BUTTON_UP) - player_data[0] |= 0x08; - // View Change 4 - if (pad_status.button & PAD_BUTTON_DOWN) - player_data[0] |= 0x04; - player_data[1] = m_rx_reply & 0xF0; - } - else if (i == 1) - { - // Paddle left - if (pad_status.button & PAD_BUTTON_X) - player_data[0] |= 0x20; - // Paddle right - if (pad_status.button & PAD_BUTTON_Y) - player_data[0] |= 0x10; - - if (m_fzdx_motion_stop) - { - player_data[0] |= 2; - } - if (m_fzdx_sensor_right) - { - player_data[0] |= 4; - } - if (m_fzdx_sensor_left) - { - player_data[0] |= 8; - } - - player_data[1] = m_rx_reply << 4; - } - break; - // Controller configuration for F-Zero AX MonsterRide - case FZeroAXMonster: - if (i == 0) - { - if (m_fzcc_sensor) - { - player_data[0] |= 0x01; - } - - // Start - if (pad_status.button & PAD_BUTTON_START) - player_data[0] |= 0x80; - // Boost - if (pad_status.button & PAD_BUTTON_A) - player_data[0] |= 0x02; - // View Change 1 - if (pad_status.button & PAD_BUTTON_RIGHT) - player_data[0] |= 0x20; - // View Change 2 - if (pad_status.button & PAD_BUTTON_LEFT) - player_data[0] |= 0x10; - // View Change 3 - if (pad_status.button & PAD_BUTTON_UP) - player_data[0] |= 0x08; - // View Change 4 - if (pad_status.button & PAD_BUTTON_DOWN) - player_data[0] |= 0x04; - - player_data[1] = m_rx_reply & 0xF0; - } - else if (i == 1) - { - // Paddle left - if (pad_status.button & PAD_BUTTON_X) - player_data[0] |= 0x20; - // Paddle right - if (pad_status.button & PAD_BUTTON_Y) - player_data[0] |= 0x10; - - if (m_fzcc_seatbelt) - { - player_data[0] |= 2; - } - if (m_fzcc_service) - { - player_data[0] |= 4; - } - if (m_fzcc_emergency) - { - player_data[0] |= 8; - } - } - break; - // Controller configuration for Virtua Striker 3 games - case VirtuaStriker3: - pad_status = Pad::GetStatus(i); - // Start - if (pad_status.button & PAD_BUTTON_START) - player_data[0] |= 0x80; - // Shoot - if (pad_status.button & PAD_BUTTON_B) - player_data[0] |= 0x01; - // Short Pass - if (pad_status.button & PAD_BUTTON_A) - player_data[1] |= 0x80; - // Long Pass - if (pad_status.button & PAD_BUTTON_X) - player_data[0] |= 0x02; - // Left - if (pad_status.button & PAD_BUTTON_LEFT) - player_data[0] |= 0x08; - // Up - if (pad_status.button & PAD_BUTTON_UP) - player_data[0] |= 0x20; - // Right - if (pad_status.button & PAD_BUTTON_RIGHT) - player_data[0] |= 0x04; - // Down - if (pad_status.button & PAD_BUTTON_DOWN) - player_data[0] |= 0x10; - break; - // Controller configuration for Virtua Striker 4 games - case VirtuaStriker4: - case VirtuaStriker4_2006: - { - pad_status = Pad::GetStatus(i); - // Start - if (pad_status.button & PAD_BUTTON_START) - player_data[0] |= 0x80; - // Long Pass - if (pad_status.button & PAD_BUTTON_X) - player_data[0] |= 0x01; - // Short Pass - if (pad_status.button & PAD_BUTTON_A) - player_data[0] |= 0x02; - // Shoot - if (pad_status.button & PAD_BUTTON_B) - player_data[1] |= 0x80; - // Dash - if (pad_status.button & PAD_BUTTON_Y) - player_data[1] |= 0x40; - // Tactics (U) - if (pad_status.button & PAD_BUTTON_LEFT) - player_data[0] |= 0x20; - // Tactics (M) - if (pad_status.button & PAD_BUTTON_UP) - player_data[0] |= 0x08; - // Tactics (D) - if (pad_status.button & PAD_BUTTON_RIGHT) - player_data[0] |= 0x04; - } - break; - // Controller configuration for Gekitou Pro Yakyuu - case GekitouProYakyuu: - pad_status = Pad::GetStatus(i); - // Start - if (pad_status.button & PAD_BUTTON_START) - player_data[0] |= 0x80; - // A - if (pad_status.button & PAD_BUTTON_B) - player_data[0] |= 0x01; - // B - if (pad_status.button & PAD_BUTTON_A) - player_data[0] |= 0x02; - // Gekitou - if (pad_status.button & PAD_TRIGGER_L) - player_data[1] |= 0x80; - // Left - if (pad_status.button & PAD_BUTTON_LEFT) - player_data[0] |= 0x08; - // Up - if (pad_status.button & PAD_BUTTON_UP) - player_data[0] |= 0x20; - // Right - if (pad_status.button & PAD_BUTTON_RIGHT) - player_data[0] |= 0x04; - // Down - if (pad_status.button & PAD_BUTTON_DOWN) - player_data[0] |= 0x10; - break; - // Controller configuration for Mario Kart and other games - default: - case MarioKartGP: - case MarioKartGP2: - { - // Start - if (pad_status.button & PAD_BUTTON_START) - player_data[0] |= 0x80; - // Item button - if (pad_status.button & PAD_BUTTON_A) - player_data[1] |= 0x20; - // VS-Cancel button - if (pad_status.button & PAD_BUTTON_B) - player_data[1] |= 0x02; - } - break; - case KeyOfAvalon: - { - // Debug On - if (pad_status.button & PAD_BUTTON_START) - player_data[0] |= 0x80; - // Switch 1 - if (pad_status.button & PAD_BUTTON_A) - player_data[0] |= 0x04; - // Switch 2 - if (pad_status.button & PAD_BUTTON_B) - player_data[0] |= 0x08; - } - break; - } - - if (player_byte_count > player_data.size()) - { - WARN_LOG_FMT(SERIALINTERFACE_JVSIO, - "JVS-IO: Command 0x20, SwitchInputs: invalid player_byte_count={}", - player_byte_count); - } - const u32 data_size = std::min(player_byte_count, u32(player_data.size())); - for (u32 j = 0; j < data_size; ++j) - message.AddData(player_data[j]); - } - break; - } - case JVSIOCommand::CoinInput: - { - if (!validate_jvs_io(1, "CoinInput")) - break; - const u32 slots = *jvs_io++; - message.AddData(StatusOkay); - static_assert(std::tuple_size{} == 2 && - std::tuple_size{} == 2); - if (slots > 2) - { - WARN_LOG_FMT(SERIALINTERFACE_JVSIO, - "JVS-IO: Command 0x21, CoinInput: invalid slots {}", slots); - } - const u32 max_slots = std::min(slots, 2u); - for (u32 i = 0; i < max_slots; i++) - { - GCPadStatus pad_status = Pad::GetStatus(i); - if ((pad_status.switches & SWITCH_COIN) && !m_coin_pressed[i]) - { - m_coin[i]++; - } - m_coin_pressed[i] = pad_status.switches & SWITCH_COIN; - message.AddData((m_coin[i] >> 8) & 0x3f); - message.AddData(m_coin[i] & 0xff); - } - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x21, CoinInput: {}", slots); - break; - } - case JVSIOCommand::AnalogInput: - { - if (!validate_jvs_io(1, "AnalogInput")) - break; - message.AddData(StatusOkay); - - const u32 analogs = *jvs_io++; - GCPadStatus pad_status = Pad::GetStatus(0); - - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x22, AnalogInput: {}", analogs); - - switch (AMMediaboard::GetGameType()) - { - case FZeroAX: - case FZeroAXMonster: - // Steering - if (m_motor_init == 1) - { - if (m_motor_force_y > 0) - { - message.AddData(0x80 - (m_motor_force_y >> 8)); - } - else - { - message.AddData((m_motor_force_y >> 8)); - } - message.AddData((u8)0); - - message.AddData(pad_status.stickY); - message.AddData((u8)0); - } - else - { - // The center for the Y axis is expected to be 78h this adjusts that - message.AddData(pad_status.stickX - 12); - message.AddData((u8)0); - - message.AddData(pad_status.stickY); - message.AddData((u8)0); - } - - // Unused - message.AddData((u8)0); - message.AddData((u8)0); - message.AddData((u8)0); - message.AddData((u8)0); - - // Gas - message.AddData(pad_status.triggerRight); - message.AddData((u8)0); - - // Brake - message.AddData(pad_status.triggerLeft); - message.AddData((u8)0); - - message.AddData((u8)0x80); // Motion Stop - message.AddData((u8)0); - - message.AddData((u8)0); - message.AddData((u8)0); - - break; - case VirtuaStriker4: - case VirtuaStriker4_2006: - { - message.AddData(-pad_status.stickY); - message.AddData((u8)0); - message.AddData(pad_status.stickX); - message.AddData((u8)0); - - pad_status = Pad::GetStatus(1); - - message.AddData(-pad_status.stickY); - message.AddData((u8)0); - message.AddData(pad_status.stickX); - message.AddData((u8)0); - } - break; - default: - case MarioKartGP: - case MarioKartGP2: - // Steering - message.AddData(pad_status.stickX); - message.AddData((u8)0); - - // Gas - message.AddData(pad_status.triggerRight); - message.AddData((u8)0); - - // Brake - message.AddData(pad_status.triggerLeft); - message.AddData((u8)0); - break; - } - break; - } - case JVSIOCommand::PositionInput: - { - if (!validate_jvs_io(1, "PositionInput")) - break; - const u32 channel = *jvs_io++; - - const GCPadStatus pad_status = Pad::GetStatus(0); - - if (pad_status.button & PAD_TRIGGER_R) - { - // Tap at center of screen (~320,240) - message.AddData("\x01\x00\x8C\x01\x95", - 5); // X=320 (0x0140), Y=240 (0x00F0) - } - else - { - message.AddData("\x01\xFF\xFF\xFF\xFF", 5); - } - - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x25, PositionInput:{}", channel); - break; - } - case JVSIOCommand::CoinSubOutput: - { - if (!validate_jvs_io(3, "CoinSubOutput")) - break; - const u32 slot = *jvs_io++; - const u8 coinh = *jvs_io++; - const u8 coinl = *jvs_io++; - - if (slot < m_coin.size()) - { - m_coin[slot] -= (coinh << 8) | coinl; - } - else - { - WARN_LOG_FMT(SERIALINTERFACE_JVSIO, - "JVS-IO: Command 0x30, CoinSubOutput: invalid slot {}", slot); - } - - message.AddData(StatusOkay); - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x30, CoinSubOutput: {}", slot); - break; - } - case JVSIOCommand::GeneralDriverOutput: - { - if (!validate_jvs_io(1, "GeneralDriverOutput")) - break; - const u32 bytes = *jvs_io++; - - if (!validate_jvs_io(bytes, "GeneralDriverOutput")) - break; - - message.AddData(StatusOkay); - - if (bytes) - { - m_io_ports.SetGenericOutputs({jvs_io, bytes}); - - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, - "JVS-IO: GPO: delay=0x{:02x}, rx_reply=0x{:02x}," - " bytes={}, buffer:\n{}", - m_delay, m_rx_reply, bytes, HexDump(jvs_io, bytes)); - - if ((AMMediaboard::GetGameType() == FZeroAX) && bytes >= 3) - { - // Handling of the motion seat used in F-Zero AXs DX version - const u16 seat_state = Common::swap16(jvs_io + 1) >> 2; - - switch (seat_state) - { - case 0x70: - m_delay++; - if ((m_delay % 10) == 0) - { - m_rx_reply = 0xFB; - } - break; - case 0xF0: - m_rx_reply = 0xF0; - break; - default: - case 0xA0: - case 0x60: - break; - } - } - - jvs_io += bytes; - } - break; - } - case JVSIOCommand::CoinAddOutput: - { - if (!validate_jvs_io(3, "CoinAddOutput")) - break; - const u32 slot = *jvs_io++; - const u8 coinh = *jvs_io++; - const u8 coinl = *jvs_io++; - - m_coin[slot] += (coinh << 8) | coinl; - - message.AddData(StatusOkay); - DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x35, CoinAddOutput: {}", slot); - break; - } - case JVSIOCommand::NAMCOCommand: - { - if (!validate_jvs_io(1, "NAMCOCommand")) - break; - const u32 namco_command = *jvs_io++; - - if (namco_command == 0x18) - { - if (!validate_jvs_io(4, "NAMCOCommand(0x18) / ID check")) - break; - // ID check - jvs_io += 4; - message.AddData(StatusOkay); - message.AddData(0xff); - } - else - { - message.AddData(StatusOkay); - ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Unknown:{:02x}", namco_command); - } - break; - } - case JVSIOCommand::Reset: - if (!validate_jvs_io(1, "Reset")) - break; - if (*jvs_io++ == 0xD9) - { - NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0xF0, Reset"); - m_delay = 0; - m_wheel_init = 0; - } - message.AddData(StatusOkay); - - m_dip_switch_1 |= 1; - break; - case JVSIOCommand::SetAddress: - if (!validate_jvs_io(1, "SetAddress")) - break; - node = *jvs_io++; - NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0xF1, SetAddress: node={}", - node); - message.AddData(node == 1); - m_dip_switch_1 &= ~1u; - break; - default: - ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Unhandled: node={}, command={:02x}", node, - jvsio_command); - break; - } - } - - message.End(); - - if (!validate_data_in_out(0, 2, "JVSIO")) + // Also accounting for the 2-byte header. + if (!validate_data_in_out(0, out_length + 2, "SerialDevice")) break; + + // Write the 2-byte header. data_out[data_offset++] = gcam_command; + data_out[data_offset++] = u8(out_length); - const u8* buf = message.m_message.data(); - const u32 len = message.m_pointer; - data_out[data_offset++] = len; - const u32 in_size = frame[0] + 1; + const auto out_span = std::span{data_out}.subspan(data_offset, out_length); - if (!validate_data_in_out(in_size, len, "JVSIO")) - break; - for (u32 i = 0; i < len; ++i) - data_out[data_offset++] = buf[i]; + m_jvs_io_board->TakeTxBytes(out_span); - data_in += in_size; + data_offset += out_length; break; } case GCAMCommand::Unknown_60: @@ -1511,7 +517,7 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) return; // Write the 2-byte header. - data_out[data_offset++] = cmd; + data_out[data_offset++] = u8(cmd); data_out[data_offset++] = u8(out_length); const auto out_span = std::span{data_out}.subspan(data_offset, out_length); @@ -1521,8 +527,6 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) data_offset += out_length; }; - m_io_ports.Update(); - process_serial_device(m_serial_device_a.get(), GCAMCommand::SerialA); process_serial_device(m_serial_device_b.get(), GCAMCommand::SerialB); @@ -1579,11 +583,10 @@ void CSIDevice_AMBaseboard::DoState(PointerWrap& p) p.Do(m_response_buffers); p.Do(m_current_response_buffer_index); - p.Do(m_coin); - p.Do(m_coin_pressed); - m_io_ports.DoState(p); + m_jvs_io_board->DoState(p); + // Serial A if (m_serial_device_a != nullptr) m_serial_device_a->DoState(p); @@ -1591,31 +594,6 @@ void CSIDevice_AMBaseboard::DoState(PointerWrap& p) // Serial B if (m_serial_device_b != nullptr) m_serial_device_b->DoState(p); - - // Serial - p.Do(m_wheel_init); - - p.Do(m_motor_init); - p.Do(m_motor_reply); - p.Do(m_motor_force_y); - - // F-Zero AX (DX) - p.Do(m_fzdx_seatbelt); - p.Do(m_fzdx_motion_stop); - p.Do(m_fzdx_sensor_right); - p.Do(m_fzdx_sensor_left); - p.Do(m_rx_reply); - - // F-Zero AX (CyCraft) - p.Do(m_fzcc_seatbelt); - p.Do(m_fzcc_sensor); - p.Do(m_fzcc_emergency); - p.Do(m_fzcc_service); - - p.Do(m_dip_switch_1); - p.Do(m_dip_switch_0); - - p.Do(m_delay); } } // namespace SerialInterface diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h index 4d1990297c..5c22c864c7 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h +++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h @@ -10,38 +10,20 @@ namespace Triforce { +class JVSIOBoard; class SerialDevice; -} +} // namespace Triforce namespace SerialInterface { -// "JAMMA Video Standard" I/O -class JVSIOMessage -{ -public: - void Start(int node); - void AddData(const u8* dst, std::size_t len, int sync); - void AddData(const void* data, std::size_t len); - void AddData(const char* data); - void AddData(u32 n); - void End(); - - u32 m_pointer = 0; - std::array m_message; - -private: - u32 m_last_start = 0; - u32 m_checksum = 0; -}; - // Triforce (GC-AM) baseboard -class CSIDevice_AMBaseboard : public ISIDevice +class CSIDevice_AMBaseboard final : public ISIDevice { public: CSIDevice_AMBaseboard(Core::System& system, SIDevices device, int device_number); + ~CSIDevice_AMBaseboard() override; - // run the SI Buffer int RunBuffer(u8* buffer, int request_length) override; DataResponse GetData(u32& hi, u32& low) override; @@ -51,89 +33,15 @@ public: void DoState(PointerWrap&) override; private: - enum GCAMCommand - { - StatusSwitches = 0x10, - SerialNumber = 0x11, - Unknown_12 = 0x12, - Unknown_14 = 0x14, - FirmVersion = 0x15, - FPGAVersion = 0x16, - RegionSettings = 0x1F, - - Unknown_21 = 0x21, - Unknown_22 = 0x22, - Unknown_23 = 0x23, - Unknown_24 = 0x24, - - SerialA = 0x31, - SerialB = 0x32, - - JVSIOA = 0x40, - JVSIOB = 0x41, - - Unknown_60 = 0x60, - }; - - enum JVSIOCommand - { - IOID = 0x10, - CommandRevision = 0x11, - JVRevision = 0x12, - CommunicationVersion = 0x13, - CheckFunctionality = 0x14, - MainID = 0x15, - - SwitchesInput = 0x20, - CoinInput = 0x21, - AnalogInput = 0x22, - RotaryInput = 0x23, - KeyCodeInput = 0x24, - PositionInput = 0x25, - GeneralSwitchInput = 0x26, - - PayoutRemain = 0x2E, - Retrans = 0x2F, - CoinSubOutput = 0x30, - PayoutAddOutput = 0x31, - GeneralDriverOutput = 0x32, - AnalogOutput = 0x33, - CharacterOutput = 0x34, - CoinAddOutput = 0x35, - PayoutSubOutput = 0x36, - GeneralDriverOutput2 = 0x37, - GeneralDriverOutput3 = 0x38, - - NAMCOCommand = 0x70, - - Reset = 0xF0, - SetAddress = 0xF1, - ChangeComm = 0xF2, - }; - - enum JVSIOStatusCode - { - StatusOkay = 1, - UnsupportedCommand = 2, - ChecksumError = 3, - AcknowledgeOverflow = 4, - }; - static constexpr u32 RESPONSE_SIZE = SerialInterfaceManager::BUFFER_SIZE; - // This value prevents F-Zero AX mag card breakage. - // It's now used for serial port reads in general. - // TODO: Verify how the hardware actually works. - static constexpr u32 SERIAL_PORT_MAX_READ_SIZE = 0x1f; - // Reply has to be delayed due a bug in the parser std::array, 2> m_response_buffers{}; u8 m_current_response_buffer_index = 0; Triforce::IOPorts m_io_ports; - std::array m_coin{}; - std::array m_coin_pressed{}; + std::unique_ptr m_jvs_io_board; // Magnetic Card Reader MagCard::MagneticCardReader::Settings m_mag_card_settings; @@ -143,30 +51,6 @@ private: // Serial B std::unique_ptr m_serial_device_b; - - u32 m_wheel_init = 0; - - u32 m_motor_init = 0; - u8 m_motor_reply[64] = {}; - s16 m_motor_force_y = 0; - - // F-Zero AX (DX) - bool m_fzdx_seatbelt = true; - bool m_fzdx_motion_stop = false; - bool m_fzdx_sensor_right = false; - bool m_fzdx_sensor_left = false; - u8 m_rx_reply = 0xF0; - - // F-Zero AX (CyCraft) - bool m_fzcc_seatbelt = true; - bool m_fzcc_sensor = false; - bool m_fzcc_emergency = false; - bool m_fzcc_service = false; - - u32 m_dip_switch_1 = 0xFE; - u32 m_dip_switch_0 = 0xFF; - - int m_delay = 0; }; } // namespace SerialInterface diff --git a/Source/Core/Core/HW/Triforce/JVSIO.cpp b/Source/Core/Core/HW/Triforce/JVSIO.cpp new file mode 100644 index 0000000000..a4aad10296 --- /dev/null +++ b/Source/Core/Core/HW/Triforce/JVSIO.cpp @@ -0,0 +1,767 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/Triforce/JVSIO.h" + +#include +#include +#include + +#include + +#include "Common/BitUtils.h" +#include "Common/ChunkFile.h" +#include "Common/Logging/Log.h" +#include "Common/ScopeGuard.h" +#include "Common/Swap.h" + +namespace +{ +constexpr u8 JVSIO_SYNC = 0xe0; +constexpr u8 JVSIO_MARK = 0xd0; + +constexpr u8 JVSIO_BROADCAST_ADDRESS = 0xff; +constexpr u8 JVSIO_HOST_ADDRESS = 0x00; + +enum class JVSIOFeature : u8 +{ + SwitchInput = 0x01, // players, buttons, 0 + CoinInput = 0x02, // slots, 0, 0 + AnalogInput = 0x03, // channels, bits, 0 + RotaryInput = 0x04, // channels, 0, 0 + KeycodeInput = 0x05, // 0, 0, 0 + ScreenPositionInput = 0x06, // X-bits, Y-bits, channels + MiscSwitchInput = 0x07, // SW-MSB, SW-LSB, 0 + + CardSystem = 0x10, // slots, 0, 0 + MedalHopper = 0x11, // channels, 0, 0 + GeneralPurposeOutput = 0x12, // slots, 0, 0 + AnalogOutput = 0x13, // channels, 0, 0 + CharacterOutput = 0x14, // width, height, type + Backup = 0x15, // 0, 0, 0 +}; + +struct JVSIOFeatureSpec +{ + JVSIOFeature feature{}; + u8 param_a{}; + u8 param_b{}; + u8 param_c{}; +}; + +enum class JVSIOCoinConditionCode : u8 +{ + Normal = 0x00, + CoinJam = 0x01, + CounterDisconnected = 0x02, + Busy = 0x03, +}; + +} // namespace + +namespace Triforce +{ + +enum class JVSIOStatusCode : u8 +{ + Okay = 1, + UnknownCommand = 2, + ChecksumError = 3, + ResponseOverflow = 4, +}; + +enum class JVSIOReportCode : u8 +{ + Okay = 1, + ParameterSizeError = 2, + ParameterDataError = 3, + Busy = 4, +}; + +enum class JVSIOCommand : u8 +{ + IOIdentify = 0x10, + CommandRevision = 0x11, + JVSRevision = 0x12, + CommVersion = 0x13, + FeatureCheck = 0x14, + MainID = 0x15, + + SwitchInput = 0x20, + CoinInput = 0x21, + AnalogInput = 0x22, + RotaryInput = 0x23, + KeycodeInput = 0x24, + ScreenPositionInput = 0x25, + MiscSwitchInput = 0x26, + + RemainingPayout = 0x2e, + DataRetransmit = 0x2f, + CoinCounterDec = 0x30, + PayoutCounterInc = 0x31, + GenericOutput = 0x32, // Multiple bytes. + AnalogOutput = 0x33, + CharacterOutput = 0x34, + CoinCounterInc = 0x35, + PayoutCounterDec = 0x36, + GenericOutputByte = 0x37, // Single byte. + GenericOutputBit = 0x38, // Single bit. + + NamcoCommand = 0x70, + + Reset = 0xf0, + SetAddress = 0xf1, + CommMethodChange = 0xf2, +}; + +class JVSIORequestReader +{ +public: + explicit JVSIORequestReader(std::span data) + : m_data{data.data()}, m_data_end{data.data() + data.size()} + { + } + + // Returns a span of all remaining bytes. + constexpr std::span PeekBytes() const { return {m_data, m_data_end}; } + + // Returns the remaining readable byte count. + constexpr std::size_t RemainingByteCount() const { return std::size_t(m_data_end - m_data); } + + constexpr u8 ReadByte() + { + DEBUG_ASSERT(RemainingByteCount() != 0); + return *(m_data++); + } + + constexpr void SkipBytes(std::size_t count) + { + DEBUG_ASSERT(RemainingByteCount() >= count); + m_data += count; + } + +private: + const u8* m_data; + const u8* m_data_end; +}; + +class JVSIOResponseWriter +{ +public: + explicit JVSIOResponseWriter(std::vector* buffer) : m_buffer{*buffer} {} + + void AddData(u8 value) { m_buffer.emplace_back(value); } + + void AddData(std::span data) + { +#if defined(__cpp_lib_containers_ranges) + m_buffer.append_range(data); +#else + m_buffer.insert(m_buffer.end(), data.begin(), data.end()); +#endif + } + + void StartFrame(u8 destination_node) + { + m_buffer.clear(); + m_buffer.reserve(2 + 255); + + // Note: The JVSIO_SYNC is not included here. + AddData(destination_node); + AddData(0); // Later becomes byte count. + AddData(0); // Later becomes status code. + } + + void EndFrame(JVSIOStatusCode status_code) + { + std::size_t count_with_checksum = m_buffer.size() - 1; + + if (count_with_checksum > 255) + { + status_code = JVSIOStatusCode::ResponseOverflow; + m_buffer.resize(3); + count_with_checksum = 2; + return; + } + + // Write byte count to header. + m_buffer[1] = u8(count_with_checksum); + m_buffer[2] = u8(status_code); + + // Write checksum. + const u8 checksum = std::accumulate(m_buffer.begin(), m_buffer.end(), u8{}); + AddData(checksum); + } + + void StartReport() + { + // To be filled in later with SetLastReportCode. + m_last_report_code_index = m_buffer.size(); + m_buffer.emplace_back(); + } + + void SetLastReportCode(JVSIOReportCode code) { m_buffer[m_last_report_code_index] = u8(code); } + +private: + std::vector& m_buffer; + + std::size_t m_last_report_code_index{}; +}; + +// Attempts to decode exactly output.size() bytes. +// On success, returns the number of escaped bytes read. +static std::optional UnescapeData(std::span input, std::span output) +{ + auto in = input.begin(); + const auto in_end = input.end(); + + auto out = output.begin(); + const auto out_end = output.end(); + + while (true) + { + if (out == out_end) + return in - input.begin(); // Success. + + if (in == in_end) + return std::nullopt; + + u8 byte_value = *(in++); + if (byte_value == JVSIO_MARK) + { + if (in == in_end) + return std::nullopt; + + byte_value = *(in++) + 0x01; + } + + *(out++) = byte_value; + } +} + +JVSIOBoard::JVSIOBoard(IOPorts* io_ports) : m_io_ports{io_ports} +{ +} + +void JVSIOBoard::Update() +{ + // Update coin counters. + const auto coin_inputs = m_io_ports->GetCoinInputs(); + for (std::size_t i = 0; i != coin_inputs.size(); ++i) + { + if (!std::exchange(m_coin_prev_states[i], coin_inputs[i]) && coin_inputs[i]) + ++m_coin_counts[i]; + } + + while (true) + { + const auto rx_span = GetRxByteSpan(); + + if (rx_span.empty()) + break; // Wait for more data. + + if (rx_span[0] != JVSIO_SYNC) + { + ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Expected JVSIO_SYNC"); + ConsumeRxBytes(1); + continue; + } + + std::array unescaped_data; + + // Read 2 header bytes. + const auto header_escaped_size = + UnescapeData(rx_span.subspan(1), std::span{unescaped_data}.first(2)); + + if (!header_escaped_size.has_value()) + break; // Wait for more data. + + const u8 destination_node = unescaped_data[0]; + const u8 payload_size = unescaped_data[1]; + + // Read remaining frame bytes. + const auto payload_escaped_size = + UnescapeData(rx_span.subspan(1 + *header_escaped_size), + std::span{unescaped_data}.subspan(2, payload_size)); + + if (!payload_escaped_size.has_value()) + break; // Wait for more data. + + Common::ScopeGuard consume_frame{ + [&] { ConsumeRxBytes(1 + *header_escaped_size + *payload_escaped_size); }}; + + if (payload_size < 1) + { + ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Empty payload"); + continue; + } + + // Verify checksum. + const auto range_to_checksum = std::span{unescaped_data}.first(payload_size + 1); + const u8 expected_checksum = + std::accumulate(range_to_checksum.begin(), range_to_checksum.end(), u8()); + if (expected_checksum != unescaped_data[payload_size + 1]) + { + ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Bad checksum"); + + JVSIOResponseWriter writer{&m_last_response}; + writer.StartFrame(JVSIO_HOST_ADDRESS); + writer.EndFrame(JVSIOStatusCode::ChecksumError); + WriteResponse(m_last_response); + continue; + } + + JVSIORequestReader request{range_to_checksum.subspan(2)}; + + if (destination_node == JVSIO_BROADCAST_ADDRESS) + { + ProcessBroadcastRequest(&request); + } + else if (destination_node == m_client_address) + { + ProcessUnicastRequest(&request); + } + else + { + WARN_LOG_FMT(SERIALINTERFACE_JVSIO, "Unexpected destination node: 0x{:02x}", + destination_node); + } + + if (const auto remaining = request.RemainingByteCount(); remaining > 0) + WARN_LOG_FMT(SERIALINTERFACE_JVSIO, "Excess request bytes: {}", remaining); + } +} + +void JVSIOBoard::ProcessBroadcastRequest(JVSIORequestReader* request) +{ + if (request->RemainingByteCount() < 1) + return; + + const auto cmd = JVSIOCommand(request->ReadByte()); + switch (cmd) + { + case JVSIOCommand::Reset: + { + if (request->RemainingByteCount() < 1) + { + ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Reset: ParameterSizeError"); + break; + } + + if (request->ReadByte() == 0xd9) + { + NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "Reset"); + m_client_address = 0; + m_io_ports->GetStatusSwitches()[1] |= 0x01u; // Update "sense" line. + } + + // TODO: Does the real hardware reset coin counts ? + + m_io_ports->ResetGenericOutputs(); + m_last_response.clear(); + break; + } + case JVSIOCommand::SetAddress: + { + if (request->RemainingByteCount() < 1) + { + ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "SetAddress: ParameterSizeError"); + break; + } + + const u8 address = request->ReadByte(); + + // Note: We don't currently support daisy chaining. + // This message would otherwise be conditionally ignored. + + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "SetAddress: {}", address); + m_client_address = address; + m_io_ports->GetStatusSwitches()[1] &= ~0x01u; // Update "sense" line. + + JVSIOResponseWriter writer{&m_last_response}; + writer.StartFrame(JVSIO_HOST_ADDRESS); + writer.AddData(u8(JVSIOReportCode::Okay)); + writer.EndFrame(JVSIOStatusCode::Okay); + WriteResponse(m_last_response); + break; + } + default: + ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "ProcessBroadcastFrame: UnknownCommand: {:02x}", u8(cmd)); + break; + } +} + +void JVSIOBoard::ProcessUnicastRequest(JVSIORequestReader* request) +{ + if (request->RemainingByteCount() > 0 && + JVSIOCommand(request->PeekBytes().front()) == JVSIOCommand::DataRetransmit) + { + request->SkipBytes(1); + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "DataRetransmit"); + WriteResponse(m_last_response); + return; + } + + JVSIOResponseWriter response{&m_last_response}; + response.StartFrame(JVSIO_HOST_ADDRESS); + + FrameContext ctx{*request, response}; + + while (true) + { + if (ctx.request.RemainingByteCount() == 0) + { + ctx.response.EndFrame(JVSIOStatusCode::Okay); + break; + } + + ctx.response.StartReport(); + + const auto cmd = JVSIOCommand(ctx.request.ReadByte()); + const auto command_result = HandleCommand(cmd, ctx); + + // Entire frame error. + if (!command_result.has_value()) + { + ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "HandleCommand: cmd:{:02x} StatusCode:{}", u8(cmd), + u8(command_result.error())); + ctx.response.EndFrame(command_result.error()); + break; + } + + ctx.response.SetLastReportCode(*command_result); + + // Command error. + if (*command_result != JVSIOReportCode::Okay) + { + ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "HandleCommand: cmd:{:02x} ReportCode:{}", u8(cmd), + u8(*command_result)); + ctx.response.EndFrame(JVSIOStatusCode::Okay); + break; + } + } + + WriteResponse(m_last_response); +} + +void JVSIOBoard::WriteResponse(std::span response) +{ + WriteTxByte(JVSIO_SYNC); + + for (const u8 byte_value : response) + { + const bool needs_escaping = byte_value == JVSIO_SYNC || byte_value == JVSIO_MARK; + if (needs_escaping) + { + WriteTxByte(JVSIO_MARK); + WriteTxByte(byte_value - 0x01); + } + else + { + WriteTxByte(byte_value); + } + } +} + +auto JVSIOBoard::HandleCommand(JVSIOCommand cmd, FrameContext ctx) -> HandlerResponse +{ + switch (cmd) + { + case JVSIOCommand::CommandRevision: + { + ctx.response.AddData(0x11); + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "CommandRevision"); + return JVSIOReportCode::Okay; + } + case JVSIOCommand::JVSRevision: + { + ctx.response.AddData(0x20); + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVSRevision"); + return JVSIOReportCode::Okay; + } + case JVSIOCommand::CommVersion: + { + ctx.response.AddData(0x10); + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "CommVersion"); + return JVSIOReportCode::Okay; + } + case JVSIOCommand::MainID: + { + const auto remaining_bytes = ctx.request.PeekBytes(); + const auto null_iter = std::ranges::find(remaining_bytes, u8{}); + + if (null_iter == remaining_bytes.end()) + return JVSIOReportCode::ParameterDataError; + + std::string_view main_id_str{reinterpret_cast(remaining_bytes.data()), + reinterpret_cast(std::to_address(null_iter))}; + ctx.request.SkipBytes(main_id_str.size() + 1); + + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "MainID: {}", main_id_str); + return JVSIOReportCode::Okay; + } + case JVSIOCommand::SwitchInput: + { + if (ctx.request.RemainingByteCount() < 2) + return JVSIOReportCode::ParameterSizeError; + + const u8 player_count = ctx.request.ReadByte(); + const u8 bytes_per_player = ctx.request.ReadByte(); + + DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "SwitchInput: player_count:{} bytes_per_player:{}", + player_count, bytes_per_player); + + ctx.response.AddData(*m_io_ports->GetSystemInputs()); + + for (u8 player_index = 0; player_index != player_count; ++player_index) + { + const auto switch_inputs = m_io_ports->GetSwitchInputs(player_index); + const auto bytes_to_use = std::min(bytes_per_player, switch_inputs.size()); + + ctx.response.AddData(switch_inputs.first(bytes_to_use)); + + // Pad data if the game requests more inputs than we have (unlikely). + for (std::size_t i = bytes_to_use; i != bytes_per_player; ++i) + ctx.response.AddData(u8{}); + } + + return JVSIOReportCode::Okay; + } + case JVSIOCommand::AnalogInput: + { + if (ctx.request.RemainingByteCount() < 1) + return JVSIOReportCode::ParameterSizeError; + + const u8 channel_count = ctx.request.ReadByte(); + + DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "AnalogInput: channel_count:{}", channel_count); + + const auto analog_inputs = m_io_ports->GetAnalogInputs(); + const auto inputs_to_use = std::min(channel_count, analog_inputs.size()); + + for (auto value : analog_inputs.first(inputs_to_use)) + ctx.response.AddData(Common::AsU8Span(Common::swap16(value))); + + // Pad data if the game requests more inputs than we have (unlikely). + for (std::size_t i = inputs_to_use; i != channel_count; ++i) + ctx.response.AddData(Common::AsU8Span(Common::swap16(IOPorts::NEUTRAL_ANALOG_VALUE))); + + return JVSIOReportCode::Okay; + } + case JVSIOCommand::GenericOutput: + { + if (ctx.request.RemainingByteCount() < 1) + return JVSIOReportCode::ParameterSizeError; + + const u8 byte_count = ctx.request.ReadByte(); + + if (ctx.request.RemainingByteCount() < byte_count) + return JVSIOReportCode::ParameterSizeError; + + const auto output_span = ctx.request.PeekBytes().first(byte_count); + + DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "GenericOutput: {}", fmt::join(output_span, " ")); + + m_io_ports->SetGenericOutputs(output_span); + + ctx.request.SkipBytes(byte_count); + + return JVSIOReportCode::Okay; + } + case JVSIOCommand::CoinInput: + { + if (ctx.request.RemainingByteCount() < 1) + return JVSIOReportCode::ParameterSizeError; + + const u8 slot_count = ctx.request.ReadByte(); + + DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "CoinInput: slot_count:{}", slot_count); + + for (u8 i = 0; i != slot_count; ++i) + { + auto condition_code = JVSIOCoinConditionCode::Normal; + u16 coin_count = 0; + + if (i < m_coin_counts.size()) + { + coin_count = m_coin_counts[i]; + } + else + { + condition_code = JVSIOCoinConditionCode::CounterDisconnected; + } + + // Top 2 bits contain the condition code. + // Bottom 14 bits contain the coin count. + const Common::BigEndianValue data{ + u16((u32(condition_code) << 14) | (coin_count & 0x3fffu))}; + + ctx.response.AddData(Common::AsU8Span(data)); + } + + return JVSIOReportCode::Okay; + } + case JVSIOCommand::CoinCounterDec: + case JVSIOCommand::CoinCounterInc: + { + if (ctx.request.RemainingByteCount() < 3) + return JVSIOReportCode::ParameterSizeError; + + const u8 slot = ctx.request.ReadByte(); + const u32 coin_msb = ctx.request.ReadByte(); + const u32 coin_lsb = ctx.request.ReadByte(); + + const auto adjustment = + s32((coin_msb << 8u) | coin_lsb) * ((cmd == JVSIOCommand::CoinCounterDec) ? -1 : +1); + + NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "CoinCounter: slot:{} adjustment:{}", slot, adjustment); + + if (slot < m_coin_counts.size()) + m_coin_counts[slot] = std::clamp(m_coin_counts[slot] + adjustment, 0, 0x3fff); + + return JVSIOReportCode::Okay; + } + default: + return std::unexpected{JVSIOStatusCode::UnknownCommand}; + } +} + +// JVS board specs largely sourced from: +// https://www.arcade-projects.com/threads/jvs-information-extractor.2071/ + +auto Namco_FCA_JVSIOBoard::HandleCommand(JVSIOCommand cmd, FrameContext ctx) -> HandlerResponse +{ + switch (cmd) + { + case JVSIOCommand::IOIdentify: + { + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "IOIdentify"); + ctx.response.AddData( + Common::AsU8Span(std::span{"namco ltd.;FCA-1;Ver1.01;JPN,Multipurpose + Rotary Encoder"})); + return JVSIOReportCode::Okay; + } + case JVSIOCommand::FeatureCheck: + { + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "FeatureCheck"); + constexpr auto features = std::to_array({ + {JVSIOFeature::SwitchInput, 1, 16}, + {JVSIOFeature::CoinInput, 2}, + {JVSIOFeature::AnalogInput, 7}, + // Disabled to avoid unnecessary implementation of JVSIOCommand::RotaryInput + // {JVSIOFeature::RotaryInput, 2}, + {JVSIOFeature::GeneralPurposeOutput, 6}, + {JVSIOFeature::AnalogOutput, 4}, + }); + ctx.response.AddData(Common::AsU8Span(features)); + ctx.response.AddData(u8{}); // End code + + return JVSIOReportCode::Okay; + } + case JVSIOCommand::NamcoCommand: + { + if (ctx.request.RemainingByteCount() < 1) + return JVSIOReportCode::ParameterSizeError; + + const u8 namco_command = ctx.request.ReadByte(); + + if (namco_command == 0x18) + { + constexpr u32 param_size = 4; + + if (ctx.request.RemainingByteCount() < param_size) + return JVSIOReportCode::ParameterSizeError; + + const auto params = ctx.request.PeekBytes().first(param_size); + + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "NAMCO_COMMAND({:02x}): {:02x}", namco_command, + fmt::join(params, " ")); + + ctx.request.SkipBytes(param_size); + ctx.response.AddData(0xff); + } + else + { + ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "Unknown NAMCO_COMMAND: {:02x}", namco_command); + return JVSIOReportCode::ParameterDataError; + } + + return JVSIOReportCode::Okay; + } + default: + return JVSIOBoard::HandleCommand(cmd, ctx); + } +} + +auto Sega_837_13551_JVSIOBoard::HandleCommand(JVSIOCommand cmd, FrameContext ctx) -> HandlerResponse +{ + switch (cmd) + { + case JVSIOCommand::IOIdentify: + { + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "IOIdentify"); + ctx.response.AddData( + Common::AsU8Span(std::span{"SEGA ENTERPRISES,LTD.;I/O BD JVS;837-13551 ;Ver1.00;98/10"})); + return JVSIOReportCode::Okay; + } + case JVSIOCommand::FeatureCheck: + { + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "FeatureCheck"); + constexpr auto features = std::to_array({ + {JVSIOFeature::SwitchInput, 2, 13}, + {JVSIOFeature::CoinInput, 2}, + {JVSIOFeature::AnalogInput, 8}, + {JVSIOFeature::GeneralPurposeOutput, 6}, + }); + ctx.response.AddData(Common::AsU8Span(features)); + ctx.response.AddData(u8{}); // End code + + return JVSIOReportCode::Okay; + } + default: + return JVSIOBoard::HandleCommand(cmd, ctx); + } +} + +auto Sega_837_13844_JVSIOBoard::HandleCommand(JVSIOCommand cmd, FrameContext ctx) -> HandlerResponse +{ + switch (cmd) + { + case JVSIOCommand::IOIdentify: + { + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "IOIdentify"); + ctx.response.AddData(Common::AsU8Span( + std::span{"SEGA ENTERPRISES,LTD.;837-13844-01 I/O CNTL BD2 ;Ver1.00;99/07"})); + return JVSIOReportCode::Okay; + } + case JVSIOCommand::FeatureCheck: + { + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "FeatureCheck"); + constexpr auto features = std::to_array({ + {JVSIOFeature::SwitchInput, 2, 12}, + {JVSIOFeature::CoinInput, 2}, + {JVSIOFeature::AnalogInput, 8}, + {JVSIOFeature::GeneralPurposeOutput, 22}, + }); + ctx.response.AddData(Common::AsU8Span(features)); + ctx.response.AddData(u8{}); // End code + + return JVSIOReportCode::Okay; + } + default: + return JVSIOBoard::HandleCommand(cmd, ctx); + } +} + +void JVSIOBoard::DoState(PointerWrap& p) +{ + SerialDevice::DoState(p); + + p.Do(m_client_address); + + p.Do(m_last_response); + + p.Do(m_coin_counts); + p.Do(m_coin_prev_states); +} + +} // namespace Triforce diff --git a/Source/Core/Core/HW/Triforce/JVSIO.h b/Source/Core/Core/HW/Triforce/JVSIO.h new file mode 100644 index 0000000000..78ec5a3091 --- /dev/null +++ b/Source/Core/Core/HW/Triforce/JVSIO.h @@ -0,0 +1,93 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "Common/CommonTypes.h" + +#include "Core/HW/Triforce/IOPorts.h" +#include "Core/HW/Triforce/SerialDevice.h" + +class PointerWrap; + +namespace Triforce +{ +enum class JVSIOStatusCode : u8; +enum class JVSIOReportCode : u8; +enum class JVSIOCommand : u8; + +class JVSIORequestReader; +class JVSIOResponseWriter; + +// "JAMMA Video Standard" I/O +class JVSIOBoard : public SerialDevice +{ +public: + explicit JVSIOBoard(IOPorts* io_ports); + + void Update() override; + + void DoState(PointerWrap& p) override; + +protected: + using HandlerResponse = std::expected; + + struct FrameContext + { + JVSIORequestReader& request; + JVSIOResponseWriter& response; + }; + + virtual HandlerResponse HandleCommand(JVSIOCommand cmd, FrameContext ctx); + +private: + void ProcessBroadcastRequest(JVSIORequestReader* request); + void ProcessUnicastRequest(JVSIORequestReader* request); + + void WriteResponse(std::span response); + + // 0 == address not yet assigned. + u8 m_client_address = 0; + + // Note: Does not include JVSIO_SYNC. + std::vector m_last_response; + + std::array m_coin_counts{}; + std::array m_coin_prev_states{}; + + IOPorts* const m_io_ports; +}; + +// Used by MarioKartGP and MarioKartGP2. +class Namco_FCA_JVSIOBoard final : public JVSIOBoard +{ +public: + using JVSIOBoard::JVSIOBoard; + + HandlerResponse HandleCommand(JVSIOCommand cmd, FrameContext ctx) override; +}; + +// Used by VirtuaStriker4 and others. +class Sega_837_13551_JVSIOBoard final : public JVSIOBoard +{ +public: + using JVSIOBoard::JVSIOBoard; + + HandlerResponse HandleCommand(JVSIOCommand cmd, FrameContext ctx) override; +}; + +// Used by FZeroAX (Deluxe). +class Sega_837_13844_JVSIOBoard final : public JVSIOBoard +{ +public: + using JVSIOBoard::JVSIOBoard; + + HandlerResponse HandleCommand(JVSIOCommand cmd, FrameContext ctx) override; +}; + +} // namespace Triforce diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 1713c0f88e..ea1679a743 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -342,8 +342,11 @@ + + + @@ -1047,8 +1050,11 @@ + + +