stella/src/emucore/OSystem.cxx

726 lines
22 KiB
C++

//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2014 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//
// $Id$
//============================================================================
#include <cassert>
#include <sstream>
#include <fstream>
#include <ctime>
#ifdef HAVE_GETTIMEOFDAY
#include <sys/time.h>
#endif
#include "bspf.hxx"
#include "MediaFactory.hxx"
#include "Sound.hxx"
#ifdef DEBUGGER_SUPPORT
#include "Debugger.hxx"
#endif
#ifdef CHEATCODE_SUPPORT
#include "CheatManager.hxx"
#endif
#include "FSNode.hxx"
#include "MD5.hxx"
#include "Cart.hxx"
#include "Settings.hxx"
#include "PropsSet.hxx"
#include "EventHandler.hxx"
#include "Menu.hxx"
#include "CommandMenu.hxx"
#include "Launcher.hxx"
#include "Widget.hxx"
#include "Console.hxx"
#include "Random.hxx"
#include "SerialPort.hxx"
#include "StateManager.hxx"
#include "Version.hxx"
#include "OSystem.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OSystem::OSystem()
: myConsole(nullptr),
myLauncherUsed(false),
myDebugger(nullptr),
myQuitLoop(false),
myRomFile(""),
myRomMD5(""),
myFeatures(""),
myBuildInfo("")
{
// Calculate startup time
myMillisAtStart = (uInt32)(time(NULL) * 1000);
// Get built-in features
#ifdef SOUND_SUPPORT
myFeatures += "Sound ";
#endif
#ifdef JOYSTICK_SUPPORT
myFeatures += "Joystick ";
#endif
#ifdef DEBUGGER_SUPPORT
myFeatures += "Debugger ";
#endif
#ifdef CHEATCODE_SUPPORT
myFeatures += "Cheats";
#endif
// Get build info
ostringstream info;
SDL_version ver;
SDL_GetVersion(&ver);
info << "Build " << STELLA_BUILD << ", using SDL " << (int)ver.major
<< "." << (int)ver.minor << "."<< (int)ver.patch
<< " [" << BSPF_ARCH << "]";
myBuildInfo = info.str();
mySettings = MediaFactory::createSettings(*this);
myRandom = make_ptr<Random>(*this);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OSystem::~OSystem()
{
// Remove any game console that is currently attached
deleteConsole();
#ifdef DEBUGGER_SUPPORT
delete myDebugger;
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool OSystem::create()
{
// Get updated paths for all configuration files
setConfigPaths();
ostringstream buf;
buf << "Stella " << STELLA_VERSION << endl
<< " Features: " << myFeatures << endl
<< " " << myBuildInfo << endl << endl
<< "Base directory: '"
<< FilesystemNode(myBaseDir).getShortPath() << "'" << endl
<< "Configuration file: '"
<< FilesystemNode(myConfigFile).getShortPath() << "'" << endl
<< "User game properties: '"
<< FilesystemNode(myPropertiesFile).getShortPath() << "'" << endl;
logMessage(buf.str(), 1);
// NOTE: The framebuffer MUST be created before any other object!!!
// Get relevant information about the video hardware
// This must be done before any graphics context is created, since
// it may be needed to initialize the size of graphical objects
myFrameBuffer = MediaFactory::createVideo(*this);
if(!myFrameBuffer->initialize())
return false;
// Create the event handler for the system
myEventHandler = MediaFactory::createEventHandler(*this);
myEventHandler->initialize();
// Create a properties set for us to use and set it up
myPropSet = make_ptr<PropertiesSet>(propertiesFile());
#ifdef CHEATCODE_SUPPORT
myCheatManager = make_ptr<CheatManager>(*this);
myCheatManager->loadCheatDatabase();
#endif
// Create menu and launcher GUI objects
myMenu = make_ptr<Menu>(*this);
myCommandMenu = make_ptr<CommandMenu>(*this);
myLauncher = make_ptr<Launcher>(*this);
myStateManager = make_ptr<StateManager>(*this);
// Create the sound object; the sound subsystem isn't actually
// opened until needed, so this is non-blocking (on those systems
// that only have a single sound device (no hardware mixing)
createSound();
// Create the serial port object
// This is used by any controller that wants to directly access
// a real serial port on the system
mySerialPort = MediaFactory::createSerialPort();
// Re-initialize random seed
myRandom->initSeed();
// Create PNG handler
myPNGLib = make_ptr<PNGLibrary>(*myFrameBuffer);
// Create ZIP handler
myZipHandler = make_ptr<ZipHandler>();
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::loadConfig()
{
mySettings->loadConfig();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::saveConfig()
{
// Ask all subsystems to save their settings
if(myFrameBuffer)
myFrameBuffer->tiaSurface().ntsc().saveConfig(*mySettings);
mySettings->saveConfig();
}
#ifdef DEBUGGER_SUPPORT
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::createDebugger(Console& console)
{
delete myDebugger; myDebugger = NULL;
myDebugger = new Debugger(*this, console);
myDebugger->initialize();
}
#endif
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::setConfigPaths()
{
// Paths are saved with special characters preserved ('~' or '.')
// We do some error checking here, so the rest of the codebase doesn't
// have to worry about it
FilesystemNode node;
string s;
validatePath(myStateDir, "statedir", myBaseDir + "state");
validatePath(mySnapshotSaveDir, "snapsavedir", defaultSnapSaveDir());
validatePath(mySnapshotLoadDir, "snaploaddir", defaultSnapLoadDir());
validatePath(myNVRamDir, "nvramdir", myBaseDir + "nvram");
validatePath(myCfgDir, "cfgdir", myBaseDir + "cfg");
s = mySettings->getString("cheatfile");
if(s == "") s = myBaseDir + "stella.cht";
node = FilesystemNode(s);
myCheatFile = node.getPath();
mySettings->setValue("cheatfile", node.getShortPath());
s = mySettings->getString("palettefile");
if(s == "") s = myBaseDir + "stella.pal";
node = FilesystemNode(s);
myPaletteFile = node.getPath();
mySettings->setValue("palettefile", node.getShortPath());
s = mySettings->getString("propsfile");
if(s == "") s = myBaseDir + "stella.pro";
node = FilesystemNode(s);
myPropertiesFile = node.getPath();
mySettings->setValue("propsfile", node.getShortPath());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::setBaseDir(const string& basedir)
{
FilesystemNode node(basedir);
if(!node.isDirectory())
node.makeDir();
myBaseDir = node.getPath();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::setConfigFile(const string& file)
{
FilesystemNode node(file);
myConfigFile = node.getPath();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::setFramerate(float framerate)
{
if(framerate > 0.0)
{
myDisplayFrameRate = framerate;
myTimePerFrame = (uInt32)(1000000.0 / myDisplayFrameRate);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FBInitStatus OSystem::createFrameBuffer()
{
// Re-initialize the framebuffer to current settings
FBInitStatus fbstatus = kFailComplete;
switch(myEventHandler->state())
{
case EventHandler::S_EMULATE:
case EventHandler::S_PAUSE:
case EventHandler::S_MENU:
case EventHandler::S_CMDMENU:
if((fbstatus = myConsole->initializeVideo()) != kSuccess)
return fbstatus;
break; // S_EMULATE, S_PAUSE, S_MENU, S_CMDMENU
case EventHandler::S_LAUNCHER:
if((fbstatus = myLauncher->initializeVideo()) != kSuccess)
return fbstatus;
break; // S_LAUNCHER
#ifdef DEBUGGER_SUPPORT
case EventHandler::S_DEBUGGER:
if((fbstatus = myDebugger->initializeVideo()) != kSuccess)
return fbstatus;
break; // S_DEBUGGER
#endif
default: // Should never happen
logMessage("ERROR: Unknown emulation state in createFrameBuffer()", 0);
break;
}
return fbstatus;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::createSound()
{
if(!mySound)
mySound = MediaFactory::createAudio(*this);
#ifndef SOUND_SUPPORT
mySettings->setValue("sound", false);
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
string OSystem::createConsole(const FilesystemNode& rom, const string& md5sum,
bool newrom)
{
// Do a little error checking; it shouldn't be necessary
if(myConsole) deleteConsole();
bool showmessage = false;
// If same ROM has been given, we reload the current one (assuming one exists)
if(!newrom && rom == myRomFile)
{
showmessage = true; // we show a message if a ROM is being reloaded
}
else
{
myRomFile = rom;
myRomMD5 = md5sum;
// Each time a new console is loaded, we simulate a cart removal
// Some carts need knowledge of this, as they behave differently
// based on how many power-cycles they've been through since plugged in
mySettings->setValue("romloadcount", 0);
}
// Create an instance of the 2600 game console
ostringstream buf;
string type, id;
try
{
myConsole = openConsole(myRomFile, myRomMD5, type, id);
}
catch(const char* err_msg)
{
myConsole = 0;
buf << "ERROR: Couldn't create console (" << err_msg << ")";
logMessage(buf.str(), 0);
return buf.str();
}
if(myConsole)
{
#ifdef DEBUGGER_SUPPORT
myConsole->addDebugger();
#endif
#ifdef CHEATCODE_SUPPORT
myCheatManager->loadCheats(myRomMD5);
#endif
myEventHandler->reset(EventHandler::S_EMULATE);
myEventHandler->setMouseControllerMode(mySettings->getString("usemouse"));
if(createFrameBuffer() != kSuccess) // Takes care of initializeVideo()
{
logMessage("ERROR: Couldn't create framebuffer for console", 0);
myEventHandler->reset(EventHandler::S_LAUNCHER);
return "ERROR: Couldn't create framebuffer for console";
}
myConsole->initializeAudio();
if(showmessage)
{
if(id == "")
myFrameBuffer->showMessage("New console created");
else
myFrameBuffer->showMessage("Multicart " + type + ", loading ROM" + id);
}
buf << "Game console created:" << endl
<< " ROM file: " << myRomFile.getShortPath() << endl << endl
<< getROMInfo(myConsole) << endl;
logMessage(buf.str(), 1);
// Update the timing info for a new console run
resetLoopTiming();
myFrameBuffer->setCursorState();
// Also check if certain virtual buttons should be held down
// These must be checked each time a new console is being created
if(mySettings->getBool("holdreset"))
myEventHandler->handleEvent(Event::ConsoleReset, 1);
if(mySettings->getBool("holdselect"))
myEventHandler->handleEvent(Event::ConsoleSelect, 1);
const string& holdjoy0 = mySettings->getString("holdjoy0");
if(BSPF_containsIgnoreCase(holdjoy0, "U"))
myEventHandler->handleEvent(Event::JoystickZeroUp, 1);
if(BSPF_containsIgnoreCase(holdjoy0, "D"))
myEventHandler->handleEvent(Event::JoystickZeroDown, 1);
if(BSPF_containsIgnoreCase(holdjoy0, "L"))
myEventHandler->handleEvent(Event::JoystickZeroLeft, 1);
if(BSPF_containsIgnoreCase(holdjoy0, "R"))
myEventHandler->handleEvent(Event::JoystickZeroRight, 1);
if(BSPF_containsIgnoreCase(holdjoy0, "F"))
myEventHandler->handleEvent(Event::JoystickZeroFire, 1);
const string& holdjoy1 = mySettings->getString("holdjoy1");
if(BSPF_containsIgnoreCase(holdjoy1, "U"))
myEventHandler->handleEvent(Event::JoystickOneUp, 1);
if(BSPF_containsIgnoreCase(holdjoy1, "D"))
myEventHandler->handleEvent(Event::JoystickOneDown, 1);
if(BSPF_containsIgnoreCase(holdjoy1, "L"))
myEventHandler->handleEvent(Event::JoystickOneLeft, 1);
if(BSPF_containsIgnoreCase(holdjoy1, "R"))
myEventHandler->handleEvent(Event::JoystickOneRight, 1);
if(BSPF_containsIgnoreCase(holdjoy1, "F"))
myEventHandler->handleEvent(Event::JoystickOneFire, 1);
#ifdef DEBUGGER_SUPPORT
if(mySettings->getBool("debug"))
myEventHandler->enterDebugMode();
#endif
}
return EmptyString;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::deleteConsole()
{
if(myConsole)
{
mySound->close();
#ifdef CHEATCODE_SUPPORT
myCheatManager->saveCheats(myConsole->properties().get(Cartridge_MD5));
#endif
ostringstream buf;
double executionTime = (double) myTimingInfo.totalTime / 1000000.0;
double framesPerSecond = (double) myTimingInfo.totalFrames / executionTime;
buf << "Game console stats:" << endl
<< " Total frames drawn: " << myTimingInfo.totalFrames << endl
<< " Total time (sec): " << executionTime << endl
<< " Frames per second: " << framesPerSecond << endl
<< endl;
logMessage(buf.str(), 1);
delete myConsole; myConsole = NULL;
#ifdef DEBUGGER_SUPPORT
delete myDebugger; myDebugger = NULL;
#endif
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool OSystem::reloadConsole()
{
deleteConsole();
return createConsole(myRomFile, myRomMD5, false) == EmptyString;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool OSystem::createLauncher(const string& startdir)
{
mySettings->setValue("tmpromdir", startdir);
bool status = false;
myEventHandler->reset(EventHandler::S_LAUNCHER);
if(createFrameBuffer() == kSuccess)
{
myLauncher->reStack();
myFrameBuffer->setCursorState();
myFrameBuffer->refresh();
setFramerate(60);
resetLoopTiming();
status = true;
}
else
logMessage("ERROR: Couldn't create launcher", 0);
myLauncherUsed = myLauncherUsed || status;
return status;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
string OSystem::getROMInfo(const FilesystemNode& romfile)
{
string md5, type, id, result = "";
Console* console = 0;
try
{
console = openConsole(romfile, md5, type, id);
}
catch(const char* err_msg)
{
ostringstream buf;
buf << "ERROR: Couldn't get ROM info (" << err_msg << ")";
return buf.str();
}
result = getROMInfo(console);
delete console;
return result;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::logMessage(const string& message, uInt8 level)
{
if(level == 0)
{
cout << message << endl << flush;
myLogMessages += message + "\n";
}
else if(level <= (uInt8)mySettings->getInt("loglevel"))
{
if(mySettings->getBool("logtoconsole"))
cout << message << endl << flush;
myLogMessages += message + "\n";
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Console* OSystem::openConsole(const FilesystemNode& romfile, string& md5,
string& type, string& id)
{
#define CMDLINE_PROPS_UPDATE(cl_name, prop_name) \
s = mySettings->getString(cl_name); \
if(s != "") props.set(prop_name, s);
Console* console = (Console*) NULL;
// Open the cartridge image and read it in
uInt8* image = 0;
uInt32 size = 0;
if((image = openROM(romfile, md5, size)) != 0)
{
// Get a valid set of properties, including any entered on the commandline
// For initial creation of the Cart, we're only concerned with the BS type
Properties props;
myPropSet->getMD5(md5, props);
string s;
CMDLINE_PROPS_UPDATE("bs", Cartridge_Type);
CMDLINE_PROPS_UPDATE("type", Cartridge_Type);
// Now create the cartridge
string cartmd5 = md5;
type = props.get(Cartridge_Type);
Cartridge* cart =
Cartridge::create(image, size, cartmd5, type, id, *this, *mySettings);
// It's possible that the cart created was from a piece of the image,
// and that the md5 (and hence the cart) has changed
if(props.get(Cartridge_MD5) != cartmd5)
{
if(!myPropSet->getMD5(cartmd5, props))
{
// Cart md5 wasn't found, so we create a new props for it
props.set(Cartridge_MD5, cartmd5);
props.set(Cartridge_Name, props.get(Cartridge_Name)+id);
myPropSet->insert(props, false);
}
}
CMDLINE_PROPS_UPDATE("channels", Cartridge_Sound);
CMDLINE_PROPS_UPDATE("ld", Console_LeftDifficulty);
CMDLINE_PROPS_UPDATE("rd", Console_RightDifficulty);
CMDLINE_PROPS_UPDATE("tv", Console_TelevisionType);
CMDLINE_PROPS_UPDATE("sp", Console_SwapPorts);
CMDLINE_PROPS_UPDATE("lc", Controller_Left);
CMDLINE_PROPS_UPDATE("rc", Controller_Right);
s = mySettings->getString("bc");
if(s != "") { props.set(Controller_Left, s); props.set(Controller_Right, s); }
CMDLINE_PROPS_UPDATE("cp", Controller_SwapPaddles);
CMDLINE_PROPS_UPDATE("ma", Controller_MouseAxis);
CMDLINE_PROPS_UPDATE("format", Display_Format);
CMDLINE_PROPS_UPDATE("ystart", Display_YStart);
CMDLINE_PROPS_UPDATE("height", Display_Height);
CMDLINE_PROPS_UPDATE("pp", Display_Phosphor);
CMDLINE_PROPS_UPDATE("ppblend", Display_PPBlend);
// Finally, create the cart with the correct properties
if(cart)
console = new Console(*this, cart, props);
}
// Free the image since we don't need it any longer
if(image != 0 && size > 0)
delete[] image;
return console;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt8* OSystem::openROM(const FilesystemNode& rom, string& md5, uInt32& size)
{
// This method has a documented side-effect:
// It not only loads a ROM and creates an array with its contents,
// but also adds a properties entry if the one for the ROM doesn't
// contain a valid name
uInt8* image = 0;
if((size = rom.read(image)) == 0)
{
delete[] image;
return (uInt8*) 0;
}
// If we get to this point, we know we have a valid file to open
// Now we make sure that the file has a valid properties entry
// To save time, only generate an MD5 if we really need one
if(md5 == "")
md5 = MD5(image, size);
// Some games may not have a name, since there may not
// be an entry in stella.pro. In that case, we use the rom name
// and reinsert the properties object
Properties props;
myPropSet->getMD5WithInsert(rom, md5, props);
return image;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
string OSystem::getROMInfo(const Console* console)
{
const ConsoleInfo& info = console->about();
ostringstream buf;
buf << " Cart Name: " << info.CartName << endl
<< " Cart MD5: " << info.CartMD5 << endl
<< " Controller 0: " << info.Control0 << endl
<< " Controller 1: " << info.Control1 << endl
<< " Display Format: " << info.DisplayFormat << endl
<< " Bankswitch Type: " << info.BankSwitch << endl;
return buf.str();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::resetLoopTiming()
{
myTimingInfo.start = myTimingInfo.virt = getTicks();
myTimingInfo.current = 0;
myTimingInfo.totalTime = 0;
myTimingInfo.totalFrames = 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::validatePath(string& path, const string& setting,
const string& defaultpath)
{
const string& s = mySettings->getString(setting) == "" ? defaultpath :
mySettings->getString(setting);
FilesystemNode node(s);
if(!node.isDirectory())
node.makeDir();
path = node.getPath();
mySettings->setValue(setting, node.getShortPath());
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt64 OSystem::getTicks() const
{
#ifdef HAVE_GETTIMEOFDAY
// Gettimeofday natively refers to the UNIX epoch (a set time in the past)
timeval now;
gettimeofday(&now, 0);
return uInt64(now.tv_sec) * 1000000 + now.tv_usec;
#else
// We use SDL_GetTicks, but add in the time when the application was
// initialized. This is necessary, since SDL_GetTicks only measures how
// long SDL has been running, which can be the same between multiple runs
// of the application.
return uInt64(SDL_GetTicks() + myMillisAtStart) * 1000;
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void OSystem::mainLoop()
{
if(mySettings->getString("timing") == "sleep")
{
// Sleep-based wait: good for CPU, bad for graphical sync
for(;;)
{
myTimingInfo.start = getTicks();
myEventHandler->poll(myTimingInfo.start);
if(myQuitLoop) break; // Exit if the user wants to quit
myFrameBuffer->update();
myTimingInfo.current = getTicks();
myTimingInfo.virt += myTimePerFrame;
// Timestamps may periodically go out of sync, particularly on systems
// that can have 'negative time' (ie, when the time seems to go backwards)
// This normally results in having a very large delay time, so we check
// for that and reset the timers when appropriate
if((myTimingInfo.virt - myTimingInfo.current) > (myTimePerFrame << 1))
{
myTimingInfo.start = myTimingInfo.current = myTimingInfo.virt = getTicks();
}
if(myTimingInfo.current < myTimingInfo.virt)
SDL_Delay(uInt32(myTimingInfo.virt - myTimingInfo.current) / 1000);
myTimingInfo.totalTime += (getTicks() - myTimingInfo.start);
myTimingInfo.totalFrames++;
}
}
else
{
// Busy-wait: bad for CPU, good for graphical sync
for(;;)
{
myTimingInfo.start = getTicks();
myEventHandler->poll(myTimingInfo.start);
if(myQuitLoop) break; // Exit if the user wants to quit
myFrameBuffer->update();
myTimingInfo.virt += myTimePerFrame;
while(getTicks() < myTimingInfo.virt)
; // busy-wait
myTimingInfo.totalTime += (getTicks() - myTimingInfo.start);
myTimingInfo.totalFrames++;
}
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
unique_ptr<ZipHandler> OSystem::myZipHandler = unique_ptr<ZipHandler>();