Supermodel/Src/Model3/DriveBoard/DriveBoard.cpp
SpinDizzy 08d4735ee8 Huge refactor of the Driveboard:
-Separate each possible boards (wheel, joystick, skipad, billboard).
-Defined a Driveboard type in Games.xml for each games.
-Due to the refactoring, Driveboard Savestates have changed (a common base data + a specific board data are saved).
-Backwards compatibility with previous save states is maintained.
-Driveboard rom section is no longer required anymore. This disables Driveboard emulation in case the rom is not found.
-Added Billboard emulation (vf3, vs2, fvipers2, von2). 7 segments and lamps Outputs are redirected to Supermodel outputs.
-Changes project to C++ 17 standard.
2021-02-18 10:29:15 +00:00

427 lines
12 KiB
C++

/**
** Supermodel
** A Sega Model 3 Arcade Emulator.
** Copyright 2011-2021 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/>.
**/
/*
* DriveBoard.cpp
*
* Implementation of the CDriveBoard class: drive board (force feedback)
* emulation.
*
* NOTE: Simulation does not yet work. Drive board ROMs are required.
*
* State Management:
* -----------------
* - IsAttached: This indicates whether the drive board is present for the game
* and should be emulated, if possible. It is determined by whether a ROM was
* passed to the initializer. Entirely simulated implementations of the drive
* board should still take a ROM, even if it contains no data. The attached
* should be set only *once* at initialization and preferably by the base
* CDriveBoard class. Do not change this flag during run-time!
* - Disabled: This state indicates emulation should not proceed. Force
* feedback must be completely halted. This flag is used only to disable the
* board due to run-time errors and must *not* be re-enabled. It must not be
* used to "temporarily" disable the board. Only the Reset() method may
* enable emulation and then only if the board is attached. A valid reason
* for disabling the board during run-time is e.g., if a loaded save state is
* incompatible (wrong format or because it was saved while the board was
* disabled, rendering its state data invalid).
* - IsDisabled: This method is used internally only and should be used to test
* whether emulation should occur. It is the combination of attachment and
* enabled state.
* - Disable: Use this to disable the board. Drive board implementations should
* override this to send stop commands to force feedback motors and then call
* CDriveBoard::Disable() to update the disabled flag.
*/
#include "Supermodel.h"
#include <cstdio>
#include <cmath>
#include <algorithm>
#define ROM_SIZE 0x9000
#define RAM_SIZE 0x2000 // Z80 RAM
static_assert(sizeof(bool) == 1); // Save state code relies on this -- we must fix this so that bools are copied to uint8_t explicitly
void CDriveBoard::SaveState(CBlockFile* SaveState)
{
SaveState->NewBlock("DriveBoard.2", __FILE__);
// Check board is attached and enabled
bool enabled = !IsDisabled();
SaveState->Write(&enabled, sizeof(enabled));
if (enabled)
{
// Check if simulated
SaveState->Write(&m_simulated, sizeof(m_simulated));
if (m_simulated)
{
// TODO - save board simulation state
}
else
{
// Save RAM state
SaveState->Write(m_ram, RAM_SIZE);
// Save interrupt and input/output state
SaveState->Write(&m_initialized, sizeof(m_initialized));
SaveState->Write(&m_allowInterrupts, sizeof(m_allowInterrupts));
SaveState->Write(&m_dataSent, sizeof(m_dataSent));
SaveState->Write(&m_dataReceived, sizeof(m_dataReceived));
// Save CPU state
m_z80.SaveState(SaveState, "DriveBoard Z80");
}
}
}
void CDriveBoard::LoadState(CBlockFile* SaveState)
{
if (SaveState->FindBlock("DriveBoard.2") != OKAY)
{
ErrorLog("Unable to load base drive board state. Save state file is corrupt.");
Disable();
return;
}
// Check that board was enabled in saved state
bool isEnabled = !IsDisabled();
bool wasEnabled = false;
bool wasSimulated = false;
SaveState->Read(&wasEnabled, sizeof(wasEnabled));
if (wasEnabled)
{
// Simulated?
SaveState->Read(&wasSimulated, sizeof(wasSimulated));
if (wasSimulated)
{
// Derived classes may have simulation state (e.g., SkiBoard)
}
else
{
// Load RAM state
SaveState->Read(m_ram, RAM_SIZE);
// Load interrupt and input/output state
SaveState->Read(&m_initialized, sizeof(m_initialized));
SaveState->Read(&m_allowInterrupts, sizeof(m_allowInterrupts));
SaveState->Read(&m_dataSent, sizeof(m_dataSent));
SaveState->Read(&m_dataReceived, sizeof(m_dataReceived));
// Load CPU state
// TODO: we should have a way to check whether this succeeds... make CZ80::LoadState() return a bool
m_z80.LoadState(SaveState, "DriveBoard Z80");
}
}
// If the board was not in the same activity and simulation state when the
// save file was generated, we cannot safely resume and must disable it
if (wasEnabled != isEnabled || wasSimulated != m_simulated)
{
Disable();
ErrorLog("Halting drive board emulation due to mismatch in active and restored states.");
}
}
void CDriveBoard::LoadLegacyState(const LegacyDriveBoardState &state, CBlockFile *SaveState)
{
static_assert(RAM_SIZE == sizeof(state.ram));
memcpy(m_ram, state.ram, RAM_SIZE);
m_initialized = state.initialized;
m_allowInterrupts = state.allowInterrupts;
m_dataSent = state.dataSent;
m_dataReceived = state.dataReceived;
m_z80.LoadState(SaveState, "DriveBoard Z80");
}
Game::DriveBoardType CDriveBoard::GetType(void)
{
return Game::DRIVE_BOARD_NONE;
}
bool CDriveBoard::IsAttached(void)
{
return m_attached;
}
bool CDriveBoard::IsSimulated(void)
{
return m_simulated;
}
bool CDriveBoard::IsDisabled(void)
{
bool enabled = !m_disabled;
return !(m_attached && enabled);
}
void CDriveBoard::Disable(void)
{
m_disabled = true;
}
void CDriveBoard::GetDIPSwitches(UINT8& dip1, UINT8& dip2)
{
dip1 = m_dip1;
dip2 = m_dip2;
}
void CDriveBoard::GetDIPSwitches(UINT8& dip1)
{
dip1 = m_dip1;
}
void CDriveBoard::SetDIPSwitches(UINT8 dip1, UINT8 dip2)
{
m_dip1 = dip1;
m_dip2 = dip2;
}
void CDriveBoard::SetDIPSwitches(UINT8 dip1)
{
m_dip1 = dip1;
}
unsigned CDriveBoard::GetForceFeedbackStrength()
{
return ((~(m_dip1 >> 2)) & 7) + 1;
}
void CDriveBoard::SetForceFeedbackStrength(unsigned strength)
{
m_dip1 = (m_dip1 & 0xE3) | (((~(strength - 1)) & 7) << 2);
}
CZ80* CDriveBoard::GetZ80(void)
{
return &m_z80;
}
void CDriveBoard::AttachInputs(CInputs* inputs, unsigned gameInputFlags)
{
m_inputs = inputs;
m_inputFlags = gameInputFlags;
DebugLog("DriveBoard attached inputs\n");
}
void CDriveBoard::AttachOutputs(COutputs* outputs)
{
m_outputs = outputs;
DebugLog("DriveBoard attached outputs\n");
}
UINT8 CDriveBoard::Read(void)
{
if (IsDisabled())
{
return 0xFF;
}
return m_dataReceived;
}
void CDriveBoard::Write(UINT8 data)
{
m_dataSent = data;
}
UINT8 CDriveBoard::Read8(UINT32 addr)
{
// TODO - shouldn't end of ROM be 0x7FFF not 0x8FFF?
if (addr < ROM_SIZE) // ROM is 0x0000-0x8FFF
return m_rom[addr];
else if (addr >= 0xE000) // RAM is 0xE000-0xFFFF
return m_ram[(addr - 0xE000) & 0x1FFF];
else
{
//DebugLog("Unhandled Z80 read of %08X (at PC = %04X)\n", addr, m_z80.GetPC());
return 0xFF;
}
}
void CDriveBoard::Write8(UINT32 addr, UINT8 data)
{
if (addr >= 0xE000) // RAM is 0xE000-0xFFFF
m_ram[(addr - 0xE000) & 0x1FFF] = data;
#ifdef DEBUG
else
DebugLog("Unhandled Z80 write to %08X (at PC = %04X)\n", addr, m_z80.GetPC());
#endif
}
UINT8 CDriveBoard::IORead8(UINT32 portNum)
{
return 0xff;
}
void CDriveBoard::IOWrite8(UINT32 portNum, UINT8 data)
{
}
void CDriveBoard::RunFrame(void)
{
if (IsDisabled())
{
return;
}
// Assuming Z80 runs @ 4.0MHz and NMI triggers @ 60.0KHz for WheelBoard and JoystickBoard
// Assuming Z80 runs @ 8.0MHz and INT triggers @ 60.0KHz for BillBoard
// TODO - find out if Z80 frequency is correct and exact frequency of NMI interrupts (just guesswork at the moment!)
int cycles = (int)(m_z80Clock * 1000000 / 60);
int loopCycles = 10000;
while (cycles > 0)
{
if (m_allowInterrupts)
{
if(m_z80NMI)
m_z80.TriggerNMI();
else
m_z80.SetINT(true);
}
cycles -= m_z80.Run(std::min<int>(loopCycles, cycles));
}
}
bool CDriveBoard::Init(const UINT8* romPtr)
{
// This constructor must present a valid ROM
if (!romPtr)
{
return ErrorLog("Internal error: no drive board ROM supplied.");
}
// Assign ROM (note that the ROM data has not yet been loaded)
m_rom = romPtr;
// Allocate memory for RAM
m_ram = new (std::nothrow) UINT8[RAM_SIZE];
if (NULL == m_ram)
{
float ramSizeMB = (float)RAM_SIZE / (float)0x100000;
return ErrorLog("Insufficient memory for drive board (needs %1.1f MB).", ramSizeMB);
}
memset(m_ram, 0, RAM_SIZE);
// Initialize Z80
m_z80.Init(this, NULL);
// We are attached
m_attached = true;
return OKAY;
}
// Dummy drive board (not attached)
bool CDriveBoard::Init(void)
{
// Use an empty dummy ROM otherwise so debugger doesn't crash if it
// tries to read our memory or if we accidentally run the Z80 (which
// should never happen in a detached board state).
if (!m_dummyROM)
{
uint8_t *rom = new (std::nothrow) uint8_t[ROM_SIZE];
if (NULL == rom)
{
return ErrorLog("Insufficient memory for drive board.");
}
memset(rom, 0xFF, ROM_SIZE);
m_dummyROM = rom;
}
bool result = Init(m_dummyROM);
m_attached = false; // force detached
return result;
}
void CDriveBoard::Reset()
{
m_initialized = false;
m_initState = 0;
m_readMode = 0;
m_boardMode = 0;
m_wheelCenter = 0x80;
m_allowInterrupts = false;
m_dataSent = 0;
m_dataReceived = 0;
m_z80.Reset(); // always reset to provide a valid Z80 state
// Configure options (cannot be done in Init() because command line settings weren't yet parsed)
SetForceFeedbackStrength(m_config["ForceFeedbackStrength"].ValueAsDefault<unsigned>(5));
// Enable only if attached -- **this is the only place this flag should ever be cleared**
if (m_attached)
m_disabled = false;
}
CDriveBoard::CDriveBoard(const Util::Config::Node& config)
: m_config(config),
m_attached(false),
m_disabled(true), // begin in disabled state -- can be enabled only 1) if attached and 2) by successful reset
m_simulated(false),
m_initialized(false),
m_allowInterrupts(false),
m_dataSent(0),
m_dataReceived(0),
m_dip1(0x00),
m_dip2(0x00),
m_initState(0),
m_statusFlags(0),
m_boardMode(0),
m_readMode(0),
m_wheelCenter(0),
m_cockpitCenter(0),
m_echoVal(0),
m_rom(NULL),
m_ram(NULL),
m_dummyROM(NULL),
m_z80Clock(4.0),
m_z80NMI(true),
m_inputs(NULL),
m_inputFlags(0),
m_outputs(NULL)
{
DebugLog("Built Drive Board\n");
}
CDriveBoard::~CDriveBoard(void)
{
if (m_ram != NULL)
{
delete[] m_ram;
m_ram = NULL;
}
if (m_dummyROM != NULL)
{
delete [] m_dummyROM;
m_dummyROM = NULL;
}
m_rom = NULL;
m_inputs = NULL;
m_outputs = NULL;
DebugLog("Destroyed Drive Board\n");
}