Added fully working deterministic save state support (non-portable.)
Rewind is now 100% deterministic, disk save states are still portable.
Added run-ahead support.
This commit is contained in:
byuu
2019-10-15 22:12:10 +09:00
parent a32b6fae74
commit 19f3cdfd5e
23 changed files with 174 additions and 35 deletions

View File

@@ -73,6 +73,8 @@ struct Stream {
write(samples); write(samples);
} }
auto serialize(serializer&) -> void;
private: private:
struct Channel { struct Channel {
vector<Filter> filters; vector<Filter> filters;

View File

@@ -115,3 +115,11 @@ auto Stream::write(const double samples[]) -> void {
audio.process(); audio.process();
} }
auto Stream::serialize(serializer& s) -> void {
for(auto& channel : channels) {
channel.resampler.serialize(s);
}
s.real(inputFrequency);
s.real(outputFrequency);
}

View File

@@ -29,7 +29,7 @@ using namespace nall;
namespace Emulator { namespace Emulator {
static const string Name = "bsnes"; static const string Name = "bsnes";
static const string Version = "111.7"; static const string Version = "111.8";
static const string Author = "byuu"; static const string Author = "byuu";
static const string License = "GPLv3"; static const string License = "GPLv3";
static const string Website = "https://byuu.org"; static const string Website = "https://byuu.org";

View File

@@ -101,6 +101,9 @@ struct Interface {
virtual auto frameSkip() -> uint { return 0; } virtual auto frameSkip() -> uint { return 0; }
virtual auto setFrameSkip(uint frameSkip) -> void {} virtual auto setFrameSkip(uint frameSkip) -> void {}
virtual auto runAhead() -> bool { return false; }
virtual auto setRunAhead(bool runAhead) -> void {}
}; };
} }

View File

