mirror of https://github.com/bsnes-emu/bsnes.git
v111.8
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:
parent
a32b6fae74
commit
19f3cdfd5e
|
@ -73,6 +73,8 @@ struct Stream {
|
|||
write(samples);
|
||||
}
|
||||
|
||||
auto serialize(serializer&) -> void;
|
||||
|
||||
private:
|
||||
struct Channel {
|
||||
vector<Filter> filters;
|
||||
|
|
|
@ -115,3 +115,11 @@ auto Stream::write(const double samples[]) -> void {
|
|||
|
||||
audio.process();
|
||||
}
|
||||
|
||||
auto Stream::serialize(serializer& s) -> void {
|
||||
for(auto& channel : channels) {
|
||||
channel.resampler.serialize(s);
|
||||
}
|
||||
s.real(inputFrequency);
|
||||
s.real(outputFrequency);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ using namespace nall;
|
|||
|
||||
namespace Emulator {
|
||||
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 License = "GPLv3";
|
||||
static const string Website = "https://byuu.org";
|
||||
|
|
|
@ -101,6 +101,9 @@ struct Interface {
|
|||
|
||||
virtual auto frameSkip() -> uint { return 0; }
|
||||
virtual auto setFrameSkip(uint frameSkip) -> void {}
|
||||
|
||||
virtual auto runAhead() -> bool { return false; }
|
||||
virtual auto setRunAhead(bool runAhead) -> void {}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ auto CPU::load() -> bool {
|
|||
|
||||
auto CPU::power(bool reset) -> void {
|
||||
WDC65816::power();
|
||||
create(Enter, system.cpuFrequency());
|
||||
Thread::create(Enter, system.cpuFrequency());
|
||||
coprocessors.reset();
|
||||
PPUcounter::reset();
|
||||
PPUcounter::scanline = {&CPU::scanline, this};
|
||||
|
|
|
@ -18,6 +18,7 @@ auto DSP::main() -> void {
|
|||
|
||||
int count = spc_dsp.sample_count();
|
||||
if(count > 0) {
|
||||
if(!system.runAhead)
|
||||
for(uint n = 0; n < count; n += 2) {
|
||||
float left = samplebuffer[n + 0] / 32768.0f;
|
||||
float right = samplebuffer[n + 1] / 32768.0f;
|
||||
|
|
|
@ -330,7 +330,15 @@ auto Interface::frameSkip() -> uint {
|
|||
|
||||
auto Interface::setFrameSkip(uint frameSkip) -> void {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -71,6 +71,9 @@ struct Interface : Emulator::Interface {
|
|||
|
||||
auto frameSkip() -> uint override;
|
||||
auto setFrameSkip(uint frameSkip) -> void override;
|
||||
|
||||
auto runAhead() -> bool override;
|
||||
auto setRunAhead(bool runAhead) -> void override;
|
||||
};
|
||||
|
||||
#include "configuration.hpp"
|
||||
|
|
|
@ -88,7 +88,7 @@ auto PPU::step(uint clocks) -> void {
|
|||
auto PPU::main() -> void {
|
||||
scanline();
|
||||
|
||||
if(system.frameCounter == 0) {
|
||||
if(system.frameCounter == 0 && !system.runAhead) {
|
||||
uint y = vcounter();
|
||||
if(y >= 1 && y <= 239) {
|
||||
step(renderCycle());
|
||||
|
@ -127,7 +127,7 @@ auto PPU::scanline() -> void {
|
|||
}
|
||||
|
||||
auto PPU::refresh() -> void {
|
||||
if(system.frameCounter == 0) {
|
||||
if(system.frameCounter == 0 && !system.runAhead) {
|
||||
auto output = this->output;
|
||||
uint pitch, width, height;
|
||||
if(!hd()) {
|
||||
|
|
|
@ -189,6 +189,8 @@ auto PPU::refresh() -> void {
|
|||
return ppufast.refresh();
|
||||
}
|
||||
|
||||
if(system.runAhead) return;
|
||||
|
||||
auto output = this->output;
|
||||
auto pitch = 512;
|
||||
auto width = 512;
|
||||
|
|
|
@ -73,13 +73,13 @@ namespace SuperFamicom {
|
|||
extern Scheduler scheduler;
|
||||
|
||||
struct Thread {
|
||||
enum : uint { Size = 16_KiB * sizeof(void*) };
|
||||
enum : uint { Size = 4_KiB * sizeof(void*) };
|
||||
|
||||
auto create(auto (*entrypoint)() -> void, uint frequency_) -> void {
|
||||
if(!thread) {
|
||||
thread = co_create(Thread::Size, entrypoint);
|
||||
} else {
|
||||
co_derive(thread, Thread::Size, entrypoint);
|
||||
thread = co_derive(thread, Thread::Size, entrypoint);
|
||||
}
|
||||
frequency = frequency_;
|
||||
clock = 0;
|
||||
|
@ -95,23 +95,26 @@ namespace SuperFamicom {
|
|||
}
|
||||
|
||||
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) {
|
||||
s.array(stack, Thread::Size);
|
||||
s.boolean(active);
|
||||
}
|
||||
|
||||
if(s.mode() == serializer::Load) {
|
||||
s.array(stack, Thread::Size);
|
||||
s.boolean(active);
|
||||
memory::copy(thread, stack, Thread::Size);
|
||||
if(active) scheduler.active = thread;
|
||||
}
|
||||
|
||||
if(s.mode() == serializer::Save) {
|
||||
memory::copy(stack, thread, Thread::Size);
|
||||
s.array(stack, Thread::Size);
|
||||
s.boolean(active);
|
||||
}
|
||||
|
||||
delete[] stack;
|
||||
}
|
||||
|
||||
cothread_t thread = nullptr;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
auto System::serialize(bool synchronize) -> serializer {
|
||||
if(!information.serializeSize[synchronize]) return {}; //should never occur
|
||||
if(synchronize) runToSave();
|
||||
|
||||
uint signature = 0x31545342;
|
||||
|
@ -38,7 +39,7 @@ auto System::unserialize(serializer& s) -> bool {
|
|||
if(string{version} != Emulator::SerializerVersion) return false;
|
||||
if(fastPPU != hacks.fastPPU) return false;
|
||||
|
||||
power(/* reset = */ false);
|
||||
if(synchronize) power(/* reset = */ false);
|
||||
serializeAll(s, synchronize);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -24,17 +24,54 @@ auto System::runToSave() -> void {
|
|||
//fallback in case of unrecognized method specified
|
||||
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;
|
||||
while(true) {
|
||||
scheduler.enter();
|
||||
if(scheduler.event == Scheduler::Event::Frame) frameEvent();
|
||||
if(scheduler.event == Scheduler::Event::Synchronized) break;
|
||||
if(scheduler.event == Scheduler::Event::Desynchronized) {
|
||||
if(method == "Fast") continue;
|
||||
if(method == "Strict") return false;
|
||||
}
|
||||
if(scheduler.event == Scheduler::Event::Desynchronized) continue;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
@ -55,9 +92,6 @@ auto System::runToSave() -> void {
|
|||
|
||||
break;
|
||||
}
|
||||
|
||||
scheduler.mode = Scheduler::Mode::Run;
|
||||
scheduler.active = cpu.thread;
|
||||
}
|
||||
|
||||
auto System::frameEvent() -> void {
|
||||
|
@ -97,8 +131,6 @@ auto System::load(Emulator::Interface* interface) -> bool {
|
|||
}
|
||||
if(cartridge.has.BSMemorySlot) bsmemory.load();
|
||||
|
||||
information.serializeSize[0] = serializeInit(0);
|
||||
information.serializeSize[1] = serializeInit(1);
|
||||
this->interface = interface;
|
||||
return information.loaded = true;
|
||||
}
|
||||
|
@ -195,6 +227,9 @@ auto System::power(bool reset) -> void {
|
|||
controllerPort1.connect(settings.controllerPort1);
|
||||
controllerPort2.connect(settings.controllerPort2);
|
||||
expansionPort.connect(settings.expansionPort);
|
||||
|
||||
information.serializeSize[0] = serializeInit(0);
|
||||
information.serializeSize[1] = serializeInit(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ struct System {
|
|||
|
||||
auto run() -> void;
|
||||
auto runToSave() -> void;
|
||||
auto runToSaveFast() -> void;
|
||||
auto runToSaveStrict() -> void;
|
||||
auto frameEvent() -> void;
|
||||
|
||||
auto load(Emulator::Interface*) -> bool;
|
||||
|
@ -23,6 +25,7 @@ struct System {
|
|||
|
||||
uint frameSkip = 0;
|
||||
uint frameCounter = 0;
|
||||
bool runAhead = 0;
|
||||
|
||||
private:
|
||||
Emulator::Interface* interface = nullptr;
|
||||
|
|
|
@ -8,8 +8,6 @@ auto InputManager::bindHotkeys() -> void {
|
|||
static int stateSlot = 1;
|
||||
static double frequency = 48000.0;
|
||||
static double volume = 0.0;
|
||||
static bool fastForwarding = false;
|
||||
static bool rewinding = false;
|
||||
|
||||
hotkeys.append(InputHotkey("Toggle Fullscreen").onPress([] {
|
||||
program.toggleVideoFullScreen();
|
||||
|
@ -32,8 +30,8 @@ auto InputManager::bindHotkeys() -> void {
|
|||
}));
|
||||
|
||||
hotkeys.append(InputHotkey("Rewind").onPress([&] {
|
||||
if(!emulator->loaded() || fastForwarding) return;
|
||||
rewinding = true;
|
||||
if(!emulator->loaded() || program.fastForwarding) return;
|
||||
program.rewinding = true;
|
||||
if(program.rewind.frequency == 0) {
|
||||
program.showMessage("Please enable rewind support in Settings->Emulator first");
|
||||
} else {
|
||||
|
@ -46,7 +44,7 @@ auto InputManager::bindHotkeys() -> void {
|
|||
Emulator::audio.setVolume(volume * 0.65);
|
||||
}
|
||||
}).onRelease([&] {
|
||||
rewinding = false;
|
||||
program.rewinding = false;
|
||||
if(!emulator->loaded()) return;
|
||||
program.rewindMode(Program::Rewind::Mode::Playing);
|
||||
program.mute &= ~Program::Mute::Rewind;
|
||||
|
@ -84,8 +82,8 @@ auto InputManager::bindHotkeys() -> void {
|
|||
}));
|
||||
|
||||
hotkeys.append(InputHotkey("Fast Forward").onPress([] {
|
||||
if(!emulator->loaded() || rewinding) return;
|
||||
fastForwarding = true;
|
||||
if(!emulator->loaded() || program.rewinding) return;
|
||||
program.fastForwarding = true;
|
||||
emulator->setFrameSkip(emulator->configuration("Hacks/PPU/Fast") == "true" ? settings.fastForward.frameSkip : 0);
|
||||
video.setBlocking(false);
|
||||
audio.setBlocking(settings.fastForward.limiter != 0);
|
||||
|
@ -101,7 +99,7 @@ auto InputManager::bindHotkeys() -> void {
|
|||
Emulator::audio.setVolume(volume * 0.65);
|
||||
}
|
||||
}).onRelease([] {
|
||||
fastForwarding = false;
|
||||
program.fastForwarding = false;
|
||||
if(!emulator->loaded()) return;
|
||||
emulator->setFrameSkip(0);
|
||||
video.setBlocking(settings.video.blocking);
|
||||
|
|
|
@ -93,7 +93,22 @@ auto Program::main() -> void {
|
|||
}
|
||||
|
||||
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()) {
|
||||
auto currentTime = chrono::timestamp();
|
||||
if(currentTime - autoSaveTime >= settings.emulator.autoSaveMemory.interval) {
|
||||
|
|
|
@ -200,6 +200,9 @@ public:
|
|||
Rewind = 1 << 4,
|
||||
};};
|
||||
uint mute = 0;
|
||||
|
||||
bool fastForwarding = false;
|
||||
bool rewinding = false;
|
||||
};
|
||||
|
||||
extern Program program;
|
||||
|
|
|
@ -35,7 +35,7 @@ auto Program::rewindRun() -> void {
|
|||
if(rewind.history.size() >= rewind.length) {
|
||||
rewind.history.takeFirst();
|
||||
}
|
||||
auto s = emulator->serialize(settings.emulator.serialization.synchronize);
|
||||
auto s = emulator->serialize(0);
|
||||
rewind.history.append(s);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -2,8 +2,30 @@ auto EnhancementSettings::create() -> void {
|
|||
setCollapsible();
|
||||
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.column(0).setAlignment(1.0);
|
||||
overclockingLayout.column(1).setAlignment(0.5);
|
||||
|
|
|
@ -115,7 +115,7 @@ auto Settings::process(bool load) -> void {
|
|||
bind(boolean, "Emulator/AutoSaveStateOnUnload", emulator.autoSaveStateOnUnload);
|
||||
bind(boolean, "Emulator/AutoLoadStateOnLoad", emulator.autoLoadStateOnLoad);
|
||||
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(text, "Emulator/Hack/Entropy", emulator.hack.entropy);
|
||||
bind(natural, "Emulator/Hack/CPU/Overclock", emulator.hack.cpu.overclock);
|
||||
|
|
|
@ -96,8 +96,10 @@ struct Settings : Markup::Node {
|
|||
bool autoLoadStateOnLoad = false;
|
||||
struct Serialization {
|
||||
string method = "Fast";
|
||||
bool synchronize = true;
|
||||
} serialization;
|
||||
struct RunAhead {
|
||||
uint frames = 0;
|
||||
} runAhead;
|
||||
struct Hack {
|
||||
bool hotfixes = true;
|
||||
string entropy = "Low";
|
||||
|
@ -330,6 +332,16 @@ struct EnhancementSettings : VerticalLayout {
|
|||
auto create() -> void;
|
||||
|
||||
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};
|
||||
TableLayout overclockingLayout{this, Size{~0, 0}};
|
||||
Label cpuLabel{&overclockingLayout, Size{0, 0}};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <nall/queue.hpp>
|
||||
#include <nall/serializer.hpp>
|
||||
#include <nall/dsp/dsp.hpp>
|
||||
|
||||
namespace nall::DSP::Resampler {
|
||||
|
@ -11,6 +12,7 @@ struct Cubic {
|
|||
inline auto pending() const -> uint;
|
||||
inline auto read() -> double;
|
||||
inline auto write(double sample) -> void;
|
||||
inline auto serialize(serializer&) -> void;
|
||||
|
||||
private:
|
||||
double inputFrequency;
|
||||
|
@ -67,4 +69,13 @@ auto Cubic::write(double sample) -> void {
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -51,6 +51,15 @@ struct serializer {
|
|||
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& {
|
||||
enum : uint { size = sizeof(T) };
|
||||
//this is rather dangerous, and not cross-platform safe;
|
||||
|
|
Loading…
Reference in New Issue