From 3e8430440aa4059ed21882cd5e6c48547a5d484e Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Sun, 8 Mar 2026 20:40:30 -0500 Subject: [PATCH 1/2] Triforce: Implement The Key of Avalon's touchscreen. Added SerialDevice base class interface. Adapted MagneticCardReader to use the SerialDevice interface. Implemented The Key of Avalon touchscreen SerialDevice. Altered CSIDevice_AMBaseboard to use SerialDevice. Made serial reads happen every GCAMCommand rather than only upon write. --- Source/Core/Core/CMakeLists.txt | 4 + .../Core/HW/MagCard/MagneticCardReader.cpp | 76 +++++++++---------- .../Core/Core/HW/MagCard/MagneticCardReader.h | 16 ++-- .../Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp | 76 ++++++++++--------- Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h | 17 +++-- Source/Core/Core/HW/Triforce/SerialDevice.cpp | 55 ++++++++++++++ Source/Core/Core/HW/Triforce/SerialDevice.h | 60 +++++++++++++++ Source/Core/Core/HW/Triforce/Touchscreen.cpp | 71 +++++++++++++++++ Source/Core/Core/HW/Triforce/Touchscreen.h | 18 +++++ Source/Core/DolphinLib.props | 4 + 10 files changed, 307 insertions(+), 90 deletions(-) create mode 100644 Source/Core/Core/HW/Triforce/SerialDevice.cpp create mode 100644 Source/Core/Core/HW/Triforce/SerialDevice.h create mode 100644 Source/Core/Core/HW/Triforce/Touchscreen.cpp create mode 100644 Source/Core/Core/HW/Triforce/Touchscreen.h diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index b81558e274..821080afca 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -294,6 +294,10 @@ add_library(core HW/StreamADPCM.h HW/SystemTimers.cpp HW/SystemTimers.h + HW/Triforce/SerialDevice.cpp + HW/Triforce/SerialDevice.h + HW/Triforce/Touchscreen.cpp + HW/Triforce/Touchscreen.h HW/VideoInterface.cpp HW/VideoInterface.h HW/WII_IPC.cpp diff --git a/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp b/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp index 107ccb391e..89ae1534d5 100644 --- a/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp +++ b/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp @@ -597,11 +597,15 @@ void MagneticCardReader::SetSError(S error_code) FinishCommand(); } -void MagneticCardReader::Process(std::vector* read, std::vector* write) +void MagneticCardReader::Update() { - while (!read->empty()) + while (true) { - const u8 first_byte = read->front(); + const auto read = GetRxByteSpan(); + if (read.empty()) + break; + + const u8 first_byte = read.front(); if (first_byte == ENQUIRY) { // ENQUIRY @@ -617,16 +621,16 @@ void MagneticCardReader::Process(std::vector* read, std::vector* write) StepStateMachine(elapsed_time); StepStatePerson(elapsed_time); - BuildPacket(*write); + BuildPacket(); - read->erase(read->begin()); // TODO: SLOW ! + ConsumeRxBytes(1); continue; } if (first_byte != START_OF_TEXT) { ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Process: Unexpected {:02x}", first_byte); - read->erase(read->begin()); // TODO: SLOW ! + ConsumeRxBytes(1); continue; } @@ -634,25 +638,25 @@ void MagneticCardReader::Process(std::vector* read, std::vector* write) // This is a command packet. The next byte provides the size. // Upon read they ACK or NACK and start processing of a command. - if (read->size() < 2) + if (read.size() < 2) break; // Wait for more data. - const std::size_t packet_size = (*read)[1]; - if (packet_size > read->size() - 2) + const std::size_t packet_size = read[1]; + if (packet_size > read.size() - 2) break; // Wait for more data. - if (ReceivePacket(std::span{*read}.subspan(2, packet_size))) + if (ReceivePacket(read.subspan(2, packet_size))) { DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "Writing ACK"); - write->emplace_back(ACK); + WriteTxByte(ACK); } else { WARN_LOG_FMT(SERIALINTERFACE_CARD, "Writing NACK"); - write->emplace_back(NACK); + WriteTxByte(NACK); } - read->erase(read->begin(), read->begin() + packet_size + 2); // TODO: SLOW ! + ConsumeRxBytes(packet_size + 2); } } @@ -858,41 +862,31 @@ void MagneticCardReader::StepStateMachine(DT elapsed_time) ++m_current_step; } -void MagneticCardReader::BuildPacket(std::vector& write_buffer) +void MagneticCardReader::BuildPacket() { // Header and footer add 6 bytes. const u8 payload_size = u8(m_command_payload.size() + 6); - // + START_OF_TEXT + the count byte - const auto total_write_size = payload_size + 2; - const auto prev_buffer_size = write_buffer.size(); - write_buffer.resize(prev_buffer_size + total_write_size); + WriteTxByte(START_OF_TEXT); // Not included in the checksum. - auto* out_ptr = write_buffer.data() + prev_buffer_size; - u8 packet_checksum = 0; + const auto lead_in = std::to_array({ + payload_size, + GetCurrentCommand(), + GetPositionValue(), + u8(m_status.p), + u8(m_status.s), + }); + WriteTxBytes(lead_in); - const auto write_and_checksum = [&](u8 value) { - *(out_ptr++) = value; - packet_checksum ^= value; - }; + WriteTxBytes(m_command_payload); - // Write the header. - *(out_ptr++) = START_OF_TEXT; - write_and_checksum(payload_size); - write_and_checksum(GetCurrentCommand()); - write_and_checksum(GetPositionValue()); - write_and_checksum(u8(m_status.p)); - write_and_checksum(u8(m_status.s)); + WriteTxByte(END_OF_TEXT); - // Write the payload. - std::ranges::for_each(m_command_payload, write_and_checksum); - - // Write the footer. - write_and_checksum(END_OF_TEXT); - *(out_ptr++) = packet_checksum; - - DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "BuildPacket: {}", - HexDump(std::span{write_buffer}.subspan(write_buffer.size() - payload_size - 2))); + // Checksum is XOR of bytes after START_OF_TEXT. + const u8 packet_checksum = std::accumulate(lead_in.begin(), lead_in.end(), u8{}, std::bit_xor{}) ^ + std::accumulate(m_command_payload.begin(), m_command_payload.end(), + END_OF_TEXT, std::bit_xor{}); + WriteTxByte(packet_checksum); } bool MagneticCardReader::IsRunningCommand() const @@ -983,6 +977,8 @@ bool MagneticCardReader::IsReadyForCard() void MagneticCardReader::DoState(PointerWrap& p) { + SerialDevice::DoState(p); + // Outgoing packet. p.Do(m_status); p.Do(m_current_command); diff --git a/Source/Core/Core/HW/MagCard/MagneticCardReader.h b/Source/Core/Core/HW/MagCard/MagneticCardReader.h index b60514a6ab..dd301d4590 100644 --- a/Source/Core/Core/HW/MagCard/MagneticCardReader.h +++ b/Source/Core/Core/HW/MagCard/MagneticCardReader.h @@ -16,10 +16,12 @@ #include "Common/ChunkFile.h" #include "Common/CommonTypes.h" +#include "Core/HW/Triforce/SerialDevice.h" + namespace MagCard { -class MagneticCardReader +class MagneticCardReader : public Triforce::SerialDevice { public: static constexpr std::size_t TRACK_SIZE = 69; // A nice amount of data. @@ -36,20 +38,16 @@ public: }; explicit MagneticCardReader(Settings* settings); - virtual ~MagneticCardReader(); + ~MagneticCardReader() override; MagneticCardReader(const MagneticCardReader&) = delete; MagneticCardReader& operator=(const MagneticCardReader&) = delete; MagneticCardReader(MagneticCardReader&&) = delete; MagneticCardReader& operator=(MagneticCardReader&&) = delete; - // TODO: This std:vector buffer interface is a bit funky.. + void Update() override; - // read = MagCard <-- Baseboard. - // write = Magcard --> Baseboard. - void Process(std::vector* read, std::vector* write); - - virtual void DoState(PointerWrap& p); + void DoState(PointerWrap& p) override; protected: // Status bytes: @@ -114,7 +112,7 @@ protected: }; bool ReceivePacket(std::span packet); - void BuildPacket(std::vector& write_buffer); + void BuildPacket(); void StepStateMachine(DT elapsed_time); void StepStatePerson(DT elapsed_time); diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp index 9209b36461..4651454321 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp @@ -27,6 +27,7 @@ #include "Core/HW/SI/SI.h" #include "Core/HW/SI/SI_Device.h" #include "Core/HW/SystemTimers.h" +#include "Core/HW/Triforce/Touchscreen.h" #include "Core/Movie.h" #include "Core/System.h" @@ -163,12 +164,16 @@ CSIDevice_AMBaseboard::CSIDevice_AMBaseboard(Core::System& system, SIDevices dev switch (AMMediaboard::GetGameType()) { case FZeroAX: - m_mag_card_reader = std::make_unique(&m_mag_card_settings); + m_serial_device_b = std::make_unique(&m_mag_card_settings); break; case MarioKartGP: case MarioKartGP2: - m_mag_card_reader = std::make_unique(&m_mag_card_settings); + m_serial_device_b = std::make_unique(&m_mag_card_settings); + break; + + case KeyOfAvalon: + m_serial_device_b = std::make_unique(); break; default: @@ -1121,43 +1126,20 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) } case GCAMCommand::SerialB: { - DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 32 (CARD-Interface)"); - if (!validate_data_in_out(1, 0, "SerialB")) break; const u32 in_length = *data_in++; - static constexpr u32 max_packet_size = 0x2f; - - // Also accounting for the 2-byte header. - if (!validate_data_in_out(in_length, max_packet_size + 2, "SerialB")) + if (!validate_data_in_out(in_length, 0, "SerialB")) break; - if (m_mag_card_reader) + if (m_serial_device_b != nullptr) { - // Append the data to our buffer. - const auto prev_size = m_mag_card_in_buffer.size(); - m_mag_card_in_buffer.resize(prev_size + in_length); - std::ranges::copy(std::span{data_in, in_length}, m_mag_card_in_buffer.data() + prev_size); - - // Send and receive data with the magnetic card reader. - m_mag_card_reader->Process(&m_mag_card_in_buffer, &m_mag_card_out_buffer); + m_serial_device_b->WriteRxBytes({data_in, in_length}); } data_in += in_length; - const auto out_length = std::min(u32(m_mag_card_out_buffer.size()), max_packet_size); - // Write the 2-byte header. - data_out[data_offset++] = gcam_command; - data_out[data_offset++] = u8(out_length); - - // Write the data. - std::copy_n(m_mag_card_out_buffer.data(), out_length, data_out.data() + data_offset); - data_offset += out_length; - - // Remove the data from our buffer. - m_mag_card_out_buffer.erase(m_mag_card_out_buffer.begin(), - m_mag_card_out_buffer.begin() + s32(out_length)); break; } case GCAMCommand::JVSIOA: @@ -2004,6 +1986,33 @@ int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length) } } + // Update attached serial devices and read data into our response buffer. + // This is done regardless of the game having just sent a SerialA/B write. + if (m_serial_device_b != nullptr) + { + m_serial_device_b->Update(); + + const auto out_length = + std::min(u32(m_serial_device_b->GetTxByteCount()), SERIAL_PORT_MAX_READ_SIZE); + + if (out_length != 0) + { + // Also accounting for the 2-byte header. + if (!validate_data_in_out(0, out_length + 2, "SerialB")) + break; + + // Write the 2-byte header. + data_out[data_offset++] = GCAMCommand::SerialB; + data_out[data_offset++] = u8(out_length); + + const auto out_span = std::span{data_out}.subspan(data_offset, out_length); + + m_serial_device_b->TakeTxBytes(out_span); + + data_offset += out_length; + } + } + data_out[0] = 0x01; // Status code ? data_out[1] = data_offset - response_header_size; // Length @@ -2071,14 +2080,9 @@ void CSIDevice_AMBaseboard::DoState(PointerWrap& p) p.Do(m_ic_write_offset); p.Do(m_ic_write_size); - // Magnetic Card Reader - if (m_mag_card_reader) - { - m_mag_card_reader->DoState(p); - - p.Do(m_mag_card_in_buffer); - p.Do(m_mag_card_out_buffer); - } + // Serial B + if (m_serial_device_b != nullptr) + m_serial_device_b->DoState(p); // Serial p.Do(m_wheel_init); diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h index 9e130160ce..85a321b424 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h +++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h @@ -7,6 +7,11 @@ #include "Core/HW/SI/SI.h" #include "Core/HW/SI/SI_Device.h" +namespace Triforce +{ +class SerialDevice; +} + namespace SerialInterface { @@ -175,6 +180,11 @@ private: 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; @@ -196,12 +206,9 @@ private: // Magnetic Card Reader MagCard::MagneticCardReader::Settings m_mag_card_settings; - std::vector m_mag_card_in_buffer; - std::vector m_mag_card_out_buffer; + // Serial B + std::unique_ptr m_serial_device_b; - std::unique_ptr m_mag_card_reader; - - // Serial u32 m_wheel_init = 0; u32 m_motor_init = 0; diff --git a/Source/Core/Core/HW/Triforce/SerialDevice.cpp b/Source/Core/Core/HW/Triforce/SerialDevice.cpp new file mode 100644 index 0000000000..678b4e34c6 --- /dev/null +++ b/Source/Core/Core/HW/Triforce/SerialDevice.cpp @@ -0,0 +1,55 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/Triforce/SerialDevice.h" + +#include "Common/Assert.h" +#include "Common/ChunkFile.h" + +namespace Triforce +{ + +void SerialDevice::WriteRxBytes(std::span bytes) +{ +#if defined(__cpp_lib_containers_ranges) + m_rx_buffer.append_range(bytes); +#else + const auto prev_size = m_rx_buffer.size(); + m_rx_buffer.resize(prev_size + bytes.size()); + std::ranges::copy(bytes, m_rx_buffer.begin() + prev_size); +#endif +} + +void SerialDevice::TakeTxBytes(std::span bytes) +{ + DEBUG_ASSERT(m_tx_buffer.size() >= bytes.size()); + + std::copy_n(m_tx_buffer.begin(), bytes.size(), bytes.data()); + m_tx_buffer.erase(m_tx_buffer.begin(), m_tx_buffer.begin() + bytes.size()); +} + +void SerialDevice::ConsumeRxBytes(std::size_t count) +{ + DEBUG_ASSERT(m_rx_buffer.size() >= count); + + m_rx_buffer.erase(m_rx_buffer.begin(), m_rx_buffer.begin() + count); +} + +void SerialDevice::WriteTxBytes(std::span bytes) +{ +#if defined(__cpp_lib_containers_ranges) + m_tx_buffer.append_range(bytes); +#else + const auto prev_size = m_tx_buffer.size(); + m_tx_buffer.resize(prev_size + bytes.size()); + std::ranges::copy(bytes, m_tx_buffer.begin() + prev_size); +#endif +} + +void SerialDevice::DoState(PointerWrap& p) +{ + p.Do(m_rx_buffer); + p.Do(m_tx_buffer); +} + +} // namespace Triforce diff --git a/Source/Core/Core/HW/Triforce/SerialDevice.h b/Source/Core/Core/HW/Triforce/SerialDevice.h new file mode 100644 index 0000000000..e3aba79635 --- /dev/null +++ b/Source/Core/Core/HW/Triforce/SerialDevice.h @@ -0,0 +1,60 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include "Common/CommonTypes.h" + +class PointerWrap; + +namespace Triforce +{ + +// Base class for devices that attach to the SerialA/B ports on the Triforce Baseboard. +class SerialDevice +{ +public: + SerialDevice() = default; + virtual ~SerialDevice() = default; + + SerialDevice(const SerialDevice&) = delete; + SerialDevice& operator=(const SerialDevice&) = delete; + SerialDevice(SerialDevice&&) = delete; + SerialDevice& operator=(SerialDevice&&) = delete; + + void WriteRxBytes(std::span bytes); + + std::size_t GetTxByteCount() const { return m_tx_buffer.size(); } + + // Caller should ensure GetTxByteCount() >= byte.size(). + void TakeTxBytes(std::span bytes); + + virtual void Update() = 0; + + virtual void DoState(PointerWrap& p); + +protected: + std::span GetRxByteSpan() const { return m_rx_buffer; } + + void ConsumeRxBytes(std::size_t count); + + void WriteTxByte(u8 byte) { m_tx_buffer.emplace_back(byte); } + void WriteTxBytes(std::span bytes); + +private: + // The stream of bytes from the baseboard to the device. + // FYI: Current device implementations tend to empty the entire buffer in one go, + // so std::vector's O(n) erase-at-front should be a non-issue. + // The contiguous data of std::vector is convenient for packet parsing. + std::vector m_rx_buffer; + + // The stream of bytes from the device to the baseboard. + // It may be read in chunks so std::vector would be less appropriate here. + std::deque m_tx_buffer; +}; + +} // namespace Triforce diff --git a/Source/Core/Core/HW/Triforce/Touchscreen.cpp b/Source/Core/Core/HW/Triforce/Touchscreen.cpp new file mode 100644 index 0000000000..eab93b741e --- /dev/null +++ b/Source/Core/Core/HW/Triforce/Touchscreen.cpp @@ -0,0 +1,71 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/Triforce/Touchscreen.h" + +#include + +#include "Common/BitUtils.h" +#include "Common/Logging/Log.h" +#include "Core/HW/GCPad.h" +#include "InputCommon/GCPadStatus.h" + +namespace +{ +#pragma pack(push, 1) +// This is the "SmartSet Data Protocol". +struct SmartSetDataPacket +{ + u8 lead_in = 0x55; + u8 cmd = 0x54; // Always 0x54. + u8 status = 0xff; // Seems to be ignored by the game. + u16 x{}; // Little endian (0-4095). + u16 y{}; // Little endian (0-4095). + u8 pressure{}; + u8 unused{}; + u8 checksum{}; // All previous bytes + 0xaa. +}; +#pragma pack(pop) +static_assert(sizeof(SmartSetDataPacket) == 10); +} // namespace + +namespace Triforce +{ + +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()); + ConsumeRxBytes(input.size()); + } + + // Our touchscreen conveniently produces exactly one packet every Update cycle. + // I'm guessing the real hardware doesn't produce ~60hz input perfectly in-sync with SI updates, + // but Avalon doesn't seem to mind. + + // We currently feed the touch screen from c-stick and right-trigger just to make it usable. + // TODO: Expose it in a better way. + + const auto pad_status = Pad::GetStatus(0); + + // For reference, the game does something like this to scale the values from 0-4095. + // Note the offsets of 4. + // Someone who cares more might want to compensate for that. + // + // x = s32(0.15625f * x) + 4; + // y = s32(480.f - (0.1171875f * y)) + 4; + + SmartSetDataPacket packet{ + .x = Common::ExpandValue(u16(pad_status.substickX), 4), + .y = Common::ExpandValue(u16(pad_status.substickY), 4), + .pressure = pad_status.triggerRight, + }; + + packet.checksum = std::accumulate(&packet.lead_in, &packet.checksum, u8{0xaa}); + + WriteTxBytes(Common::AsU8Span(packet)); +} + +} // namespace Triforce diff --git a/Source/Core/Core/HW/Triforce/Touchscreen.h b/Source/Core/Core/HW/Triforce/Touchscreen.h new file mode 100644 index 0000000000..1be3382572 --- /dev/null +++ b/Source/Core/Core/HW/Triforce/Touchscreen.h @@ -0,0 +1,18 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Core/HW/Triforce/SerialDevice.h" + +namespace Triforce +{ + +// The touchscreen input used by The Key of Avalon games. +class Touchscreen final : public SerialDevice +{ +public: + void Update() override; +}; + +} // namespace Triforce diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index c6c9c485dd..7d63c5a4b9 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -341,6 +341,8 @@ + + @@ -1041,6 +1043,8 @@ + + From cca880de1662db2436568d3bd8b2887c50f8dc60 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Fri, 20 Feb 2026 15:52:56 -0600 Subject: [PATCH 2/2] 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 c41dc9e46b..5b284d1060 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 = 179; // Last changed in PR 14406 +constexpr u32 STATE_VERSION = 180; // Last changed in PR 14452 // Increase this if the StateExtendedHeader definition changes constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217