@@ -67,7 +67,7 @@ auto CPU::load() -> bool {
auto CPU::power(bool reset) -> void { auto CPU::power(bool reset) -> void {
WDC65816::power(); WDC65816::power();
create(Enter, system.cpuFrequency()); Thread::create(Enter, system.cpuFrequency());
coprocessors.reset(); coprocessors.reset();
PPUcounter::reset(); PPUcounter::reset();
PPUcounter::scanline = {&CPU::scanline, this}; PPUcounter::scanline = {&CPU::scanline, this};

View File

@@ -18,6 +18,7 @@ auto DSP::main() -> void {
int count = spc_dsp.sample_count(); int count = spc_dsp.sample_count();
if(count > 0) { if(count > 0) {
if(!system.runAhead)
for(uint n = 0; n < count; n += 2) { for(uint n = 0; n < count; n += 2) {
float left = samplebuffer[n + 0] / 32768.0f; float left = samplebuffer[n + 0] / 32768.0f;
float right = samplebuffer[n + 1] / 32768.0f; float right = samplebuffer[n + 1] / 32768.0f;

View File

@@ -330,7 +330,15 @@ auto Interface::frameSkip() -> uint {
auto Interface::setFrameSkip(uint frameSkip) -> void { auto Interface::setFrameSkip(uint frameSkip) -> void {
system.frameSkip = frameSkip; system.frameSkip = frameSkip;
system.frameCounter = 0; system.frameCounter = frameSkip;
}
auto Interface::runAhead() -> bool {
return system.runAhead;
}
auto Interface::setRunAhead(bool runAhead) -> void {
system.runAhead = runAhead;
} }
} }

View File

@@ -71,6 +71,9 @@ struct Interface : Emulator::Interface {
auto frameSkip() -> uint override; auto frameSkip() -> uint override;
auto setFrameSkip(uint frameSkip) -> void override; auto setFrameSkip(uint frameSkip) -> void override;
auto runAhead() -> bool override;
auto setRunAhead(bool runAhead) -> void override;
}; };
#include "configuration.hpp" #include "configuration.hpp"

View File

@@ -88,7 +88,7 @@ auto PPU::step(uint clocks) -> void {
auto PPU::main() -> void { auto PPU::main() -> void {
scanline(); scanline();
if(system.frameCounter == 0) { if(system.frameCounter == 0 && !system.runAhead) {
uint y = vcounter(); uint y = vcounter();
if(y >= 1 && y <= 239) { if(y >= 1 && y <= 239) {
step(renderCycle()); step(renderCycle());
@@ -127,7 +127,7 @@ auto PPU::scanline() -> void {
} }
auto PPU::refresh() -> void { auto PPU::refresh() -> void {
if(system.frameCounter == 0) { if(system.frameCounter == 0 && !system.runAhead) {
auto output = this->output; auto output = this->output;
uint pitch, width, height; uint pitch, width, height;
if(!hd()) { if(!hd()) {

View File

@@ -189,6 +189,8 @@ auto PPU::refresh() -> void {
return ppufast.refresh(); return ppufast.refresh();
} }
if(system.runAhead) return;
auto output = this->output; auto output = this->output;
auto pitch = 512; auto pitch = 512;
auto width = 512; auto width = 512;

View File

@@ -73,13 +73,13 @@ namespace SuperFamicom {
extern Scheduler scheduler; extern Scheduler scheduler;
struct Thread { struct Thread {
enum : uint { Size = 16_KiB * sizeof(void*) }; enum : uint { Size = 4_KiB * sizeof(void*) };
auto create(auto (*entrypoint)() -> void, uint frequency_) -> void { auto create(auto (*entrypoint)() -> void, uint frequency_) -> void {
if(!thread) { if(!thread) {
thread = co_create(Thread::Size, entrypoint); thread = co_create(Thread::Size, entrypoint);
} else { } else {
co_derive(thread, Thread::Size, entrypoint); thread = co_derive(thread, Thread::Size, entrypoint);
} }
frequency = frequency_; frequency = frequency_;
clock = 0; clock = 0;
@@ -95,23 +95,26 @@ namespace SuperFamicom {
} }
auto serializeStack(serializer& s) -> void { auto serializeStack(serializer& s) -> void {
auto stack = new uint8_t[Thread::Size]; static uint8_t stack[Thread::Size];
bool active = co_active() == thread;
if(s.mode() == serializer::Size) { if(s.mode() == serializer::Size) {
s.array(stack, Thread::Size); s.array(stack, Thread::Size);
s.boolean(active);
} }
if(s.mode() == serializer::Load) { if(s.mode() == serializer::Load) {
s.array(stack, Thread::Size); s.array(stack, Thread::Size);
s.boolean(active);
memory::copy(thread, stack, Thread::Size); memory::copy(thread, stack, Thread::Size);
if(active) scheduler.active = thread;
} }
if(s.mode() == serializer::Save) { if(s.mode() == serializer::Save) {
memory::copy(stack, thread, Thread::Size); memory::copy(stack, thread, Thread::Size);
s.array(stack, Thread::Size); s.array(stack, Thread::Size);
s.boolean(active);
} }
delete[] stack;
} }
cothread_t thread = nullptr; cothread_t thread = nullptr;

View File

@@ -1,4 +1,5 @@
auto System::serialize(bool synchronize) -> serializer { auto System::serialize(bool synchronize) -> serializer {
if(!information.serializeSize[synchronize]) return {}; //should never occur
if(synchronize) runToSave(); if(synchronize) runToSave();
uint signature = 0x31545342; uint signature = 0x31545342;
@@ -38,7 +39,7 @@ auto System::unserialize(serializer& s) -> bool {
if(string{version} != Emulator::SerializerVersion) return false; if(string{version} != Emulator::SerializerVersion) return false;
if(fastPPU != hacks.fastPPU) return false; if(fastPPU != hacks.fastPPU) return false;
power(/* reset = */ false); if(synchronize) power(/* reset = */ false);
serializeAll(s, synchronize); serializeAll(s, synchronize);
return true; return true;
} }

View File

@@ -24,17 +24,54 @@ auto System::runToSave() -> void {
//fallback in case of unrecognized method specified //fallback in case of unrecognized method specified
if(method != "Fast" && method != "Strict") method = "Fast"; if(method != "Fast" && method != "Strict") method = "Fast";
auto synchronize = [&](cothread_t thread) -> bool { scheduler.mode = Scheduler::Mode::Synchronize;
scheduler.mode = Scheduler::Mode::Synchronize; if(method == "Fast") runToSaveFast();
if(method == "Strict") runToSaveStrict();
scheduler.mode = Scheduler::Mode::Run;
scheduler.active = cpu.thread;
}
auto System::runToSaveFast() -> void {
//run the emulator normally until the CPU thread naturally hits a synchronization point
while(true) {
scheduler.enter();
if(scheduler.event == Scheduler::Event::Frame) frameEvent();
if(scheduler.event == Scheduler::Event::Synchronized) {
if(scheduler.active != cpu.thread) continue;
break;
}
if(scheduler.event == Scheduler::Event::Desynchronized) continue;
}
//ignore any desynchronization events to force all other threads to their synchronization points
auto synchronize = [&](cothread_t thread) -> void {
scheduler.active = thread; scheduler.active = thread;
while(true) { while(true) {
scheduler.enter(); scheduler.enter();
if(scheduler.event == Scheduler::Event::Frame) frameEvent(); if(scheduler.event == Scheduler::Event::Frame) frameEvent();
if(scheduler.event == Scheduler::Event::Synchronized) break; if(scheduler.event == Scheduler::Event::Synchronized) break;
if(scheduler.event == Scheduler::Event::Desynchronized) { if(scheduler.event == Scheduler::Event::Desynchronized) continue;
if(method == "Fast") continue; }
if(method == "Strict") return false; };
}
synchronize(smp.thread);
synchronize(ppu.thread);
for(auto coprocessor : cpu.coprocessors) {
synchronize(coprocessor->thread);
}
}
auto System::runToSaveStrict() -> void {
//run every thread until it cleanly hits a synchronization point
//if it fails, start resynchronizing every thread again
auto synchronize = [&](cothread_t thread) -> bool {
scheduler.active = thread;
while(true) {
scheduler.enter();
if(scheduler.event == Scheduler::Event::Frame) frameEvent();
if(scheduler.event == Scheduler::Event::Synchronized) break;
if(scheduler.event == Scheduler::Event::Desynchronized) return false;
} }
return true; return true;
}; };
@@ -55,9 +92,6 @@ auto System::runToSave() -> void {
break; break;
} }
scheduler.mode = Scheduler::Mode::Run;
scheduler.active = cpu.thread;
} }
auto System::frameEvent() -> void { auto System::frameEvent() -> void {
@@ -97,8 +131,6 @@ auto System::load(Emulator::Interface* interface) -> bool {
} }
if(cartridge.has.BSMemorySlot) bsmemory.load(); if(cartridge.has.BSMemorySlot) bsmemory.load();
information.serializeSize[0] = serializeInit(0);
information.serializeSize[1] = serializeInit(1);
this->interface = interface; this->interface = interface;
return information.loaded = true; return information.loaded = true;
} }
@@ -195,6 +227,9 @@ auto System::power(bool reset) -> void {
controllerPort1.connect(settings.controllerPort1); controllerPort1.connect(settings.controllerPort1);
controllerPort2.connect(settings.controllerPort2); controllerPort2.connect(settings.controllerPort2);
expansionPort.connect(settings.expansionPort); expansionPort.connect(settings.expansionPort);
information.serializeSize[0] = serializeInit(0);
information.serializeSize[1] = serializeInit(1);
} }
} }

View File

@@ -10,6 +10,8 @@ struct System {
auto run() -> void; auto run() -> void;
auto runToSave() -> void; auto runToSave() -> void;
auto runToSaveFast() -> void;
auto runToSaveStrict() -> void;
auto frameEvent() -> void; auto frameEvent() -> void;
auto load(Emulator::Interface*) -> bool; auto load(Emulator::Interface*) -> bool;
@@ -23,6 +25,7 @@ struct System {
uint frameSkip = 0; uint frameSkip = 0;
uint frameCounter = 0; uint frameCounter = 0;
bool runAhead = 0;
private: private:
Emulator::Interface* interface = nullptr; Emulator::Interface* interface = nullptr;

View File

@@ -8,8 +8,6 @@ auto InputManager::bindHotkeys() -> void {
static int stateSlot = 1; static int stateSlot = 1;
static double frequency = 48000.0; static double frequency = 48000.0;
static double volume = 0.0; static double volume = 0.0;
static bool fastForwarding = false;
static bool rewinding = false;
hotkeys.append(InputHotkey("Toggle Fullscreen").onPress([] { hotkeys.append(InputHotkey("Toggle Fullscreen").onPress([] {
program.toggleVideoFullScreen(); program.toggleVideoFullScreen();
@@ -32,8 +30,8 @@ auto InputManager::bindHotkeys() -> void {
})); }));
hotkeys.append(InputHotkey("Rewind").onPress([&] { hotkeys.append(InputHotkey("Rewind").onPress([&] {
if(!emulator->loaded() || fastForwarding) return; if(!emulator->loaded() || program.fastForwarding) return;
rewinding = true; program.rewinding = true;
if(program.rewind.frequency == 0) { if(program.rewind.frequency == 0) {
program.showMessage("Please enable rewind support in Settings->Emulator first"); program.showMessage("Please enable rewind support in Settings->Emulator first");
} else { } else {
@@ -46,7 +44,7 @@ auto InputManager::bindHotkeys() -> void {
Emulator::audio.setVolume(volume * 0.65); Emulator::audio.setVolume(volume * 0.65);
} }
}).onRelease([&] { }).onRelease([&] {
rewinding = false; program.rewinding = false;
if(!emulator->loaded()) return; if(!emulator->loaded()) return;
program.rewindMode(Program::Rewind::Mode::Playing); program.rewindMode(Program::Rewind::Mode::Playing);
program.mute &= ~Program::Mute::Rewind; program.mute &= ~Program::Mute::Rewind;
@@ -84,8 +82,8 @@ auto InputManager::bindHotkeys() -> void {
})); }));
hotkeys.append(InputHotkey("Fast Forward").onPress([] { hotkeys.append(InputHotkey("Fast Forward").onPress([] {
if(!emulator->loaded() || rewinding) return; if(!emulator->loaded() || program.rewinding) return;
fastForwarding = true; program.fastForwarding = true;
emulator->setFrameSkip(emulator->configuration("Hacks/PPU/Fast") == "true" ? settings.fastForward.frameSkip : 0); emulator->setFrameSkip(emulator->configuration("Hacks/PPU/Fast") == "true" ? settings.fastForward.frameSkip : 0);
video.setBlocking(false); video.setBlocking(false);
audio.setBlocking(settings.fastForward.limiter != 0); audio.setBlocking(settings.fastForward.limiter != 0);
@@ -101,7 +99,7 @@ auto InputManager::bindHotkeys() -> void {
Emulator::audio.setVolume(volume * 0.65); Emulator::audio.setVolume(volume * 0.65);
} }
}).onRelease([] { }).onRelease([] {
fastForwarding = false; program.fastForwarding = false;
if(!emulator->loaded()) return; if(!emulator->loaded()) return;
emulator->setFrameSkip(0); emulator->setFrameSkip(0);
video.setBlocking(settings.video.blocking); video.setBlocking(settings.video.blocking);

View File

@@ -93,7 +93,22 @@ auto Program::main() -> void {
} }
rewindRun(); rewindRun();
emulator->run();
if(!settings.emulator.runAhead.frames || fastForwarding || rewinding) {
emulator->run();
} else {
emulator->setRunAhead(true);
emulator->run();
auto state = emulator->serialize(0);
if(settings.emulator.runAhead.frames >= 2) emulator->run();
if(settings.emulator.runAhead.frames >= 3) emulator->run();
if(settings.emulator.runAhead.frames >= 4) emulator->run();
emulator->setRunAhead(false);
emulator->run();
state.setMode(serializer::Mode::Load);
emulator->unserialize(state);
}
if(emulatorSettings.autoSaveMemory.checked()) { if(emulatorSettings.autoSaveMemory.checked()) {
auto currentTime = chrono::timestamp(); auto currentTime = chrono::timestamp();
if(currentTime - autoSaveTime >= settings.emulator.autoSaveMemory.interval) { if(currentTime - autoSaveTime >= settings.emulator.autoSaveMemory.interval) {

View File

@@ -200,6 +200,9 @@ public:
Rewind = 1 << 4, Rewind = 1 << 4,
};}; };};
uint mute = 0; uint mute = 0;
bool fastForwarding = false;
bool rewinding = false;
}; };
extern Program program; extern Program program;

View File

@@ -35,7 +35,7 @@ auto Program::rewindRun() -> void {
if(rewind.history.size() >= rewind.length) { if(rewind.history.size() >= rewind.length) {
rewind.history.takeFirst(); rewind.history.takeFirst();
} }
auto s = emulator->serialize(settings.emulator.serialization.synchronize); auto s = emulator->serialize(0);
rewind.history.append(s); rewind.history.append(s);
return; return;
} }

View File

@@ -2,8 +2,30 @@ auto EnhancementSettings::create() -> void {
setCollapsible(); setCollapsible();
setVisible(false); setVisible(false);
overclockingLabel.setText("Overclocking").setFont(Font().setBold()); runAheadLabel.setText("Run-Ahead").setFont(Font().setBold());
runAhead0.setText("Disabled").onActivate([&] {
settings.emulator.runAhead.frames = 0;
});
runAhead1.setText("One Frame").onActivate([&] {
settings.emulator.runAhead.frames = 1;
});
runAhead2.setText("Two Frames").onActivate([&] {
settings.emulator.runAhead.frames = 2;
});
runAhead3.setText("Three Frames").onActivate([&] {
settings.emulator.runAhead.frames = 3;
});
runAhead4.setText("Four Frames").onActivate([&] {
settings.emulator.runAhead.frames = 4;
});
if(settings.emulator.runAhead.frames == 0) runAhead0.setChecked();
if(settings.emulator.runAhead.frames == 1) runAhead1.setChecked();
if(settings.emulator.runAhead.frames == 2) runAhead2.setChecked();
if(settings.emulator.runAhead.frames == 3) runAhead3.setChecked();
if(settings.emulator.runAhead.frames == 4) runAhead4.setChecked();
runAheadSpacer.setColor({192, 192, 192});
overclockingLabel.setText("Overclocking").setFont(Font().setBold());
overclockingLayout.setSize({3, 3}); overclockingLayout.setSize({3, 3});
overclockingLayout.column(0).setAlignment(1.0); overclockingLayout.column(0).setAlignment(1.0);
overclockingLayout.column(1).setAlignment(0.5); overclockingLayout.column(1).setAlignment(0.5);

View File

@@ -115,7 +115,7 @@ auto Settings::process(bool load) -> void {
bind(boolean, "Emulator/AutoSaveStateOnUnload", emulator.autoSaveStateOnUnload); bind(boolean, "Emulator/AutoSaveStateOnUnload", emulator.autoSaveStateOnUnload);
bind(boolean, "Emulator/AutoLoadStateOnLoad", emulator.autoLoadStateOnLoad); bind(boolean, "Emulator/AutoLoadStateOnLoad", emulator.autoLoadStateOnLoad);
bind(text, "Emulator/Serialization/Method", emulator.serialization.method); bind(text, "Emulator/Serialization/Method", emulator.serialization.method);
bind(boolean, "Emulator/Serialization/Synchronize", emulator.serialization.synchronize); bind(natural, "Emulator/RunAhead/Frames", emulator.runAhead.frames);
bind(boolean, "Emulator/Hack/Hotfixes", emulator.hack.hotfixes); bind(boolean, "Emulator/Hack/Hotfixes", emulator.hack.hotfixes);
bind(text, "Emulator/Hack/Entropy", emulator.hack.entropy); bind(text, "Emulator/Hack/Entropy", emulator.hack.entropy);
bind(natural, "Emulator/Hack/CPU/Overclock", emulator.hack.cpu.overclock); bind(natural, "Emulator/Hack/CPU/Overclock", emulator.hack.cpu.overclock);

View File

@@ -96,8 +96,10 @@ struct Settings : Markup::Node {
bool autoLoadStateOnLoad = false; bool autoLoadStateOnLoad = false;
struct Serialization { struct Serialization {
string method = "Fast"; string method = "Fast";
bool synchronize = true;
} serialization; } serialization;
struct RunAhead {
uint frames = 0;
} runAhead;
struct Hack { struct Hack {
bool hotfixes = true; bool hotfixes = true;
string entropy = "Low"; string entropy = "Low";
@@ -330,6 +332,16 @@ struct EnhancementSettings : VerticalLayout {
auto create() -> void; auto create() -> void;
public: public:
Label runAheadLabel{this, Size{~0, 0}, 2};
HorizontalLayout runAheadLayout{this, Size{~0, 0}};
RadioLabel runAhead0{&runAheadLayout, Size{0, 0}};
RadioLabel runAhead1{&runAheadLayout, Size{0, 0}};
RadioLabel runAhead2{&runAheadLayout, Size{0, 0}};
RadioLabel runAhead3{&runAheadLayout, Size{0, 0}};
RadioLabel runAhead4{&runAheadLayout, Size{0, 0}};
Group runAheadGroup{&runAhead0, &runAhead1, &runAhead2, &runAhead3, &runAhead4};
Canvas runAheadSpacer{this, Size{~0, 1}};
//
Label overclockingLabel{this, Size{~0, 0}, 2}; Label overclockingLabel{this, Size{~0, 0}, 2};
TableLayout overclockingLayout{this, Size{~0, 0}}; TableLayout overclockingLayout{this, Size{~0, 0}};
Label cpuLabel{&overclockingLayout, Size{0, 0}}; Label cpuLabel{&overclockingLayout, Size{0, 0}};

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <nall/queue.hpp> #include <nall/queue.hpp>
#include <nall/serializer.hpp>
#include <nall/dsp/dsp.hpp> #include <nall/dsp/dsp.hpp>
namespace nall::DSP::Resampler { namespace nall::DSP::Resampler {
@@ -11,6 +12,7 @@ struct Cubic {
inline auto pending() const -> uint; inline auto pending() const -> uint;
inline auto read() -> double; inline auto read() -> double;
inline auto write(double sample) -> void; inline auto write(double sample) -> void;
inline auto serialize(serializer&) -> void;
private: private:
double inputFrequency; double inputFrequency;
@@ -67,4 +69,13 @@ auto Cubic::write(double sample) -> void {
mu -= 1.0; mu -= 1.0;
} }
auto Cubic::serialize(serializer& s) -> void {
s.real(inputFrequency);
s.real(outputFrequency);
s.real(ratio);
s.real(fraction);
s.array(history);
samples.serialize(s);
}
} }

View File

@@ -51,6 +51,15 @@ struct serializer {
return _capacity; return _capacity;
} }
auto setMode(Mode mode) -> bool {
if(_mode == Mode::Save && mode == Mode::Load) {
_mode = mode;
_size = 0;
return true;
}
return false;
}
template<typename T> auto real(T& value) -> serializer& { template<typename T> auto real(T& value) -> serializer& {
enum : uint { size = sizeof(T) }; enum : uint { size = sizeof(T) };
//this is rather dangerous, and not cross-platform safe; //this is rather dangerous, and not cross-platform safe;