Preliminary update to the GameCube to GBA link cable emulation. Fixes Zelda Wind Waker's Tingle Tuner connection, Pac-Man Vs, Final Fantasy: Crystal Chronicles multiplayer, and most other Gamecube to GBA link cable games.

* Changed the SI buffer processing so that transfers do not have to be completed instantly
* Added a second socket at port 49420 (0xc10c) which sends clock information to the GBA slaves
* Handled disconnections from the GBA and GC
* Made the transfers asynchronous
* Blocks the socket before the connection times out

Requires VBA-M SVN 1235 or later.
This commit is contained in:
skidau 2015-03-20 12:36:46 +11:00
parent 337f89959b
commit c3344eaa88
5 changed files with 353 additions and 77 deletions

View File

@ -23,8 +23,9 @@ namespace SerialInterface
{ {
static int changeDevice; static int changeDevice;
static int et_transfer_pending;
void RunSIBuffer(); void RunSIBuffer(u64 userdata, int cyclesLate);
void UpdateInterrupts(); void UpdateInterrupts();
// SI Interrupt Types // SI Interrupt Types
@ -274,6 +275,7 @@ void Init()
memset(g_SIBuffer, 0, 128); memset(g_SIBuffer, 0, 128);
changeDevice = CoreTiming::RegisterEvent("ChangeSIDevice", ChangeDeviceCallback); changeDevice = CoreTiming::RegisterEvent("ChangeSIDevice", ChangeDeviceCallback);
et_transfer_pending = CoreTiming::RegisterEvent("SITransferPending", RunSIBuffer);
} }
void Shutdown() void Shutdown()
@ -346,7 +348,17 @@ void RegisterMMIO(MMIO::Mapping* mmio, u32 base)
if (tmpComCSR.TCINT) g_ComCSR.TCINT = 0; if (tmpComCSR.TCINT) g_ComCSR.TCINT = 0;
// be careful: run si-buffer after updating the INT flags // be careful: run si-buffer after updating the INT flags
if (tmpComCSR.TSTART) RunSIBuffer(); if (tmpComCSR.TSTART)
{
g_ComCSR.TSTART = 1;
RunSIBuffer(0, 0);
}
else if (g_ComCSR.TSTART)
{
CoreTiming::RemoveEvent(et_transfer_pending);
}
if (!g_ComCSR.TSTART)
UpdateInterrupts(); UpdateInterrupts();
}) })
); );
@ -519,8 +531,10 @@ SIDevices GetDeviceType(int channel)
return g_Channel[channel].m_pDevice->GetDeviceType(); return g_Channel[channel].m_pDevice->GetDeviceType();
} }
void RunSIBuffer() void RunSIBuffer(u64 userdata, int cyclesLate)
{ {
if (g_ComCSR.TSTART)
{
// Math inLength // Math inLength
int inLength = g_ComCSR.INLNGTH; int inLength = g_ComCSR.INLNGTH;
if (inLength == 0) if (inLength == 0)
@ -535,13 +549,22 @@ void RunSIBuffer()
else else
outLength++; outLength++;
int numOutput = g_Channel[g_ComCSR.CHANNEL].m_pDevice->RunBuffer(g_SIBuffer, inLength); int numOutput = 0;
DEBUG_LOG(SERIALINTERFACE, "RunSIBuffer (intLen: %i outLen: %i) (processed: %i)", inLength, outLength, numOutput); numOutput = g_Channel[g_ComCSR.CHANNEL].m_pDevice->RunBuffer(g_SIBuffer, inLength);
// Transfer completed DEBUG_LOG(SERIALINTERFACE, "RunSIBuffer chan: %d inLen: %i outLen: %i processed: %i", g_ComCSR.CHANNEL, inLength, outLength, numOutput);
GenerateSIInterrupt(INT_TCINT);
if (numOutput != 0)
{
g_ComCSR.TSTART = 0; g_ComCSR.TSTART = 0;
GenerateSIInterrupt(INT_TCINT);
}
else
{
CoreTiming::ScheduleEvent(g_Channel[g_ComCSR.CHANNEL].m_pDevice->TransferInterval() - cyclesLate, et_transfer_pending);
}
}
} }
int GetTicksToNextSIPoll() int GetTicksToNextSIPoll()

