diff --git a/README.md b/README.md index 2af9a8ac..01dbade7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,10 @@ bsnes is a multi-platform Super Nintendo (Super Famicom) emulator that focuses on performance, features, and ease of use. bsnes currently enjoys 100% known, bug-free compatibility with the entire SNES -library when configured to its most accurate settings. +library when configured to its most accurate settings, giving it the same +accuracy level as higan. Accuracy can also optionally be traded for performance, +allowing bsnes to operate more than 300% faster than higan while still remaining +almost as accurate. Development ----------- @@ -20,6 +23,7 @@ If stability is required, please download the latest stable release from the Unique Features --------------- + - 100% (known) bug-free compatibility with the entire officially licensed SNES games library - True Super Game Boy emulation (using the SameBoy core by Lior Halphon) - HD mode 7 graphics with optional supersampling (by DerKoun) - Low-level emulation of all SNES coprocessors (DSP-n, ST-01n, Cx4) @@ -56,6 +60,8 @@ Standard Features - Frame advance support - Screenshot support - Cheat code search support + - Movie recording and playback support + - Rewind support Links ----- diff --git a/bsnes/emulator/emulator.hpp b/bsnes/emulator/emulator.hpp index fa173bdd..adec9aa8 100644 --- a/bsnes/emulator/emulator.hpp +++ b/bsnes/emulator/emulator.hpp @@ -32,7 +32,7 @@ using namespace nall; namespace Emulator { static const string Name = "bsnes"; - static const string Version = "107.11"; + static const string Version = "107.12"; static const string Author = "byuu"; static const string License = "GPLv3"; static const string Website = "https://byuu.org/"; diff --git a/bsnes/target-bsnes/input/hotkeys.cpp b/bsnes/target-bsnes/input/hotkeys.cpp index ae5d0f53..28fc6d2f 100644 --- a/bsnes/target-bsnes/input/hotkeys.cpp +++ b/bsnes/target-bsnes/input/hotkeys.cpp @@ -13,6 +13,12 @@ auto InputManager::bindHotkeys() -> void { cheatEditor.enableCheats.setChecked(!cheatEditor.enableCheats.checked()).doToggle(); })); + hotkeys.append(InputHotkey("Rewind").onPress([&] { + program.rewindMode(Program::Rewind::Mode::Rewinding); + }).onRelease([&] { + program.rewindMode(Program::Rewind::Mode::Playing); + })); + hotkeys.append(InputHotkey("Save State").onPress([&] { program.saveState({"Quick/Slot ", stateSlot}); })); diff --git a/bsnes/target-bsnes/presentation/presentation.cpp b/bsnes/target-bsnes/presentation/presentation.cpp index c119ccbd..89e319c4 100644 --- a/bsnes/target-bsnes/presentation/presentation.cpp +++ b/bsnes/target-bsnes/presentation/presentation.cpp @@ -151,6 +151,11 @@ auto Presentation::create() -> void { pauseEmulation.setText("Pause Emulation").onToggle([&] { if(pauseEmulation.checked()) audio.clear(); }); + movieMenu.setIcon(Icon::Emblem::Video).setText("Movie"); + moviePlay.setIcon(Icon::Media::Play).setText("Play").onActivate([&] { program.moviePlay(); }); + movieRecord.setIcon(Icon::Media::Record).setText("Record").onActivate([&] { program.movieRecord(false); }); + movieRecordFromBeginning.setIcon(Icon::Media::Record).setText("Reset & Record").onActivate([&] { program.movieRecord(true); }); + movieStop.setIcon(Icon::Media::Stop).setText("Stop").onActivate([&] { program.movieStop(); }); frameAdvance.setIcon(Icon::Media::Next).setText("Frame Advance").onActivate([&] { pauseEmulation.setChecked(false); program.frameAdvance = true; diff --git a/bsnes/target-bsnes/presentation/presentation.hpp b/bsnes/target-bsnes/presentation/presentation.hpp index 8790a17e..52025e68 100644 --- a/bsnes/target-bsnes/presentation/presentation.hpp +++ b/bsnes/target-bsnes/presentation/presentation.hpp @@ -93,7 +93,7 @@ struct Presentation : Window { Menu toolsMenu{&menuBar}; Menu saveState{&toolsMenu}; Menu loadState{&toolsMenu}; - MenuSeparator toolsSeparatorA{&toolsMenu}; + MenuSeparator toolsSeparatorB{&toolsMenu}; Menu speedMenu{&toolsMenu}; MenuRadioItem speedSlowest{&speedMenu}; MenuRadioItem speedSlow{&speedMenu}; @@ -101,10 +101,16 @@ struct Presentation : Window { MenuRadioItem speedFast{&speedMenu}; MenuRadioItem speedFastest{&speedMenu}; Group speedGroup{&speedSlowest, &speedSlow, &speedNormal, &speedFast, &speedFastest}; - MenuCheckItem pauseEmulation{&toolsMenu}; + MenuSeparator speedSeparator{&speedMenu}; + MenuCheckItem pauseEmulation{&speedMenu}; + Menu movieMenu{&toolsMenu}; + MenuItem moviePlay{&movieMenu}; + MenuItem movieRecord{&movieMenu}; + MenuItem movieRecordFromBeginning{&movieMenu}; + MenuItem movieStop{&movieMenu}; MenuItem frameAdvance{&toolsMenu}; MenuItem captureScreenshot{&toolsMenu}; - MenuSeparator toolsSeparatorB{&toolsMenu}; + MenuSeparator toolsSeparatorC{&toolsMenu}; MenuItem cheatFinder{&toolsMenu}; MenuItem cheatEditor{&toolsMenu}; MenuItem stateManager{&toolsMenu}; diff --git a/bsnes/target-bsnes/program/game.cpp b/bsnes/target-bsnes/program/game.cpp index 9685abc7..1790fff9 100644 --- a/bsnes/target-bsnes/program/game.cpp +++ b/bsnes/target-bsnes/program/game.cpp @@ -46,6 +46,8 @@ auto Program::load() -> void { presentation.pauseEmulation.setChecked(false); presentation.updateProgramIcon(); presentation.updateStatusIcon(); + rewindReset(); //starts rewind state recording + movieMode(Movie::Mode::Inactive); //to set initial movie menu state cheatFinder.restart(); //clear any old cheat search results cheatEditor.loadCheats(); stateManager.loadStates(); @@ -284,6 +286,7 @@ auto Program::save() -> void { auto Program::reset() -> void { if(!emulator->loaded()) return; + rewindReset(); //don't allow rewinding past a reset point hackCompatibility(); emulator->reset(); showMessage("Game reset"); @@ -291,6 +294,8 @@ auto Program::reset() -> void { auto Program::unload() -> void { if(!emulator->loaded()) return; + rewindReset(); //free up memory that is no longer needed + movieStop(); //in case a movie is currently being played or recorded cheatEditor.saveCheats(); toolsWindow.setVisible(false); if(emulatorSettings.autoSaveStateOnUnload.checked()) { diff --git a/bsnes/target-bsnes/program/movies.cpp b/bsnes/target-bsnes/program/movies.cpp new file mode 100644 index 00000000..e2130ee7 --- /dev/null +++ b/bsnes/target-bsnes/program/movies.cpp @@ -0,0 +1,113 @@ +auto Program::movieMode(Movie::Mode mode) -> void { + movie.mode = mode; + + if(movie.mode == Movie::Mode::Inactive) { + presentation.moviePlay.setEnabled(true); + presentation.movieRecord.setEnabled(true); + presentation.movieStop.setEnabled(false); + } + + if(movie.mode == Movie::Mode::Playing) { + presentation.moviePlay.setEnabled(false); + presentation.movieRecord.setEnabled(false); + presentation.movieStop.setEnabled(true); + } + + if(movie.mode == Movie::Mode::Recording) { + presentation.moviePlay.setEnabled(false); + presentation.movieRecord.setEnabled(false); + presentation.movieStop.setEnabled(true); + } +} + +auto Program::moviePlay() -> void { + BrowserDialog dialog; + dialog.setTitle("Play Movie"); + dialog.setPath(Path::desktop()); + dialog.setFilters({string{"Movies (.bsv)|*.bsv"}}); + if(auto location = dialog.openFile()) { + if(auto fp = file::open(location, file::mode::read)) { + bool failed = false; + if(fp.read() != 'B') failed = true; + if(fp.read() != 'S') failed = true; + if(fp.read() != 'V') failed = true; + if(fp.read() != '1') failed = true; + if(uint32_t size = fp.readl(4L)) { + if(fp.size() - fp.offset() < size) failed = true; + if(!failed) { + auto data = new uint8_t[size]; + fp.read({data, size}); + serializer s{data, size}; + if(!emulator->unserialize(s)) failed = true; + } + } else { + emulator->power(); + } + if(!failed) { + movieMode(Movie::Mode::Playing); + movie.input.reset(); + while(fp.size() - fp.offset() >= 2) { + movie.input.append(fp.readl(2L)); + } + showMessage("Movie playback started"); + } else { + showMessage("Movie format not supported"); + } + } + } +} + +auto Program::movieRecord(bool fromBeginning) -> void { + if(movie.mode == Movie::Mode::Inactive) { + movieMode(Movie::Mode::Recording); + if(fromBeginning) { + emulator->power(); + movie.state = {}; + } else { + movie.state = emulator->serialize(); + } + movie.input.reset(); + showMessage("Movie recording started"); + } +} + +auto Program::movieStop() -> void { + if(movie.mode == Movie::Mode::Inactive) { + return; + } + + if(movie.mode == Movie::Mode::Playing) { + showMessage("Movie playback stopped"); + } + + if(movie.mode == Movie::Mode::Recording) { + //stop recording more inputs while attempting to save file asynchronously + movieMode(Movie::Mode::Inactive); + + BrowserDialog dialog; + dialog.setTitle("Save Movie"); + dialog.setPath(Path::desktop()); + dialog.setFilters({string{"Movies (.bsv)|*.bsv"}}); + if(auto location = dialog.saveFile()) { + if(!location.endsWith(".bsv")) location.append(".bsv"); + if(auto fp = file::open(location, file::mode::write)) { + fp.write('B'); + fp.write('S'); + fp.write('V'); + fp.write('1'); + fp.writel(movie.state.size(), 4L); + fp.write({movie.state.data(), movie.state.size()}); + for(auto& input : movie.input) fp.writel(input, 2L); + showMessage("Movie recorded"); + } else { + showMessage("Movie could not be recorded"); + } + } else { + showMessage("Movie not recorded"); + } + } + + movieMode(Movie::Mode::Inactive); + movie.state = {}; + movie.input.reset(); +} diff --git a/bsnes/target-bsnes/program/platform.cpp b/bsnes/target-bsnes/program/platform.cpp index a6014a10..c4503f87 100644 --- a/bsnes/target-bsnes/program/platform.cpp +++ b/bsnes/target-bsnes/program/platform.cpp @@ -261,13 +261,24 @@ auto Program::audioFrame(const float* samples, uint channels) -> void { } auto Program::inputPoll(uint port, uint device, uint input) -> int16 { + int16 value = 0; if(focused() || emulatorSettings.allowInput().checked()) { inputManager.poll(); if(auto mapping = inputManager.mapping(port, device, input)) { - return mapping->poll(); + value = mapping->poll(); } } - return 0; + if(movie.mode == Movie::Mode::Recording) { + movie.input.append(value); + } else if(movie.mode == Movie::Mode::Playing) { + if(movie.input) { + value = movie.input.takeFirst(); + } + if(!movie.input) { + movieStop(); + } + } + return value; } auto Program::inputRumble(uint port, uint device, uint input, bool enable) -> void { diff --git a/bsnes/target-bsnes/program/program.cpp b/bsnes/target-bsnes/program/program.cpp index 7265692d..288b2da1 100644 --- a/bsnes/target-bsnes/program/program.cpp +++ b/bsnes/target-bsnes/program/program.cpp @@ -5,6 +5,8 @@ #include "game-rom.cpp" #include "paths.cpp" #include "states.cpp" +#include "movies.cpp" +#include "rewind.cpp" #include "video.cpp" #include "audio.cpp" #include "input.cpp" @@ -85,6 +87,7 @@ auto Program::main() -> void { return; } + rewindRun(); emulator->run(); if(emulatorSettings.autoSaveMemory.checked()) { auto currentTime = chrono::timestamp(); diff --git a/bsnes/target-bsnes/program/program.hpp b/bsnes/target-bsnes/program/program.hpp index 679322ad..9f594f9f 100644 --- a/bsnes/target-bsnes/program/program.hpp +++ b/bsnes/target-bsnes/program/program.hpp @@ -64,6 +64,29 @@ struct Program : Lock, Emulator::Platform { auto removeState(string filename) -> bool; auto renameState(string from, string to) -> bool; + //movies.cpp + struct Movie { + enum Mode : uint { Inactive, Playing, Recording } mode = Mode::Inactive; + serializer state; + vector input; + } movie; + auto movieMode(Movie::Mode) -> void; + auto moviePlay() -> void; + auto movieRecord(bool fromBeginning) -> void; + auto movieStop() -> void; + + //rewind.cpp + struct Rewind { + enum Mode : uint { Playing, Rewinding } mode = Mode::Playing; + vector history; + uint length = 0; + uint frequency = 0; + uint counter = 0; //in frames + } rewind; + auto rewindMode(Rewind::Mode) -> void; + auto rewindReset() -> void; + auto rewindRun() -> void; + //video.cpp auto updateVideoDriver(Window parent) -> void; auto updateVideoExclusive() -> void; diff --git a/bsnes/target-bsnes/program/rewind.cpp b/bsnes/target-bsnes/program/rewind.cpp new file mode 100644 index 00000000..a1e55eaa --- /dev/null +++ b/bsnes/target-bsnes/program/rewind.cpp @@ -0,0 +1,42 @@ +auto Program::rewindMode(Rewind::Mode mode) -> void { + rewind.mode = mode; + rewind.counter = 0; +} + +auto Program::rewindReset() -> void { + rewindMode(Rewind::Mode::Playing); + rewind.history.reset(); + rewind.frequency = settings.emulator.rewind.frequency; + rewind.length = settings.emulator.rewind.length; +} + +auto Program::rewindRun() -> void { + if(rewind.frequency == 0) return; //rewind disabled? + + if(rewind.mode == Rewind::Mode::Rewinding) { + if(rewind.history.size() == 0) return rewindMode(Rewind::Mode::Playing); //nothing left to rewind? + if(++rewind.counter < rewind.frequency / 4) return; + + rewind.counter = 0; + auto t = rewind.history.takeLast(); + serializer s{t.data(), t.size()}; //convert serializer::Save to serializer::Load + if(!rewind.history) { + showMessage("Rewind history exhausted"); + rewindReset(); + } + emulator->unserialize(s); + return; + } + + if(rewind.mode == Rewind::Mode::Playing) { + if(++rewind.counter < rewind.frequency) return; + + rewind.counter = 0; + if(rewind.history.size() >= rewind.length) { + rewind.history.takeFirst(); + } + auto s = emulator->serialize(); + rewind.history.append(s); + return; + } +} diff --git a/bsnes/target-bsnes/program/states.cpp b/bsnes/target-bsnes/program/states.cpp index 5f1f7be3..871fe527 100644 --- a/bsnes/target-bsnes/program/states.cpp +++ b/bsnes/target-bsnes/program/states.cpp @@ -65,6 +65,7 @@ auto Program::loadState(string filename) -> bool { auto serializerRLE = Decode::RLE<1>({memory.data() + 3 * sizeof(uint), memory.size() - 3 * sizeof(uint)}); serializer s{serializerRLE.data(), (uint)serializerRLE.size()}; if(!emulator->unserialize(s)) return showMessage({"[", prefix, "] is in incompatible format"}), false; + rewindReset(); //do not allow rewinding past a state load event return showMessage({"Loaded [", prefix, "]"}), true; } else { return showMessage({"[", prefix, "] not found"}), false; diff --git a/bsnes/target-bsnes/settings/emulator.cpp b/bsnes/target-bsnes/settings/emulator.cpp index 40f89a43..a8b9275f 100644 --- a/bsnes/target-bsnes/settings/emulator.cpp +++ b/bsnes/target-bsnes/settings/emulator.cpp @@ -35,6 +35,42 @@ auto EmulatorSettings::create() -> void { autoLoadStateOnLoad.setText("Auto-resume on load").setChecked(settings.emulator.autoLoadStateOnLoad).onToggle([&] { settings.emulator.autoLoadStateOnLoad = autoLoadStateOnLoad.checked(); }); + rewindFrequencyLabel.setText("Rewind Frequency:"); + rewindFrequencyOption.append(ComboButtonItem().setText("Disabled")); + rewindFrequencyOption.append(ComboButtonItem().setText("Every 10 frames")); + rewindFrequencyOption.append(ComboButtonItem().setText("Every 20 frames")); + rewindFrequencyOption.append(ComboButtonItem().setText("Every 30 frames")); + rewindFrequencyOption.append(ComboButtonItem().setText("Every 40 frames")); + rewindFrequencyOption.append(ComboButtonItem().setText("Every 50 frames")); + rewindFrequencyOption.append(ComboButtonItem().setText("Every 60 frames")); + if(settings.emulator.rewind.frequency == 0) rewindFrequencyOption.item(0).setSelected(); + if(settings.emulator.rewind.frequency == 10) rewindFrequencyOption.item(1).setSelected(); + if(settings.emulator.rewind.frequency == 20) rewindFrequencyOption.item(2).setSelected(); + if(settings.emulator.rewind.frequency == 30) rewindFrequencyOption.item(3).setSelected(); + if(settings.emulator.rewind.frequency == 40) rewindFrequencyOption.item(4).setSelected(); + if(settings.emulator.rewind.frequency == 50) rewindFrequencyOption.item(5).setSelected(); + if(settings.emulator.rewind.frequency == 60) rewindFrequencyOption.item(6).setSelected(); + rewindFrequencyOption.onChange([&] { + settings.emulator.rewind.frequency = rewindFrequencyOption.selected().offset() * 10; + program.rewindReset(); + }); + rewindLengthLabel.setText("Rewind Length:"); + rewindLengthOption.append(ComboButtonItem().setText( "10 states")); + rewindLengthOption.append(ComboButtonItem().setText( "20 states")); + rewindLengthOption.append(ComboButtonItem().setText( "40 states")); + rewindLengthOption.append(ComboButtonItem().setText( "80 states")); + rewindLengthOption.append(ComboButtonItem().setText("160 states")); + rewindLengthOption.append(ComboButtonItem().setText("320 states")); + if(settings.emulator.rewind.length == 10) rewindLengthOption.item(0).setSelected(); + if(settings.emulator.rewind.length == 20) rewindLengthOption.item(1).setSelected(); + if(settings.emulator.rewind.length == 40) rewindLengthOption.item(2).setSelected(); + if(settings.emulator.rewind.length == 80) rewindLengthOption.item(3).setSelected(); + if(settings.emulator.rewind.length == 160) rewindLengthOption.item(4).setSelected(); + if(settings.emulator.rewind.length == 320) rewindLengthOption.item(5).setSelected(); + rewindLengthOption.onChange([&] { + settings.emulator.rewind.length = 10 << rewindLengthOption.selected().offset(); + program.rewindReset(); + }); optionsSpacer.setColor({192, 192, 192}); ppuLabel.setText("PPU (video)").setFont(Font().setBold()); @@ -71,7 +107,7 @@ auto EmulatorSettings::create() -> void { settings.emulator.hack.ppu.mode7.perspective = mode7Perspective.checked(); emulator->configure("Hacks/PPU/Mode7/Perspective", settings.emulator.hack.ppu.mode7.perspective); }); - mode7Supersample.setText("Supersample").setChecked(settings.emulator.hack.ppu.mode7.supersample).onToggle([&] { + mode7Supersample.setText("Supersampling").setChecked(settings.emulator.hack.ppu.mode7.supersample).onToggle([&] { settings.emulator.hack.ppu.mode7.supersample = mode7Supersample.checked(); emulator->configure("Hacks/PPU/Mode7/Supersample", settings.emulator.hack.ppu.mode7.supersample); }); diff --git a/bsnes/target-bsnes/settings/settings.cpp b/bsnes/target-bsnes/settings/settings.cpp index a257539b..160c4382 100644 --- a/bsnes/target-bsnes/settings/settings.cpp +++ b/bsnes/target-bsnes/settings/settings.cpp @@ -98,6 +98,8 @@ auto Settings::process(bool load) -> void { bind(natural, "Emulator/AutoSaveMemory/Interval", emulator.autoSaveMemory.interval); bind(boolean, "Emulator/AutoSaveStateOnUnload", emulator.autoSaveStateOnUnload); bind(boolean, "Emulator/AutoLoadStateOnLoad", emulator.autoLoadStateOnLoad); + bind(natural, "Emulator/Rewind/Frequency", emulator.rewind.frequency); + bind(natural, "Emulator/Rewind/Length", emulator.rewind.length); bind(boolean, "Emulator/Hack/PPU/Fast", emulator.hack.ppu.fast); bind(boolean, "Emulator/Hack/PPU/NoSpriteLimit", emulator.hack.ppu.noSpriteLimit); bind(natural, "Emulator/Hack/PPU/Mode7/Scale", emulator.hack.ppu.mode7.scale); diff --git a/bsnes/target-bsnes/settings/settings.hpp b/bsnes/target-bsnes/settings/settings.hpp index 8fd469f8..03b74267 100644 --- a/bsnes/target-bsnes/settings/settings.hpp +++ b/bsnes/target-bsnes/settings/settings.hpp @@ -76,6 +76,10 @@ struct Settings : Markup::Node { } autoSaveMemory; bool autoSaveStateOnUnload = false; bool autoLoadStateOnLoad = false; + struct Rewind { + uint frequency = 0; + uint length = 80; + } rewind; struct Hack { struct PPU { bool fast = true; @@ -266,6 +270,11 @@ public: HorizontalLayout autoStateLayout{&layout, Size{~0, 0}}; CheckLabel autoSaveStateOnUnload{&autoStateLayout, Size{0, 0}}; CheckLabel autoLoadStateOnLoad{&autoStateLayout, Size{0, 0}}; + HorizontalLayout rewindLayout{&layout, Size{~0, 0}}; + Label rewindFrequencyLabel{&rewindLayout, Size{0, 0}}; + ComboButton rewindFrequencyOption{&rewindLayout, Size{0, 0}}; + Label rewindLengthLabel{&rewindLayout, Size{0, 0}}; + ComboButton rewindLengthOption{&rewindLayout, Size{0, 0}}; Canvas optionsSpacer{&layout, Size{~0, 1}}; Label ppuLabel{&layout, Size{~0, 0}, 2}; HorizontalLayout ppuLayout{&layout, Size{~0, 0}};