Core: Added runahead support (1 to 10 frames)
This commit is contained in:
parent
fd02e406b0
commit
c7675bcd49
24 changed files with 1143 additions and 964 deletions
277
Core/Console.cpp
277
Core/Console.cpp
|
@ -720,6 +720,17 @@ void Console::RunSlaveCpu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Console::RunFrame()
|
||||||
|
{
|
||||||
|
uint32_t frameCount = _ppu->GetFrameCount();
|
||||||
|
while(_ppu->GetFrameCount() == frameCount) {
|
||||||
|
_cpu->Exec();
|
||||||
|
if(_slave) {
|
||||||
|
RunSlaveCpu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Console::Run()
|
void Console::Run()
|
||||||
{
|
{
|
||||||
Timer clockTimer;
|
Timer clockTimer;
|
||||||
|
@ -729,7 +740,6 @@ void Console::Run()
|
||||||
double targetTime;
|
double targetTime;
|
||||||
double lastFrameMin = 9999;
|
double lastFrameMin = 9999;
|
||||||
double lastFrameMax = 0;
|
double lastFrameMax = 0;
|
||||||
uint32_t lastFrameNumber = -1;
|
|
||||||
double lastDelay = GetFrameDelay();
|
double lastDelay = GetFrameDelay();
|
||||||
|
|
||||||
_runLock.Acquire();
|
_runLock.Acquire();
|
||||||
|
@ -753,131 +763,135 @@ void Console::Run()
|
||||||
bool crashed = false;
|
bool crashed = false;
|
||||||
try {
|
try {
|
||||||
while(true) {
|
while(true) {
|
||||||
_cpu->Exec();
|
stringstream runAheadState;
|
||||||
|
bool useRunAhead = _settings->GetRunAheadFrames() > 0 && !_debugger && !_rewindManager->IsRewinding();
|
||||||
if(_slave) {
|
if(useRunAhead) {
|
||||||
RunSlaveCpu();
|
RunFrameWithRunAhead(runAheadState);
|
||||||
|
} else {
|
||||||
|
RunFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_ppu->GetFrameCount() != lastFrameNumber) {
|
_soundMixer->ProcessEndOfFrame();
|
||||||
_soundMixer->ProcessEndOfFrame();
|
if(_slave) {
|
||||||
|
_slave->_soundMixer->ProcessEndOfFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_historyViewer) {
|
||||||
|
_historyViewer->ProcessEndOfFrame();
|
||||||
|
}
|
||||||
|
_rewindManager->ProcessEndOfFrame();
|
||||||
|
_settings->DisableOverclocking(_disableOcNextFrame || IsNsf());
|
||||||
|
_disableOcNextFrame = false;
|
||||||
|
|
||||||
|
//Update model (ntsc/pal) and get delay for next frame
|
||||||
|
UpdateNesModel(true);
|
||||||
|
double delay = GetFrameDelay();
|
||||||
|
|
||||||
|
if(_resetRunTimers || delay != lastDelay || (clockTimer.GetElapsedMS() - targetTime) > 300) {
|
||||||
|
//Reset the timers, this can happen in 3 scenarios:
|
||||||
|
//1) Target frame rate changed
|
||||||
|
//2) The console was reset/power cycled or the emulation was paused (with or without the debugger)
|
||||||
|
//3) As a satefy net, if we overshoot our target by over 300 milliseconds, the timer is reset, too.
|
||||||
|
// This can happen when something slows the emulator down severely (or when breaking execution in VS when debugging Mesen itself, etc.)
|
||||||
|
clockTimer.Reset();
|
||||||
|
targetTime = 0;
|
||||||
|
|
||||||
|
_resetRunTimers = false;
|
||||||
|
lastDelay = delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetTime += delay;
|
||||||
|
|
||||||
|
bool displayDebugInfo = _settings->CheckFlag(EmulationFlags::DisplayDebugInfo);
|
||||||
|
if(displayDebugInfo) {
|
||||||
|
double lastFrameTime = lastFrameTimer.GetElapsedMS();
|
||||||
|
lastFrameTimer.Reset();
|
||||||
|
frameDurations[frameDurationIndex] = lastFrameTime;
|
||||||
|
frameDurationIndex = (frameDurationIndex + 1) % 60;
|
||||||
|
|
||||||
|
DisplayDebugInformation(lastFrameTime, lastFrameMin, lastFrameMax, frameDurations);
|
||||||
if(_slave) {
|
if(_slave) {
|
||||||
_slave->_soundMixer->ProcessEndOfFrame();
|
_slave->DisplayDebugInformation(lastFrameTime, lastFrameMin, lastFrameMax, frameDurations);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(_historyViewer) {
|
//When sleeping for a long time (e.g <= 25% speed), sleep in small chunks and check to see if we need to stop sleeping between each sleep call
|
||||||
_historyViewer->ProcessEndOfFrame();
|
while(targetTime - clockTimer.GetElapsedMS() > 50) {
|
||||||
}
|
clockTimer.WaitUntil(clockTimer.GetElapsedMS() + 40);
|
||||||
_rewindManager->ProcessEndOfFrame();
|
if(delay != GetFrameDelay() || _stop || _settings->NeedsPause() || _pauseCounter > 0) {
|
||||||
_settings->DisableOverclocking(_disableOcNextFrame || IsNsf());
|
|
||||||
_disableOcNextFrame = false;
|
|
||||||
|
|
||||||
//Update model (ntsc/pal) and get delay for next frame
|
|
||||||
UpdateNesModel(true);
|
|
||||||
double delay = GetFrameDelay();
|
|
||||||
|
|
||||||
if(_resetRunTimers || delay != lastDelay || (clockTimer.GetElapsedMS() - targetTime) > 300) {
|
|
||||||
//Reset the timers, this can happen in 3 scenarios:
|
|
||||||
//1) Target frame rate changed
|
|
||||||
//2) The console was reset/power cycled or the emulation was paused (with or without the debugger)
|
|
||||||
//3) As a satefy net, if we overshoot our target by over 300 milliseconds, the timer is reset, too.
|
|
||||||
// This can happen when something slows the emulator down severely (or when breaking execution in VS when debugging Mesen itself, etc.)
|
|
||||||
clockTimer.Reset();
|
|
||||||
targetTime = 0;
|
targetTime = 0;
|
||||||
|
|
||||||
_resetRunTimers = false;
|
|
||||||
lastDelay = delay;
|
|
||||||
}
|
|
||||||
|
|
||||||
targetTime += delay;
|
|
||||||
|
|
||||||
bool displayDebugInfo = _settings->CheckFlag(EmulationFlags::DisplayDebugInfo);
|
|
||||||
if(displayDebugInfo) {
|
|
||||||
double lastFrameTime = lastFrameTimer.GetElapsedMS();
|
|
||||||
lastFrameTimer.Reset();
|
|
||||||
frameDurations[frameDurationIndex] = lastFrameTime;
|
|
||||||
frameDurationIndex = (frameDurationIndex + 1) % 60;
|
|
||||||
|
|
||||||
DisplayDebugInformation(lastFrameTime, lastFrameMin, lastFrameMax, frameDurations);
|
|
||||||
if(_slave) {
|
|
||||||
_slave->DisplayDebugInformation(lastFrameTime, lastFrameMin, lastFrameMax, frameDurations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//When sleeping for a long time (e.g <= 25% speed), sleep in small chunks and check to see if we need to stop sleeping between each sleep call
|
|
||||||
while(targetTime - clockTimer.GetElapsedMS() > 50) {
|
|
||||||
clockTimer.WaitUntil(clockTimer.GetElapsedMS() + 40);
|
|
||||||
if(delay != GetFrameDelay() || _stop || _settings->NeedsPause() || _pauseCounter > 0) {
|
|
||||||
targetTime = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Sleep until we're ready to start the next frame
|
|
||||||
clockTimer.WaitUntil(targetTime);
|
|
||||||
|
|
||||||
if(_pauseCounter > 0) {
|
|
||||||
//Need to temporarely pause the emu (to save/load a state, etc.)
|
|
||||||
_runLock.Release();
|
|
||||||
|
|
||||||
//Spin wait until we are allowed to start again
|
|
||||||
while(_pauseCounter > 0) { }
|
|
||||||
|
|
||||||
_runLock.Acquire();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_pauseOnNextFrameRequested) {
|
|
||||||
//Used by "Run Single Frame" option
|
|
||||||
_settings->SetFlags(EmulationFlags::Paused);
|
|
||||||
_pauseOnNextFrameRequested = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool pausedRequired = _settings->NeedsPause();
|
|
||||||
if(pausedRequired && !_stop && !_settings->CheckFlag(EmulationFlags::DebuggerWindowEnabled)) {
|
|
||||||
_notificationManager->SendNotification(ConsoleNotificationType::GamePaused);
|
|
||||||
|
|
||||||
//Prevent audio from looping endlessly while game is paused
|
|
||||||
_soundMixer->StopAudio();
|
|
||||||
if(_slave) {
|
|
||||||
_slave->_soundMixer->StopAudio();
|
|
||||||
}
|
|
||||||
|
|
||||||
_runLock.Release();
|
|
||||||
|
|
||||||
PlatformUtilities::EnableScreensaver();
|
|
||||||
PlatformUtilities::RestoreTimerResolution();
|
|
||||||
while(pausedRequired && !_stop && !_settings->CheckFlag(EmulationFlags::DebuggerWindowEnabled)) {
|
|
||||||
//Sleep until emulation is resumed
|
|
||||||
std::this_thread::sleep_for(std::chrono::duration<int, std::milli>(30));
|
|
||||||
pausedRequired = _settings->NeedsPause();
|
|
||||||
_paused = true;
|
|
||||||
}
|
|
||||||
_paused = false;
|
|
||||||
|
|
||||||
PlatformUtilities::DisableScreensaver();
|
|
||||||
_runLock.Acquire();
|
|
||||||
_notificationManager->SendNotification(ConsoleNotificationType::GameResumed);
|
|
||||||
lastFrameTimer.Reset();
|
|
||||||
|
|
||||||
//Reset the timer to avoid speed up after a pause
|
|
||||||
_resetRunTimers = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_settings->CheckFlag(EmulationFlags::UseHighResolutionTimer)) {
|
|
||||||
PlatformUtilities::EnableHighResolutionTimer();
|
|
||||||
} else {
|
|
||||||
PlatformUtilities::RestoreTimerResolution();
|
|
||||||
}
|
|
||||||
|
|
||||||
_systemActionManager->ProcessSystemActions();
|
|
||||||
|
|
||||||
lastFrameNumber = _ppu->GetFrameCount();
|
|
||||||
|
|
||||||
if(_stop) {
|
|
||||||
_stop = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Sleep until we're ready to start the next frame
|
||||||
|
clockTimer.WaitUntil(targetTime);
|
||||||
|
|
||||||
|
if(useRunAhead) {
|
||||||
|
_settings->SetRunAheadFrameFlag(true);
|
||||||
|
LoadState(runAheadState);
|
||||||
|
_settings->SetRunAheadFrameFlag(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_pauseCounter > 0) {
|
||||||
|
//Need to temporarely pause the emu (to save/load a state, etc.)
|
||||||
|
_runLock.Release();
|
||||||
|
|
||||||
|
//Spin wait until we are allowed to start again
|
||||||
|
while(_pauseCounter > 0) { }
|
||||||
|
|
||||||
|
_runLock.Acquire();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_pauseOnNextFrameRequested) {
|
||||||
|
//Used by "Run Single Frame" option
|
||||||
|
_settings->SetFlags(EmulationFlags::Paused);
|
||||||
|
_pauseOnNextFrameRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pausedRequired = _settings->NeedsPause();
|
||||||
|
if(pausedRequired && !_stop && !_settings->CheckFlag(EmulationFlags::DebuggerWindowEnabled)) {
|
||||||
|
_notificationManager->SendNotification(ConsoleNotificationType::GamePaused);
|
||||||
|
|
||||||
|
//Prevent audio from looping endlessly while game is paused
|
||||||
|
_soundMixer->StopAudio();
|
||||||
|
if(_slave) {
|
||||||
|
_slave->_soundMixer->StopAudio();
|
||||||
|
}
|
||||||
|
|
||||||
|
_runLock.Release();
|
||||||
|
|
||||||
|
PlatformUtilities::EnableScreensaver();
|
||||||
|
PlatformUtilities::RestoreTimerResolution();
|
||||||
|
while(pausedRequired && !_stop && !_settings->CheckFlag(EmulationFlags::DebuggerWindowEnabled)) {
|
||||||
|
//Sleep until emulation is resumed
|
||||||
|
std::this_thread::sleep_for(std::chrono::duration<int, std::milli>(30));
|
||||||
|
pausedRequired = _settings->NeedsPause();
|
||||||
|
_paused = true;
|
||||||
|
}
|
||||||
|
_paused = false;
|
||||||
|
|
||||||
|
PlatformUtilities::DisableScreensaver();
|
||||||
|
_runLock.Acquire();
|
||||||
|
_notificationManager->SendNotification(ConsoleNotificationType::GameResumed);
|
||||||
|
lastFrameTimer.Reset();
|
||||||
|
|
||||||
|
//Reset the timer to avoid speed up after a pause
|
||||||
|
_resetRunTimers = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_settings->CheckFlag(EmulationFlags::UseHighResolutionTimer)) {
|
||||||
|
PlatformUtilities::EnableHighResolutionTimer();
|
||||||
|
} else {
|
||||||
|
PlatformUtilities::RestoreTimerResolution();
|
||||||
|
}
|
||||||
|
|
||||||
|
_systemActionManager->ProcessSystemActions();
|
||||||
|
|
||||||
|
if(_stop) {
|
||||||
|
_stop = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch(const std::runtime_error &ex) {
|
} catch(const std::runtime_error &ex) {
|
||||||
crashed = true;
|
crashed = true;
|
||||||
|
@ -926,6 +940,26 @@ void Console::Run()
|
||||||
_notificationManager->SendNotification(ConsoleNotificationType::EmulationStopped);
|
_notificationManager->SendNotification(ConsoleNotificationType::EmulationStopped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Console::RunFrameWithRunAhead(std::stringstream& runAheadState)
|
||||||
|
{
|
||||||
|
uint32_t runAheadFrames = _settings->GetRunAheadFrames();
|
||||||
|
_settings->SetRunAheadFrameFlag(true);
|
||||||
|
//Run a single frame and save the state (no audio/video)
|
||||||
|
RunFrame();
|
||||||
|
SaveState(runAheadState);
|
||||||
|
while(runAheadFrames > 1) {
|
||||||
|
//Run extra frames if the requested run ahead frame count is higher than 1
|
||||||
|
runAheadFrames--;
|
||||||
|
RunFrame();
|
||||||
|
}
|
||||||
|
_apu->EndFrame();
|
||||||
|
_settings->SetRunAheadFrameFlag(false);
|
||||||
|
|
||||||
|
//Run one frame normally (with audio/video output)
|
||||||
|
RunFrame();
|
||||||
|
_apu->EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
void Console::ResetRunTimers()
|
void Console::ResetRunTimers()
|
||||||
{
|
{
|
||||||
_resetRunTimers = true;
|
_resetRunTimers = true;
|
||||||
|
@ -1023,6 +1057,9 @@ double Console::GetFrameDelay()
|
||||||
void Console::SaveState(ostream &saveStream)
|
void Console::SaveState(ostream &saveStream)
|
||||||
{
|
{
|
||||||
if(_initialized) {
|
if(_initialized) {
|
||||||
|
//Send any unprocessed sound to the SoundMixer - needed for rewind
|
||||||
|
_apu->EndFrame();
|
||||||
|
|
||||||
_cpu->SaveSnapshot(&saveStream);
|
_cpu->SaveSnapshot(&saveStream);
|
||||||
_ppu->SaveSnapshot(&saveStream);
|
_ppu->SaveSnapshot(&saveStream);
|
||||||
_memoryManager->SaveSnapshot(&saveStream);
|
_memoryManager->SaveSnapshot(&saveStream);
|
||||||
|
@ -1050,6 +1087,9 @@ void Console::LoadState(istream &loadStream)
|
||||||
void Console::LoadState(istream &loadStream, uint32_t stateVersion)
|
void Console::LoadState(istream &loadStream, uint32_t stateVersion)
|
||||||
{
|
{
|
||||||
if(_initialized) {
|
if(_initialized) {
|
||||||
|
//Send any unprocessed sound to the SoundMixer - needed for rewind
|
||||||
|
_apu->EndFrame();
|
||||||
|
|
||||||
_cpu->LoadSnapshot(&loadStream, stateVersion);
|
_cpu->LoadSnapshot(&loadStream, stateVersion);
|
||||||
_ppu->LoadSnapshot(&loadStream, stateVersion);
|
_ppu->LoadSnapshot(&loadStream, stateVersion);
|
||||||
_memoryManager->LoadSnapshot(&loadStream, stateVersion);
|
_memoryManager->LoadSnapshot(&loadStream, stateVersion);
|
||||||
|
@ -1080,9 +1120,6 @@ void Console::LoadState(istream &loadStream, uint32_t stateVersion)
|
||||||
|
|
||||||
void Console::LoadState(uint8_t *buffer, uint32_t bufferSize)
|
void Console::LoadState(uint8_t *buffer, uint32_t bufferSize)
|
||||||
{
|
{
|
||||||
//Send any unprocessed sound to the SoundMixer - needed for rewind
|
|
||||||
_apu->EndFrame();
|
|
||||||
|
|
||||||
stringstream stream;
|
stringstream stream;
|
||||||
stream.write((char*)buffer, bufferSize);
|
stream.write((char*)buffer, bufferSize);
|
||||||
stream.seekg(0, ios::beg);
|
stream.seekg(0, ios::beg);
|
||||||
|
|
|
@ -100,6 +100,8 @@ private:
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
std::thread::id _emulationThreadId;
|
std::thread::id _emulationThreadId;
|
||||||
|
|
||||||
|
void RunFrameWithRunAhead(std::stringstream& runAheadState);
|
||||||
|
|
||||||
void LoadHdPack(VirtualFile &romFile, VirtualFile &patchFile);
|
void LoadHdPack(VirtualFile &romFile, VirtualFile &patchFile);
|
||||||
|
|
||||||
void UpdateNesModel(bool sendNotification);
|
void UpdateNesModel(bool sendNotification);
|
||||||
|
@ -156,6 +158,7 @@ public:
|
||||||
|
|
||||||
void RunSingleFrame();
|
void RunSingleFrame();
|
||||||
void RunSlaveCpu();
|
void RunSlaveCpu();
|
||||||
|
void RunFrame();
|
||||||
bool UpdateHdPackMode();
|
bool UpdateHdPackMode();
|
||||||
|
|
||||||
shared_ptr<SystemActionManager> GetSystemActionManager();
|
shared_ptr<SystemActionManager> GetSystemActionManager();
|
||||||
|
|
|
@ -293,8 +293,10 @@ void ControlManager::UpdateInputState()
|
||||||
debugger->ProcessEvent(EventType::InputPolled);
|
debugger->ProcessEvent(EventType::InputPolled);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(IInputRecorder* recorder : _inputRecorders) {
|
if(!_console->GetSettings()->IsRunAheadFrame()) {
|
||||||
recorder->RecordInput(_controlDevices);
|
for(IInputRecorder* recorder : _inputRecorders) {
|
||||||
|
recorder->RecordInput(_controlDevices);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Used by VS System games
|
//Used by VS System games
|
||||||
|
|
|
@ -655,6 +655,9 @@ private:
|
||||||
uint32_t _sampleRate = 48000;
|
uint32_t _sampleRate = 48000;
|
||||||
AudioFilterSettings _audioFilterSettings;
|
AudioFilterSettings _audioFilterSettings;
|
||||||
|
|
||||||
|
uint32_t _runAheadFrames = 0;
|
||||||
|
bool _isRunAheadFrame = false;
|
||||||
|
|
||||||
NesModel _model = NesModel::Auto;
|
NesModel _model = NesModel::Auto;
|
||||||
PpuModel _ppuModel = PpuModel::Ppu2C02;
|
PpuModel _ppuModel = PpuModel::Ppu2C02;
|
||||||
|
|
||||||
|
@ -992,6 +995,26 @@ public:
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetRunAheadFrames(uint32_t frameCount)
|
||||||
|
{
|
||||||
|
_runAheadFrames = frameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t GetRunAheadFrames()
|
||||||
|
{
|
||||||
|
return _runAheadFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetRunAheadFrameFlag(bool disabled)
|
||||||
|
{
|
||||||
|
_isRunAheadFrame = disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsRunAheadFrame()
|
||||||
|
{
|
||||||
|
return _isRunAheadFrame;
|
||||||
|
}
|
||||||
|
|
||||||
//0: No limit, Number: % of default speed (50/60fps)
|
//0: No limit, Number: % of default speed (50/60fps)
|
||||||
void SetEmulationSpeed(uint32_t emulationSpeed, bool displaySpeed = false)
|
void SetEmulationSpeed(uint32_t emulationSpeed, bool displaySpeed = false)
|
||||||
{
|
{
|
||||||
|
|
|
@ -48,6 +48,10 @@ void RewindManager::ClearBuffer()
|
||||||
|
|
||||||
void RewindManager::ProcessNotification(ConsoleNotificationType type, void * parameter)
|
void RewindManager::ProcessNotification(ConsoleNotificationType type, void * parameter)
|
||||||
{
|
{
|
||||||
|
if(_settings->IsRunAheadFrame()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(type == ConsoleNotificationType::PpuFrameDone) {
|
if(type == ConsoleNotificationType::PpuFrameDone) {
|
||||||
_hasHistory = _history.size() >= 2;
|
_hasHistory = _history.size() >= 2;
|
||||||
if(_settings->GetRewindBufferSize() > 0) {
|
if(_settings->GetRewindBufferSize() > 0) {
|
||||||
|
|
|
@ -164,7 +164,7 @@ void SoundMixer::PlayAudioBuffer(uint32_t time)
|
||||||
_crossFeedFilter.ApplyFilter(_outputBuffer, sampleCount, filterSettings.CrossFadeRatio);
|
_crossFeedFilter.ApplyFilter(_outputBuffer, sampleCount, filterSettings.CrossFadeRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(rewindManager && rewindManager->SendAudio(_outputBuffer, (uint32_t)sampleCount, _sampleRate)) {
|
if(!_settings->IsRunAheadFrame() && rewindManager && rewindManager->SendAudio(_outputBuffer, (uint32_t)sampleCount, _sampleRate)) {
|
||||||
bool isRecording = _waveRecorder || _console->GetVideoRenderer()->IsRecording();
|
bool isRecording = _waveRecorder || _console->GetVideoRenderer()->IsRecording();
|
||||||
if(isRecording) {
|
if(isRecording) {
|
||||||
shared_ptr<WaveRecorder> recorder = _waveRecorder;
|
shared_ptr<WaveRecorder> recorder = _waveRecorder;
|
||||||
|
|
|
@ -41,11 +41,9 @@ public:
|
||||||
{
|
{
|
||||||
if(_needReset) {
|
if(_needReset) {
|
||||||
SetBit(SystemActionManager::Buttons::ResetButton);
|
SetBit(SystemActionManager::Buttons::ResetButton);
|
||||||
_needReset = false;
|
|
||||||
}
|
}
|
||||||
if(_needPowerCycle) {
|
if(_needPowerCycle) {
|
||||||
SetBit(SystemActionManager::Buttons::PowerButton);
|
SetBit(SystemActionManager::Buttons::PowerButton);
|
||||||
_needPowerCycle = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +70,7 @@ public:
|
||||||
shared_ptr<Console> console = _console;
|
shared_ptr<Console> console = _console;
|
||||||
if(console) {
|
if(console) {
|
||||||
if(IsPressed(SystemActionManager::Buttons::ResetButton)) {
|
if(IsPressed(SystemActionManager::Buttons::ResetButton)) {
|
||||||
|
_needReset = false;
|
||||||
console->ResetComponents(true);
|
console->ResetComponents(true);
|
||||||
console->GetControlManager()->UpdateInputState();
|
console->GetControlManager()->UpdateInputState();
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
VideoDecoder::VideoDecoder(shared_ptr<Console> console)
|
VideoDecoder::VideoDecoder(shared_ptr<Console> console)
|
||||||
{
|
{
|
||||||
_console = console;
|
_console = console;
|
||||||
|
_settings = _console->GetSettings();
|
||||||
_frameChanged = false;
|
_frameChanged = false;
|
||||||
_stopFlag = false;
|
_stopFlag = false;
|
||||||
UpdateVideoFilter();
|
UpdateVideoFilter();
|
||||||
|
@ -158,6 +159,10 @@ uint32_t VideoDecoder::GetFrameCount()
|
||||||
|
|
||||||
void VideoDecoder::UpdateFrameSync(void *ppuOutputBuffer, HdScreenInfo *hdScreenInfo)
|
void VideoDecoder::UpdateFrameSync(void *ppuOutputBuffer, HdScreenInfo *hdScreenInfo)
|
||||||
{
|
{
|
||||||
|
if(_settings->IsRunAheadFrame()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_frameNumber = _console->GetFrameCount();
|
_frameNumber = _console->GetFrameCount();
|
||||||
_hdScreenInfo = hdScreenInfo;
|
_hdScreenInfo = hdScreenInfo;
|
||||||
_ppuOutputBuffer = (uint16_t*)ppuOutputBuffer;
|
_ppuOutputBuffer = (uint16_t*)ppuOutputBuffer;
|
||||||
|
@ -167,6 +172,10 @@ void VideoDecoder::UpdateFrameSync(void *ppuOutputBuffer, HdScreenInfo *hdScreen
|
||||||
|
|
||||||
void VideoDecoder::UpdateFrame(void *ppuOutputBuffer, HdScreenInfo *hdScreenInfo)
|
void VideoDecoder::UpdateFrame(void *ppuOutputBuffer, HdScreenInfo *hdScreenInfo)
|
||||||
{
|
{
|
||||||
|
if(_settings->IsRunAheadFrame()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(_frameChanged) {
|
if(_frameChanged) {
|
||||||
//Last frame isn't done decoding yet - sometimes Signal() introduces a 25-30ms delay
|
//Last frame isn't done decoding yet - sometimes Signal() introduces a 25-30ms delay
|
||||||
while(_frameChanged) {
|
while(_frameChanged) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ class VideoDecoder
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
shared_ptr<Console> _console;
|
shared_ptr<Console> _console;
|
||||||
|
EmulationSettings* _settings;
|
||||||
uint16_t *_ppuOutputBuffer = nullptr;
|
uint16_t *_ppuOutputBuffer = nullptr;
|
||||||
HdScreenInfo *_hdScreenInfo = nullptr;
|
HdScreenInfo *_hdScreenInfo = nullptr;
|
||||||
bool _hdFilterEnabled = false;
|
bool _hdFilterEnabled = false;
|
||||||
|
|
|
@ -38,6 +38,7 @@ namespace Mesen.GUI.Config
|
||||||
[MinMax(0, 5000)] public UInt32 EmulationSpeed = 100;
|
[MinMax(0, 5000)] public UInt32 EmulationSpeed = 100;
|
||||||
[MinMax(0, 5000)] public UInt32 TurboSpeed = 300;
|
[MinMax(0, 5000)] public UInt32 TurboSpeed = 300;
|
||||||
[MinMax(0, 5000)] public UInt32 RewindSpeed = 100;
|
[MinMax(0, 5000)] public UInt32 RewindSpeed = 100;
|
||||||
|
[MinMax(0, 10)] public UInt32 RunAheadFrames = 0;
|
||||||
|
|
||||||
public EmulationInfo()
|
public EmulationInfo()
|
||||||
{
|
{
|
||||||
|
@ -49,6 +50,7 @@ namespace Mesen.GUI.Config
|
||||||
|
|
||||||
InteropEmu.SetEmulationSpeed(emulationInfo.EmulationSpeed);
|
InteropEmu.SetEmulationSpeed(emulationInfo.EmulationSpeed);
|
||||||
InteropEmu.SetTurboRewindSpeed(emulationInfo.TurboSpeed, emulationInfo.RewindSpeed);
|
InteropEmu.SetTurboRewindSpeed(emulationInfo.TurboSpeed, emulationInfo.RewindSpeed);
|
||||||
|
InteropEmu.SetRunAheadFrames(emulationInfo.RunAheadFrames);
|
||||||
|
|
||||||
InteropEmu.SetFlag(EmulationFlags.Mmc3IrqAltBehavior, emulationInfo.UseAlternativeMmc3Irq);
|
InteropEmu.SetFlag(EmulationFlags.Mmc3IrqAltBehavior, emulationInfo.UseAlternativeMmc3Irq);
|
||||||
InteropEmu.SetFlag(EmulationFlags.AllowInvalidInput, emulationInfo.AllowInvalidInput);
|
InteropEmu.SetFlag(EmulationFlags.AllowInvalidInput, emulationInfo.AllowInvalidInput);
|
||||||
|
|
|
@ -308,6 +308,8 @@
|
||||||
<Control ID="lblEmulationSpeed">Velocitat d'emulació:</Control>
|
<Control ID="lblEmulationSpeed">Velocitat d'emulació:</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = Velocitat màxima)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = Velocitat màxima)</Control>
|
||||||
<Control ID="lblRewindSpeed">Velocitat del rebobinat:</Control>
|
<Control ID="lblRewindSpeed">Velocitat del rebobinat:</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">frames (reduces input lag, increases CPU usage)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">Avançat</Control>
|
<Control ID="tpgAdvanced">Avançat</Control>
|
||||||
<Control ID="lblDeveloperSettings">Recommended settings for developers (homebrew / ROM hacking)</Control>
|
<Control ID="lblDeveloperSettings">Recommended settings for developers (homebrew / ROM hacking)</Control>
|
||||||
|
|
|
@ -308,6 +308,8 @@
|
||||||
<Control ID="lblTurboSpeed">Fast Forward Speed:</Control>
|
<Control ID="lblTurboSpeed">Fast Forward Speed:</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = Maximum speed)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = Maximum speed)</Control>
|
||||||
<Control ID="lblRewindSpeed">Rewind Speed:</Control>
|
<Control ID="lblRewindSpeed">Rewind Speed:</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">frames (reduces input lag, increases CPU usage)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">Advanced</Control>
|
<Control ID="tpgAdvanced">Advanced</Control>
|
||||||
<Control ID="lblDeveloperSettings">Recommended settings for developers (homebrew / ROM hacking)</Control>
|
<Control ID="lblDeveloperSettings">Recommended settings for developers (homebrew / ROM hacking)</Control>
|
||||||
|
|
|
@ -307,6 +307,8 @@
|
||||||
<Control ID="lblEmulationSpeed">Velocidad de emulación:</Control>
|
<Control ID="lblEmulationSpeed">Velocidad de emulación:</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = Velocidad máxima)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = Velocidad máxima)</Control>
|
||||||
<Control ID="lblRewindSpeed">Velocidad de rebobinado:</Control>
|
<Control ID="lblRewindSpeed">Velocidad de rebobinado:</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">frames (reduces input lag, increases CPU usage)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">Avanzado</Control>
|
<Control ID="tpgAdvanced">Avanzado</Control>
|
||||||
<Control ID="lblDeveloperSettings">Configuración recomendada para desarrolladores (homebrew / ROM hacking)</Control>
|
<Control ID="lblDeveloperSettings">Configuración recomendada para desarrolladores (homebrew / ROM hacking)</Control>
|
||||||
|
|
|
@ -308,6 +308,8 @@
|
||||||
<Control ID="lblTurboSpeed">Vitesse d'avance rapide :</Control>
|
<Control ID="lblTurboSpeed">Vitesse d'avance rapide :</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = Vitesse maximale)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = Vitesse maximale)</Control>
|
||||||
<Control ID="lblRewindSpeed">Vitesse du rembobinage :</Control>
|
<Control ID="lblRewindSpeed">Vitesse du rembobinage :</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">images (réduit l'input lag, au coût de la performance)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">Avancé</Control>
|
<Control ID="tpgAdvanced">Avancé</Control>
|
||||||
<Control ID="lblDeveloperSettings">Options recommandées pour les développeurs (homebrew / ROM hacking)</Control>
|
<Control ID="lblDeveloperSettings">Options recommandées pour les développeurs (homebrew / ROM hacking)</Control>
|
||||||
|
|
|
@ -308,6 +308,8 @@
|
||||||
<Control ID="lblTurboSpeed">Velocità avanti veloce:</Control>
|
<Control ID="lblTurboSpeed">Velocità avanti veloce:</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = Velocità massima)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = Velocità massima)</Control>
|
||||||
<Control ID="lblRewindSpeed">Velocità Riavvolgimento:</Control>
|
<Control ID="lblRewindSpeed">Velocità Riavvolgimento:</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">frames (reduces input lag, increases CPU usage)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">Avanzato</Control>
|
<Control ID="tpgAdvanced">Avanzato</Control>
|
||||||
<Control ID="lblDeveloperSettings">Opzioni raccomandate per sviluppatori (homebrew / ROM hacking)</Control>
|
<Control ID="lblDeveloperSettings">Opzioni raccomandate per sviluppatori (homebrew / ROM hacking)</Control>
|
||||||
|
|
|
@ -307,6 +307,8 @@
|
||||||
<Control ID="lblTurboSpeed">早送りの速度:</Control>
|
<Control ID="lblTurboSpeed">早送りの速度:</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = 最高速度)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = 最高速度)</Control>
|
||||||
<Control ID="lblRewindSpeed">巻き戻しの速度:</Control>
|
<Control ID="lblRewindSpeed">巻き戻しの速度:</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">フレーム (入力遅延低下)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">詳細設定</Control>
|
<Control ID="tpgAdvanced">詳細設定</Control>
|
||||||
<Control ID="lblDeveloperSettings">開発者向け設定 (自作ソフト / ROM hacking)</Control>
|
<Control ID="lblDeveloperSettings">開発者向け設定 (自作ソフト / ROM hacking)</Control>
|
||||||
|
|
|
@ -308,6 +308,8 @@
|
||||||
<Control ID="lblTurboSpeed">Velocidade de avanço rápido:</Control>
|
<Control ID="lblTurboSpeed">Velocidade de avanço rápido:</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = Velocidade máxima)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = Velocidade máxima)</Control>
|
||||||
<Control ID="lblRewindSpeed">Velocidade de rebobinamento:</Control>
|
<Control ID="lblRewindSpeed">Velocidade de rebobinamento:</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">frames (reduces input lag, increases CPU usage)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">Avançado</Control>
|
<Control ID="tpgAdvanced">Avançado</Control>
|
||||||
<Control ID="lblDeveloperSettings">Configurações recomendadas para programadores (homebrew / ROM hacking)</Control>
|
<Control ID="lblDeveloperSettings">Configurações recomendadas para programadores (homebrew / ROM hacking)</Control>
|
||||||
|
|
|
@ -307,6 +307,8 @@
|
||||||
<Control ID="lblTurboSpeed">Перемотка:</Control>
|
<Control ID="lblTurboSpeed">Перемотка:</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = Максимальная скорость)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = Максимальная скорость)</Control>
|
||||||
<Control ID="lblRewindSpeed">Скорость перемотки:</Control>
|
<Control ID="lblRewindSpeed">Скорость перемотки:</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">frames (reduces input lag, increases CPU usage)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">Расширенные</Control>
|
<Control ID="tpgAdvanced">Расширенные</Control>
|
||||||
<Control ID="lblDeveloperSettings">Рекомендованные настройки для разработчиков (homebrew / ROM hacking)</Control>
|
<Control ID="lblDeveloperSettings">Рекомендованные настройки для разработчиков (homebrew / ROM hacking)</Control>
|
||||||
|
|
|
@ -307,6 +307,8 @@
|
||||||
<Control ID="lblTurboSpeed">Перемотка:</Control>
|
<Control ID="lblTurboSpeed">Перемотка:</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = Максимальна швидкiсть)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = Максимальна швидкiсть)</Control>
|
||||||
<Control ID="lblRewindSpeed">Швидкість перемотування:</Control>
|
<Control ID="lblRewindSpeed">Швидкість перемотування:</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">frames (reduces input lag, increases CPU usage)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">Розширені</Control>
|
<Control ID="tpgAdvanced">Розширені</Control>
|
||||||
<Control ID="lblDeveloperSettings">Recommended settings for developers (homebrew / ROM hacking)</Control>
|
<Control ID="lblDeveloperSettings">Recommended settings for developers (homebrew / ROM hacking)</Control>
|
||||||
|
|
|
@ -334,6 +334,8 @@
|
||||||
<Control ID="lblTurboSpeed">快进速度:</Control>
|
<Control ID="lblTurboSpeed">快进速度:</Control>
|
||||||
<Control ID="lblRewindSpeedHint">% (0 = 最快)</Control>
|
<Control ID="lblRewindSpeedHint">% (0 = 最快)</Control>
|
||||||
<Control ID="lblRewindSpeed">快退速度:</Control>
|
<Control ID="lblRewindSpeed">快退速度:</Control>
|
||||||
|
<Control ID="lblRunAhead">Run Ahead:</Control>
|
||||||
|
<Control ID="lblRunAheadFrames">frames (reduces input lag, increases CPU usage)</Control>
|
||||||
|
|
||||||
<Control ID="tpgAdvanced">高级</Control>
|
<Control ID="tpgAdvanced">高级</Control>
|
||||||
<Control ID="lblDeveloperSettings">开发者推荐设置 (自作/修改 ROM)</Control>
|
<Control ID="lblDeveloperSettings">开发者推荐设置 (自作/修改 ROM)</Control>
|
||||||
|
|
1758
GUI.NET/Forms/Config/frmEmulationConfig.Designer.cs
generated
1758
GUI.NET/Forms/Config/frmEmulationConfig.Designer.cs
generated
File diff suppressed because it is too large
Load diff
|
@ -27,6 +27,7 @@ namespace Mesen.GUI.Forms.Config
|
||||||
AddBinding("EmulationSpeed", nudEmulationSpeed);
|
AddBinding("EmulationSpeed", nudEmulationSpeed);
|
||||||
AddBinding("TurboSpeed", nudTurboSpeed);
|
AddBinding("TurboSpeed", nudTurboSpeed);
|
||||||
AddBinding("RewindSpeed", nudRewindSpeed);
|
AddBinding("RewindSpeed", nudRewindSpeed);
|
||||||
|
AddBinding("RunAheadFrames", nudRunAheadFrames);
|
||||||
|
|
||||||
AddBinding("UseAlternativeMmc3Irq", chkUseAlternativeMmc3Irq);
|
AddBinding("UseAlternativeMmc3Irq", chkUseAlternativeMmc3Irq);
|
||||||
AddBinding("AllowInvalidInput", chkAllowInvalidInput);
|
AddBinding("AllowInvalidInput", chkAllowInvalidInput);
|
||||||
|
|
|
@ -31,6 +31,7 @@ namespace Mesen.GUI
|
||||||
[DllImport(DLLPath)] public static extern void HistoryViewerRelease();
|
[DllImport(DLLPath)] public static extern void HistoryViewerRelease();
|
||||||
[DllImport(DLLPath)] public static extern void HistoryViewerRun();
|
[DllImport(DLLPath)] public static extern void HistoryViewerRun();
|
||||||
[DllImport(DLLPath)] public static extern void HistoryViewerStop();
|
[DllImport(DLLPath)] public static extern void HistoryViewerStop();
|
||||||
|
|
||||||
[DllImport(DLLPath)] public static extern UInt32 HistoryViewerGetHistoryLength();
|
[DllImport(DLLPath)] public static extern UInt32 HistoryViewerGetHistoryLength();
|
||||||
[DllImport(DLLPath)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool HistoryViewerSaveMovie([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]string movieFile, UInt32 startPosition, UInt32 endPosition);
|
[DllImport(DLLPath)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool HistoryViewerSaveMovie([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]string movieFile, UInt32 startPosition, UInt32 endPosition);
|
||||||
[DllImport(DLLPath)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool HistoryViewerCreateSaveState([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]string outfileFile, UInt32 position);
|
[DllImport(DLLPath)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool HistoryViewerCreateSaveState([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]string outfileFile, UInt32 position);
|
||||||
|
@ -192,6 +193,7 @@ namespace Mesen.GUI
|
||||||
[DllImport(DLLPath)] public static extern void SetSampleRate(UInt32 sampleRate);
|
[DllImport(DLLPath)] public static extern void SetSampleRate(UInt32 sampleRate);
|
||||||
[DllImport(DLLPath)] public static extern void SetAudioLatency(UInt32 msLatency);
|
[DllImport(DLLPath)] public static extern void SetAudioLatency(UInt32 msLatency);
|
||||||
[DllImport(DLLPath)] public static extern void SetAudioFilterSettings(AudioFilterSettings settings);
|
[DllImport(DLLPath)] public static extern void SetAudioFilterSettings(AudioFilterSettings settings);
|
||||||
|
[DllImport(DLLPath)] public static extern void SetRunAheadFrames(UInt32 frameCount);
|
||||||
|
|
||||||
[DllImport(DLLPath)] public static extern NesModel GetNesModel();
|
[DllImport(DLLPath)] public static extern NesModel GetNesModel();
|
||||||
[DllImport(DLLPath)] public static extern void SetNesModel(NesModel model);
|
[DllImport(DLLPath)] public static extern void SetNesModel(NesModel model);
|
||||||
|
|
|
@ -627,6 +627,7 @@ namespace InteropEmu {
|
||||||
DllExport void __stdcall SetSampleRate(uint32_t sampleRate) { _settings->SetSampleRate(sampleRate); }
|
DllExport void __stdcall SetSampleRate(uint32_t sampleRate) { _settings->SetSampleRate(sampleRate); }
|
||||||
DllExport void __stdcall SetAudioLatency(uint32_t msLatency) { _settings->SetAudioLatency(msLatency); }
|
DllExport void __stdcall SetAudioLatency(uint32_t msLatency) { _settings->SetAudioLatency(msLatency); }
|
||||||
DllExport void __stdcall SetAudioFilterSettings(AudioFilterSettings settings) { _settings->SetAudioFilterSettings(settings); }
|
DllExport void __stdcall SetAudioFilterSettings(AudioFilterSettings settings) { _settings->SetAudioFilterSettings(settings); }
|
||||||
|
DllExport void __stdcall SetRunAheadFrames(uint32_t frameCount) { _settings->SetRunAheadFrames(frameCount); }
|
||||||
|
|
||||||
DllExport NesModel __stdcall GetNesModel() { return _console->GetModel(); }
|
DllExport NesModel __stdcall GetNesModel() { return _console->GetModel(); }
|
||||||
DllExport void __stdcall SetNesModel(uint32_t model) { _settings->SetNesModel((NesModel)model); }
|
DllExport void __stdcall SetNesModel(uint32_t model) { _settings->SetNesModel((NesModel)model); }
|
||||||
|
|
Loading…
Add table
Reference in a new issue