Mesen2/Core/Shared/Emulator.cpp

1173 lines
31 KiB
C++

#include "pch.h"
#include <assert.h>
#include "Shared/Emulator.h"
#include "Shared/NotificationManager.h"
#include "Shared/Audio/SoundMixer.h"
#include "Shared/Audio/AudioPlayerHud.h"
#include "Shared/Video/VideoDecoder.h"
#include "Shared/Video/VideoRenderer.h"
#include "Shared/Video/DebugHud.h"
#include "Shared/FrameLimiter.h"
#include "Shared/MessageManager.h"
#include "Shared/KeyManager.h"
#include "Shared/EmuSettings.h"
#include "Shared/SaveStateManager.h"
#include "Shared/Video/DebugStats.h"
#include "Shared/RewindManager.h"
#include "Shared/ShortcutKeyHandler.h"
#include "Shared/EmulatorLock.h"
#include "Shared/DebuggerRequest.h"
#include "Shared/Movies/MovieManager.h"
#include "Shared/BatteryManager.h"
#include "Shared/CheatManager.h"
#include "Shared/SystemActionManager.h"
#include "Shared/TimingInfo.h"
#include "Shared/HistoryViewer.h"
#include "Netplay/GameServer.h"
#include "Netplay/GameClient.h"
#include "Shared/Interfaces/IConsole.h"
#include "Shared/Interfaces/IBarcodeReader.h"
#include "Shared/Interfaces/ITapeRecorder.h"
#include "Shared/BaseControlManager.h"
#include "SNES/SnesConsole.h"
#include "SNES/SnesDefaultVideoFilter.h"
#include "NES/NesConsole.h"
#include "Gameboy/Gameboy.h"
#include "PCE/PceConsole.h"
#include "SMS/SmsConsole.h"
#include "GBA/GbaConsole.h"
#include "WS/WsConsole.h"
#include "Debugger/Debugger.h"
#include "Debugger/BaseEventManager.h"
#include "Debugger/DebugTypes.h"
#include "Debugger/DebugUtilities.h"
#include "Utilities/Serializer.h"
#include "Utilities/Timer.h"
#include "Utilities/VirtualFile.h"
#include "Utilities/PlatformUtilities.h"
#include "Utilities/FolderUtilities.h"
#include "Shared/MemoryOperationType.h"
#include "Shared/EventType.h"
Emulator::Emulator() :
_settings(new EmuSettings(this)),
_debugHud(new DebugHud()),
_scriptHud(new DebugHud()),
_notificationManager(new NotificationManager()),
_batteryManager(new BatteryManager()),
_soundMixer(new SoundMixer(this)),
_videoRenderer(new VideoRenderer(this)),
_videoDecoder(new VideoDecoder(this)),
_saveStateManager(new SaveStateManager(this)),
_cheatManager(new CheatManager(this)),
_movieManager(new MovieManager(this)),
_historyViewer(new HistoryViewer(this)),
_gameServer(new GameServer(this)),
_gameClient(new GameClient(this)),
_rewindManager(new RewindManager(this))
{
_paused = false;
_pauseOnNextFrame = false;
_stopFlag = false;
_isRunAheadFrame = false;
_lockCounter = 0;
_threadPaused = false;
_debugRequestCount = 0;
_blockDebuggerRequestCount = 0;
_videoDecoder->Init();
}
Emulator::~Emulator()
{
}
void Emulator::Initialize(bool enableShortcuts)
{
_systemActionManager.reset(new SystemActionManager(this));
if(enableShortcuts) {
_shortcutKeyHandler.reset(new ShortcutKeyHandler(this));
_notificationManager->RegisterNotificationListener(_shortcutKeyHandler);
}
_videoDecoder->StartThread();
_videoRenderer->StartThread();
}
void Emulator::Release()
{
Stop(true);
_gameClient->Disconnect();
_gameServer->StopServer();
_videoDecoder->StopThread();
_videoRenderer->StopThread();
_shortcutKeyHandler.reset();
}
void Emulator::Run()
{
if(!_console) {
return;
}
while(!_runLock.TryAcquire(50)) {
if(_stopFlag) {
return;
}
}
_stopFlag = false;
_isRunAheadFrame = false;
PlatformUtilities::EnableHighResolutionTimer();
PlatformUtilities::DisableScreensaver();
_emulationThreadId = std::this_thread::get_id();
_frameDelay = GetFrameDelay();
_stats.reset(new DebugStats());
_frameLimiter.reset(new FrameLimiter(_frameDelay));
_lastFrameTimer.Reset();
while(!_stopFlag) {
bool useRunAhead = _settings->GetEmulationConfig().RunAheadFrames > 0 && !_debugger && !_audioPlayerHud && !_rewindManager->IsRewinding() && _settings->GetEmulationSpeed() > 0 && _settings->GetEmulationSpeed() <= 100;
if(useRunAhead) {
RunFrameWithRunAhead();
} else {
_console->RunFrame();
_rewindManager->ProcessEndOfFrame();
_historyViewer->ProcessEndOfFrame();
ProcessSystemActions();
}
ProcessAutoSaveState();
WaitForLock();
if(_pauseOnNextFrame) {
_pauseOnNextFrame = false;
_paused = true;
}
if(_paused && !_stopFlag && !_debugger) {
WaitForPauseEnd();
}
}
_emulationThreadId = thread::id();
if(_runLock.IsLockedByCurrentThread()) {
//Lock might not be held by current frame is _stopFlag was set to interrupt the thread
_runLock.Release();
}
PlatformUtilities::EnableScreensaver();
PlatformUtilities::RestoreTimerResolution();
}
void Emulator::ProcessAutoSaveState()
{
if(_autoSaveStateFrameCounter > 0) {
_autoSaveStateFrameCounter--;
if(_autoSaveStateFrameCounter == 0) {
_saveStateManager->SaveState(SaveStateManager::AutoSaveStateIndex, false);
}
} else {
uint32_t saveStateDelay = _settings->GetPreferences().AutoSaveStateDelay;
if(saveStateDelay > 0) {
_autoSaveStateFrameCounter = (uint32_t)(GetFps() * saveStateDelay * 60);
}
}
}
bool Emulator::ProcessSystemActions()
{
if(_systemActionManager->IsResetPressed()) {
Reset();
shared_ptr<Debugger> debugger = _debugger.lock();
if(debugger) {
debugger->ResetSuspendCounter();
}
return true;
} else if(_systemActionManager->IsPowerCyclePressed()) {
PowerCycle();
return true;
}
return false;
}
void Emulator::RunFrameWithRunAhead()
{
stringstream runAheadState;
uint32_t frameCount = _settings->GetEmulationConfig().RunAheadFrames;
//Run a single frame and save the state (no audio/video)
_isRunAheadFrame = true;
_console->RunFrame();
Serialize(runAheadState, false, 0);
while(frameCount > 1) {
//Run extra frames if the requested run ahead frame count is higher than 1
frameCount--;
_console->RunFrame();
}
_isRunAheadFrame = false;
//Run one frame normally (with audio/video output)
_console->RunFrame();
_rewindManager->ProcessEndOfFrame();
_historyViewer->ProcessEndOfFrame();
bool wasReset = ProcessSystemActions();
if(!wasReset) {
//Load the state we saved earlier
_isRunAheadFrame = true;
Deserialize(runAheadState, SaveStateManager::FileFormatVersion, false);
_isRunAheadFrame = false;
}
}
void Emulator::OnBeforeSendFrame()
{
if(!_isRunAheadFrame) {
if(_audioPlayerHud) {
_audioPlayerHud->Draw(GetFrameCount(), GetFps());
}
if(_stats && _settings->GetPreferences().ShowDebugInfo) {
double lastFrameTime = _lastFrameTimer.GetElapsedMS();
_lastFrameTimer.Reset();
_stats->DisplayStats(this, lastFrameTime);
}
}
}
void Emulator::ProcessEndOfFrame()
{
if(!_isRunAheadFrame) {
_frameLimiter->ProcessFrame();
while(_frameLimiter->WaitForNextFrame()) {
if(_stopFlag || _frameDelay != GetFrameDelay() || _paused || _pauseOnNextFrame || _lockCounter > 0) {
//Need to process another event, stop sleeping
break;
}
}
double newFrameDelay = GetFrameDelay();
if(newFrameDelay != _frameDelay) {
_frameDelay = newFrameDelay;
_frameLimiter->SetDelay(_frameDelay);
}
_console->GetControlManager()->ProcessEndOfFrame();
}
_frameRunning = false;
}
void Emulator::Stop(bool sendNotification, bool preventRecentGameSave, bool saveBattery)
{
BlockDebuggerRequests();
_stopFlag = true;
_notificationManager->SendNotification(ConsoleNotificationType::BeforeGameUnload);
ResetDebugger();
if(_emuThread) {
_emuThread->join();
_emuThread.release();
}
if(_console && saveBattery) {
//Only save battery on power off, otherwise SaveBattery() is called by LoadRom()
_console->SaveBattery();
}
if(!preventRecentGameSave && _console && !_settings->GetPreferences().DisableGameSelectionScreen && !_audioPlayerHud) {
RomInfo romInfo = GetRomInfo();
_saveStateManager->SaveRecentGame(romInfo.RomFile.GetFileName(), romInfo.RomFile, romInfo.PatchFile);
}
if(sendNotification) {
_notificationManager->SendNotification(ConsoleNotificationType::BeforeEmulationStop);
}
_movieManager->Stop();
_videoDecoder->StopThread();
_rewindManager->Reset();
if(_console) {
_console.reset();
}
OnBeforePause(true);
if(sendNotification) {
_notificationManager->SendNotification(ConsoleNotificationType::EmulationStopped);
}
_blockDebuggerRequestCount--;
}
void Emulator::Reset()
{
Lock();
_console->Reset();
//Ensure reset button flag is off before recording input for first frame
_systemActionManager->ResetState();
_console->GetControlManager()->UpdateInputState();
_console->GetControlManager()->ResetLagCounter();
_videoRenderer->ClearFrame();
_notificationManager->SendNotification(ConsoleNotificationType::GameReset);
ProcessEvent(EventType::Reset);
Unlock();
}
void Emulator::ReloadRom(bool forPowerCycle)
{
RomInfo info = GetRomInfo();
//Cast RomFile/PatchFile to string to make sure the file is reloaded from the disk
//In some scenarios, the file might be in memory already, which will prevent the reload
//from actually reloading the rom from the disk.
if(!LoadRom((string)info.RomFile, (string)info.PatchFile, !forPowerCycle, forPowerCycle)) {
if(forPowerCycle) {
//Power cycle failed (rom not longer exists, etc.), reset flag
//(otherwise power cycle will continue to be attempted on each frame)
_systemActionManager->ResetState();
//Unsuspend debugger, otherwise deadlocks can occur after a power cycle fails when the debugger is active
SuspendDebugger(true);
}
}
}
void Emulator::PowerCycle()
{
ReloadRom(true);
}
bool Emulator::LoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom, bool forPowerCycle)
{
bool result = false;
try {
result = InternalLoadRom(romFile, patchFile, stopRom, forPowerCycle);
} catch(std::exception& ex) {
_videoDecoder->StartThread();
_videoRenderer->StartThread();
MessageManager::DisplayMessage("Error", "UnexpectedError", ex.what());
Stop(false, true, false);
}
if(!result) {
_notificationManager->SendNotification(ConsoleNotificationType::GameLoadFailed);
}
return result;
}
bool Emulator::InternalLoadRom(VirtualFile romFile, VirtualFile patchFile, bool stopRom, bool forPowerCycle)
{
if(!romFile.IsValid()) {
MessageManager::DisplayMessage("Error", "CouldNotLoadFile", romFile.GetFileName());
return false;
}
if(IsEmulationThread()) {
_threadPaused = true;
}
BlockDebuggerRequests();
auto emuLock = AcquireLock();
auto dbgLock = _debuggerLock.AcquireSafe();
auto lock = _loadLock.AcquireSafe();
//Once emulation is stopped, warn the UI that a game is about to be loaded
//This allows the UI to finish processing pending calls to the debug tools, etc.
_notificationManager->SendNotification(ConsoleNotificationType::BeforeGameLoad);
bool wasPaused = IsPaused();
//Keep a reference to the original debugger
shared_ptr<Debugger> debugger = _debugger.lock();
bool debuggerActive = debugger != nullptr;
//Unset _debugger to ensure nothing calls the debugger while initializing the new rom
ResetDebugger();
if(patchFile.IsValid()) {
if(romFile.ApplyPatch(patchFile)) {
MessageManager::DisplayMessage("Patch", "ApplyingPatch", patchFile.GetFileName());
}
}
if(_console) {
//Make sure the battery is saved to disk before we load another game (or reload the same game)
_console->SaveBattery();
}
_soundMixer->StopAudio();
if(!forPowerCycle) {
_movieManager->Stop();
}
//Keep copy of current memory types, to allow keeping ROM changes when power cycling
ConsoleMemoryInfo originalConsoleMemory[DebugUtilities::GetMemoryTypeCount()] = {};
memcpy(originalConsoleMemory, _consoleMemory, sizeof(_consoleMemory));
unique_ptr<IConsole> console;
LoadRomResult result = LoadRomResult::UnknownType;
//Try loading the rom, give priority to file extension, then trying to check for file signatures if extension is unknown
TryLoadRom(romFile, result, console, false);
TryLoadRom(romFile, result, console, true);
if(result != LoadRomResult::Success) {
MessageManager::DisplayMessage("Error", "CouldNotLoadFile", romFile.GetFileName());
if(debugger) {
_debugger.reset(debugger);
debugger->ResetSuspendCounter();
}
_blockDebuggerRequestCount--;
return false;
}
//Cleanup debugger instance if one was active
if(debugger) {
debugger->Release();
debugger.reset();
}
if(stopRom) {
//Only update the recent game entry if the game that was loaded is a different game
bool gameChanged = (string)_rom.RomFile != (string)romFile || (string)_rom.PatchFile != (string)patchFile;
Stop(false, !gameChanged, false);
memset(originalConsoleMemory, 0, sizeof(originalConsoleMemory));
}
_videoDecoder->StopThread();
_videoRenderer->StopThread();
//Cast VirtualFiles to string to ensure the original file data isn't kept in memory
_rom.RomFile = (string)romFile;
_rom.PatchFile = (string)patchFile;
_rom.Format = console->GetRomFormat();
_rom.DipSwitches = console->GetDipSwitchInfo();
if(_rom.Format == RomFormat::Spc || _rom.Format == RomFormat::Nsf || _rom.Format == RomFormat::Gbs || _rom.Format == RomFormat::PceHes) {
_audioPlayerHud.reset(new AudioPlayerHud(this));
} else {
_audioPlayerHud.reset();
}
_cheatManager->ClearCheats(false);
uint32_t pollCounter = 0;
if(forPowerCycle && console->GetControlManager()) {
//When power cycling, poll counter must be preserved to allow movies to playback properly
pollCounter = console->GetControlManager()->GetPollCounter();
}
InitConsole(console, originalConsoleMemory, forPowerCycle);
//Restore pollcounter (used by movies when a power cycle is in the movie)
_console->GetControlManager()->SetPollCounter(pollCounter);
_rewindManager->InitHistory();
if(debuggerActive || _settings->CheckFlag(EmulationFlags::ConsoleMode)) {
InitDebugger();
}
_notificationManager->RegisterNotificationListener(_rewindManager);
//Ensure power cycle flag is off before recording input for first frame
_systemActionManager->ResetState();
_console->GetControlManager()->UpdateControlDevices();
_console->GetControlManager()->UpdateInputState();
_autoSaveStateFrameCounter = 0;
//Mark the thread as paused, and release the debugger lock to avoid
//deadlocks with DebugBreakHelper if GameLoaded event starts the debugger
_blockDebuggerRequestCount--;
dbgLock.Release();
_threadPaused = true;
bool needPause = wasPaused && _debugger;
if(needPause) {
//Break on the current instruction if emulation was already paused
//(must be done after setting _threadPaused to true)
_debugger->Step(GetCpuTypes()[0], 1, StepType::Step, BreakSource::Pause);
}
GameLoadedEventParams params = { needPause, forPowerCycle };
_notificationManager->SendNotification(ConsoleNotificationType::GameLoaded, &params);
_threadPaused = false;
if(!forPowerCycle && !_audioPlayerHud) {
ConsoleRegion region = _console->GetRegion();
string modelName = region == ConsoleRegion::Pal ? "PAL" : (region == ConsoleRegion::Dendy ? "Dendy" : "NTSC");
MessageManager::DisplayMessage(modelName, FolderUtilities::GetFilename(GetRomInfo().RomFile.GetFileName(), false));
}
_videoDecoder->StartThread();
_videoRenderer->StartThread();
if(stopRom) {
_stopFlag = false;
_emuThread.reset(new thread(&Emulator::Run, this));
}
return true;
}
void Emulator::InitConsole(unique_ptr<IConsole>& newConsole, ConsoleMemoryInfo originalConsoleMemory[], bool preserveRom)
{
if(preserveRom && _console) {
//When power cycling, copy over the content of any ROM memory from the previous instance
magic_enum::enum_for_each<MemoryType>([&](MemoryType memType) {
if(DebugUtilities::IsRom(memType)) {
uint32_t orgSize = originalConsoleMemory[(int)memType].Size;
if(orgSize > 0 && GetMemory(memType).Size == orgSize) {
memcpy(_consoleMemory[(int)memType].Memory, originalConsoleMemory[(int)memType].Memory, orgSize);
}
}
});
}
_console.reset(newConsole);
_consoleType = _console->GetConsoleType();
_notificationManager->RegisterNotificationListener(_console.lock());
}
void Emulator::TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_ptr<IConsole>& console, bool useFileSignature)
{
TryLoadRom<NesConsole>(romFile, result, console, useFileSignature);
TryLoadRom<SnesConsole>(romFile, result, console, useFileSignature);
TryLoadRom<Gameboy>(romFile, result, console, useFileSignature);
TryLoadRom<PceConsole>(romFile, result, console, useFileSignature);
TryLoadRom<SmsConsole>(romFile, result, console, useFileSignature);
TryLoadRom<GbaConsole>(romFile, result, console, useFileSignature);
TryLoadRom<WsConsole>(romFile, result, console, useFileSignature);
}
template<typename T>
void Emulator::TryLoadRom(VirtualFile& romFile, LoadRomResult& result, unique_ptr<IConsole>& console, bool useFileSignature)
{
if(result == LoadRomResult::UnknownType) {
string romExt = romFile.GetFileExtension();
vector<string> extensions = T::GetSupportedExtensions();
if(std::find(extensions.begin(), extensions.end(), romExt) != extensions.end() || (useFileSignature && romFile.CheckFileSignature(T::GetSupportedSignatures()))) {
//Keep a copy of the current state of _consoleMemory
ConsoleMemoryInfo consoleMemory[DebugUtilities::GetMemoryTypeCount()] = {};
memcpy(consoleMemory, _consoleMemory, sizeof(_consoleMemory));
//Attempt to load rom with specified core
memset(_consoleMemory, 0, sizeof(_consoleMemory));
//Change filename for batterymanager to allow loading the correct files
bool hasBattery = _batteryManager->HasBattery();
_batteryManager->Initialize(FolderUtilities::GetFilename(romFile.GetFileName(), false));
console.reset(new T(this));
result = console->LoadRom(romFile);
if(result != LoadRomResult::Success) {
//Restore state if load fails
memcpy(_consoleMemory, consoleMemory, sizeof(_consoleMemory));
_batteryManager->Initialize(FolderUtilities::GetFilename(_rom.RomFile.GetFileName(), false), hasBattery);
}
}
}
}
string Emulator::GetHash(HashType type)
{
shared_ptr<IConsole> console = _console.lock();
string hash = console->GetHash(type);
if(hash.size()) {
return hash;
} else if(type == HashType::Sha1) {
return _rom.RomFile.GetSha1Hash();
} else if(type == HashType::Sha1Cheat) {
return _rom.RomFile.GetSha1Hash();
}
return "";
}
uint32_t Emulator::GetCrc32()
{
return _rom.RomFile.GetCrc32();
}
PpuFrameInfo Emulator::GetPpuFrame()
{
shared_ptr<IConsole> console = GetConsole();
return console ? console->GetPpuFrame() : PpuFrameInfo {};
}
ConsoleRegion Emulator::GetRegion()
{
shared_ptr<IConsole> console = GetConsole();
return console ? console->GetRegion() : ConsoleRegion::Ntsc;
}
shared_ptr<IConsole> Emulator::GetConsole()
{
return _console.lock();
}
IConsole* Emulator::GetConsoleUnsafe()
{
#ifdef _DEBUG
if(!IsEmulationThread()) {
throw std::runtime_error("GetConsoleUnsafe should only be called from the emulation thread");
}
#endif
return _console.get();
}
ConsoleType Emulator::GetConsoleType()
{
return _consoleType;
}
vector<CpuType> Emulator::GetCpuTypes()
{
shared_ptr<IConsole> console = GetConsole();
return console ? console->GetCpuTypes() : vector<CpuType>{};
}
TimingInfo Emulator::GetTimingInfo(CpuType cpuType)
{
shared_ptr<IConsole> console = GetConsole();
return console ? console->GetTimingInfo(cpuType) : TimingInfo {};
}
uint64_t Emulator::GetMasterClock()
{
#if DEBUG
if(!IsEmulationThread()) {
throw std::runtime_error("called on wrong thread");
}
#endif
return _console->GetMasterClock();
}
uint32_t Emulator::GetMasterClockRate()
{
#if DEBUG
if(!IsEmulationThread()) {
throw std::runtime_error("called on wrong thread");
}
#endif
//TODOv2 this is not accurate when overclocking options are turned on
return _console->GetMasterClockRate();
}
uint32_t Emulator::GetFrameCount()
{
return GetPpuFrame().FrameCount;
}
uint32_t Emulator::GetLagCounter()
{
shared_ptr<IConsole> console = GetConsole();
return console ? console->GetControlManager()->GetLagCounter() : 0;
}
void Emulator::ResetLagCounter()
{
shared_ptr<IConsole> console = GetConsole();
if(console) {
console->GetControlManager()->ResetLagCounter();
}
}
bool Emulator::HasControlDevice(ControllerType type)
{
shared_ptr<IConsole> console = GetConsole();
return console ? console->GetControlManager()->HasControlDevice(type) : false;
}
void Emulator::RegisterInputRecorder(IInputRecorder* recorder)
{
shared_ptr<IConsole> console = GetConsole();
if(console) {
console->GetControlManager()->RegisterInputRecorder(recorder);
}
}
void Emulator::UnregisterInputRecorder(IInputRecorder* recorder)
{
shared_ptr<IConsole> console = GetConsole();
if(console) {
console->GetControlManager()->UnregisterInputRecorder(recorder);
}
}
void Emulator::RegisterInputProvider(IInputProvider* provider)
{
shared_ptr<IConsole> console = GetConsole();
if(console) {
console->GetControlManager()->RegisterInputProvider(provider);
}
}
void Emulator::UnregisterInputProvider(IInputProvider* provider)
{
shared_ptr<IConsole> console = GetConsole();
if(console) {
console->GetControlManager()->UnregisterInputProvider(provider);
}
}
double Emulator::GetFps()
{
shared_ptr<IConsole> console = GetConsole();
double fps = console ? console->GetFps() : 60.0;
if(_settings->GetVideoConfig().IntegerFpsMode) {
fps = std::round(fps);
}
return fps;
}
double Emulator::GetFrameDelay()
{
uint32_t emulationSpeed = _settings->GetEmulationSpeed();
double frameDelay;
if(emulationSpeed == 0) {
frameDelay = 0;
} else {
frameDelay = 1000 / GetFps();
frameDelay /= (emulationSpeed / 100.0);
}
return frameDelay;
}
void Emulator::PauseOnNextFrame()
{
//Used by "Run single frame" shortcut
shared_ptr<Debugger> debugger = _debugger.lock();
if(debugger) {
debugger->PauseOnNextFrame();
} else {
_pauseOnNextFrame = true;
_paused = false;
}
}
void Emulator::Pause()
{
shared_ptr<Debugger> debugger = _debugger.lock();
if(debugger) {
debugger->Step(GetCpuTypes()[0], 1, StepType::Step, BreakSource::Pause);
} else {
_paused = true;
}
}
void Emulator::Resume()
{
shared_ptr<Debugger> debugger = _debugger.lock();
if(debugger) {
debugger->Run();
} else {
_paused = false;
}
}
bool Emulator::IsPaused()
{
shared_ptr<Debugger> debugger = _debugger.lock();
if(debugger) {
return debugger->IsPaused();
} else {
return _paused;
}
}
void Emulator::OnBeforePause(bool clearAudioBuffer)
{
//Prevent audio from looping endlessly while game is paused
_soundMixer->StopAudio(clearAudioBuffer);
//Stop force feedback
KeyManager::SetForceFeedback(0);
}
void Emulator::WaitForPauseEnd()
{
_notificationManager->SendNotification(ConsoleNotificationType::GamePaused);
OnBeforePause(false);
_runLock.Release();
PlatformUtilities::EnableScreensaver();
PlatformUtilities::RestoreTimerResolution();
while(_paused && !_rewindManager->IsRewinding() && !_stopFlag && !_debugger) {
//Sleep until emulation is resumed
std::this_thread::sleep_for(std::chrono::duration<int, std::milli>(30));
if(_systemActionManager->IsResetPending()) {
//Reset/power cycle was pressed, stop waiting and process it now
break;
}
}
PlatformUtilities::DisableScreensaver();
PlatformUtilities::EnableHighResolutionTimer();
while(!_stopFlag && !_runLock.TryAcquire(50)) { }
if(!_stopFlag) {
_notificationManager->SendNotification(ConsoleNotificationType::GameResumed);
}
}
EmulatorLock Emulator::AcquireLock(bool allowDebuggerLock)
{
//When allowDebuggerLock is true, the debugger is allowed to pause the emulation thread
//instead of using the emulator's lock to pause the emulation at the end of the next frame
//This is to allow, for example, loading or save a save state while the debugger tools are
//opened without forcing the emulation to run for an entire frame each time.
//However, sometimes this causes issues, e.g calling AcquireLock and then LoadRom from the
//same thread while the debugger is active will cause a deadlock. This is why this behavior
//can be disabled, as required.
return EmulatorLock(this, allowDebuggerLock);
}
void Emulator::Lock()
{
SuspendDebugger(false);
_lockCounter++;
_runLock.Acquire();
}
void Emulator::Unlock()
{
SuspendDebugger(true);
_runLock.Release();
_lockCounter--;
}
bool Emulator::IsThreadPaused()
{
return !_emuThread || _threadPaused;
}
void Emulator::SuspendDebugger(bool release)
{
shared_ptr<Debugger> debugger = _debugger.lock();
if(debugger) {
debugger->SuspendDebugger(release);
}
}
void Emulator::WaitForLock()
{
if(_lockCounter > 0) {
//Need to temporarely pause the emu (to save/load a state, etc.)
_runLock.Release();
_threadPaused = true;
//Spin wait until we are allowed to start again
while(_lockCounter > 0 && !_stopFlag) {}
shared_ptr<Debugger> debugger = _debugger.lock();
if(debugger) {
while(debugger->HasBreakRequest() && !_stopFlag) {}
}
if(!_stopFlag) {
_threadPaused = false;
_runLock.Acquire();
}
}
}
void Emulator::Serialize(ostream& out, bool includeSettings, int compressionLevel)
{
Serializer s(SaveStateManager::FileFormatVersion, true);
if(includeSettings) {
SV(_settings);
}
s.Stream(_console, "");
s.SaveTo(out, compressionLevel);
}
DeserializeResult Emulator::Deserialize(istream& in, uint32_t fileFormatVersion, bool includeSettings, optional<ConsoleType> srcConsoleType, bool sendNotification)
{
Serializer s(fileFormatVersion, false);
if(!s.LoadFrom(in)) {
return DeserializeResult::InvalidFile;
}
if(includeSettings) {
SV(_settings);
}
if(srcConsoleType.has_value() && srcConsoleType.value() != _console->GetConsoleType()) {
//Used to allow save states taken on GB/GBC/SGB to be loaded on any of the 3 systems
SaveStateCompatInfo compatInfo = _console->ValidateSaveStateCompatibility(srcConsoleType.value());
if(!compatInfo.IsCompatible) {
MessageManager::DisplayMessage("SaveStates", "SaveStateWrongSystem");
return DeserializeResult::SpecificError;
}
s.RemoveKeys(compatInfo.FieldsToRemove);
if(!compatInfo.PrefixToAdd.empty()) {
s.AddKeyPrefix(compatInfo.PrefixToAdd);
} else if(!compatInfo.PrefixToRemove.empty()) {
s.RemoveKeyPrefix(compatInfo.PrefixToRemove);
}
if(!s.IsValid()) {
MessageManager::DisplayMessage("SaveStates", "SaveStateWrongSystem");
return DeserializeResult::SpecificError;
}
}
s.Stream(_console, "");
if(s.HasError()) {
return DeserializeResult::SpecificError;
}
if(sendNotification) {
_notificationManager->SendNotification(ConsoleNotificationType::StateLoaded);
}
return DeserializeResult::Success;
}
BaseVideoFilter* Emulator::GetVideoFilter(bool getDefaultFilter)
{
shared_ptr<IConsole> console = GetConsole();
return console ? console->GetVideoFilter(getDefaultFilter) : new SnesDefaultVideoFilter(this);
}
void Emulator::GetScreenRotationOverride(uint32_t& rotation)
{
shared_ptr<IConsole> console = GetConsole();
if(console) {
console->GetScreenRotationOverride(rotation);
}
}
void Emulator::InputBarcode(uint64_t barcode, uint32_t digitCount)
{
shared_ptr<IConsole> console = GetConsole();
if(console) {
shared_ptr<IBarcodeReader> reader = console->GetControlManager()->GetControlDevice<IBarcodeReader>();
if(reader) {
auto lock = AcquireLock();
reader->InputBarcode(barcode, digitCount);
}
}
}
void Emulator::ProcessTapeRecorderAction(TapeRecorderAction action, string filename)
{
shared_ptr<IConsole> console = GetConsole();
if(console) {
shared_ptr<ITapeRecorder> recorder = console->GetControlManager()->GetControlDevice<ITapeRecorder>();
if(recorder) {
auto lock = AcquireLock();
recorder->ProcessTapeRecorderAction(action, filename);
}
}
}
ShortcutState Emulator::IsShortcutAllowed(EmulatorShortcut shortcut, uint32_t shortcutParam)
{
shared_ptr<IConsole> console = GetConsole();
return console ? console->IsShortcutAllowed(shortcut, shortcutParam) : ShortcutState::Default;
}
bool Emulator::IsKeyboardConnected()
{
shared_ptr<IConsole> console = GetConsole();
return console ? console->GetControlManager()->IsKeyboardConnected() : false;
}
void Emulator::BlockDebuggerRequests()
{
//Block all new debugger calls
auto lock = _debuggerLock.AcquireSafe();
_blockDebuggerRequestCount++;
if(_debugger) {
//Ensure any thread waiting on DebugBreakHelper is allowed to resume/finish (prevent deadlock)
_debugger->ResetSuspendCounter();
}
while(_debugRequestCount > 0) {
//Wait until debugger calls are all done
std::this_thread::sleep_for(std::chrono::duration<int, std::milli>(10));
}
}
DebuggerRequest Emulator::GetDebugger(bool autoInit)
{
if(IsRunning() && _blockDebuggerRequestCount == 0) {
auto lock = _debuggerLock.AcquireSafe();
if(IsRunning() && _blockDebuggerRequestCount == 0) {
if(!_debugger && autoInit) {
InitDebugger();
}
return DebuggerRequest(this);
}
}
return DebuggerRequest(nullptr);
}
void Emulator::ResetDebugger(bool startDebugger)
{
shared_ptr<Debugger> currentDbg = _debugger.lock();
if(currentDbg) {
currentDbg->SuspendDebugger(false);
}
if(_emulationThreadId == std::this_thread::get_id()) {
_debugger.reset(startDebugger ? new Debugger(this, _console.get()) : nullptr);
} else {
//Need to pause emulator to change _debugger (when not called from the emulation thread)
auto emuLock = AcquireLock();
_debugger.reset(startDebugger ? new Debugger(this, _console.get()) : nullptr);
}
}
void Emulator::InitDebugger()
{
if(!_debugger) {
//Lock to make sure we don't try to start debuggers in 2 separate threads at once
auto lock = _debuggerLock.AcquireSafe();
if(!_debugger) {
BlockDebuggerRequests();
ResetDebugger(true);
_blockDebuggerRequestCount--;
//_paused should be false while debugger is enabled
_paused = false;
}
}
}
void Emulator::StopDebugger()
{
//Pause/unpause the regular emulation thread based on the debugger's pause state
_paused = IsPaused();
if(_debugger) {
auto lock = _debuggerLock.AcquireSafe();
if(_debugger) {
BlockDebuggerRequests();
ResetDebugger();
_blockDebuggerRequestCount--;
}
}
}
bool Emulator::IsEmulationThread()
{
return _emulationThreadId == std::this_thread::get_id();
}
void Emulator::SetStopCode(int32_t stopCode)
{
if(_stopCode != 0) {
//If a non-0 code was already set, keep the previous value
return;
}
_stopCode = stopCode;
if(!_stopFlag && !_stopRequested) {
_stopRequested = true;
thread stopEmuTask([this]() {
Stop(true);
});
stopEmuTask.detach();
}
}
void Emulator::RegisterMemory(MemoryType type, void* memory, uint32_t size)
{
_consoleMemory[(int)type] = { memory, size };
}
ConsoleMemoryInfo Emulator::GetMemory(MemoryType type)
{
return _consoleMemory[(int)type];
}
AudioTrackInfo Emulator::GetAudioTrackInfo()
{
shared_ptr<IConsole> console = GetConsole();
if(!console) {
return {};
}
AudioTrackInfo track = console->GetAudioTrackInfo();
AudioConfig audioCfg = _settings->GetAudioConfig();
if(track.Length <= 0 && audioCfg.AudioPlayerEnableTrackLength) {
track.Length = audioCfg.AudioPlayerTrackLength;
track.FadeLength = 1;
}
return track;
}
void Emulator::ProcessAudioPlayerAction(AudioPlayerActionParams p)
{
shared_ptr<IConsole> console = GetConsole();
if(console) {
console->ProcessAudioPlayerAction(p);
}
}
void Emulator::ProcessEvent(EventType type, std::optional<CpuType> cpuType)
{
if(_debugger) {
_debugger->ProcessEvent(type, cpuType);
}
}
template<CpuType cpuType>
void Emulator::AddDebugEvent(DebugEventType evtType)
{
if(_debugger) {
_debugger->GetEventManager(cpuType)->AddEvent(evtType);
}
}
void Emulator::BreakIfDebugging(CpuType sourceCpu, BreakSource source)
{
if(_debugger) {
_debugger->BreakImmediately(sourceCpu, source);
}
}
template void Emulator::AddDebugEvent<CpuType::Snes>(DebugEventType evtType);
template void Emulator::AddDebugEvent<CpuType::Gameboy>(DebugEventType evtType);
template void Emulator::AddDebugEvent<CpuType::Nes>(DebugEventType evtType);
template void Emulator::AddDebugEvent<CpuType::Pce>(DebugEventType evtType);