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.

* Added a second socket at port 49420 (0xc10c) which sends clock information
* Handled disconnections from the GBA and GC
* Made the transfers asynchronous
* Blocks the socket before the connection times out

Requires Dolphin 4.0-5899 or later.

git-svn-id: https://svn.code.sf.net/p/vbam/code/trunk@1235 a31d4220-a93d-0410-bf67-fe4944624d44
This commit is contained in:
skidau 2015-03-20 02:11:12 +00:00
parent 1ac06a6e66
commit 8b0b049230
13 changed files with 174 additions and 31 deletions

View File

@ -3016,10 +3016,11 @@ void CPUUpdateRegister(u32 address, u16 value)
break; break;
case COMM_JOY_TRANS_H: case COMM_JOY_TRANS_H:
UPDATE_REG(COMM_JOY_TRANS_H, value); UPDATE_REG(COMM_JOY_TRANS_H, value);
UPDATE_REG(COMM_JOYSTAT, READ16LE(&ioMem[COMM_JOYSTAT]) | JOYSTAT_SEND);
break; break;
case COMM_JOYSTAT: case COMM_JOYSTAT:
UPDATE_REG(COMM_JOYSTAT, (READ16LE(&ioMem[COMM_JOYSTAT]) & 0xf) | (value & 0xf0)); UPDATE_REG(COMM_JOYSTAT, (READ16LE(&ioMem[COMM_JOYSTAT]) & 0x0a) | (value & ~0x0a));
break; break;
#endif #endif
@ -3590,8 +3591,8 @@ void CPULoop(int ticks)
#ifndef NO_LINK #ifndef NO_LINK
// shuffle2: what's the purpose? // shuffle2: what's the purpose?
if(gba_link_enabled) //if(gba_link_enabled)
cpuNextEvent = 1; //cpuNextEvent = 1;
#endif #endif
cpuBreakLoop = false; cpuBreakLoop = false;
@ -4040,12 +4041,12 @@ void CPULoop(int ticks)
ticks -= clockTicks; ticks -= clockTicks;
if (gba_joybus_enabled)
JoyBusUpdate(clockTicks);
#ifndef NO_LINK #ifndef NO_LINK
if (gba_link_enabled) if (gba_link_enabled)
LinkUpdate(clockTicks); LinkUpdate(clockTicks);
if (gba_joybus_enabled)
JoyBusUpdate(clockTicks);
#endif #endif
cpuNextEvent = CPUUpdateTicks(); cpuNextEvent = CPUUpdateTicks();
@ -4063,7 +4064,7 @@ void CPULoop(int ticks)
#ifndef NO_LINK #ifndef NO_LINK
// shuffle2: what's the purpose? // shuffle2: what's the purpose?
if(gba_link_enabled) if(gba_link_enabled || gba_joybus_active)
cpuNextEvent = 1; cpuNextEvent = 1;
#endif #endif

View File

