mirror of
https://github.com/RetroDECK/Supermodel.git
synced 2024-12-01 10:05:40 +00:00
627 lines
17 KiB
C++
627 lines
17 KiB
C++
/**
|
|
** Supermodel
|
|
** A Sega Model 3 Arcade Emulator.
|
|
** Copyright 2011-2020 Bart Trzynadlowski, Nik Henson, Ian Curtis,
|
|
** Harry Tuttle, and Spindizzi
|
|
**
|
|
** This file is part of Supermodel.
|
|
**
|
|
** Supermodel is free software: you can redistribute it and/or modify it under
|
|
** the terms of the GNU General Public License as published by the Free
|
|
** Software Foundation, either version 3 of the License, or (at your option)
|
|
** any later version.
|
|
**
|
|
** Supermodel is distributed in the hope that it will be useful, but WITHOUT
|
|
** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
** FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
** more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License along
|
|
** with Supermodel. If not, see <http://www.gnu.org/licenses/>.
|
|
**/
|
|
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include "Supermodel.h"
|
|
#include "SimNetBoard.h"
|
|
|
|
// these make 16-bit read/writes much neater
|
|
#define RAM16 *(uint16_t*)&RAM
|
|
#define CommRAM16 *(uint16_t*)&CommRAM
|
|
|
|
static const uint64_t netGUID = 0x5bf177da34872;
|
|
|
|
inline bool CSimNetBoard::IsGame(const char* gameName)
|
|
{
|
|
return (m_gameInfo.name == gameName) || (m_gameInfo.parent == gameName);
|
|
}
|
|
|
|
CSimNetBoard::CSimNetBoard(const Util::Config::Node& config) : m_config(config)
|
|
{
|
|
}
|
|
|
|
CSimNetBoard::~CSimNetBoard(void)
|
|
{
|
|
m_quit = true;
|
|
|
|
if (m_connectThread.joinable())
|
|
m_connectThread.join();
|
|
}
|
|
|
|
void CSimNetBoard::SaveState(CBlockFile* SaveState)
|
|
{
|
|
|
|
}
|
|
|
|
void CSimNetBoard::LoadState(CBlockFile* SaveState)
|
|
{
|
|
|
|
}
|
|
|
|
bool CSimNetBoard::Init(uint8_t* netRAMPtr, uint8_t* netBufferPtr)
|
|
{
|
|
RAM = netRAMPtr;
|
|
Buffer = netBufferPtr;
|
|
|
|
CommRAM = Buffer;
|
|
externalCommRAM = Buffer + 0x10000;
|
|
|
|
m_attached = m_gameInfo.netboard_present && m_config["Network"].ValueAs<bool>();
|
|
|
|
if (!m_attached)
|
|
return 0;
|
|
|
|
if (IsGame("daytona2") || IsGame("harley") || IsGame("scud") || IsGame("srally2") ||
|
|
IsGame("skichamp") || IsGame("spikeout") || IsGame("spikeofe"))
|
|
m_gameType = GameType::one;
|
|
else if (IsGame("lemans24") || IsGame("von2") || IsGame("dirtdvls"))
|
|
m_gameType = GameType::two;
|
|
else
|
|
return ErrorLog("Game not recognized or supported");
|
|
|
|
m_state = State::start;
|
|
|
|
//netsocks
|
|
port_in = m_config["PortIn"].ValueAs<unsigned>();
|
|
port_out = m_config["PortOut"].ValueAs<unsigned>();
|
|
addr_out = m_config["AddressOut"].ValueAs<std::string>();
|
|
|
|
nets = std::make_unique<TCPSend>(addr_out, port_out);
|
|
netr = std::make_unique<TCPReceive>(port_in);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void CSimNetBoard::RunFrame(void)
|
|
{
|
|
if (!IsRunning())
|
|
return;
|
|
|
|
switch (m_state)
|
|
{
|
|
case State::start:
|
|
if (!m_connected && !m_connectThread.joinable())
|
|
m_connectThread = std::thread(&CSimNetBoard::ConnectProc, this);
|
|
m_status0 = 0;
|
|
m_status1 = IsGame("dirtdvls") ? 0x4004 : 0xe000;
|
|
m_state = State::init;
|
|
break;
|
|
|
|
case State::init:
|
|
memset(CommRAM, 0, 0x20000);
|
|
if (m_gameType == GameType::one)
|
|
{
|
|
if (m_status0 & 0x8000) // has main board changed this register?
|
|
{
|
|
m_IRQ2ack |= 0x01; // simulate IRQ 2 ack
|
|
if (m_status0 == 0xf000)
|
|
{
|
|
// initialization complete
|
|
m_status1 = 0;
|
|
CommRAM16[0x72] = FLIPENDIAN16(0x1); // is this necessary?
|
|
m_state = State::testing;
|
|
}
|
|
m_status0 = 0; // 0 should work for all init subroutines
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// type 2 performs initialization on its own
|
|
m_status1 = 0;
|
|
m_state = State::testing;
|
|
m_counter = 0;
|
|
}
|
|
break;
|
|
|
|
case State::testing:
|
|
if (m_gameType == GameType::one)
|
|
{
|
|
m_status0 += 1; // type 1 games require this to be incremented every frame
|
|
|
|
if (!m_connected)
|
|
break;
|
|
|
|
uint8_t numMachines, machineIndex;
|
|
|
|
if (RAM16[0x400] == 0) // master
|
|
{
|
|
// flush receive buffer
|
|
while (netr->CheckDataAvailable())
|
|
{
|
|
netr->Receive();
|
|
}
|
|
|
|
// check all linked instances have the same GUID
|
|
nets->Send(&netGUID, sizeof(netGUID));
|
|
auto& recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
uint64_t testGUID;
|
|
memcpy(&testGUID, &recv_data[0], recv_data.size());
|
|
if (testGUID != netGUID)
|
|
testGUID = 0;
|
|
|
|
// send the GUID for one more loop
|
|
nets->Send(&testGUID, sizeof(testGUID));
|
|
netr->Receive();
|
|
|
|
if (testGUID != netGUID)
|
|
{
|
|
ErrorLog("unable to verify connection. Make sure all machines are using same build!");
|
|
m_state = State::error;
|
|
break;
|
|
}
|
|
|
|
// master has an index of zero
|
|
machineIndex = 0;
|
|
nets->Send(&machineIndex, sizeof(machineIndex));
|
|
|
|
// receive back the number of other linked machines
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
numMachines = recv_data[0];
|
|
|
|
// send the number of other linked machines
|
|
nets->Send(&numMachines, sizeof(numMachines));
|
|
netr->Receive();
|
|
}
|
|
else
|
|
{
|
|
// receive GUID from the previous machine and check it matches
|
|
auto& recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
uint64_t testGUID;
|
|
memcpy(&testGUID, &recv_data[0], recv_data.size());
|
|
if (testGUID != netGUID)
|
|
testGUID = 0;
|
|
nets->Send(&testGUID, sizeof(testGUID));
|
|
|
|
// one more time, in case a later machine has a GUID mismatch
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
memcpy(&testGUID, &recv_data[0], recv_data.size());
|
|
if (testGUID != netGUID)
|
|
testGUID = 0;
|
|
nets->Send(&testGUID, sizeof(testGUID));
|
|
|
|
if (testGUID != netGUID)
|
|
{
|
|
ErrorLog("unable to verify connection. Make sure all machines are using same build!");
|
|
m_state = State::error;
|
|
break;
|
|
}
|
|
|
|
// receive the previous machine's index, increment it, send it to the next machine
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
machineIndex = recv_data[0] + 1;
|
|
nets->Send(&machineIndex, sizeof(machineIndex));
|
|
|
|
// receive the number of other linked machines and forward it on
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
numMachines = recv_data[0];
|
|
nets->Send(&numMachines, sizeof(numMachines));
|
|
}
|
|
|
|
// if there are no other linked machines, only continue if Supermodel is linked to itself
|
|
// there might be more than one machine set to master which would cause glitches
|
|
if ((numMachines == 0) && ((port_in != port_out) || (addr_out.compare("127.0.0.1") != 0)))
|
|
{
|
|
ErrorLog("no slave machines detected. Make sure only one machine is set to master!");
|
|
m_state = State::error;
|
|
break;
|
|
}
|
|
|
|
m_numMachines = numMachines + 1;
|
|
|
|
m_status0 = 0; // supposed to cycle between 0 and 1 (also 2 for Daytona 2); doesn't seem to matter
|
|
m_status1 = 0x2021 + (numMachines * 0x20) + machineIndex;
|
|
|
|
CommRAM16[0x0] = RAM16[0x400]; // 0 if master, 1 if slave
|
|
CommRAM16[0x2] = numMachines;
|
|
CommRAM16[0x4] = machineIndex;
|
|
|
|
m_counter = 0;
|
|
CommRAM16[0x6] = 0;
|
|
|
|
m_segmentSize = RAM16[0x404];
|
|
|
|
// don't know if these are actually required, but it never hurts to include them
|
|
CommRAM16[0x8] = FLIPENDIAN16(0x100 + m_segmentSize);
|
|
CommRAM16[0xa] = FLIPENDIAN16(RAM16[0x402] - m_segmentSize - 1);
|
|
CommRAM16[0xc] = FLIPENDIAN16(0x100);
|
|
CommRAM16[0xe] = FLIPENDIAN16(RAM16[0x402] - m_segmentSize + 0x200);
|
|
|
|
m_state = State::ready;
|
|
}
|
|
else
|
|
{
|
|
if (!m_connected)
|
|
break;
|
|
|
|
// we have to track both playable and non-playable machines for type 2
|
|
struct
|
|
{
|
|
uint8_t total;
|
|
uint8_t playable;
|
|
} numMachines, machineIndex;
|
|
|
|
if (RAM16[0x200] == 0) // master
|
|
{
|
|
// flush receive buffer
|
|
while (netr->CheckDataAvailable())
|
|
netr->Receive();
|
|
|
|
// check all linked instances have the same GUID
|
|
nets->Send(&netGUID, sizeof(netGUID));
|
|
auto& recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
|
|
uint64_t testGUID;
|
|
memcpy(&testGUID, &recv_data[0], recv_data.size());
|
|
if (testGUID != netGUID)
|
|
testGUID = 0;
|
|
|
|
// send the GUID for one more loop
|
|
nets->Send(&testGUID, sizeof(testGUID));
|
|
netr->Receive();
|
|
|
|
if (testGUID != netGUID)
|
|
{
|
|
ErrorLog("unable to verify connection. Make sure all machines are using same build!");
|
|
m_state = State::error;
|
|
break;
|
|
}
|
|
|
|
// master has indices set to zero
|
|
machineIndex.total = 0, machineIndex.playable = 0;
|
|
nets->Send(&machineIndex, sizeof(machineIndex));
|
|
|
|
// receive back the number of other linked machines
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
memcpy(&numMachines, &recv_data[0], recv_data.size());
|
|
|
|
// send the number of other linked machines
|
|
nets->Send(&numMachines, sizeof(numMachines));
|
|
netr->Receive();
|
|
}
|
|
else if (RAM16[0x200] < 0x8000) // slave
|
|
{
|
|
// receive GUID from the previous machine and check it matches
|
|
auto& recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
uint64_t testGUID;
|
|
memcpy(&testGUID, &recv_data[0], recv_data.size());
|
|
if (testGUID != netGUID)
|
|
testGUID = 0;
|
|
nets->Send(&testGUID, sizeof(testGUID));
|
|
|
|
// one more time, in case a later machine has a GUID mismatch
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
memcpy(&testGUID, &recv_data[0], recv_data.size());
|
|
if (testGUID != netGUID)
|
|
testGUID = 0;
|
|
nets->Send(&testGUID, sizeof(testGUID));
|
|
|
|
if (testGUID != netGUID)
|
|
{
|
|
ErrorLog("unable to verify connection. Make sure all machines are using same build!");
|
|
m_state = State::error;
|
|
break;
|
|
}
|
|
|
|
// receive the indices of the previous machine and increment them
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
memcpy(&machineIndex, &recv_data[0], recv_data.size());
|
|
machineIndex.total++, machineIndex.playable++;
|
|
|
|
// send our indices to the next machine
|
|
nets->Send(&machineIndex, sizeof(machineIndex));
|
|
|
|
// receive the number of machines
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
memcpy(&numMachines, &recv_data[0], recv_data.size());
|
|
|
|
// forward the number of machines
|
|
nets->Send(&numMachines, sizeof(numMachines));
|
|
}
|
|
else
|
|
{
|
|
// relay/satellite
|
|
|
|
// receive GUID from the previous machine and check it matches
|
|
auto& recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
uint64_t testGUID;
|
|
memcpy(&testGUID, &recv_data[0], recv_data.size());
|
|
if (testGUID != netGUID)
|
|
testGUID = 0;
|
|
nets->Send(&testGUID, sizeof(testGUID));
|
|
|
|
// one more time, in case a later machine has a GUID mismatch
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
memcpy(&testGUID, &recv_data[0], recv_data.size());
|
|
if (testGUID != netGUID)
|
|
testGUID = 0;
|
|
nets->Send(&testGUID, sizeof(testGUID));
|
|
|
|
if (testGUID != netGUID)
|
|
{
|
|
ErrorLog("unable to verify connection. Make sure all machines are using same build!");
|
|
m_state = State::error;
|
|
break;
|
|
}
|
|
|
|
// receive the indices of the previous machine; don't increment the playable index
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
memcpy(&machineIndex, &recv_data[0], recv_data.size());
|
|
machineIndex.total++;
|
|
|
|
// send our indices to the next machine
|
|
nets->Send(&machineIndex, sizeof(machineIndex));
|
|
|
|
// receive the number of machines
|
|
recv_data = netr->Receive();
|
|
if (recv_data.empty())
|
|
break;
|
|
memcpy(&numMachines, &recv_data[0], recv_data.size());
|
|
|
|
// forward the number of machines
|
|
nets->Send(&numMachines, sizeof(numMachines));
|
|
|
|
// indicate that this machine is a relay/satellite
|
|
if (!IsGame("dirtdvls"))
|
|
machineIndex.playable |= 0x80;
|
|
}
|
|
|
|
// if there are no other linked machines, only continue if Supermodel is linked to itself
|
|
// there might be more than one machine set to master which would cause glitches
|
|
if ((numMachines.total == 0) && ((port_in != port_out) || (addr_out.compare("127.0.0.1") != 0)))
|
|
{
|
|
ErrorLog("no slave machines detected. Make sure only one machine is set to master!");
|
|
if (IsGame("dirtdvls"))
|
|
m_status1 = 0x8085; // seems like the netboard code writers really liked their CPU model numbers
|
|
m_state = State::error;
|
|
break;
|
|
}
|
|
|
|
m_numMachines = numMachines.total + 1;
|
|
|
|
m_status0 = 5; // probably not necessary
|
|
if (IsGame("dirtdvls"))
|
|
m_status1 = (numMachines.playable << 4) | machineIndex.playable | 0x7400;
|
|
else
|
|
m_status1 = (numMachines.playable << 8) | machineIndex.playable;
|
|
|
|
CommRAM16[0x0] = RAM16[0x200]; // master/slave/relay status
|
|
CommRAM16[0x2] = (numMachines.playable << 8) | numMachines.total;
|
|
CommRAM16[0x4] = (machineIndex.playable << 8) | machineIndex.total;
|
|
|
|
m_counter = 0;
|
|
CommRAM16[0x6] = 0;
|
|
|
|
m_segmentSize = RAM16[0x204];
|
|
|
|
// don't know if these are actually required, but it never hurts to include them
|
|
CommRAM16[0x8] = FLIPENDIAN16(0x100 + m_segmentSize);
|
|
CommRAM16[0xa] = FLIPENDIAN16(RAM16[0x206]);
|
|
CommRAM16[0xc] = FLIPENDIAN16(0x100);
|
|
CommRAM16[0xe] = FLIPENDIAN16(RAM16[0x206] + 0x80);
|
|
|
|
m_state = State::ready;
|
|
}
|
|
break;
|
|
|
|
case State::ready:
|
|
m_counter++;
|
|
CommRAM16[0x6] = FLIPENDIAN16(m_counter);
|
|
|
|
// we only send what we need to; helps cut down on bandwidth
|
|
// each machine has to receive back its own data (TODO: copy this data manually?)
|
|
for (int i = 0; i < m_numMachines; i++)
|
|
{
|
|
nets->Send(CommRAM + 0x100 + i * m_segmentSize, m_segmentSize);
|
|
auto& recv_data = netr->Receive();
|
|
if (recv_data.size() == 0)
|
|
{
|
|
// link broken - send an "empty" packet to alert other machines
|
|
nets->Send(nullptr, 0);
|
|
m_state = State::error;
|
|
if (m_gameType == GameType::one)
|
|
m_status1 = 0x40; // send "link broken" message to mainboard
|
|
break;
|
|
}
|
|
memcpy(CommRAM + 0x100 + (i + 1) * m_segmentSize, recv_data.data(), recv_data.size());
|
|
}
|
|
|
|
// swap CommRAM banks
|
|
if (m_commbank)
|
|
{
|
|
m_commbank = false;
|
|
CommRAM = Buffer;
|
|
externalCommRAM = Buffer + 0x10000;
|
|
}
|
|
else
|
|
{
|
|
m_commbank = true;
|
|
CommRAM = Buffer + 0x10000;
|
|
externalCommRAM = Buffer;
|
|
}
|
|
|
|
break;
|
|
|
|
case State::error:
|
|
// do nothing
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CSimNetBoard::Reset(void)
|
|
{
|
|
// if netboard was active, send an "empty" packet so the other machines don't get stuck waiting for data
|
|
if (m_state == State::ready)
|
|
{
|
|
nets->Send(nullptr, 0);
|
|
netr->Receive();
|
|
}
|
|
|
|
m_running = false;
|
|
m_state = State::start;
|
|
}
|
|
|
|
bool CSimNetBoard::IsAttached(void)
|
|
{
|
|
return m_attached;
|
|
}
|
|
|
|
bool CSimNetBoard::IsRunning(void)
|
|
{
|
|
return m_attached && m_running;
|
|
}
|
|
|
|
void CSimNetBoard::GetGame(const Game& gameInfo)
|
|
{
|
|
m_gameInfo = gameInfo;
|
|
}
|
|
|
|
void CSimNetBoard::ConnectProc(void)
|
|
{
|
|
using namespace std::chrono_literals;
|
|
|
|
if (m_connected)
|
|
return;
|
|
|
|
printf("Connecting to %s:%i ..\n", addr_out.c_str(), port_out);
|
|
|
|
// wait until TCPSend has connected to the next machine
|
|
while (!nets->Connect())
|
|
{
|
|
if (m_quit)
|
|
return;
|
|
}
|
|
|
|
// wait until TCPReceive has accepted a connection from the previous machine
|
|
while (!netr->Connected())
|
|
{
|
|
if (m_quit)
|
|
return;
|
|
std::this_thread::sleep_for(1ms);
|
|
}
|
|
|
|
printf("Successfully connected.\n");
|
|
|
|
m_connected = true;
|
|
}
|
|
|
|
uint8_t CSimNetBoard::ReadCommRAM8(unsigned addr)
|
|
{
|
|
return externalCommRAM[addr];
|
|
}
|
|
|
|
uint16_t CSimNetBoard::ReadCommRAM16(unsigned addr)
|
|
{
|
|
return *(uint16_t*)&externalCommRAM[addr];
|
|
}
|
|
|
|
uint32_t CSimNetBoard::ReadCommRAM32(unsigned addr)
|
|
{
|
|
return *(uint32_t*)&externalCommRAM[addr];
|
|
}
|
|
|
|
void CSimNetBoard::WriteCommRAM8(unsigned addr, uint8_t data)
|
|
{
|
|
externalCommRAM[addr] = data;
|
|
}
|
|
|
|
void CSimNetBoard::WriteCommRAM16(unsigned addr, uint16_t data)
|
|
{
|
|
*(uint16_t*)&externalCommRAM[addr] = data;
|
|
}
|
|
|
|
void CSimNetBoard::WriteCommRAM32(unsigned addr, uint32_t data)
|
|
{
|
|
*(uint32_t*)&externalCommRAM[addr] = data;
|
|
}
|
|
|
|
uint16_t CSimNetBoard::ReadIORegister(unsigned reg)
|
|
{
|
|
if (!IsRunning())
|
|
return 0;
|
|
|
|
switch (reg)
|
|
{
|
|
case 0x00:
|
|
return m_IRQ2ack;
|
|
case 0x88:
|
|
return m_status0;
|
|
case 0x8a:
|
|
return m_status1;
|
|
default:
|
|
ErrorLog("read from unknown IO register 0x%02x", reg);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void CSimNetBoard::WriteIORegister(unsigned reg, uint16_t data)
|
|
{
|
|
switch (reg)
|
|
{
|
|
case 0x00:
|
|
m_IRQ2ack = data;
|
|
break;
|
|
case 0x88:
|
|
m_status0 = data;
|
|
break;
|
|
case 0x8a:
|
|
m_status1 = data;
|
|
break;
|
|
case 0xc0:
|
|
if (data == 0)
|
|
Reset();
|
|
m_running = (data != 0);
|
|
break;
|
|
default:
|
|
ErrorLog("write to unknown IO register 0x%02x", reg);
|
|
}
|
|
} |