From 49518164bb1b82099bbf2aec50d03925b6409079 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Mon, 23 Mar 2026 00:42:56 -0500 Subject: [PATCH 1/4] Triforce/Touchscreen: Fix LOG type. --- Source/Core/Core/HW/Triforce/Touchscreen.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/HW/Triforce/Touchscreen.cpp b/Source/Core/Core/HW/Triforce/Touchscreen.cpp index eab93b741e..6f59415854 100644 --- a/Source/Core/Core/HW/Triforce/Touchscreen.cpp +++ b/Source/Core/Core/HW/Triforce/Touchscreen.cpp @@ -37,7 +37,8 @@ void Touchscreen::Update() if (const auto input = GetRxByteSpan(); !input.empty()) { // The Key of Avalon doesn't write to the device, it only reads. - WARN_LOG_FMT(AMMEDIABOARD, "Unexpected write of {} bytes to touchscreen.", input.size()); + WARN_LOG_FMT(SERIALINTERFACE_AMBB, "Unexpected write of {} bytes to touchscreen.", + input.size()); ConsumeRxBytes(input.size()); } From 5c912e881ee11422e9cd063ce4c4b8aa2c952f33 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Mon, 23 Mar 2026 00:51:13 -0500 Subject: [PATCH 2/4] IOPorts: Add additional functionality to handle analog input, coin input, et al. Created IOAdapter classes for FZeroAX games. Created SerialDevice classes for MarioKartGP and FZeroAX FFB steering wheels. Added game-specific input handling to the various IOAdapter classes. --- Source/Core/Core/HW/Triforce/FZeroAX.cpp | 275 ++++++++++++++++ Source/Core/Core/HW/Triforce/FZeroAX.h | 70 ++++ Source/Core/Core/HW/Triforce/IOPorts.cpp | 316 +++++++++++++++---- Source/Core/Core/HW/Triforce/IOPorts.h | 92 ++++-- Source/Core/Core/HW/Triforce/MarioKartGP.cpp | 138 ++++++++ Source/Core/Core/HW/Triforce/MarioKartGP.h | 47 +++ 6 files changed, 833 insertions(+), 105 deletions(-) create mode 100644 Source/Core/Core/HW/Triforce/FZeroAX.cpp create mode 100644 Source/Core/Core/HW/Triforce/FZeroAX.h create mode 100644 Source/Core/Core/HW/Triforce/MarioKartGP.cpp create mode 100644 Source/Core/Core/HW/Triforce/MarioKartGP.h diff --git a/Source/Core/Core/HW/Triforce/FZeroAX.cpp b/Source/Core/Core/HW/Triforce/FZeroAX.cpp new file mode 100644 index 0000000000..dde106882c --- /dev/null +++ b/Source/Core/Core/HW/Triforce/FZeroAX.cpp @@ -0,0 +1,275 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/Triforce/FZeroAX.h" + +#include + +#include + +#include "Common/BitUtils.h" +#include "Common/ChunkFile.h" +#include "Common/Logging/Log.h" +#include "Common/Swap.h" + +#include "Core/HW/GCPad.h" + +#include "InputCommon/GCPadStatus.h" + +namespace Triforce +{ + +void FZeroAXCommon_IOAdapter::Update() +{ + auto* const io_ports = GetIOPorts(); + + // Horizontal Scanning Frequency switch. + // Required for booting via Sega Boot. + io_ports->GetStatusSwitches()[0] &= ~0x20u; + + const GCPadStatus pad_status = Pad::GetStatus(0); + + const auto switch_inputs_0 = io_ports->GetSwitchInputs(0); + // Start + if (pad_status.button & PAD_BUTTON_START) + switch_inputs_0[0] |= 0x80; + // View Change 1 + if (pad_status.button & PAD_BUTTON_RIGHT) + switch_inputs_0[0] |= 0x20; + // View Change 2 + if (pad_status.button & PAD_BUTTON_LEFT) + switch_inputs_0[0] |= 0x10; + // View Change 3 + if (pad_status.button & PAD_BUTTON_UP) + switch_inputs_0[0] |= 0x08; + // View Change 4 + if (pad_status.button & PAD_BUTTON_DOWN) + switch_inputs_0[0] |= 0x04; + // Boost + if (pad_status.button & PAD_BUTTON_A) + switch_inputs_0[0] |= 0x02; + + const auto switch_inputs_1 = io_ports->GetSwitchInputs(1); + // Paddle left + if (pad_status.button & PAD_BUTTON_X) + switch_inputs_1[0] |= 0x20; + // Paddle right + if (pad_status.button & PAD_BUTTON_Y) + switch_inputs_1[0] |= 0x10; + + const auto analog_inputs = io_ports->GetAnalogInputs(); + + // Steering + if (m_steering_wheel->IsInitializing()) + { + // Override X position during initialization to make the calibration happy. + analog_inputs[0] = + (m_steering_wheel->GetServoPosition() * (1 << 9)) + IOPorts::NEUTRAL_ANALOG_VALUE; + } + else + { + analog_inputs[0] = Common::ExpandValue(pad_status.stickX, 8); + } + + analog_inputs[1] = Common::ExpandValue(pad_status.stickY, 8); + + // Gas + analog_inputs[4] = Common::ExpandValue(pad_status.triggerRight, 8); + // Brake + analog_inputs[5] = Common::ExpandValue(pad_status.triggerLeft, 8); + // Seat Motion + analog_inputs[6] = IOPorts::NEUTRAL_ANALOG_VALUE; +} + +void FZeroAXCommon_IOAdapter::HandleGenericOutputsChanged(std::span bits_set, + std::span bits_cleared) +{ + const u8 bits_changed_0 = bits_set[0] | bits_cleared[0]; + + constexpr auto LED_NAMES = std::to_array>({ + {0x80, "START BUTTON"}, + {0x20, "VIEW CHANGE 1"}, + {0x10, "VIEW CHANGE 2"}, + {0x08, "VIEW CHANGE 3"}, + {0x04, "VIEW CHANGE 4"}, + {0x40, "BOOST"}, + }); + + for (const auto& [led_value, led_name] : LED_NAMES) + { + if (bits_changed_0 & led_value) + { + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: {}: {}", led_name, + (bits_set[0] & led_value) ? "ON" : "OFF"); + } + } +} + +void FZeroAXDeluxe_IOAdapter::Update() +{ + auto* const io_ports = GetIOPorts(); + const auto generic_outputs = io_ports->GetGenericOutputs(); + + // Not fully understood trickery to satisfy the game's initialization sequence. + const u16 seat_state = Common::swap16(generic_outputs.data() + 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; + } + + constexpr bool seatbelt = true; + constexpr bool motion_stop = false; + constexpr bool sensor_left = false; + constexpr bool sensor_right = false; + + const auto switch_inputs_p0 = io_ports->GetSwitchInputs(0); + + if (seatbelt) + switch_inputs_p0[0] |= 0x01; + + switch_inputs_p0[1] = m_rx_reply & 0xF0; + + const auto switch_inputs_p1 = io_ports->GetSwitchInputs(1); + + if (sensor_left) + switch_inputs_p1[0] |= 0x08; + if (sensor_right) + switch_inputs_p1[0] |= 0x04; + if (motion_stop) + switch_inputs_p1[0] |= 0x02; + + switch_inputs_p1[1] = m_rx_reply << 4; +} + +void FZeroAXMonster_IOAdapter::Update() +{ + auto* const io_ports = GetIOPorts(); + + constexpr bool sensor = false; + constexpr bool emergency = false; + constexpr bool service = false; + constexpr bool seatbelt = true; + + const auto switch_inputs_p0 = io_ports->GetSwitchInputs(0); + + if (sensor) + switch_inputs_p0[0] |= 0x01; + + const auto switch_inputs_p1 = io_ports->GetSwitchInputs(1); + + if (emergency) + switch_inputs_p1[0] |= 0x08; + if (service) + switch_inputs_p1[0] |= 0x04; + if (seatbelt) + switch_inputs_p1[0] |= 0x02; +} + +void FZeroAXDeluxe_IOAdapter::DoState(PointerWrap& p) +{ + p.Do(m_delay); + p.Do(m_rx_reply); +} + +void FZeroAXSteeringWheel::Update() +{ + constexpr std::size_t REQUEST_SIZE = 4; + + std::size_t rx_position = 0; + while (true) + { + const auto rx_span = GetRxByteSpan().subspan(rx_position); + + if (rx_span.size() < REQUEST_SIZE) + break; // Wait for more data. + + const auto request = rx_span.first(); + + // The first byte is XOR'd with 0x80. + // The last byte is an XOR of the previous bytes. + if (std::accumulate(request.begin(), request.end(), u8{0x80}, std::bit_xor{}) != 0) + { + WARN_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Bad checksum!"); + ++rx_position; + continue; + } + + ProcessRequest(request); + rx_position += REQUEST_SIZE; + } + + ConsumeRxBytes(rx_position); +} + +void FZeroAXSteeringWheel::ProcessRequest(std::span request) +{ + DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Request: {:02x}", fmt::join(request, " ")); + + // The first byte is XOR'd with 0x80. + const u8 cmd = request[0] ^ 0x80u; + switch (cmd) + { + case 0: // Power on/off ? + // Sent before force commands: 00 01 + // Sent after force commands: 00 00 + case 1: // Set Maximum? + case 2: + break; + + case 4: // Move Steering Wheel + { + // This seems to be a u8 value but the MSb is the LSb of the previous byte. + // e.g. 01 7f -> ff + + // This produces a value in the range around [-56, +56]. + m_servo_position = s8(0x80 - (u8(request[1] << 7u) | request[2])); + + DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: servo_position: {}", m_servo_position); + break; + } + + case 6: // nice + case 9: + default: + break; + + // Switch back to normal controls + case 7: + m_init_state = 2; + break; + + // Reset + case 0x7F: + m_init_state = 1; + break; + } + + // Simple 4 byte response. + WriteTxBytes(std::array{}); +} + +bool FZeroAXSteeringWheel::IsInitializing() const +{ + return m_init_state == 1; +} + +void FZeroAXSteeringWheel::DoState(PointerWrap& p) +{ + p.Do(m_init_state); + p.Do(m_servo_position); +} + +} // namespace Triforce diff --git a/Source/Core/Core/HW/Triforce/FZeroAX.h b/Source/Core/Core/HW/Triforce/FZeroAX.h new file mode 100644 index 0000000000..18688336e2 --- /dev/null +++ b/Source/Core/Core/HW/Triforce/FZeroAX.h @@ -0,0 +1,70 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Core/HW/Triforce/IOPorts.h" +#include "Core/HW/Triforce/SerialDevice.h" + +namespace Triforce +{ + +// FFB wheel used by FZeroAX and FZeroAXMonster. +class FZeroAXSteeringWheel final : public SerialDevice +{ +public: + void Update() override; + + bool IsInitializing() const; + + s8 GetServoPosition() const { return m_servo_position; } + + void DoState(PointerWrap&) override; + +private: + void ProcessRequest(std::span); + + u8 m_init_state = 0; + + s8 m_servo_position = 0; +}; + +// Used for both FZeroAX and FZeroAXMonster. +class FZeroAXCommon_IOAdapter final : public IOAdapter +{ +public: + explicit FZeroAXCommon_IOAdapter(FZeroAXSteeringWheel* steering_wheel) + : m_steering_wheel{steering_wheel} + { + } + + void Update() override; + +protected: + void HandleGenericOutputsChanged(std::span bits_set, + std::span bits_cleared) override; + +private: + FZeroAXSteeringWheel* const m_steering_wheel; +}; + +// Includes seat motion handling. +class FZeroAXDeluxe_IOAdapter final : public IOAdapter +{ +public: + void Update() override; + + void DoState(PointerWrap&) override; + +private: + u32 m_delay = 0; + u8 m_rx_reply = 0xF0; +}; + +class FZeroAXMonster_IOAdapter final : public IOAdapter +{ +public: + void Update() override; +}; + +} // namespace Triforce diff --git a/Source/Core/Core/HW/Triforce/IOPorts.cpp b/Source/Core/Core/HW/Triforce/IOPorts.cpp index 2a600f94cd..a393987d0f 100644 --- a/Source/Core/Core/HW/Triforce/IOPorts.cpp +++ b/Source/Core/Core/HW/Triforce/IOPorts.cpp @@ -3,27 +3,63 @@ #include "Core/HW/Triforce/IOPorts.h" -#include +#include #include +#include "Common/Assert.h" #include "Common/BitUtils.h" #include "Common/ChunkFile.h" +#include "Core/HW/DVD/AMMediaboard.h" +#include "Core/HW/GCPad.h" +#include "Core/HW/SI/SI.h" +#include "Core/HW/SI/SI_Device.h" #include "Core/HW/Triforce/ICCardReader.h" +#include "Core/System.h" + +#include "InputCommon/GCPadStatus.h" +#include "VideoCommon/OnScreenDisplay.h" namespace Triforce { void IOPorts::Update() { + m_system_inputs = 0x00; + m_switch_inputs.fill(0x00); + m_analog_inputs.fill(NEUTRAL_ANALOG_VALUE); + m_coin_inputs.fill(false); + std::ranges::for_each(m_io_adapters, &IOAdapter::Update); } +std::span IOPorts::GetSwitchInputs(u32 player_index) +{ + ASSERT(player_index < PLAYER_COUNT); + + return std::span{m_switch_inputs}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index, + SWITCH_INPUT_BYTES_PER_PLAYER); +} + +std::span IOPorts::GetSwitchInputs(u32 player_index) const +{ + ASSERT(player_index < PLAYER_COUNT); + + return std::span{m_switch_inputs}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index, + SWITCH_INPUT_BYTES_PER_PLAYER); +} + void IOPorts::DoState(PointerWrap& p) { - p.Do(m_switch_input_data); - p.Do(m_generic_output_data); + p.Do(m_status_switches); + + // Input states need not be state saved. They are updated before use. + + p.Do(m_generic_outputs); + + for (auto& io_adapter : m_io_adapters) + io_adapter->DoState(p); } void IOPorts::AddIOAdapter(std::unique_ptr adapter) @@ -37,42 +73,33 @@ IOAdapter::~IOAdapter() = default; void IOPorts::SetGenericOutputs(std::span bytes) { - const auto bytes_to_copy = std::min(bytes.size(), GENERIC_OUTPUT_BYTE_COUNT); - - if (bytes.size() > m_generic_output_data.size()) + if (bytes.size() > m_generic_outputs.size()) { - WARN_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: GenericOutputs: Unexpected byte count: {}", + WARN_LOG_FMT(SERIALINTERFACE_JVSIO, "SetGenericOutputs: Unexpected byte count: {}", bytes.size()); } - decltype(m_generic_output_data) bits_set{}; - decltype(m_generic_output_data) bits_cleared{}; + const auto bytes_to_copy = std::min(bytes.size(), GENERIC_OUTPUT_BYTE_COUNT); + + const bool no_change = std::ranges::equal(bytes.first(bytes_to_copy), + std::span{m_generic_outputs}.first(bytes_to_copy)); + if (no_change) + return; + + decltype(m_generic_outputs) bits_set{}; + decltype(m_generic_outputs) bits_cleared{}; for (std::size_t i = 0; i != bytes_to_copy; ++i) { - bits_set[i] = u8(~m_generic_output_data[i]) & bytes[i]; - bits_cleared[i] = m_generic_output_data[i] & u8(~bytes[i]); + bits_set[i] = u8(~m_generic_outputs[i]) & bytes[i]; + bits_cleared[i] = m_generic_outputs[i] & u8(~bytes[i]); - m_generic_output_data[i] = bytes[i]; + m_generic_outputs[i] = bytes[i]; } - bool bits_changed = false; - - if (std::ranges::any_of(bits_set, std::bind_front(std::not_equal_to{}, 0x00))) - { - bits_changed = true; - INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: GenericOutputs: bits_set: {:02x}", - fmt::join(bits_set, " ")); - } - if (std::ranges::any_of(bits_cleared, std::bind_front(std::not_equal_to{}, 0x00))) - { - bits_changed = true; - INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: GenericOutputs: bits_cleared: {:02x}", - fmt::join(bits_cleared, " ")); - } - - if (!bits_changed) - return; + DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "SetGenericOutputs: set:{:02x} clr:{:02x} ({:02x})", + fmt::join(bits_set, " "), fmt::join(bits_cleared, " "), + fmt::join(m_generic_outputs, " ")); for (auto& adapter : m_io_adapters) { @@ -80,36 +107,109 @@ void IOPorts::SetGenericOutputs(std::span bytes) } } +void IOPorts::ResetGenericOutputs() +{ + SetGenericOutputs(std::array{}); +} + void IOAdapter::Update() { } +void IOAdapter::DoState(PointerWrap& p) +{ +} + void IOAdapter::HandleGenericOutputsChanged(std::span bits_set, std::span bits_cleared) { } -void MarioKartGPCommon_IOAdapter::HandleGenericOutputsChanged(std::span bits_set, - std::span bits_cleared) +void Common_IOAdapter::Update() { - const u8 bits_changed_0 = bits_set[0] | bits_cleared[0]; + auto* const io_ports = GetIOPorts(); + const auto coin_inputs = io_ports->GetCoinInputs(); - if (bits_changed_0 & ITEM_LIGHT_BIT) + // Test/Service input is also possible this way, but we handle them via JVS IO instead. + // const auto status_switches = io_ports->GetStatusSwitches(); + // status_switches[0] &= ~0x80; // Test + // status_switches[0] &= ~0x40; // Service + + for (int i = 0; i != IOPorts::PLAYER_COUNT; ++i) { - INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Item Button: {}", - (bits_set[0] & ITEM_LIGHT_BIT) ? "ON" : "OFF"); + if (m_system.GetSerialInterface().GetDeviceType(i) != SerialInterface::SIDEVICE_AM_BASEBOARD) + continue; + + const GCPadStatus pad_status = Pad::GetStatus(i); + + // Test button + if (pad_status.switches & SWITCH_TEST) + { + if (AMMediaboard::GetTestMenu()) + { + *io_ports->GetSystemInputs() |= 0x80u; + } + else + { + // Trying to access the test menu without SegaBoot present will cause a crash. + OSD::AddMessage("Test menu is disabled due to missing SegaBoot.", OSD::Duration::NORMAL, + OSD::Color::RED); + } + } + + const auto switch_inputs = io_ports->GetSwitchInputs(i); + + // Service button + if (pad_status.switches & SWITCH_SERVICE) + switch_inputs[0] |= 0x40; + + // Coin button + if (pad_status.switches & SWITCH_COIN) + coin_inputs[i] = true; } +} - if (bits_changed_0 & CANCEL_LIGHT_BIT) +void VirtuaStriker3_IOAdapter::Update() +{ + auto* const io_ports = GetIOPorts(); + + for (int i = 0; i != IOPorts::PLAYER_COUNT; ++i) { - INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Cancel Button: {}", - (bits_set[0] & CANCEL_LIGHT_BIT) ? "ON" : "OFF"); + const auto switch_inputs = io_ports->GetSwitchInputs(i); + const GCPadStatus pad_status = Pad::GetStatus(i); + + // Start + if (pad_status.button & PAD_BUTTON_START) + switch_inputs[0] |= 0x80; + // Up + if (pad_status.button & PAD_BUTTON_UP) + switch_inputs[0] |= 0x20; + // Down + if (pad_status.button & PAD_BUTTON_DOWN) + switch_inputs[0] |= 0x10; + // Left + if (pad_status.button & PAD_BUTTON_LEFT) + switch_inputs[0] |= 0x08; + // Right + if (pad_status.button & PAD_BUTTON_RIGHT) + switch_inputs[0] |= 0x04; + // Long Pass + if (pad_status.button & PAD_BUTTON_X) + switch_inputs[0] |= 0x02; + // Shoot + if (pad_status.button & PAD_BUTTON_B) + switch_inputs[0] |= 0x01; + + // Short Pass + if (pad_status.button & PAD_BUTTON_A) + switch_inputs[1] |= 0x80; } } void VirtuaStriker4Common_IOAdapter::Update() { - const auto generic_outputs = GetIOPorts()->GetGenericOutputs(); + auto* const io_ports = GetIOPorts(); + const auto generic_outputs = io_ports->GetGenericOutputs(); const bool is_slot_a_locked = generic_outputs[0] & SLOT_A_LOCK_BIT; const bool is_slot_b_locked = generic_outputs[0] & SLOT_B_LOCK_BIT; @@ -119,30 +219,65 @@ void VirtuaStriker4Common_IOAdapter::Update() if (is_slot_a_locked) { - // If slot 1 is locked, insert slot 2. - if (m_card_reader_b->IsReadyToInsertCard()) - m_card_reader_b->InsertCard(); + // If slot A is locked, insert slot B. + if (m_card_readers[1]->IsReadyToInsertCard()) + m_card_readers[1]->InsertCard(); } else { - // If slot 1 is unlocked, remove slot 2 if unlocked. - if (m_card_reader_b->IsCardPresent() && !is_slot_b_locked) - m_card_reader_b->EjectCard(); + // If slot A is unlocked, remove slot B if unlocked. + if (m_card_readers[1]->IsCardPresent() && !is_slot_b_locked) + m_card_readers[1]->EjectCard(); } - if (m_card_reader_a->IsReadyToInsertCard()) - m_card_reader_a->InsertCard(); + // Insert slot A. + if (m_card_readers[0]->IsReadyToInsertCard()) + m_card_readers[0]->InsertCard(); - const auto p1_inputs = GetIOPorts()->GetSwitchInputs(0); - const auto p2_inputs = GetIOPorts()->GetSwitchInputs(1); + const auto analog_inputs = io_ports->GetAnalogInputs(); - // Bit 0x10 of each player's 1st byte is a card presence switch. - Common::SetBit(p1_inputs[0], 4, m_card_reader_a->IsCardPresent()); - Common::SetBit(p2_inputs[0], 4, m_card_reader_b->IsCardPresent()); + for (int i = 0; i != IOPorts::PLAYER_COUNT; ++i) + { + const auto switch_inputs = io_ports->GetSwitchInputs(i); - // Bit 0x20 of each player's 2nd byte is some kind of "eject" sensor. - Common::SetBit(p1_inputs[1], 5, m_card_reader_a->IsEjecting()); - Common::SetBit(p2_inputs[1], 5, m_card_reader_b->IsEjecting()); + // Bit 0x10 of each player's 1st byte is a card presence switch. + Common::SetBit(switch_inputs[0], 4, m_card_readers[i]->IsCardPresent()); + + // Bit 0x20 of each player's 2nd byte is some kind of "eject" sensor. + Common::SetBit(switch_inputs[1], 5, m_card_readers[i]->IsEjecting()); + + const GCPadStatus pad_status = Pad::GetStatus(i); + + // Start + if (pad_status.button & PAD_BUTTON_START) + switch_inputs[0] |= 0x80; + // Tactics (U) + if (pad_status.button & PAD_BUTTON_LEFT) + switch_inputs[0] |= 0x20; + // Tactics (M) + if (pad_status.button & PAD_BUTTON_UP) + switch_inputs[0] |= 0x08; + // Tactics (D) + if (pad_status.button & PAD_BUTTON_RIGHT) + switch_inputs[0] |= 0x04; + // Short Pass + if (pad_status.button & PAD_BUTTON_A) + switch_inputs[0] |= 0x02; + // Long Pass + if (pad_status.button & PAD_BUTTON_X) + switch_inputs[0] |= 0x01; + + // Shoot + if (pad_status.button & PAD_BUTTON_B) + switch_inputs[1] |= 0x80; + // Dash + if (pad_status.button & PAD_BUTTON_Y) + switch_inputs[1] |= 0x40; + + // Movement + analog_inputs[(2 * i) + 0] = Common::ExpandValue(0xff - pad_status.stickY, 8); + analog_inputs[(2 * i) + 1] = Common::ExpandValue(pad_status.stickX, 8); + } } void VirtuaStriker4Common_IOAdapter::HandleGenericOutputsChanged(std::span bits_set, @@ -172,27 +307,55 @@ void VirtuaStriker4_2006_IOAdapter::HandleGenericOutputsChanged(std::spanEjectCard(); + m_card_readers[0]->EjectCard(); if (bits_set[0] & SLOT_B_EJECT_BIT) - m_card_reader_b->EjectCard(); + m_card_readers[1]->EjectCard(); } void GekitouProYakyuu_IOAdapter::Update() { - // Gekitou isn't as picky as VS4. We can insert both cards simultaneously. - for (const auto& card_reader : {m_card_reader_a, m_card_reader_b}) + auto* const io_ports = GetIOPorts(); + + for (int i = 0; i != IOPorts::PLAYER_COUNT; ++i) { - if (card_reader->IsReadyToInsertCard()) - card_reader->InsertCard(); + // Gekitou isn't as picky as VS4. We can insert cards whenever. + if (m_card_readers[i]->IsReadyToInsertCard()) + m_card_readers[i]->InsertCard(); + + const auto switch_inputs = io_ports->GetSwitchInputs(i); + + // Bit 0x40 of each player's 2nd byte is a card presence switch. + Common::SetBit(switch_inputs[1], 6, m_card_readers[i]->IsCardPresent()); + + const GCPadStatus pad_status = Pad::GetStatus(i); + + // Start + if (pad_status.button & PAD_BUTTON_START) + switch_inputs[0] |= 0x80; + // Up + if (pad_status.button & PAD_BUTTON_UP) + switch_inputs[0] |= 0x20; + // Down + if (pad_status.button & PAD_BUTTON_DOWN) + switch_inputs[0] |= 0x10; + // Left + if (pad_status.button & PAD_BUTTON_LEFT) + switch_inputs[0] |= 0x08; + // Right + if (pad_status.button & PAD_BUTTON_RIGHT) + switch_inputs[0] |= 0x04; + // B + if (pad_status.button & PAD_BUTTON_A) + switch_inputs[0] |= 0x02; + // A + if (pad_status.button & PAD_BUTTON_B) + switch_inputs[0] |= 0x01; + + // Gekitou + if (pad_status.button & PAD_TRIGGER_L) + switch_inputs[1] |= 0x80; } - - const auto p1_inputs = GetIOPorts()->GetSwitchInputs(0); - const auto p2_inputs = GetIOPorts()->GetSwitchInputs(1); - - // Bit 0x40 of each player's 2nd byte is a card presence switch. - Common::SetBit(p1_inputs[1], 6, m_card_reader_a->IsCardPresent()); - Common::SetBit(p2_inputs[1], 6, m_card_reader_b->IsCardPresent()); } void GekitouProYakyuu_IOAdapter::HandleGenericOutputsChanged(std::span bits_set, @@ -205,10 +368,10 @@ void GekitouProYakyuu_IOAdapter::HandleGenericOutputsChanged(std::span // I guess we need to treat this as an alternative to ICCardCommand::Eject. if (bits_cleared[0] & SLOT_A_LOCK_BIT) - m_card_reader_a->EjectCard(); + m_card_readers[0]->EjectCard(); if (bits_cleared[0] & SLOT_B_LOCK_BIT) - m_card_reader_b->EjectCard(); + m_card_readers[1]->EjectCard(); } void KeyOfAvalon_IOAdapter::Update() @@ -221,6 +384,19 @@ void KeyOfAvalon_IOAdapter::Update() if (m_card_reader->IsReadyToInsertCard()) m_card_reader->InsertCard(); + + const auto switch_inputs = GetIOPorts()->GetSwitchInputs(0); + const GCPadStatus pad_status = Pad::GetStatus(0); + + // Debug On + if (pad_status.button & PAD_BUTTON_START) + switch_inputs[0] |= 0x80; + // Switch 2 + if (pad_status.button & PAD_BUTTON_B) + switch_inputs[0] |= 0x08; + // Switch 1 + if (pad_status.button & PAD_BUTTON_A) + switch_inputs[0] |= 0x04; } } // namespace Triforce diff --git a/Source/Core/Core/HW/Triforce/IOPorts.h b/Source/Core/Core/HW/Triforce/IOPorts.h index a5379c3a50..7fa3f7ae8c 100644 --- a/Source/Core/Core/HW/Triforce/IOPorts.h +++ b/Source/Core/Core/HW/Triforce/IOPorts.h @@ -8,11 +8,15 @@ #include #include -#include "Common/Assert.h" #include "Common/CommonTypes.h" class PointerWrap; +namespace Core +{ +class System; +} + namespace Triforce { @@ -21,47 +25,61 @@ class IOAdapter; // Triforce GPIO peripherals connect to JVS-IO and eachother in game specific ways. // This class hopes to handle those customizable connections. -// TODO: Use this for other JVS-IO (Analog Input, Coin, etc.). class IOPorts { public: void Update(); - std::span GetSwitchInputs(u32 player_index) - { - ASSERT(player_index < PLAYER_COUNT); + std::span GetStatusSwitches() { return m_status_switches; } + std::span GetStatusSwitches() const { return m_status_switches; } - return std::span{m_switch_input_data}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index, - SWITCH_INPUT_BYTES_PER_PLAYER); - } - std::span GetSwitchInputs(u32 player_index) const - { - ASSERT(player_index < PLAYER_COUNT); + u8* GetSystemInputs() { return &m_system_inputs; } + const u8* GetSystemInputs() const { return &m_system_inputs; } - return std::span{m_switch_input_data}.subspan(SWITCH_INPUT_BYTES_PER_PLAYER * player_index, - SWITCH_INPUT_BYTES_PER_PLAYER); - } + std::span GetSwitchInputs(u32 player_index); + std::span GetSwitchInputs(u32 player_index) const; - std::span GetGenericOutputs() { return m_generic_output_data; } - std::span GetGenericOutputs() const { return m_generic_output_data; } + std::span GetAnalogInputs() { return m_analog_inputs; } + std::span GetAnalogInputs() const { return m_analog_inputs; } + + std::span GetGenericOutputs() const { return m_generic_outputs; } void SetGenericOutputs(std::span bytes); + void ResetGenericOutputs(); + + static constexpr std::size_t PLAYER_COUNT = 2; + static constexpr std::size_t COIN_SLOT_COUNT = 2; + + std::span GetCoinInputs() { return m_coin_inputs; } + std::span GetCoinInputs() const { return m_coin_inputs; } void AddIOAdapter(std::unique_ptr adapter); void DoState(PointerWrap& p); + static constexpr u16 NEUTRAL_ANALOG_VALUE = 0x8000; + private: std::vector> m_io_adapters; - static constexpr std::size_t PLAYER_COUNT = 2; + std::array m_status_switches{0xff, 0xff}; + + // The first byte in JVSIO switch input data. + u8 m_system_inputs = 0x00; + static constexpr std::size_t SWITCH_INPUT_BYTES_PER_PLAYER = 2; - std::array m_switch_input_data{}; + std::array m_switch_inputs{}; + + static constexpr std::size_t ANALOG_INPUT_COUNT = 8; + + std::array m_analog_inputs{}; static constexpr std::size_t GENERIC_OUTPUT_BYTE_COUNT = 4; - std::array m_generic_output_data{}; + std::array m_generic_outputs{}; + + std::array m_coin_inputs{}; }; class IOAdapter @@ -79,6 +97,8 @@ public: virtual void Update(); + virtual void DoState(PointerWrap& p); + protected: IOPorts* GetIOPorts() { return m_io_ports; } @@ -92,16 +112,21 @@ private: IOPorts* m_io_ports{}; }; -// Used for both MarioKartGP and MarioKartGP2. -class MarioKartGPCommon_IOAdapter final : public IOAdapter +class Common_IOAdapter final : public IOAdapter { -protected: - void HandleGenericOutputsChanged(std::span bits_set, - std::span bits_cleared) override; +public: + explicit Common_IOAdapter(Core::System& system) : m_system{system} {} + + void Update() override; private: - static constexpr u8 ITEM_LIGHT_BIT = 0x04; - static constexpr u8 CANCEL_LIGHT_BIT = 0x08; + Core::System& m_system; +}; + +class VirtuaStriker3_IOAdapter final : public IOAdapter +{ +public: + void Update() override; }; // Common functionality for both VirtuaStriker4 and VirtuaStriker4_2006. @@ -109,7 +134,7 @@ class VirtuaStriker4Common_IOAdapter final : public IOAdapter { public: VirtuaStriker4Common_IOAdapter(ICCardReader* card_reader_a, ICCardReader* card_reader_b) - : m_card_reader_a{card_reader_a}, m_card_reader_b{card_reader_b} + : m_card_readers{card_reader_a, card_reader_b} { } @@ -123,15 +148,14 @@ protected: std::span bits_cleared) override; private: - ICCardReader* const m_card_reader_a; - ICCardReader* const m_card_reader_b; + std::array const m_card_readers; }; class VirtuaStriker4_2006_IOAdapter final : public IOAdapter { public: VirtuaStriker4_2006_IOAdapter(ICCardReader* card_reader_a, ICCardReader* card_reader_b) - : m_card_reader_a{card_reader_a}, m_card_reader_b{card_reader_b} + : m_card_readers{card_reader_a, card_reader_b} { } @@ -143,15 +167,14 @@ private: static constexpr u8 SLOT_A_EJECT_BIT = 0x80; static constexpr u8 SLOT_B_EJECT_BIT = 0x20; - ICCardReader* const m_card_reader_a; - ICCardReader* const m_card_reader_b; + std::array const m_card_readers; }; class GekitouProYakyuu_IOAdapter final : public IOAdapter { public: GekitouProYakyuu_IOAdapter(ICCardReader* card_reader_a, ICCardReader* card_reader_b) - : m_card_reader_a{card_reader_a}, m_card_reader_b{card_reader_b} + : m_card_readers{card_reader_a, card_reader_b} { } @@ -165,8 +188,7 @@ private: static constexpr u8 SLOT_A_LOCK_BIT = 0x40; static constexpr u8 SLOT_B_LOCK_BIT = 0x10; - ICCardReader* const m_card_reader_a; - ICCardReader* const m_card_reader_b; + std::array const m_card_readers; }; class KeyOfAvalon_IOAdapter final : public IOAdapter diff --git a/Source/Core/Core/HW/Triforce/MarioKartGP.cpp b/Source/Core/Core/HW/Triforce/MarioKartGP.cpp new file mode 100644 index 0000000000..63203b4403 --- /dev/null +++ b/Source/Core/Core/HW/Triforce/MarioKartGP.cpp @@ -0,0 +1,138 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/Triforce/MarioKartGP.h" + +#include + +#include "Common/BitUtils.h" +#include "Common/ChunkFile.h" +#include "Common/Logging/Log.h" +#include "Common/Swap.h" + +#include "Core/HW/GCPad.h" + +#include "InputCommon/GCPadStatus.h" + +namespace Triforce +{ + +void MarioKartGPCommon_IOAdapter::Update() +{ + auto* const io_ports = GetIOPorts(); + + // TODO: Devices (e.g. Camera, Networking) can be disabled but it's not yet fully figured out. + constexpr u8 DISABLE_DEVICES = 0xff; + io_ports->GetStatusSwitches()[0] &= DISABLE_DEVICES; + + const GCPadStatus pad_status = Pad::GetStatus(0); + + const auto switch_inputs = io_ports->GetSwitchInputs(0); + // Start + if (pad_status.button & PAD_BUTTON_START) + switch_inputs[0] |= 0x80; + + // Item button + if (pad_status.button & PAD_BUTTON_A) + switch_inputs[1] |= 0x20; + // VS-Cancel button + if (pad_status.button & PAD_BUTTON_B) + switch_inputs[1] |= 0x02; + + const auto analog_inputs = io_ports->GetAnalogInputs(); + // Steering + analog_inputs[0] = Common::ExpandValue(pad_status.stickX, 8); + // Gas + analog_inputs[1] = Common::ExpandValue(pad_status.triggerRight, 8); + // Brake + analog_inputs[2] = Common::ExpandValue(pad_status.triggerLeft, 8); +} + +void MarioKartGPCommon_IOAdapter::HandleGenericOutputsChanged(std::span bits_set, + std::span bits_cleared) +{ + if (bits_set[0] & 0x80u) + m_steering_wheel->Reset(); + + const u8 bits_changed_0 = bits_set[0] | bits_cleared[0]; + + constexpr auto LED_NAMES = std::to_array>({ + {0x04, "ITEM BUTTON"}, + {0x08, "CANCEL BUTTON"}, + }); + + for (const auto& [led_value, led_name] : LED_NAMES) + { + if (bits_changed_0 & led_value) + { + INFO_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: {}: {}", led_name, + (bits_set[0] & led_value) ? "ON" : "OFF"); + } + } +} + +void MarioKartGPSteeringWheel::Update() +{ + constexpr std::size_t REQUEST_SIZE = 10; + + std::size_t rx_position = 0; + while (true) + { + const auto rx_span = GetRxByteSpan().subspan(rx_position); + + if (rx_span.size() < REQUEST_SIZE) + break; // Wait for more data. + + ProcessRequest(rx_span.first()); + rx_position += REQUEST_SIZE; + } + + ConsumeRxBytes(rx_position); +} + +void MarioKartGPSteeringWheel::ProcessRequest(std::span request) +{ + DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Request: {:02x}", fmt::join(request, " ")); + + const u8 cmd = request[3]; + if (cmd != 0x01) + { + WARN_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Unknown command: {:02x}", cmd); + return; + } + + const u16 centering_force = Common::swap16(request.data() + 4); + const u16 friction_force = Common::swap16(request.data() + 6); + const u16 roll = Common::swap16(request.data() + 8); + + DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: FFB: {:04x} {:04x} {:04x}", centering_force, + friction_force, roll); + + switch (m_init_state) + { + case 0: + WriteTxBytes(std::array{'E', '0', '0'}); // Error + ++m_init_state; + break; + case 1: + WriteTxBytes(std::array{'C', '0', '6'}); // Power Off + ++m_init_state; + break; + default: + WriteTxBytes(std::array{'C', '0', '1'}); // Power On + break; + } +} + +void MarioKartGPSteeringWheel::Reset() +{ + INFO_LOG_FMT(SERIALINTERFACE_AMBB, "SteeringWheel: Reset"); + m_init_state = 0; +} + +void MarioKartGPSteeringWheel::DoState(PointerWrap& p) +{ + p.Do(m_init_state); +} + +} // namespace Triforce diff --git a/Source/Core/Core/HW/Triforce/MarioKartGP.h b/Source/Core/Core/HW/Triforce/MarioKartGP.h new file mode 100644 index 0000000000..163bfbb61b --- /dev/null +++ b/Source/Core/Core/HW/Triforce/MarioKartGP.h @@ -0,0 +1,47 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Core/HW/Triforce/IOPorts.h" +#include "Core/HW/Triforce/SerialDevice.h" + +namespace Triforce +{ + +// FFB wheel used by MarioKartGP and MarioKartGP2. +class MarioKartGPSteeringWheel final : public SerialDevice +{ +public: + void Update() override; + + void Reset(); + + void DoState(PointerWrap&) override; + +private: + void ProcessRequest(std::span); + + u8 m_init_state = 0; +}; + +// Used for both MarioKartGP and MarioKartGP2. +class MarioKartGPCommon_IOAdapter final : public IOAdapter +{ +public: + explicit MarioKartGPCommon_IOAdapter(MarioKartGPSteeringWheel* steering_wheel) + : m_steering_wheel{steering_wheel} + { + } + + void Update() override; + +protected: + void HandleGenericOutputsChanged(std::span bits_set, + std::span bits_cleared) override; + +private: + MarioKartGPSteeringWheel* const m_steering_wheel; +}; + +} // namespace Triforce From 5ec42165b7c9740041447f1e632f2a125e5377d2 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Mon, 23 Mar 2026 00:51:36 -0500 Subject: [PATCH 3/4] 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 @@ + + + From 02911dbc427bb43d2592e542366b9b1d6002ae9f Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sat, 21 Mar 2026 10:05:09 -0500 Subject: [PATCH 4/4] State: Increase STATE_VERSION. --- Source/Core/Core/State.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp index fa6544d61b..3819a2c0fb 100644 --- a/Source/Core/Core/State.cpp +++ b/Source/Core/Core/State.cpp @@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs static Common::WorkQueueThreadSP s_compress_and_dump_thread; // Don't forget to increase this after doing changes on the savestate system -constexpr u32 STATE_VERSION = 182; // Last changed in PR 14482 +constexpr u32 STATE_VERSION = 183; // Last changed in PR 14501 // Increase this if the StateExtendedHeader definition changes constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217