* added movie recording and playback support
* added rewind support
This commit is contained in:
byuu 2019-07-23 02:13:28 +09:00
parent 2e5f6c56c6
commit 32e2abdd90
15 changed files with 276 additions and 8 deletions

View File

@ -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
-----

View File

@ -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/";

View File

@ -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});
}));

View File

@ -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;

View File

@ -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};

View File

@ -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()) {

View File

@ -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();
}

View File

@ -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 {

View File

@ -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();

View File

@ -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<int16> 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<serializer> 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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
});

View File

@ -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);

View File

@ -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}};