View File

@ -39,6 +39,10 @@ int ISIDevice::RunBuffer(u8* _pBuffer, int _iLength)
return 0; return 0;
} }
int ISIDevice::TransferInterval()
{
return 0;
}
// Stub class for saying nothing is attached, and not having to deal with null pointers :) // Stub class for saying nothing is attached, and not having to deal with null pointers :)
class CSIDevice_Null : public ISIDevice class CSIDevice_Null : public ISIDevice

View File

@ -73,6 +73,7 @@ public:
// Run the SI Buffer // Run the SI Buffer
virtual int RunBuffer(u8* _pBuffer, int _iLength); virtual int RunBuffer(u8* _pBuffer, int _iLength);
virtual int TransferInterval();
// Return true on new data // Return true on new data
virtual bool GetData(u32& _Hi, u32& _Low) = 0; virtual bool GetData(u32& _Hi, u32& _Low) = 0;

View File

@ -5,36 +5,103 @@
#include <queue> #include <queue>
#include "Common/CommonFuncs.h" #include "Common/CommonFuncs.h"
#include "Common/Flag.h"
#include "Common/StdMakeUnique.h" #include "Common/StdMakeUnique.h"
#include "Common/Thread.h" #include "Common/Thread.h"
#include "Common/Logging/Log.h" #include "Common/Logging/Log.h"
#include "Core/ConfigManager.h"
#include "Core/CoreTiming.h"
#include "Core/HW/SI_Device.h" #include "Core/HW/SI_Device.h"
#include "Core/HW/SI_DeviceGBA.h" #include "Core/HW/SI_DeviceGBA.h"
#include "Core/HW/SystemTimers.h"
#include "Core/HW/VideoInterface.h"
#include "SFML/Network.hpp" #include "SFML/Network.hpp"
static std::thread connectionThread; static std::thread connectionThread;
static std::queue<std::unique_ptr<sf::TcpSocket>> waiting_socks; static std::queue<std::unique_ptr<sf::TcpSocket>> waiting_socks;
static std::queue<std::unique_ptr<sf::TcpSocket>> waiting_clocks;
static std::mutex cs_gba; static std::mutex cs_gba;
namespace { volatile bool server_running; } static std::mutex cs_gba_clk;
static u8 num_connected;
namespace { Common::Flag server_running; }
enum EJoybusCmds
{
CMD_RESET = 0xff,
CMD_STATUS = 0x00,
CMD_READ = 0x14,
CMD_WRITE = 0x15
};
const u64 BITS_PER_SECOND = 115200;
const u64 BYTES_PER_SECOND = BITS_PER_SECOND / 8;
u8 GetNumConnected()
{
int num_ports_connected = num_connected;
if (num_ports_connected == 0)
num_ports_connected = 1;
return num_ports_connected;
}
// --- GameBoy Advance "Link Cable" --- // --- GameBoy Advance "Link Cable" ---
int GetTransferTime(u8 cmd)
{
u64 bytes_transferred = 0;
switch (cmd)
{
case CMD_RESET:
case CMD_STATUS:
{
bytes_transferred = 4;
break;
}
case CMD_READ:
{
bytes_transferred = 6;
break;
}
case CMD_WRITE:
{
bytes_transferred = 1;
break;
}
default:
{
bytes_transferred = 1;
break;
}
}
return (int)(bytes_transferred * SystemTimers::GetTicksPerSecond() / (GetNumConnected() * BYTES_PER_SECOND));
}
static void GBAConnectionWaiter() static void GBAConnectionWaiter()
{ {
server_running = true; server_running.Set();
Common::SetCurrentThreadName("GBA Connection Waiter"); Common::SetCurrentThreadName("GBA Connection Waiter");
sf::TcpListener server; sf::TcpListener server;
sf::TcpListener clock_server;
// "dolphin gba" // "dolphin gba"
if (server.listen(0xd6ba) != sf::Socket::Done) if (server.listen(0xd6ba) != sf::Socket::Done)
return; return;
// "clock"
if (clock_server.listen(0xc10c) != sf::Socket::Done)
return;
server.setBlocking(false); server.setBlocking(false);
clock_server.setBlocking(false);
auto new_client = std::make_unique<sf::TcpSocket>(); auto new_client = std::make_unique<sf::TcpSocket>();
while (server_running) while (server_running.IsSet())
{ {
if (server.accept(*new_client) == sf::Socket::Done) if (server.accept(*new_client) == sf::Socket::Done)
{ {
@ -43,13 +110,21 @@ static void GBAConnectionWaiter()
new_client = std::make_unique<sf::TcpSocket>(); new_client = std::make_unique<sf::TcpSocket>();
} }
SLEEP(1); if (clock_server.accept(*new_client) == sf::Socket::Done)
{
std::lock_guard<std::mutex> lk(cs_gba_clk);
waiting_clocks.push(std::move(new_client));
new_client = std::make_unique<sf::TcpSocket>();
}
Common::SleepCurrentThread(1);
} }
} }
void GBAConnectionWaiter_Shutdown() void GBAConnectionWaiter_Shutdown()
{ {
server_running = false; server_running.Clear();
if (connectionThread.joinable()) if (connectionThread.joinable())
connectionThread.join(); connectionThread.join();
} }
@ -70,69 +145,229 @@ static bool GetAvailableSock(std::unique_ptr<sf::TcpSocket>& sock_to_fill)
return sock_filled; return sock_filled;
} }
GBASockServer::GBASockServer() static bool GetNextClock(std::unique_ptr<sf::TcpSocket>& sock_to_fill)
{
bool sock_filled = false;
std::lock_guard<std::mutex> lk(cs_gba_clk);
if (!waiting_clocks.empty())
{
sock_to_fill = std::move(waiting_clocks.front());
waiting_clocks.pop();
sock_filled = true;
}
return sock_filled;
}
GBASockServer::GBASockServer(int _iDeviceNumber)
{ {
if (!connectionThread.joinable()) if (!connectionThread.joinable())
connectionThread = std::thread(GBAConnectionWaiter); connectionThread = std::thread(GBAConnectionWaiter);
cmd = 0;
num_connected = 0;
last_time_slice = 0;
booted = false;
device_number = _iDeviceNumber;
} }
GBASockServer::~GBASockServer() GBASockServer::~GBASockServer()
{ {
Disconnect();
} }
// Blocking, since GBA must always send lower byte of REG_JOYSTAT void GBASockServer::Disconnect()
void GBASockServer::Transfer(char* si_buffer)
{ {
if (!client || client->getLocalPort() == 0) if (client)
{
num_connected--;
client->disconnect();
client = nullptr;
}
if (clock_sync)
{
clock_sync->disconnect();
clock_sync = nullptr;
}
last_time_slice = 0;
booted = false;
}
void GBASockServer::ClockSync()
{
if (!clock_sync)
if (!GetNextClock(clock_sync))
return;
u32 time_slice = 0;
if (last_time_slice == 0)
{
num_connected++;
last_time_slice = CoreTiming::GetTicks();
time_slice = (u32)(SystemTimers::GetTicksPerSecond() / 60);
}
else
{
time_slice = (u32)(CoreTiming::GetTicks() - last_time_slice);
}
time_slice = (u32)((u64)time_slice * 16777216 / SystemTimers::GetTicksPerSecond());
last_time_slice = CoreTiming::GetTicks();
char bytes[4] = { 0, 0, 0, 0 };
bytes[0] = (time_slice >> 24) & 0xff;
bytes[1] = (time_slice >> 16) & 0xff;
bytes[2] = (time_slice >> 8) & 0xff;
bytes[3] = time_slice & 0xff;
sf::Socket::Status status = clock_sync->send(bytes, 4);
if (status == sf::Socket::Disconnected)
{
clock_sync->disconnect();
clock_sync = nullptr;
}
}
void GBASockServer::Send(u8* si_buffer)
{
if (!client)
if (!GetAvailableSock(client)) if (!GetAvailableSock(client))
return; return;
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
current_data[i] = si_buffer[i ^ 3]; send_data[i] = si_buffer[i ^ 3];
u8 cmd = *current_data; cmd = (u8)send_data[0];
if (cmd == CMD_WRITE)
client->send(current_data, sizeof(current_data));
else
client->send(current_data, 1);
DEBUG_LOG(SERIALINTERFACE, "> command %02x %02x%02x%02x%02x",
(u8)current_data[0], (u8)current_data[1], (u8)current_data[2],
(u8)current_data[3], (u8)current_data[4]);
memset(current_data, 0, sizeof(current_data));
size_t num_received = 0;
if (client->receive(current_data, sizeof(current_data), num_received) == sf::Socket::Disconnected)
client->disconnect();
DEBUG_LOG(SERIALINTERFACE, "< %02x%02x%02x%02x%02x",
(u8)current_data[0], (u8)current_data[1], (u8)current_data[2],
(u8)current_data[3], (u8)current_data[4]);
#ifdef _DEBUG #ifdef _DEBUG
size_t num_expecting = 3; NOTICE_LOG(SERIALINTERFACE, "%01d cmd %02x [> %02x%02x%02x%02x]",
if (cmd == CMD_READ) device_number,
num_expecting = 5; (u8)send_data[0], (u8)send_data[1], (u8)send_data[2],
else if (cmd == CMD_WRITE) (u8)send_data[3], (u8)send_data[4]);
num_expecting = 1; #endif
if (num_received != num_expecting)
ERROR_LOG(SERIALINTERFACE, "%x:%x:%x", (u8)cmd, client->setBlocking(false);
(unsigned int)num_received, (unsigned int)num_expecting); sf::Socket::Status status;
if (cmd == CMD_WRITE)
status = client->send(send_data, sizeof(send_data));
else
status = client->send(send_data, 1);
if (cmd != CMD_STATUS)
booted = true;
if (status == sf::Socket::Disconnected)
Disconnect();
time_cmd_sent = CoreTiming::GetTicks();
}
int GBASockServer::Receive(u8* si_buffer)
{
if (!client)
if (!GetAvailableSock(client))
return 5;
size_t num_received = 0;
u64 transferTime = GetTransferTime((u8)send_data[0]);
bool block = (CoreTiming::GetTicks() - time_cmd_sent) > transferTime;
if (cmd == CMD_STATUS && !booted)
block = false;
if (block)
{
sf::SocketSelector Selector;
Selector.add(*client);
Selector.wait(sf::milliseconds(1000));
}
sf::Socket::Status recv_stat = client->receive(recv_data, sizeof(recv_data), num_received);
if (recv_stat == sf::Socket::Disconnected)
{
Disconnect();
return 5;
}
if (recv_stat == sf::Socket::NotReady)
num_received = 0;
if (num_received > sizeof(recv_data))
num_received = sizeof(recv_data);
if (num_received > 0)
{
#ifdef _DEBUG
if ((u8)send_data[0] == 0x00 || (u8)send_data[0] == 0xff)
{
WARN_LOG(SERIALINTERFACE, "%01d [< %02x%02x%02x%02x%02x] (%d)",
device_number,
(u8)recv_data[0], (u8)recv_data[1], (u8)recv_data[2],
(u8)recv_data[3], (u8)recv_data[4],
num_received);
}
else
{
ERROR_LOG(SERIALINTERFACE, "%01d [< %02x%02x%02x%02x%02x] (%d)",
device_number,
(u8)recv_data[0], (u8)recv_data[1], (u8)recv_data[2],
(u8)recv_data[3], (u8)recv_data[4],
num_received);
}
#endif #endif
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
si_buffer[i ^ 3] = current_data[i]; si_buffer[i ^ 3] = recv_data[i];
}
return (int)num_received;
} }
CSIDevice_GBA::CSIDevice_GBA(SIDevices _device, int _iDeviceNumber) CSIDevice_GBA::CSIDevice_GBA(SIDevices _device, int _iDeviceNumber)
: ISIDevice(_device, _iDeviceNumber) : ISIDevice(_device, _iDeviceNumber)
, GBASockServer() , GBASockServer(_iDeviceNumber)
{ {
waiting_for_response = false;
}
CSIDevice_GBA::~CSIDevice_GBA()
{
GBASockServer::Disconnect();
} }
int CSIDevice_GBA::RunBuffer(u8* _pBuffer, int _iLength) int CSIDevice_GBA::RunBuffer(u8* _pBuffer, int _iLength)
{ {
Transfer((char*)_pBuffer); if (!waiting_for_response)
return _iLength; {
for (int i = 0; i < 5; i++)
send_data[i] = _pBuffer[i ^ 3];
num_data_received = 0;
ClockSync();
Send(_pBuffer);
timestamp_sent = CoreTiming::GetTicks();
waiting_for_response = true;
}
if (waiting_for_response && num_data_received == 0)
{
num_data_received = Receive(_pBuffer);
}
if ((GetTransferTime(send_data[0])) > (int)(CoreTiming::GetTicks() - timestamp_sent))
{
return 0;
}
else
{
if (num_data_received != 0)
waiting_for_response = false;
return num_data_received;
}
}
int CSIDevice_GBA::TransferInterval()
{
return GetTransferTime(send_data[0]);
} }