@ -30,6 +30,7 @@ const char *MakeInstanceFilename(const char *Input)
// Joybus // Joybus
bool gba_joybus_enabled = false; bool gba_joybus_enabled = false;
bool gba_joybus_active = false;
// If disabled, gba core won't call any (non-joybus) link functions // If disabled, gba core won't call any (non-joybus) link functions
bool gba_link_enabled = false; bool gba_link_enabled = false;
@ -388,30 +389,84 @@ void JoyBusShutdown()
dol = NULL; dol = NULL;
} }
const u64 TICKS_PER_FRAME = 16777216 / 60;
const u64 TICKS_PER_SECOND = 16777216;
const u64 BITS_PER_SECOND = 115200;
const u64 BYTES_PER_SECOND = BITS_PER_SECOND / 8;
static u32 lastjoybusupdate = 0;
static u32 nextjoybusupdate = 0;
static u32 lastcommand = 0;
static bool booted = false;
void JoyBusUpdate(int ticks) void JoyBusUpdate(int ticks)
{ {
linktime += ticks; lastjoybusupdate += ticks;
static int lastjoybusupdate = 0; lastcommand += ticks;
// Kinda ugly hack to update joybus stuff intermittently bool joybus_activated = ((READ16LE(&ioMem[COMM_RCNT])) >> 14) == 3;
if (linktime > lastjoybusupdate + 0x3000) gba_joybus_active = dol && gba_joybus_enabled && joybus_activated;
if ((lastjoybusupdate > nextjoybusupdate))
{ {
lastjoybusupdate = linktime; if (!joybus_activated)
{
if (dol && booted)
{
JoyBusShutdown();
}
char data[5] = {0x10, 0, 0, 0, 0}; // init with invalid cmd lastjoybusupdate = 0;
std::vector<char> resp; nextjoybusupdate = 0;
lastcommand = 0;
return;
}
if (!dol) if (!dol)
{
booted = false;
JoyBusConnect(); JoyBusConnect();
}
dol->ReceiveClock(false);
if (dol->IsDisconnected())
{
JoyBusShutdown();
nextjoybusupdate = TICKS_PER_SECOND * 2; // try to connect after 2 seconds
lastjoybusupdate = 0;
lastcommand = 0;
return;
}
dol->ClockSync(lastjoybusupdate);
char data[5] = { 0x10, 0, 0, 0, 0 }; // init with invalid cmd
std::vector<char> resp;
u8 cmd = 0x10;
if (lastcommand > (TICKS_PER_FRAME * 4))
{
cmd = dol->ReceiveCmd(data, true);
}
else
{
cmd = dol->ReceiveCmd(data, false);
}
u8 cmd = dol->ReceiveCmd(data);
switch (cmd) { switch (cmd) {
case JOY_CMD_RESET: case JOY_CMD_RESET:
UPDATE_REG(COMM_JOYCNT, READ16LE(&ioMem[COMM_JOYCNT]) | JOYCNT_RESET); UPDATE_REG(COMM_JOYCNT, READ16LE(&ioMem[COMM_JOYCNT]) | JOYCNT_RESET);
resp.push_back(0x00); // GBA device ID
resp.push_back(0x04);
nextjoybusupdate = TICKS_PER_SECOND / BYTES_PER_SECOND;
break;
case JOY_CMD_STATUS: case JOY_CMD_STATUS:
resp.push_back(0x00); // GBA device ID resp.push_back(0x00); // GBA device ID
resp.push_back(0x04); resp.push_back(0x04);
nextjoybusupdate = TICKS_PER_SECOND / BYTES_PER_SECOND;
break; break;
case JOY_CMD_READ: case JOY_CMD_READ:
@ -419,8 +474,10 @@ void JoyBusUpdate(int ticks)
resp.push_back((u8)(READ16LE(&ioMem[COMM_JOY_TRANS_L]) >> 8)); resp.push_back((u8)(READ16LE(&ioMem[COMM_JOY_TRANS_L]) >> 8));
resp.push_back((u8)(READ16LE(&ioMem[COMM_JOY_TRANS_H]) & 0xff)); resp.push_back((u8)(READ16LE(&ioMem[COMM_JOY_TRANS_H]) & 0xff));
resp.push_back((u8)(READ16LE(&ioMem[COMM_JOY_TRANS_H]) >> 8)); resp.push_back((u8)(READ16LE(&ioMem[COMM_JOY_TRANS_H]) >> 8));
UPDATE_REG(COMM_JOYSTAT, READ16LE(&ioMem[COMM_JOYSTAT]) & ~JOYSTAT_SEND);
UPDATE_REG(COMM_JOYCNT, READ16LE(&ioMem[COMM_JOYCNT]) | JOYCNT_SEND_COMPLETE); UPDATE_REG(COMM_JOYCNT, READ16LE(&ioMem[COMM_JOYCNT]) | JOYCNT_SEND_COMPLETE);
nextjoybusupdate = TICKS_PER_SECOND / BYTES_PER_SECOND;
booted = true;
break; break;
case JOY_CMD_WRITE: case JOY_CMD_WRITE:
@ -428,22 +485,35 @@ void JoyBusUpdate(int ticks)
UPDATE_REG(COMM_JOY_RECV_H, (u16)((u16)data[4] << 8) | (u8)data[3]); UPDATE_REG(COMM_JOY_RECV_H, (u16)((u16)data[4] << 8) | (u8)data[3]);
UPDATE_REG(COMM_JOYSTAT, READ16LE(&ioMem[COMM_JOYSTAT]) | JOYSTAT_RECV); UPDATE_REG(COMM_JOYSTAT, READ16LE(&ioMem[COMM_JOYSTAT]) | JOYSTAT_RECV);
UPDATE_REG(COMM_JOYCNT, READ16LE(&ioMem[COMM_JOYCNT]) | JOYCNT_RECV_COMPLETE); UPDATE_REG(COMM_JOYCNT, READ16LE(&ioMem[COMM_JOYCNT]) | JOYCNT_RECV_COMPLETE);
nextjoybusupdate = TICKS_PER_SECOND / BYTES_PER_SECOND;
booted = true;
break; break;
default: default:
nextjoybusupdate = TICKS_PER_SECOND / 40000;
lastjoybusupdate = 0;
return; // ignore return; // ignore
} }
lastjoybusupdate = 0;
resp.push_back((u8)READ16LE(&ioMem[COMM_JOYSTAT])); resp.push_back((u8)READ16LE(&ioMem[COMM_JOYSTAT]));
if (cmd == JOY_CMD_READ)
{
UPDATE_REG(COMM_JOYSTAT, READ16LE(&ioMem[COMM_JOYSTAT]) & ~JOYSTAT_SEND);
}
dol->Send(resp); dol->Send(resp);
// Generate SIO interrupt if we can // Generate SIO interrupt if we can
if ( ((cmd == JOY_CMD_RESET) || (cmd == JOY_CMD_READ) || (cmd == JOY_CMD_WRITE)) if (((cmd == JOY_CMD_RESET) || (cmd == JOY_CMD_READ) || (cmd == JOY_CMD_WRITE))
&& (READ16LE(&ioMem[COMM_JOYCNT]) & JOYCNT_INT_ENABLE) ) && (READ16LE(&ioMem[COMM_JOYCNT]) & JOYCNT_INT_ENABLE) )
{ {
IF |= 0x80; IF |= 0x80;
UPDATE_REG(0x202, IF); UPDATE_REG(0x202, IF);
} }
lastcommand = 0;
} }
} }
@ -451,6 +521,9 @@ static void ReInitLink();
void LinkUpdate(int ticks) void LinkUpdate(int ticks)
{ {
if (((READ16LE(&ioMem[COMM_RCNT])) >> 14) == 3)
return;
// this actually gets called every single instruction, so keep default // this actually gets called every single instruction, so keep default
// path as short as possible // path as short as possible

View File

@ -145,6 +145,7 @@ typedef struct {
} LANLINKDATA; } LANLINKDATA;
extern bool gba_joybus_enabled; extern bool gba_joybus_enabled;
extern bool gba_joybus_active;
extern bool gba_link_enabled; extern bool gba_link_enabled;
extern sf::IPAddress joybusHostAddr; extern sf::IPAddress joybusHostAddr;

View File

@ -12,14 +12,23 @@ GBASockClient::GBASockClient(sf::IPAddress _server_addr)
server_addr = _server_addr; server_addr = _server_addr;
client.Connect(0xd6ba, server_addr); client.Connect(0xd6ba, server_addr);
//client.SetBlocking(false); client.SetBlocking(false);
clock_client.Connect(0xc10c, server_addr);
clock_client.SetBlocking(false);
clock_sync = 0;
is_disconnected = false;
} }
GBASockClient::~GBASockClient() GBASockClient::~GBASockClient()
{ {
client.Close(); client.Close();
clock_client.Close();
} }
u32 clock_sync_ticks = 0;
void GBASockClient::Send(std::vector<char> data) void GBASockClient::Send(std::vector<char> data)
{ {
char* plain_data = new char[data.size()]; char* plain_data = new char[data.size()];
@ -31,12 +40,60 @@ void GBASockClient::Send(std::vector<char> data)
} }
// Returns cmd for convenience // Returns cmd for convenience
char GBASockClient::ReceiveCmd(char* data_in) char GBASockClient::ReceiveCmd(char* data_in, bool block)
{ {
std::size_t num_received; if (IsDisconnected())
client.Receive(data_in, 5, num_received); return data_in[0];
std::size_t num_received = 0;
if (block || clock_sync == 0)
{
sf::SelectorTCP Selector;
Selector.Add(client);
Selector.Wait(6);
}
if (client.Receive(data_in, 5, num_received) == sf::Socket::Disconnected)
Disconnect();
return data_in[0]; return data_in[0];
} }
void GBASockClient::ReceiveClock(bool block)
{
if (IsDisconnected())
return;
char sync_ticks[4] = { 0, 0, 0, 0 };
std::size_t num_received = 0;
if (clock_client.Receive(sync_ticks, 4, num_received) == sf::Socket::Disconnected)
Disconnect();
if (num_received == 4)
{
clock_sync_ticks = 0;
for (int i = 0; i < 4; i++)
clock_sync_ticks |= (u8)(sync_ticks[i]) << ((3 - i) * 8);
clock_sync += clock_sync_ticks;
}
}
void GBASockClient::ClockSync(u32 ticks)
{
if (clock_sync > (s32)ticks)
clock_sync -= (s32)ticks;
else
clock_sync = 0;
}
void GBASockClient::Disconnect()
{
is_disconnected = true;
client.Close();
clock_client.Close();
}
bool GBASockClient::IsDisconnected()
{
return !client.IsValid() || !clock_client.IsValid() || is_disconnected;
}
#endif // NO_LINK #endif // NO_LINK

