mirror of
https://github.com/flyinghead/flycast.git
synced 2026-11-08 01:48:31 -10:00
421 lines
9.5 KiB
C++
421 lines
9.5 KiB
C++
#ifndef LIBRETRO
|
|
#include "types.h"
|
|
#include "emulator.h"
|
|
#include "hw/mem/addrspace.h"
|
|
#include "cfg/cfg.h"
|
|
#include "cfg/option.h"
|
|
#include "log/LogManager.h"
|
|
#include "ui/gui.h"
|
|
#include "oslib/oslib.h"
|
|
#include "oslib/directory.h"
|
|
#include "oslib/storage.h"
|
|
#include "debug/gdb_server.h"
|
|
#include "archive/rzip.h"
|
|
#include "ui/mainui.h"
|
|
#include "input/gamepad_device.h"
|
|
#include "lua/lua.h"
|
|
#include "stdclass.h"
|
|
#include "serialize.h"
|
|
#include "oslib/i18n.h"
|
|
#include "input/maplelink.h"
|
|
#include <time.h>
|
|
#ifdef TARGET_UWP
|
|
#include <winrt/Windows.System.h>
|
|
#include <winrt/Windows.Foundation.h>
|
|
#endif
|
|
|
|
static std::string lastStateFile;
|
|
static time_t lastStateTime;
|
|
|
|
#if defined(__ANDROID__)
|
|
#include <android/api-level.h>
|
|
// fmemopen was added in Marshmallow (API 23)
|
|
#if __ANDROID_API__ >= 23
|
|
#define HAS_FMEMOPEN
|
|
#endif
|
|
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__GLIBC__)
|
|
// Standard POSIX platforms usually have fmemopen
|
|
#define HAS_FMEMOPEN
|
|
#endif
|
|
|
|
#ifdef HAS_FMEMOPEN
|
|
const u32 QUICKSAVE_DEFAULT_SIZE = 32 * 1024 * 1024; // 32 MB
|
|
static u8 quicksave_buf[QUICKSAVE_DEFAULT_SIZE] = {0};
|
|
#endif
|
|
|
|
struct SavestateHeader
|
|
{
|
|
void init()
|
|
{
|
|
memcpy(magic, MAGIC, sizeof(magic));
|
|
creationDate = time(nullptr);
|
|
version = Deserializer::Current;
|
|
pngSize = 0;
|
|
}
|
|
|
|
bool isValid() const {
|
|
return !memcmp(magic, MAGIC, sizeof(magic));
|
|
}
|
|
|
|
char magic[8];
|
|
u64 creationDate;
|
|
u32 version;
|
|
u32 pngSize;
|
|
// png data
|
|
|
|
static constexpr const char *MAGIC = "FLYSAVE1";
|
|
};
|
|
|
|
int flycast_init(int argc, char* argv[])
|
|
{
|
|
#if defined(TEST_AUTOMATION)
|
|
setbuf(stdout, 0);
|
|
setbuf(stderr, 0);
|
|
settings.aica.muteAudio = true;
|
|
#endif
|
|
try {
|
|
if (!addrspace::reserve())
|
|
{
|
|
ERROR_LOG(VMEM, "Failed to alloc mem");
|
|
return -1;
|
|
}
|
|
config::parseCommandLine(argc, argv);
|
|
if (config::loadInt("naomi", "BoardId") != 0)
|
|
{
|
|
settings.naomi.multiboard = true;
|
|
settings.naomi.slave = true;
|
|
}
|
|
settings.naomi.drivingSimSlave = config::loadInt("naomi", "DrivingSimSlave");
|
|
|
|
config::Settings::instance().reset();
|
|
LogManager::Shutdown();
|
|
if (!config::open())
|
|
{
|
|
LogManager::Init();
|
|
NOTICE_LOG(BOOT, "Config directory is not set. Starting onboarding");
|
|
gui_open_onboarding();
|
|
}
|
|
else
|
|
{
|
|
LogManager::Init();
|
|
config::Settings::instance().load(false);
|
|
}
|
|
i18n::reloadLanguage();
|
|
gui_init();
|
|
os_CreateWindow();
|
|
os_SetupInput();
|
|
|
|
if(config::GDB)
|
|
debugger::init(config::GDBPort + config::loadInt("naomi", "BoardId"));
|
|
lua::init();
|
|
|
|
if(config::ProfilerEnabled)
|
|
LogManager::GetInstance()->SetEnable(LogTypes::PROFILER, true);
|
|
|
|
return 0;
|
|
} catch (const std::exception& e) {
|
|
ERROR_LOG(BOOT, "flycast_init failed: %s", e.what());
|
|
return 1;
|
|
} catch (...) {
|
|
ERROR_LOG(BOOT, "flycast_init: unknown exception");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
#ifndef __ANDROID__
|
|
void dc_exit()
|
|
{
|
|
try {
|
|
emu.unloadGame();
|
|
} catch (...) { }
|
|
mainui_stop();
|
|
|
|
#ifdef TARGET_UWP
|
|
extern std::string launchOnExitUri;
|
|
if (!launchOnExitUri.empty())
|
|
{
|
|
INFO_LOG(BOOT, "Launching exit URI: %s", launchOnExitUri.c_str());
|
|
try {
|
|
using namespace winrt::Windows::System;
|
|
using namespace winrt::Windows::Foundation;
|
|
|
|
auto wUri = winrt::to_hstring(launchOnExitUri);
|
|
Uri uri(wUri);
|
|
auto asyncOp = Launcher::LaunchUriAsync(uri);
|
|
asyncOp.get();
|
|
} catch (...) {
|
|
ERROR_LOG(BOOT, "Failed to launch exit URI");
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
void SaveSettings()
|
|
{
|
|
config::Settings::instance().save();
|
|
GamepadDevice::SaveMaplePorts();
|
|
|
|
#ifdef __ANDROID__
|
|
void SaveAndroidSettings();
|
|
SaveAndroidSettings();
|
|
#endif
|
|
LogManager::GetInstance()->UpdateConfig();
|
|
}
|
|
|
|
void flycast_term()
|
|
{
|
|
gui_cancel_load();
|
|
lua::term();
|
|
emu.term();
|
|
os_DestroyWindow();
|
|
gui_term();
|
|
os_TermInput();
|
|
}
|
|
|
|
bool dc_savestateAllowed() {
|
|
return !settings.content.path.empty() && !settings.network.online
|
|
&& !settings.naomi.multiboard && !MapleLink::StorageEnabled();
|
|
}
|
|
|
|
void dc_savestate(int index, const u8 *pngData, u32 pngSize)
|
|
{
|
|
if (!dc_savestateAllowed())
|
|
return;
|
|
|
|
lastStateFile.clear();
|
|
|
|
Serializer ser;
|
|
dc_serialize(ser);
|
|
|
|
void *data = malloc(ser.size());
|
|
if (data == nullptr)
|
|
{
|
|
WARN_LOG(SAVESTATE, "Failed to save state - could not malloc %d bytes", (int)ser.size());
|
|
os_notify(i18n::T("Save state failed - memory full"), 5000);
|
|
return;
|
|
}
|
|
|
|
ser = Serializer(data, ser.size());
|
|
dc_serialize(ser);
|
|
|
|
hostfs::File *f = nullptr;
|
|
std::string filename = "";
|
|
#ifdef HAS_FMEMOPEN
|
|
if (index == -2)
|
|
{
|
|
// in-ram savestate
|
|
filename = "RAM";
|
|
f = new hostfs::StdFile(fmemopen(quicksave_buf, QUICKSAVE_DEFAULT_SIZE, "wb"));
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
// regular file savestate
|
|
filename = hostfs::getSavestatePath(index, true);
|
|
f = hostfs::storage().openFile(filename.c_str(), "wb");
|
|
}
|
|
|
|
if (f == nullptr)
|
|
{
|
|
WARN_LOG(SAVESTATE, "Failed to save state - could not open %s for writing", filename.c_str());
|
|
os_notify(i18n::T("Cannot open save file"), 5000);
|
|
free(data);
|
|
return;
|
|
}
|
|
|
|
RZipFile zipFile;
|
|
SavestateHeader header;
|
|
header.init();
|
|
header.pngSize = pngSize;
|
|
if (f->write(&header, sizeof(header), 1) != 1)
|
|
goto fail;
|
|
if (pngSize > 0 && f->write(pngData, 1, pngSize) != pngSize)
|
|
goto fail;
|
|
|
|
#if 0
|
|
// Uncompressed savestate
|
|
f->write(data, 1, ser.size());
|
|
delete f;
|
|
#else
|
|
if (!zipFile.Open(f, true))
|
|
goto fail;
|
|
if (zipFile.Write(data, ser.size()) != ser.size())
|
|
goto fail;
|
|
zipFile.Close();
|
|
#endif
|
|
|
|
free(data);
|
|
NOTICE_LOG(SAVESTATE, "Saved state to %s size %d", filename.c_str(), (int)ser.size());
|
|
os_notify(i18n::T("State saved"), 2000);
|
|
return;
|
|
|
|
fail:
|
|
WARN_LOG(SAVESTATE, "Failed to save state - error writing %s", filename.c_str());
|
|
os_notify(i18n::T("Error saving state"), 5000);
|
|
if (zipFile.rawFile() != nullptr)
|
|
zipFile.Close();
|
|
else
|
|
delete f;
|
|
free(data);
|
|
// delete failed savestate?
|
|
}
|
|
|
|
void dc_loadstate(int index)
|
|
{
|
|
if (!dc_savestateAllowed() || settings.raHardcoreMode)
|
|
return;
|
|
u32 total_size = 0;
|
|
|
|
hostfs::File *f = nullptr;
|
|
std::string filename = "";
|
|
#ifdef HAS_FMEMOPEN
|
|
if (index == -2)
|
|
{
|
|
// in-ram savestate
|
|
filename = "RAM";
|
|
f = new hostfs::StdFile(fmemopen(quicksave_buf, QUICKSAVE_DEFAULT_SIZE, "rb"));
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
// regular file savestate
|
|
filename = hostfs::getSavestatePath(index, false);
|
|
f = hostfs::storage().openFile(filename, "rb");
|
|
}
|
|
|
|
if (f == nullptr)
|
|
{
|
|
WARN_LOG(SAVESTATE, "Failed to load state - could not open %s for reading", filename.c_str());
|
|
os_notify(i18n::T("Save state not found"), 2000);
|
|
return;
|
|
}
|
|
SavestateHeader header;
|
|
if (f->read(&header, sizeof(header), 1) == 1)
|
|
{
|
|
if (!header.isValid())
|
|
// seek to beginning of file if this isn't a valid header (legacy savestate)
|
|
f->seek(0, SEEK_SET);
|
|
else
|
|
// skip png data
|
|
f->seek(header.pngSize, SEEK_CUR);
|
|
}
|
|
else {
|
|
// probably not a valid savestate but we'll fail later
|
|
f->seek(0, SEEK_SET);
|
|
}
|
|
|
|
if (index == -1 && config::GGPOEnable)
|
|
{
|
|
long pos = f->tell();
|
|
MD5Sum().add(f)
|
|
.getDigest(settings.network.md5.savestate);
|
|
f->seek(pos, SEEK_SET);
|
|
}
|
|
RZipFile zipFile;
|
|
if (zipFile.Open(f, false)) {
|
|
total_size = (u32)zipFile.Size();
|
|
}
|
|
else
|
|
{
|
|
long pos = f->tell();
|
|
f->seek(0, SEEK_END);
|
|
total_size = (u32)f->tell() - pos;
|
|
f->seek(pos, SEEK_SET);
|
|
}
|
|
void *data = malloc(total_size);
|
|
if (data == nullptr)
|
|
{
|
|
WARN_LOG(SAVESTATE, "Failed to load state - could not malloc %d bytes", total_size);
|
|
os_notify(i18n::T("Failed to load state"), 5000, i18n::T("Not enough memory"));
|
|
if (zipFile.rawFile() == nullptr)
|
|
delete f;
|
|
else
|
|
zipFile.Close();
|
|
return;
|
|
}
|
|
|
|
size_t read_size;
|
|
if (zipFile.rawFile() != nullptr)
|
|
{
|
|
read_size = zipFile.Read(data, total_size);
|
|
zipFile.Close();
|
|
}
|
|
else
|
|
{
|
|
read_size = f->read(data, 1, total_size);
|
|
delete f;
|
|
}
|
|
if (read_size != total_size)
|
|
{
|
|
WARN_LOG(SAVESTATE, "Failed to load state - I/O error");
|
|
os_notify(i18n::T("Failed to load state"), 5000, i18n::T("I/O error"));
|
|
free(data);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Deserializer deser(data, total_size);
|
|
emu.loadstate(deser);
|
|
NOTICE_LOG(SAVESTATE, "Loaded state ver %d from %s size %d", deser.version(), filename.c_str(), total_size);
|
|
if (deser.size() != total_size)
|
|
// Note: this isn't true for RA savestates
|
|
WARN_LOG(SAVESTATE, "Savestate size %d but only %d bytes used", total_size, (int)deser.size());
|
|
} catch (const Deserializer::Exception& e) {
|
|
ERROR_LOG(SAVESTATE, "%s", e.what());
|
|
os_notify(i18n::T("Failed to load state"), 5000, e.what());
|
|
}
|
|
|
|
free(data);
|
|
}
|
|
|
|
time_t dc_getStateCreationDate(int index)
|
|
{
|
|
std::string filename = hostfs::getSavestatePath(index, false);
|
|
if (filename != lastStateFile)
|
|
{
|
|
lastStateFile = filename;
|
|
hostfs::File *f = hostfs::storage().openFile(filename, "rb");
|
|
if (f == nullptr)
|
|
lastStateTime = 0;
|
|
else
|
|
{
|
|
SavestateHeader header;
|
|
if (f->read(&header, sizeof(header), 1) != 1 || !header.isValid())
|
|
{
|
|
delete f;
|
|
try {
|
|
hostfs::FileInfo fileInfo = hostfs::storage().getFileInfo(filename);
|
|
lastStateTime = fileInfo.updateTime;
|
|
} catch (...) {
|
|
lastStateTime = 0;
|
|
}
|
|
}
|
|
else {
|
|
delete f;
|
|
lastStateTime = (time_t)header.creationDate;
|
|
}
|
|
}
|
|
}
|
|
return lastStateTime;
|
|
}
|
|
|
|
void dc_getStateScreenshot(int index, std::vector<u8>& pngData)
|
|
{
|
|
pngData.clear();
|
|
std::string filename = hostfs::getSavestatePath(index, false);
|
|
hostfs::File *f = hostfs::storage().openFile(filename, "rb");
|
|
if (f == nullptr)
|
|
return;
|
|
SavestateHeader header;
|
|
if (f->read(&header, sizeof(header), 1) == 1 && header.isValid() && header.pngSize != 0)
|
|
{
|
|
pngData.resize(header.pngSize);
|
|
if (f->read(pngData.data(), 1, pngData.size()) != pngData.size())
|
|
pngData.clear();
|
|
}
|
|
delete f;
|
|
}
|
|
|
|
#endif
|