Merge pull request #14452 from jordan-woyak/triforce-avalon-touchscreen

Triforce: Implement The Key of Avalon's touchscreen.
This commit is contained in:
JMC47 2026-03-12 21:49:08 -04:00 committed by GitHub
commit f0096310bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 308 additions and 91 deletions

View File

@ -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

View File

@ -597,11 +597,15 @@ void MagneticCardReader::SetSError(S error_code)
FinishCommand();
}
void MagneticCardReader::Process(std::vector<u8>* read, std::vector<u8>* 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<u8>* read, std::vector<u8>* 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<u8>* read, std::vector<u8>* 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<u8>& 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<u8>({
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);

View File

@ -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<u8>* read, std::vector<u8>* write);
virtual void DoState(PointerWrap& p);
void DoState(PointerWrap& p) override;
protected:
// Status bytes:
@ -114,7 +112,7 @@ protected:
};
bool ReceivePacket(std::span<const u8> packet);
void BuildPacket(std::vector<u8>& write_buffer);
void BuildPacket();
void StepStateMachine(DT elapsed_time);
void StepStatePerson(DT elapsed_time);

View File

@ -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<MagCard::C1231BR>(&m_mag_card_settings);
m_serial_device_b = std::make_unique<MagCard::C1231BR>(&m_mag_card_settings);
break;
case MarioKartGP:
case MarioKartGP2:
m_mag_card_reader = std::make_unique<MagCard::C1231LR>(&m_mag_card_settings);
m_serial_device_b = std::make_unique<MagCard::C1231LR>(&m_mag_card_settings);
break;
case KeyOfAvalon:
m_serial_device_b = std::make_unique<Triforce::Touchscreen>();
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);

View File

@ -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<std::array<u8, RESPONSE_SIZE>, 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<u8> m_mag_card_in_buffer;
std::vector<u8> m_mag_card_out_buffer;
// Serial B
std::unique_ptr<Triforce::SerialDevice> m_serial_device_b;
std::unique_ptr<MagCard::MagneticCardReader> m_mag_card_reader;
// Serial
u32 m_wheel_init = 0;
u32 m_motor_init = 0;

View File

@ -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<const u8> 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<u8> 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<const u8> 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

View File

@ -0,0 +1,60 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <deque>
#include <span>
#include <vector>
#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<const u8> bytes);
std::size_t GetTxByteCount() const { return m_tx_buffer.size(); }
// Caller should ensure GetTxByteCount() >= byte.size().
void TakeTxBytes(std::span<u8> bytes);
virtual void Update() = 0;
virtual void DoState(PointerWrap& p);
protected:
std::span<const u8> 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<const u8> 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<u8> 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<u8> m_tx_buffer;
};
} // namespace Triforce

View File

@ -0,0 +1,71 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/Triforce/Touchscreen.h"
#include <numeric>
#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

View File

@ -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

View File

@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> 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

View File

@ -341,6 +341,8 @@
<ClInclude Include="Core\HW\Sram.h" />
<ClInclude Include="Core\HW\StreamADPCM.h" />
<ClInclude Include="Core\HW\SystemTimers.h" />
<ClInclude Include="Core\HW\Triforce\SerialDevice.h" />
<ClInclude Include="Core\HW\Triforce\Touchscreen.h" />
<ClInclude Include="Core\HW\VideoInterface.h" />
<ClInclude Include="Core\HW\WII_IPC.h" />
<ClInclude Include="Core\HW\Wiimote.h" />
@ -1041,6 +1043,8 @@
<ClCompile Include="Core\HW\Sram.cpp" />
<ClCompile Include="Core\HW\StreamADPCM.cpp" />
<ClCompile Include="Core\HW\SystemTimers.cpp" />
<ClCompile Include="Core\HW\Triforce\SerialDevice.cpp" />
<ClCompile Include="Core\HW\Triforce\Touchscreen.cpp" />
<ClCompile Include="Core\HW\VideoInterface.cpp" />
<ClCompile Include="Core\HW\WII_IPC.cpp" />
<ClCompile Include="Core\HW\Wiimote.cpp" />