View File

@ -10,9 +10,18 @@ public:
~GBASockClient(); ~GBASockClient();
void Send(std::vector<char> data); void Send(std::vector<char> data);
char ReceiveCmd(char* data_in); char ReceiveCmd(char* data_in, bool block);
void ReceiveClock(bool block);
void ClockSync(u32 ticks);
void Disconnect();
bool IsDisconnected();
private: private:
sf::IPAddress server_addr; sf::IPAddress server_addr;
sf::SocketTCP client; sf::SocketTCP client;
sf::SocketTCP clock_client;
s32 clock_sync;
bool is_disconnected;
}; };

View File

@ -33,6 +33,8 @@ extern bool skipBios;
extern int frameSkip; extern int frameSkip;
extern bool speedup; extern bool speedup;
extern bool synchronize; extern bool synchronize;
extern bool gba_joybus_enabled;
extern bool gba_joybus_active;
extern bool cpuDisableSfx; extern bool cpuDisableSfx;
extern bool cpuIsMultiBoot; extern bool cpuIsMultiBoot;
extern bool parseDebug; extern bool parseDebug;

View File

@ -180,7 +180,7 @@ void Direct3DDisplay::prepareDisplayMode()
dpp.hDeviceWindow = theApp.m_pMainWnd->GetSafeHwnd(); dpp.hDeviceWindow = theApp.m_pMainWnd->GetSafeHwnd();
dpp.FullScreen_RefreshRateInHz = ( dpp.Windowed == TRUE ) ? 0 : theApp.fsFrequency; dpp.FullScreen_RefreshRateInHz = ( dpp.Windowed == TRUE ) ? 0 : theApp.fsFrequency;
dpp.Flags = 0; dpp.Flags = 0;
dpp.PresentationInterval = theApp.vsync ? D3DPRESENT_INTERVAL_ONE : D3DPRESENT_INTERVAL_IMMEDIATE; dpp.PresentationInterval = (theApp.vsync && !gba_joybus_active) ? D3DPRESENT_INTERVAL_ONE : D3DPRESENT_INTERVAL_IMMEDIATE;
// D3DPRESENT_INTERVAL_ONE means VSync ON // D3DPRESENT_INTERVAL_ONE means VSync ON