View File

@ -12,38 +12,51 @@
// GameBoy Advance "Link Cable" // GameBoy Advance "Link Cable"
u8 GetNumConnected();
int GetTransferTime(u8 cmd);
void GBAConnectionWaiter_Shutdown(); void GBAConnectionWaiter_Shutdown();
class GBASockServer class GBASockServer
{ {
public: public:
GBASockServer(); GBASockServer(int _iDeviceNumber);
~GBASockServer(); ~GBASockServer();
void Transfer(char* si_buffer); void Disconnect();
void ClockSync();
void Send(u8* si_buffer);
int Receive(u8* si_buffer);
private: private:
enum EJoybusCmds
{
CMD_RESET = 0xff,
CMD_STATUS = 0x00,
CMD_READ = 0x14,
CMD_WRITE = 0x15
};
std::unique_ptr<sf::TcpSocket> client; std::unique_ptr<sf::TcpSocket> client;
char current_data[5]; std::unique_ptr<sf::TcpSocket> clock_sync;
char send_data[5];
char recv_data[5];
u64 time_cmd_sent;
u64 last_time_slice;
u8 device_number;
u8 cmd;
bool booted;
}; };
class CSIDevice_GBA : public ISIDevice, private GBASockServer class CSIDevice_GBA : public ISIDevice, private GBASockServer
{ {
public: public:
CSIDevice_GBA(SIDevices device, int _iDeviceNumber); CSIDevice_GBA(SIDevices device, int _iDeviceNumber);
~CSIDevice_GBA() {} ~CSIDevice_GBA();
// Run the SI Buffer
virtual int RunBuffer(u8* _pBuffer, int _iLength) override; virtual int RunBuffer(u8* _pBuffer, int _iLength) override;
virtual int TransferInterval() override;
virtual bool GetData(u32& _Hi, u32& _Low) override { return true; } virtual bool GetData(u32& _Hi, u32& _Low) override { return false; }
virtual void SendCommand(u32 _Cmd, u8 _Poll) override {} virtual void SendCommand(u32 _Cmd, u8 _Poll) override {}
private:
u8 send_data[5];
int num_data_received;
u64 timestamp_sent;
bool waiting_for_response;
}; };