//============================================================================ // // 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 #include #include #include #ifdef HAVE_GETTIMEOFDAY #include #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(*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(propertiesFile()); #ifdef CHEATCODE_SUPPORT myCheatManager = make_ptr(*this); myCheatManager->loadCheatDatabase(); #endif // Create menu and launcher GUI objects myMenu = make_ptr(*this); myCommandMenu = make_ptr(*this); myLauncher = make_ptr(*this); myStateManager = make_ptr(*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(*myFrameBuffer); // Create ZIP handler myZipHandler = make_ptr(); 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 OSystem::myZipHandler = unique_ptr();