mirror of https://github.com/bsnes-emu/bsnes.git
v107.12
* added movie recording and playback support * added rewind support
This commit is contained in:
parent
2e5f6c56c6
commit
32e2abdd90
|
@ -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
|
||||
-----
|
||||
|
|
|
@ -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/";
|
||||
|
|
|
@ -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});
|
||||
}));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}};
|
||||
|
|
Loading…
Reference in New Issue