View File

@ -240,7 +240,7 @@ void DirectSound::write(u16 * finalWave, int length)
LPVOID lpvPtr2; LPVOID lpvPtr2;
DWORD dwBytes2 = 0; DWORD dwBytes2 = 0;
if( !speedup && synchronize && !theApp.throttle ) { if( !speedup && synchronize && !theApp.throttle && !gba_joybus_active) {
hr = dsbSecondary->GetStatus(&status); hr = dsbSecondary->GetStatus(&status);
if( status & DSBSTATUS_PLAYING ) { if( status & DSBSTATUS_PLAYING ) {
if( !soundPaused ) { if( !soundPaused ) {

View File

@ -1218,7 +1218,7 @@ void MainWnd::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
} }
} else { } else {
theApp.wasPaused = true; theApp.wasPaused = true;
if(theApp.pauseWhenInactive) { if(theApp.pauseWhenInactive && !gba_joybus_active) {
if(emulating) { if(emulating) {
soundPause(); soundPause();
} }

View File

@ -281,7 +281,7 @@ void OpenAL::write(u16 * finalWave, int length)
} }
} }
if( !speedup && synchronize && !theApp.throttle ) { if( !speedup && synchronize && !theApp.throttle && !gba_joybus_active) {
// wait until at least one buffer has finished // wait until at least one buffer has finished
while( nBuffersProcessed == 0 ) { while( nBuffersProcessed == 0 ) {
winlog( " waiting...\n" ); winlog( " waiting...\n" );

View File

@ -336,7 +336,7 @@ bool OpenGLDisplay::initialize()
initializeMatrices( theApp.surfaceSizeX, theApp.surfaceSizeY ); initializeMatrices( theApp.surfaceSizeX, theApp.surfaceSizeY );
setVSync( theApp.vsync ); setVSync( theApp.vsync && !gba_joybus_active );
#ifdef MMX #ifdef MMX
if(!theApp.disableMMX) if(!theApp.disableMMX)

View File

@ -2706,7 +2706,7 @@ void Sm60FPS_Sleep()
if( theApp.autoFrameSkip ) { if( theApp.autoFrameSkip ) {
u32 dwTimePass = Sm60FPS::dwTimeElapse + (GetTickCount() - Sm60FPS::dwTime0); u32 dwTimePass = Sm60FPS::dwTimeElapse + (GetTickCount() - Sm60FPS::dwTime0);
u32 dwTimeShould = (u32)(Sm60FPS::nFrameCnt * Sm60FPS::K_fDT); u32 dwTimeShould = (u32)(Sm60FPS::nFrameCnt * Sm60FPS::K_fDT);
if( dwTimeShould > dwTimePass ) { if (dwTimeShould > dwTimePass && !gba_joybus_active) {
Sleep(dwTimeShould - dwTimePass); Sleep(dwTimeShould - dwTimePass);
} }
} }

View File

@ -451,7 +451,7 @@ void XAudio2_Output::write(u16 * finalWave, int length)
break; break;
} else { } else {
// the maximum number of buffers is currently queued // the maximum number of buffers is currently queued
if( synchronize && !speedup && !theApp.throttle ) { if( synchronize && !speedup && !theApp.throttle && !gba_joybus_active ) {
// wait for one buffer to finish playing // wait for one buffer to finish playing
if (WaitForSingleObject( notify.hBufferEndEvent, 10000 ) == WAIT_TIMEOUT) { if (WaitForSingleObject( notify.hBufferEndEvent, 10000 ) == WAIT_TIMEOUT) {
device_changed = true; device_changed = true;