1 | //============================================================================ |
2 | // |
3 | // SSSS tt lll lll |
4 | // SS SS tt ll ll |
5 | // SS tttttt eeee ll ll aaaa |
6 | // SSSS tt ee ee ll ll aa |
7 | // SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator" |
8 | // SS SS tt ee ll ll aa aa |
9 | // SSSS ttt eeeee llll llll aaaaa |
10 | // |
11 | // Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony |
12 | // and the Stella Team |
13 | // |
14 | // See the file "License.txt" for information on usage and redistribution of |
15 | // this file, and for a DISCLAIMER OF ALL WARRANTIES. |
16 | //============================================================================ |
17 | |
18 | #include <cassert> |
19 | #include <functional> |
20 | |
21 | #include "bspf.hxx" |
22 | #include "Logger.hxx" |
23 | |
24 | #include "MediaFactory.hxx" |
25 | #include "Sound.hxx" |
26 | |
27 | #ifdef CHEATCODE_SUPPORT |
28 | #include "CheatManager.hxx" |
29 | #endif |
30 | #ifdef DEBUGGER_SUPPORT |
31 | #include "Debugger.hxx" |
32 | #endif |
33 | #ifdef GUI_SUPPORT |
34 | #include "Menu.hxx" |
35 | #include "CommandMenu.hxx" |
36 | #include "Launcher.hxx" |
37 | #include "TimeMachine.hxx" |
38 | #include "Widget.hxx" |
39 | #endif |
40 | #ifdef SQLITE_SUPPORT |
41 | #include "KeyValueRepositorySqlite.hxx" |
42 | #include "SettingsDb.hxx" |
43 | #endif |
44 | |
45 | #include "FSNode.hxx" |
46 | #include "MD5.hxx" |
47 | #include "Cart.hxx" |
48 | #include "CartDetector.hxx" |
49 | #include "FrameBuffer.hxx" |
50 | #include "TIASurface.hxx" |
51 | #include "TIAConstants.hxx" |
52 | #include "Settings.hxx" |
53 | #include "PropsSet.hxx" |
54 | #include "EventHandler.hxx" |
55 | #include "PNGLibrary.hxx" |
56 | #include "Console.hxx" |
57 | #include "Random.hxx" |
58 | #include "StateManager.hxx" |
59 | #include "TimerManager.hxx" |
60 | #include "Version.hxx" |
61 | #include "TIA.hxx" |
62 | #include "DispatchResult.hxx" |
63 | #include "EmulationWorker.hxx" |
64 | #include "AudioSettings.hxx" |
65 | #include "repository/KeyValueRepositoryNoop.hxx" |
66 | #include "repository/KeyValueRepositoryConfigfile.hxx" |
67 | #include "M6532.hxx" |
68 | |
69 | #include "OSystem.hxx" |
70 | |
71 | using namespace std::chrono; |
72 | |
73 | namespace { |
74 | constexpr uInt32 FPS_METER_QUEUE_SIZE = 100; |
75 | } |
76 | |
77 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
78 | OSystem::OSystem() |
79 | : myLauncherUsed(false), |
80 | myQuitLoop(false), |
81 | mySettingsLoaded(false), |
82 | myFpsMeter(FPS_METER_QUEUE_SIZE) |
83 | { |
84 | // Get built-in features |
85 | #ifdef SOUND_SUPPORT |
86 | myFeatures += "Sound " ; |
87 | #endif |
88 | #ifdef JOYSTICK_SUPPORT |
89 | myFeatures += "Joystick " ; |
90 | #endif |
91 | #ifdef DEBUGGER_SUPPORT |
92 | myFeatures += "Debugger " ; |
93 | #endif |
94 | #ifdef CHEATCODE_SUPPORT |
95 | myFeatures += "Cheats " ; |
96 | #endif |
97 | #ifdef PNG_SUPPORT |
98 | myFeatures += "PNG " ; |
99 | #endif |
100 | #ifdef ZIP_SUPPORT |
101 | myFeatures += "ZIP" ; |
102 | #endif |
103 | |
104 | // Get build info |
105 | ostringstream info; |
106 | |
107 | info << "Build " << STELLA_BUILD << ", using " << MediaFactory::backendName() |
108 | << " [" << BSPF::ARCH << "]" ; |
109 | myBuildInfo = info.str(); |
110 | |
111 | mySettings = MediaFactory::createSettings(); |
112 | |
113 | myPropSet = make_unique<PropertiesSet>(); |
114 | |
115 | Logger::instance().setLogCallback( |
116 | std::bind(&OSystem::logMessage, this, std::placeholders::_1, std::placeholders::_2) |
117 | ); |
118 | } |
119 | |
120 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
121 | OSystem::~OSystem() |
122 | { |
123 | Logger::instance().clearLogCallback(); |
124 | } |
125 | |
126 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
127 | bool OSystem::create() |
128 | { |
129 | ostringstream buf; |
130 | buf << "Stella " << STELLA_VERSION << endl |
131 | << " Features: " << myFeatures << endl |
132 | << " " << myBuildInfo << endl << endl |
133 | << "Base directory: '" |
134 | << FilesystemNode(myBaseDir).getShortPath() << "'" << endl |
135 | << "State directory: '" |
136 | << FilesystemNode(myStateDir).getShortPath() << "'" << endl |
137 | << "NVRam directory: '" |
138 | << FilesystemNode(myNVRamDir).getShortPath() << "'" << endl; |
139 | |
140 | if(!myConfigFile.empty()) |
141 | buf << "Configuration file: '" |
142 | << FilesystemNode(myConfigFile).getShortPath() << "'" << endl; |
143 | |
144 | buf << "Game properties: '" |
145 | << FilesystemNode(myPropertiesFile).getShortPath() << "'" << endl |
146 | << "Cheat file: '" |
147 | << FilesystemNode(myCheatFile).getShortPath() << "'" << endl |
148 | << "Palette file: '" |
149 | << FilesystemNode(myPaletteFile).getShortPath() << "'" << endl; |
150 | Logger::info(buf.str()); |
151 | |
152 | // NOTE: The framebuffer MUST be created before any other object!!! |
153 | // Get relevant information about the video hardware |
154 | // This must be done before any graphics context is created, since |
155 | // it may be needed to initialize the size of graphical objects |
156 | try { myFrameBuffer = MediaFactory::createVideo(*this); } |
157 | catch(...) { return false; } |
158 | if(!myFrameBuffer->initialize()) |
159 | return false; |
160 | |
161 | // Create the event handler for the system |
162 | myEventHandler = MediaFactory::createEventHandler(*this); |
163 | myEventHandler->initialize(); |
164 | |
165 | myStateManager = make_unique<StateManager>(*this); |
166 | myTimerManager = make_unique<TimerManager>(); |
167 | myAudioSettings = make_unique<AudioSettings>(*mySettings); |
168 | |
169 | // Create the sound object; the sound subsystem isn't actually |
170 | // opened until needed, so this is non-blocking (on those systems |
171 | // that only have a single sound device (no hardware mixing)) |
172 | createSound(); |
173 | |
174 | // Create random number generator |
175 | myRandom = make_unique<Random>(uInt32(TimerManager::getTicks())); |
176 | |
177 | #ifdef CHEATCODE_SUPPORT |
178 | myCheatManager = make_unique<CheatManager>(*this); |
179 | myCheatManager->loadCheatDatabase(); |
180 | #endif |
181 | |
182 | #ifdef GUI_SUPPORT |
183 | // Create various subsystems (menu and launcher GUI objects, etc) |
184 | myMenu = make_unique<Menu>(*this); |
185 | myCommandMenu = make_unique<CommandMenu>(*this); |
186 | myTimeMachine = make_unique<TimeMachine>(*this); |
187 | myLauncher = make_unique<Launcher>(*this); |
188 | #endif |
189 | |
190 | #ifdef PNG_SUPPORT |
191 | // Create PNG handler |
192 | myPNGLib = make_unique<PNGLibrary>(*this); |
193 | #endif |
194 | |
195 | myPropSet->load(myPropertiesFile); |
196 | |
197 | return true; |
198 | } |
199 | |
200 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
201 | void OSystem::loadConfig(const Settings::Options& options) |
202 | { |
203 | // Get base directory and config file from derived class |
204 | // It will decide whether it can override its default location |
205 | getBaseDirAndConfig(myBaseDir, myConfigFile, |
206 | myDefaultSaveDir, myDefaultLoadDir, |
207 | ourOverrideBaseDirWithApp, ourOverrideBaseDir); |
208 | |
209 | // Get fully-qualified pathnames, and make directories when needed |
210 | FilesystemNode node(myBaseDir); |
211 | if(!node.isDirectory()) |
212 | node.makeDir(); |
213 | myBaseDir = node.getPath(); |
214 | if(!myConfigFile.empty()) |
215 | myConfigFile = FilesystemNode(myConfigFile).getPath(); |
216 | |
217 | FilesystemNode save(myDefaultSaveDir); |
218 | if(!save.isDirectory()) |
219 | save.makeDir(); |
220 | myDefaultSaveDir = save.getShortPath(); |
221 | |
222 | FilesystemNode load(myDefaultLoadDir); |
223 | if(!load.isDirectory()) |
224 | load.makeDir(); |
225 | myDefaultLoadDir = load.getShortPath(); |
226 | |
227 | #ifdef SQLITE_SUPPORT |
228 | mySettingsDb = make_shared<SettingsDb>(myBaseDir, "settings" ); |
229 | if(!mySettingsDb->initialize()) |
230 | mySettingsDb.reset(); |
231 | #endif |
232 | |
233 | mySettings->setRepository(createSettingsRepository()); |
234 | |
235 | Logger::debug("Loading config options ..." ); |
236 | mySettings->load(options); |
237 | mySettingsLoaded = true; |
238 | |
239 | // Get updated paths for all configuration files |
240 | setConfigPaths(); |
241 | } |
242 | |
243 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
244 | void OSystem::saveConfig() |
245 | { |
246 | // Ask all subsystems to save their settings |
247 | if(myFrameBuffer) |
248 | { |
249 | // Save the last windowed position and display on system shutdown |
250 | myFrameBuffer->updateWindowedPos(); |
251 | settings().setValue("display" , myFrameBuffer->getCurrentDisplayIndex()); |
252 | |
253 | Logger::debug("Saving TV effects options ..." ); |
254 | myFrameBuffer->tiaSurface().ntsc().saveConfig(settings()); |
255 | } |
256 | |
257 | Logger::debug("Saving config options ..." ); |
258 | mySettings->save(); |
259 | |
260 | if(myPropSet && myPropSet->save(myPropertiesFile)) |
261 | Logger::debug("Saving properties set ..." ); |
262 | } |
263 | |
264 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
265 | void OSystem::setConfigPaths() |
266 | { |
267 | // Make sure all required directories actually exist |
268 | auto buildDirIfRequired = [](string& path, const string& pathToBuild) |
269 | { |
270 | FilesystemNode node(pathToBuild); |
271 | if(!node.isDirectory()) |
272 | node.makeDir(); |
273 | |
274 | path = node.getPath(); |
275 | }; |
276 | |
277 | buildDirIfRequired(myStateDir, myBaseDir + "state" ); |
278 | buildDirIfRequired(myNVRamDir, myBaseDir + "nvram" ); |
279 | |
280 | #ifdef PNG_SUPPORT |
281 | mySnapshotSaveDir = mySettings->getString("snapsavedir" ); |
282 | if(mySnapshotSaveDir == "" ) mySnapshotSaveDir = defaultSaveDir(); |
283 | buildDirIfRequired(mySnapshotSaveDir, mySnapshotSaveDir); |
284 | |
285 | mySnapshotLoadDir = mySettings->getString("snaploaddir" ); |
286 | if(mySnapshotLoadDir == "" ) mySnapshotLoadDir = defaultLoadDir(); |
287 | buildDirIfRequired(mySnapshotLoadDir, mySnapshotLoadDir); |
288 | #endif |
289 | |
290 | myCheatFile = FilesystemNode(myBaseDir + "stella.cht" ).getPath(); |
291 | myPaletteFile = FilesystemNode(myBaseDir + "stella.pal" ).getPath(); |
292 | myPropertiesFile = FilesystemNode(myBaseDir + "stella.pro" ).getPath(); |
293 | |
294 | #if 0 |
295 | // Debug code |
296 | auto dbgPath = [](const string& desc, const string& location) |
297 | { |
298 | cerr << desc << ": " << location << endl; |
299 | }; |
300 | dbgPath("base dir " , myBaseDir); |
301 | dbgPath("state dir " , myStateDir); |
302 | dbgPath("nvram dir " , myNVRamDir); |
303 | dbgPath("ssave dir " , mySnapshotSaveDir); |
304 | dbgPath("sload dir " , mySnapshotLoadDir); |
305 | dbgPath("cheat file" , myCheatFile); |
306 | dbgPath("pal file " , myPaletteFile); |
307 | dbgPath("pro file " , myPropertiesFile); |
308 | dbgPath("INI file " , myConfigFile); |
309 | #endif |
310 | } |
311 | |
312 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
313 | FBInitStatus OSystem::createFrameBuffer() |
314 | { |
315 | // Re-initialize the framebuffer to current settings |
316 | FBInitStatus fbstatus = FBInitStatus::FailComplete; |
317 | switch(myEventHandler->state()) |
318 | { |
319 | case EventHandlerState::EMULATION: |
320 | case EventHandlerState::PAUSE: |
321 | #ifdef GUI_SUPPORT |
322 | case EventHandlerState::OPTIONSMENU: |
323 | case EventHandlerState::CMDMENU: |
324 | case EventHandlerState::TIMEMACHINE: |
325 | #endif |
326 | if((fbstatus = myConsole->initializeVideo()) != FBInitStatus::Success) |
327 | return fbstatus; |
328 | break; |
329 | |
330 | #ifdef GUI_SUPPORT |
331 | case EventHandlerState::LAUNCHER: |
332 | if((fbstatus = myLauncher->initializeVideo()) != FBInitStatus::Success) |
333 | return fbstatus; |
334 | break; |
335 | #endif |
336 | |
337 | #ifdef DEBUGGER_SUPPORT |
338 | case EventHandlerState::DEBUGGER: |
339 | if((fbstatus = myDebugger->initializeVideo()) != FBInitStatus::Success) |
340 | return fbstatus; |
341 | break; |
342 | #endif |
343 | |
344 | case EventHandlerState::NONE: // Should never happen |
345 | default: |
346 | Logger::error("ERROR: Unknown emulation state in createFrameBuffer()" ); |
347 | break; |
348 | } |
349 | return fbstatus; |
350 | } |
351 | |
352 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
353 | void OSystem::createSound() |
354 | { |
355 | if(!mySound) |
356 | mySound = MediaFactory::createAudio(*this, *myAudioSettings); |
357 | #ifndef SOUND_SUPPORT |
358 | myAudioSettings->setEnabled(false); |
359 | #endif |
360 | } |
361 | |
362 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
363 | string OSystem::createConsole(const FilesystemNode& rom, const string& md5sum, |
364 | bool newrom) |
365 | { |
366 | bool showmessage = false; |
367 | |
368 | // If same ROM has been given, we reload the current one (assuming one exists) |
369 | if(!newrom && rom == myRomFile) |
370 | { |
371 | showmessage = true; // we show a message if a ROM is being reloaded |
372 | } |
373 | else |
374 | { |
375 | myRomFile = rom; |
376 | myRomMD5 = md5sum; |
377 | |
378 | // Each time a new console is loaded, we simulate a cart removal |
379 | // Some carts need knowledge of this, as they behave differently |
380 | // based on how many power-cycles they've been through since plugged in |
381 | mySettings->setValue("romloadcount" , 0); |
382 | } |
383 | |
384 | // Create an instance of the 2600 game console |
385 | ostringstream buf; |
386 | |
387 | myEventHandler->handleConsoleStartupEvents(); |
388 | |
389 | try |
390 | { |
391 | closeConsole(); |
392 | myConsole = openConsole(myRomFile, myRomMD5); |
393 | } |
394 | catch(const runtime_error& e) |
395 | { |
396 | buf << "ERROR: Couldn't create console (" << e.what() << ")" ; |
397 | Logger::error(buf.str()); |
398 | return buf.str(); |
399 | } |
400 | |
401 | if(myConsole) |
402 | { |
403 | #ifdef DEBUGGER_SUPPORT |
404 | myDebugger = make_unique<Debugger>(*this, *myConsole); |
405 | myDebugger->initialize(); |
406 | myConsole->attachDebugger(*myDebugger); |
407 | #endif |
408 | #ifdef CHEATCODE_SUPPORT |
409 | myCheatManager->loadCheats(myRomMD5); |
410 | #endif |
411 | myEventHandler->reset(EventHandlerState::EMULATION); |
412 | myEventHandler->setMouseControllerMode(mySettings->getString("usemouse" )); |
413 | if(createFrameBuffer() != FBInitStatus::Success) // Takes care of initializeVideo() |
414 | { |
415 | Logger::error("ERROR: Couldn't create framebuffer for console" ); |
416 | myEventHandler->reset(EventHandlerState::LAUNCHER); |
417 | return "ERROR: Couldn't create framebuffer for console" ; |
418 | } |
419 | myConsole->initializeAudio(); |
420 | |
421 | if(showmessage) |
422 | { |
423 | const string& id = myConsole->cartridge().multiCartID(); |
424 | if(id == "" ) |
425 | myFrameBuffer->showMessage("New console created" ); |
426 | else |
427 | myFrameBuffer->showMessage("Multicart " + |
428 | myConsole->cartridge().detectedType() + ", loading ROM" + id); |
429 | } |
430 | buf << "Game console created:" << endl |
431 | << " ROM file: " << myRomFile.getShortPath() << endl << endl |
432 | << getROMInfo(*myConsole); |
433 | Logger::info(buf.str()); |
434 | |
435 | myFrameBuffer->setCursorState(); |
436 | |
437 | myEventHandler->handleConsoleStartupEvents(); |
438 | myConsole->riot().update(); |
439 | |
440 | #ifdef DEBUGGER_SUPPORT |
441 | if(mySettings->getBool("debug" )) |
442 | myEventHandler->enterDebugMode(); |
443 | #endif |
444 | } |
445 | return EmptyString; |
446 | } |
447 | |
448 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
449 | bool OSystem::reloadConsole() |
450 | { |
451 | return createConsole(myRomFile, myRomMD5, false) == EmptyString; |
452 | } |
453 | |
454 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
455 | bool OSystem::hasConsole() const |
456 | { |
457 | return myConsole != nullptr && |
458 | myEventHandler->state() != EventHandlerState::LAUNCHER; |
459 | } |
460 | |
461 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
462 | bool OSystem::createLauncher(const string& startdir) |
463 | { |
464 | closeConsole(); |
465 | |
466 | if(mySound) |
467 | mySound->close(); |
468 | |
469 | mySettings->setValue("tmpromdir" , startdir); |
470 | bool status = false; |
471 | |
472 | #ifdef GUI_SUPPORT |
473 | myEventHandler->reset(EventHandlerState::LAUNCHER); |
474 | if(createFrameBuffer() == FBInitStatus::Success) |
475 | { |
476 | myLauncher->reStack(); |
477 | myFrameBuffer->setCursorState(); |
478 | |
479 | status = true; |
480 | } |
481 | else |
482 | Logger::error("ERROR: Couldn't create launcher" ); |
483 | #endif |
484 | |
485 | myLauncherUsed = myLauncherUsed || status; |
486 | return status; |
487 | } |
488 | |
489 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
490 | string OSystem::getROMInfo(const FilesystemNode& romfile) |
491 | { |
492 | unique_ptr<Console> console; |
493 | try |
494 | { |
495 | string md5; |
496 | console = openConsole(romfile, md5); |
497 | } |
498 | catch(const runtime_error& e) |
499 | { |
500 | ostringstream buf; |
501 | buf << "ERROR: Couldn't get ROM info (" << e.what() << ")" ; |
502 | return buf.str(); |
503 | } |
504 | |
505 | return getROMInfo(*console); |
506 | } |
507 | |
508 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
509 | void OSystem::logMessage(const string& message, Logger::Level level) |
510 | { |
511 | if(level == Logger::Level::ERR) |
512 | { |
513 | cout << message << endl << std::flush; |
514 | myLogMessages += message + "\n" ; |
515 | } |
516 | else if(int(level) <= uInt8(mySettings->getInt("loglevel" )) || !mySettingsLoaded) |
517 | { |
518 | if(mySettings->getBool("logtoconsole" )) |
519 | cout << message << endl << std::flush; |
520 | myLogMessages += message + "\n" ; |
521 | } |
522 | } |
523 | |
524 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
525 | void OSystem::resetFps() |
526 | { |
527 | myFpsMeter.reset(); |
528 | } |
529 | |
530 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
531 | unique_ptr<Console> OSystem::openConsole(const FilesystemNode& romfile, string& md5) |
532 | { |
533 | unique_ptr<Console> console; |
534 | |
535 | // Open the cartridge image and read it in |
536 | ByteBuffer image; |
537 | uInt32 size = 0; |
538 | if((image = openROM(romfile, md5, size)) != nullptr) |
539 | { |
540 | // Get a valid set of properties, including any entered on the commandline |
541 | // For initial creation of the Cart, we're only concerned with the BS type |
542 | Properties props; |
543 | myPropSet->getMD5(md5, props); |
544 | |
545 | // Local helper method |
546 | auto CMDLINE_PROPS_UPDATE = [&](const string& name, PropType prop) |
547 | { |
548 | const string& s = mySettings->getString(name); |
549 | if(s != "" ) props.set(prop, s); |
550 | }; |
551 | |
552 | CMDLINE_PROPS_UPDATE("bs" , PropType::Cart_Type); |
553 | CMDLINE_PROPS_UPDATE("type" , PropType::Cart_Type); |
554 | CMDLINE_PROPS_UPDATE("startbank" , PropType::Cart_StartBank); |
555 | |
556 | // Now create the cartridge |
557 | string cartmd5 = md5; |
558 | const string& type = props.get(PropType::Cart_Type); |
559 | unique_ptr<Cartridge> cart = |
560 | CartDetector::create(romfile, image, size, cartmd5, type, *mySettings); |
561 | |
562 | // It's possible that the cart created was from a piece of the image, |
563 | // and that the md5 (and hence the cart) has changed |
564 | if(props.get(PropType::Cart_MD5) != cartmd5) |
565 | { |
566 | if(!myPropSet->getMD5(cartmd5, props)) |
567 | { |
568 | // Cart md5 wasn't found, so we create a new props for it |
569 | props.set(PropType::Cart_MD5, cartmd5); |
570 | props.set(PropType::Cart_Name, props.get(PropType::Cart_Name)+cart->multiCartID()); |
571 | myPropSet->insert(props, false); |
572 | } |
573 | } |
574 | |
575 | CMDLINE_PROPS_UPDATE("sp" , PropType::Console_SwapPorts); |
576 | CMDLINE_PROPS_UPDATE("lc" , PropType::Controller_Left); |
577 | CMDLINE_PROPS_UPDATE("rc" , PropType::Controller_Right); |
578 | const string& s = mySettings->getString("bc" ); |
579 | if(s != "" ) { |
580 | props.set(PropType::Controller_Left, s); |
581 | props.set(PropType::Controller_Right, s); |
582 | } |
583 | CMDLINE_PROPS_UPDATE("cp" , PropType::Controller_SwapPaddles); |
584 | CMDLINE_PROPS_UPDATE("ma" , PropType::Controller_MouseAxis); |
585 | CMDLINE_PROPS_UPDATE("channels" , PropType::Cart_Sound); |
586 | CMDLINE_PROPS_UPDATE("ld" , PropType::Console_LeftDiff); |
587 | CMDLINE_PROPS_UPDATE("rd" , PropType::Console_RightDiff); |
588 | CMDLINE_PROPS_UPDATE("tv" , PropType::Console_TVType); |
589 | CMDLINE_PROPS_UPDATE("format" , PropType::Display_Format); |
590 | CMDLINE_PROPS_UPDATE("ystart" , PropType::Display_YStart); |
591 | CMDLINE_PROPS_UPDATE("pp" , PropType::Display_Phosphor); |
592 | CMDLINE_PROPS_UPDATE("ppblend" , PropType::Display_PPBlend); |
593 | |
594 | // Finally, create the cart with the correct properties |
595 | if(cart) |
596 | console = make_unique<Console>(*this, cart, props, *myAudioSettings); |
597 | } |
598 | |
599 | return console; |
600 | } |
601 | |
602 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
603 | void OSystem::closeConsole() |
604 | { |
605 | if(myConsole) |
606 | { |
607 | #ifdef CHEATCODE_SUPPORT |
608 | // If a previous console existed, save cheats before creating a new one |
609 | myCheatManager->saveCheats(myConsole->properties().get(PropType::Cart_MD5)); |
610 | #endif |
611 | myConsole.reset(); |
612 | } |
613 | } |
614 | |
615 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
616 | ByteBuffer OSystem::openROM(const FilesystemNode& rom, string& md5, uInt32& size) |
617 | { |
618 | // This method has a documented side-effect: |
619 | // It not only loads a ROM and creates an array with its contents, |
620 | // but also adds a properties entry if the one for the ROM doesn't |
621 | // contain a valid name |
622 | |
623 | ByteBuffer image; |
624 | if((size = rom.read(image)) == 0) |
625 | return nullptr; |
626 | |
627 | // If we get to this point, we know we have a valid file to open |
628 | // Now we make sure that the file has a valid properties entry |
629 | // To save time, only generate an MD5 if we really need one |
630 | if(md5 == "" ) |
631 | md5 = MD5::hash(image, size); |
632 | |
633 | // Some games may not have a name, since there may not |
634 | // be an entry in stella.pro. In that case, we use the rom name |
635 | // and reinsert the properties object |
636 | Properties props; |
637 | myPropSet->getMD5WithInsert(rom, md5, props); |
638 | |
639 | return image; |
640 | } |
641 | |
642 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
643 | string OSystem::getROMInfo(const Console& console) |
644 | { |
645 | const ConsoleInfo& info = console.about(); |
646 | ostringstream buf; |
647 | |
648 | buf << " Cart Name: " << info.CartName << endl |
649 | << " Cart MD5: " << info.CartMD5 << endl |
650 | << " Controller 0: " << info.Control0 << endl |
651 | << " Controller 1: " << info.Control1 << endl |
652 | << " Display Format: " << info.DisplayFormat << endl |
653 | << " Bankswitch Type: " << info.BankSwitch << endl; |
654 | |
655 | return buf.str(); |
656 | } |
657 | |
658 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
659 | float OSystem::frameRate() const |
660 | { |
661 | return myConsole ? myConsole->getFramerate() : 0; |
662 | } |
663 | |
664 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
665 | double OSystem::dispatchEmulation(EmulationWorker& emulationWorker) |
666 | { |
667 | if (!myConsole) return 0.; |
668 | |
669 | TIA& tia(myConsole->tia()); |
670 | EmulationTiming& timing(myConsole->emulationTiming()); |
671 | DispatchResult dispatchResult; |
672 | |
673 | // Check whether we have a frame pending for rendering... |
674 | bool framePending = tia.newFramePending(); |
675 | // ... and copy it to the frame buffer. It is important to do this before |
676 | // the worker is started to avoid racing. |
677 | if (framePending) { |
678 | myFpsMeter.render(tia.framesSinceLastRender()); |
679 | tia.renderToFrameBuffer(); |
680 | } |
681 | |
682 | // Start emulation on a dedicated thread. It will do its own scheduling to sync 6507 and real time |
683 | // and will run until we stop the worker. |
684 | emulationWorker.start( |
685 | timing.cyclesPerSecond(), |
686 | timing.maxCyclesPerTimeslice(), |
687 | timing.minCyclesPerTimeslice(), |
688 | &dispatchResult, |
689 | &tia |
690 | ); |
691 | |
692 | // Render the frame. This may block, but emulation will continue to run on the worker, so the |
693 | // audio pipeline is kept fed :) |
694 | if (framePending) myFrameBuffer->updateInEmulationMode(myFpsMeter.fps()); |
695 | |
696 | // Stop the worker and wait until it has finished |
697 | uInt64 totalCycles = emulationWorker.stop(); |
698 | |
699 | // Handle the dispatch result |
700 | switch (dispatchResult.getStatus()) { |
701 | case DispatchResult::Status::ok: |
702 | break; |
703 | |
704 | case DispatchResult::Status::debugger: |
705 | #ifdef DEBUGGER_SUPPORT |
706 | myDebugger->start( |
707 | dispatchResult.getMessage(), |
708 | dispatchResult.getAddress(), |
709 | dispatchResult.wasReadTrap() |
710 | ); |
711 | #endif |
712 | |
713 | break; |
714 | |
715 | case DispatchResult::Status::fatal: |
716 | #ifdef DEBUGGER_SUPPORT |
717 | myDebugger->startWithFatalError(dispatchResult.getMessage()); |
718 | #else |
719 | cerr << dispatchResult.getMessage() << endl; |
720 | #endif |
721 | break; |
722 | |
723 | default: |
724 | throw runtime_error("invalid emulation dispatch result" ); |
725 | } |
726 | |
727 | // Handle frying |
728 | if (dispatchResult.getStatus() == DispatchResult::Status::ok && myEventHandler->frying()) |
729 | myConsole->fry(); |
730 | |
731 | // Return the 6507 time used in seconds |
732 | return static_cast<double>(totalCycles) / static_cast<double>(timing.cyclesPerSecond()); |
733 | } |
734 | |
735 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
736 | void OSystem::mainLoop() |
737 | { |
738 | // 6507 time |
739 | time_point<high_resolution_clock> virtualTime = high_resolution_clock::now(); |
740 | // The emulation worker |
741 | EmulationWorker emulationWorker; |
742 | |
743 | myFpsMeter.reset(TIAConstants::initialGarbageFrames); |
744 | |
745 | for(;;) |
746 | { |
747 | bool wasEmulation = myEventHandler->state() == EventHandlerState::EMULATION; |
748 | |
749 | myEventHandler->poll(TimerManager::getTicks()); |
750 | if(myQuitLoop) break; // Exit if the user wants to quit |
751 | |
752 | if (!wasEmulation && myEventHandler->state() == EventHandlerState::EMULATION) { |
753 | myFpsMeter.reset(); |
754 | virtualTime = high_resolution_clock::now(); |
755 | } |
756 | |
757 | double timesliceSeconds; |
758 | |
759 | if (myEventHandler->state() == EventHandlerState::EMULATION) |
760 | // Dispatch emulation and render frame (if applicable) |
761 | timesliceSeconds = dispatchEmulation(emulationWorker); |
762 | else { |
763 | // Render the GUI with 60 Hz in all other modes |
764 | timesliceSeconds = 1. / 60.; |
765 | myFrameBuffer->update(); |
766 | } |
767 | |
768 | duration<double> timeslice(timesliceSeconds); |
769 | virtualTime += duration_cast<high_resolution_clock::duration>(timeslice); |
770 | time_point<high_resolution_clock> now = high_resolution_clock::now(); |
771 | |
772 | // We allow 6507 time to lag behind by one frame max |
773 | double maxLag = myConsole |
774 | ? ( |
775 | static_cast<double>(myConsole->emulationTiming().cyclesPerFrame()) / |
776 | static_cast<double>(myConsole->emulationTiming().cyclesPerSecond()) |
777 | ) |
778 | : 0; |
779 | |
780 | if (duration_cast<duration<double>>(now - virtualTime).count() > maxLag) |
781 | // If 6507 time is lagging behind more than one frame we reset it to real time |
782 | virtualTime = now; |
783 | else if (virtualTime > now) { |
784 | // Wait until we have caught up with 6507 time |
785 | std::this_thread::sleep_until(virtualTime); |
786 | } |
787 | } |
788 | |
789 | // Cleanup time |
790 | #ifdef CHEATCODE_SUPPORT |
791 | if(myConsole) |
792 | myCheatManager->saveCheats(myConsole->properties().get(PropType::Cart_MD5)); |
793 | |
794 | myCheatManager->saveCheatDatabase(); |
795 | #endif |
796 | } |
797 | |
798 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
799 | shared_ptr<KeyValueRepository> OSystem::createSettingsRepository() |
800 | { |
801 | #ifdef SQLITE_SUPPORT |
802 | return mySettingsDb |
803 | ? shared_ptr<KeyValueRepository>(mySettingsDb, &mySettingsDb->settingsRepository()) |
804 | : make_shared<KeyValueRepositoryNoop>(); |
805 | #else |
806 | if (myConfigFile.empty()) |
807 | return make_shared<KeyValueRepositoryNoop>(); |
808 | |
809 | return make_shared<KeyValueRepositoryConfigfile>(myConfigFile); |
810 | #endif |
811 | } |
812 | |
813 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
814 | string OSystem::ourOverrideBaseDir = "" ; |
815 | bool OSystem::ourOverrideBaseDirWithApp = false; |
816 | |