Skip to content

Commit

Permalink
Add save state and screen access options to the web server
Browse files Browse the repository at this point in the history
  • Loading branch information
Ken Murdock committed Apr 18, 2024
1 parent a1c3c7e commit 3807824
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 60 deletions.
3 changes: 2 additions & 1 deletion src/core/gpu.cc
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,8 @@ void PCSX::GPU::Command::processWrite(Buffer &buf, Logged::Origin origin, uint32
} break;
}
if (gotUnknown && (value != 0)) {
g_system->log(LogClass::GPU, "Got an unknown GPU data word: %08x\n", value);
g_system->log(LogClass::GPU, "Got an unknown GPU data word: %08x (cmdType: %hhu, command: %hhu)\n", value,
cmdType, command);

Check notice on line 768 in src/core/gpu.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

ℹ Getting worse: Complex Method

PCSX::GPU::Command::processWrite already has high cyclomatic complexity, and now it increases in Lines of Code from 98 to 99. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/core/logger.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ enum class LogClass : unsigned {
LUA, // logs emitted by the Lua VM
SPU, // spu information
GPU, // gpu information
WEBSERVER, // web server information
};

template <LogClass logClass, bool enabled>
Expand Down
229 changes: 229 additions & 0 deletions src/core/web-server.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "core/psxmem.h"
#include "core/r3000a.h"
#include "core/system.h"
#include "gui/gui.h"
#include "http-parser/http_parser.h"
#include "lua/luawrapper.h"
#include "magic_enum/include/magic_enum/magic_enum_all.hpp"
Expand Down Expand Up @@ -530,6 +531,230 @@ class CDExecutor : public PCSX::WebExecutor {
virtual ~CDExecutor() = default;
};

class StateExecutor : public PCSX::WebExecutor {
virtual bool match(PCSX::WebClient* client, const PCSX::UrlData& urldata) final {
return PCSX::StringsHelpers::startsWith(urldata.path, c_prefix);
}
virtual bool execute(PCSX::WebClient* client, PCSX::RequestData& request) final {
if (PCSX::g_gui == nullptr) {
client->write("HTTP/1.1 400 Bad Request\r\n\r\nSave states unavailable in CLI/no-UI mode.");
return false;
}
auto path = request.urlData.path.substr(c_prefix.length());

if (request.method == PCSX::RequestData::Method::HTTP_HTTP_GET) {
if (path == "usage") {
nlohmann::json j;
for (uint32_t i = 0; i < 10; ++i) {
j["slots"][i] = PCSX::g_gui->getSaveStateExists(i);
}
const auto& namedSaves = PCSX::g_gui->getNamedSaveStates();
for (uint32_t i = 0; i < namedSaves.size(); ++i) {
const auto& filenamePair = namedSaves[i];
j["named"][i]["name"] = filenamePair.second;
j["named"][i]["filepath"] = filenamePair.first.string();
}
write200(client, j);
return true;
} else if (path == "load" || path == "save" || path == "delete") {
auto vars = parseQuery(request.urlData.query);
auto islot = vars.find("slot");
auto iname = vars.find("name");
if ((islot == vars.end()) && (iname == vars.end())) {
client->write("HTTP/1.1 400 Bad Request\r\n\r\n");
return true;
}
if ((islot != vars.end()) && (iname != vars.end())) {
client->write("HTTP/1.1 400 Bad Request\r\n\r\n");
return true;
}
if (islot != vars.end()) {
std::string message;
int slot = -1;
try {
slot = std::stoul(islot->second);
} catch (std::exception const& ex) {
message = fmt::format(
"HTTP/1.1 400 Bad Request\r\n\r\nFailed to parse state slot value \"{}\".", islot->second);
client->write(std::move(message));
return true;
}
if (slot < 0 || slot >= 10) {
message =
fmt::format("HTTP/1.1 400 Bad Request\r\n\r\nState slot index {} out of range 0-9.", slot);
} else {
bool success = false;
if (path == "load") {
success = PCSX::g_gui->loadSaveStateSlot(slot);
} else if (path == "save") {
success = PCSX::g_gui->saveSaveStateSlot(slot);
} else if (path == "delete") {
success = PCSX::g_gui->deleteSaveStateSlot(slot);
}
if (success) {
message =
fmt::format("HTTP/1.1 200 OK\r\n\r\nState slot index {} {} successful.", slot, path);
} else {
message = fmt::format("HTTP/1.1 400 Bad Request\r\n\r\nState slot index {} {} failed.",
slot, path);
}
}
client->write(std::move(message));
return true;
} else if (iname != vars.end()) {
std::string message;
auto name = iname->second;
if (name.empty()) {
message = "HTTP/1.1 400 Bad Request\r\n\r\nState name is empty.";
} else if (name.length() > PCSX::Widgets::NamedSaveStates::NAMED_SAVE_STATE_LENGTH_MAX) {
message = fmt::format(
"HTTP/1.1 400 Bad Request\r\n\r\nState name \"{}\" exceeds {} characters in length.", name,
PCSX::Widgets::NamedSaveStates::NAMED_SAVE_STATE_LENGTH_MAX);
} else {
for (char c : name) {
if (!PCSX::Widgets::NamedSaveStates::TextFilters::isValid(c)) {
message = fmt::format(
"HTTP/1.1 400 Bad Request\r\n\r\nState name \"{}\" includes invalid character(s).",
name);
break;
}
}
}
if (message.empty()) {
std::filesystem::path saveFilepath(PCSX::g_gui->buildSaveStateFilename(name));
bool success = false;
if (path == "load") {
success = PCSX::g_gui->loadSaveState(saveFilepath);
} else if (path == "save") {
success = PCSX::g_gui->saveSaveState(saveFilepath);
} else if (path == "delete") {
success = PCSX::g_gui->deleteSaveState(saveFilepath);
}
if (success) {
message =
fmt::format("HTTP/1.1 200 OK\r\n\r\nState slot name \"{}\" {} successful.", name, path);
} else {
message = fmt::format("HTTP/1.1 400 Bad Request\r\n\r\nState slot name \"{}\" {} failed.",
name, path);
}
}
client->write(std::move(message));
return true;
}
}
}
return false;
}

public:
const std::string_view c_prefix = "/api/v1/state/";
StateExecutor() = default;
virtual ~StateExecutor() = default;
};

clip::image convertScreenshotToImage(PCSX::GPU::ScreenShot&& screenshot) {
clip::image_spec spec;
spec.width = screenshot.width;
spec.height = screenshot.height;
if (screenshot.bpp == PCSX::GPU::ScreenShot::BPP_16) {
spec.bits_per_pixel = 16;
spec.bytes_per_row = screenshot.width * 2;
spec.red_mask = 0x1f; // 0x7c00;
spec.green_mask = 0x3e0;
spec.blue_mask = 0x7c00; // 0x1f;
spec.alpha_mask = 0;
spec.red_shift = 0; // 10;
spec.green_shift = 5;
spec.blue_shift = 10; // 0;
spec.alpha_shift = 0;
} else {
spec.bits_per_pixel = 24;
spec.bytes_per_row = screenshot.width * 3;
spec.red_mask = 0xff0000;
spec.green_mask = 0xff00;
spec.blue_mask = 0xff;
spec.alpha_mask = 0;
spec.red_shift = 16;
spec.green_shift = 8;
spec.blue_shift = 0;
spec.alpha_shift = 0;
}
clip::image img(screenshot.data.data(), spec);
return img.to_rgba8888();
}

bool writeImagePNG(std::string filename, clip::image&& img) { return img.export_to_png(filename); }

bool writeImagePNG(PCSX::WebClient* client, clip::image&& img) {
std::vector<uint8_t> pngData;
bool success = img.export_to_png(pngData);
if (!success) {
client->write("HTTP/1.1 400 Bad Request\r\n\r\n");
return false;
}
client->write(std::string("HTTP/1.1 200 OK\r\n"));
client->write(std::string("Cache-Control: no-cache, must-revalidate\r\n"));
client->write(std::string("Expires: Fri, 31 Dec 1999 23:59:59 GMT\r\n"));
client->write(std::string("Content-Type: image/png\r\n"));
client->write(std::string("Content-Length: " + std::to_string(pngData.size()) + "\r\n\r\n"));
PCSX::Slice slice;
slice.copy(pngData.data(), pngData.size());
client->write(std::move(slice));
return true;
}

class ScreenExecutor : public PCSX::WebExecutor {
virtual bool match(PCSX::WebClient* client, const PCSX::UrlData& urldata) final {
return PCSX::StringsHelpers::startsWith(urldata.path, c_prefix);
}
virtual bool execute(PCSX::WebClient* client, PCSX::RequestData& request) final {
auto path = request.urlData.path.substr(c_prefix.length());

if (request.method == PCSX::RequestData::Method::HTTP_HTTP_GET) {
if (path == "save") {
auto vars = parseQuery(request.urlData.query);
auto ifilepath = vars.find("filepath");
if (ifilepath == vars.end()) {
client->write("HTTP/1.1 400 Bad Request\r\n\r\n");
return true;
}
std::string message;
std::filesystem::path path = std::filesystem::path(ifilepath->second.c_str());
if (path.is_relative()) {
std::filesystem::path persistentDir = PCSX::g_system->getPersistentDir();
if (persistentDir.empty()) {
persistentDir = std::filesystem::current_path();
}
path = persistentDir / path;
}
auto screenshot = PCSX::g_emulator->m_gpu->takeScreenShot();
clip::image img = convertScreenshotToImage(std::move(screenshot));
bool success = writeImagePNG(path.string(), std::move(img));
if (success) {
message =
fmt::format("HTTP/1.1 200 OK\r\n\r\nScreenshot saved successfully to \"{}\".", path.string());
} else {
message = fmt::format("HTTP/1.1 400 Bad Request\r\n\r\nFailed to save screenshot to \"{}\".",
path.string());
}
client->write(std::move(message));
return true;
} else if (path == "still") {
auto screenshot = PCSX::g_emulator->m_gpu->takeScreenShot();
clip::image img = convertScreenshotToImage(std::move(screenshot));
writeImagePNG(client, std::move(img));
return true;
}
}
return false;
}

public:
const std::string_view c_prefix = "/api/v1/screen/";
ScreenExecutor() = default;
virtual ~ScreenExecutor() = default;
};

} // namespace

std::multimap<std::string, std::string> PCSX::WebExecutor::parseQuery(const std::string& query) {
Expand Down Expand Up @@ -594,6 +819,8 @@ PCSX::WebServer::WebServer() : m_listener(g_system->m_eventBus) {
m_executors.push_back(new FlowExecutor());
m_executors.push_back(new LuaExecutor());
m_executors.push_back(new CDExecutor());
m_executors.push_back(new StateExecutor());
m_executors.push_back(new ScreenExecutor());
m_listener.listen<Events::SettingsLoaded>([this](const auto& event) {
auto& debugSettings = g_emulator->settings.get<Emulator::SettingDebugSettings>();
if (debugSettings.get<Emulator::DebugSettings::WebServer>() && (m_serverStatus != SERVER_STARTED)) {
Expand Down Expand Up @@ -771,6 +998,8 @@ struct PCSX::WebClient::WebClientImpl {
copyField(m_requestData.urlData.query, UF_QUERY);
copyField(m_requestData.urlData.fragment, UF_FRAGMENT);
copyField(m_requestData.urlData.userInfo, UF_USERINFO);
g_system->log(LogClass::WEBSERVER, "Received web api request, path: %s, query: %s\n",
m_requestData.urlData.path.c_str(), m_requestData.urlData.query.c_str());
return findExecutor() ? 0 : 1;
}
int onStatus(const Slice& slice) { return 0; }
Expand Down
54 changes: 43 additions & 11 deletions src/gui/gui.cc
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ static GLFWwindow* getGLFWwindowFromImGuiViewport(ImGuiViewport* viewport) {
return *reinterpret_cast<GLFWwindow**>(viewport->PlatformUserData);
}

PCSX::GUI* PCSX::GUI::s_gui = nullptr;
PCSX::GUI* PCSX::g_gui = nullptr;

void PCSX::GUI::setFullscreen(bool fullscreen) {
m_fullscreen = fullscreen;
Expand Down Expand Up @@ -634,27 +634,27 @@ void PCSX::GUI::init(std::function<void()> applyArguments) {
};
m_createWindowOldCallback = platform_io.Platform_CreateWindow;
platform_io.Platform_CreateWindow = [](ImGuiViewport* viewport) {
if (s_gui->m_createWindowOldCallback) s_gui->m_createWindowOldCallback(viewport);
if (g_gui->m_createWindowOldCallback) g_gui->m_createWindowOldCallback(viewport);
auto window = getGLFWwindowFromImGuiViewport(viewport);
glfwSetKeyCallback(window, glfwKeyCallbackTrampoline);
auto id = viewport->ID;
s_gui->m_nvgSubContextes[id] = nvgCreateSubContextGL(s_gui->m_nvgContext);
g_gui->m_nvgSubContextes[id] = nvgCreateSubContextGL(g_gui->m_nvgContext);
};
m_onChangedViewportOldCallback = platform_io.Platform_OnChangedViewport;
platform_io.Platform_OnChangedViewport = [](ImGuiViewport* viewport) {
if (s_gui->m_onChangedViewportOldCallback) s_gui->m_onChangedViewportOldCallback(viewport);
s_gui->changeScale(viewport->DpiScale);
if (g_gui->m_onChangedViewportOldCallback) g_gui->m_onChangedViewportOldCallback(viewport);
g_gui->changeScale(viewport->DpiScale);
};
m_destroyWindowOldCallback = platform_io.Platform_DestroyWindow;
platform_io.Platform_DestroyWindow = [](ImGuiViewport* viewport) {
auto id = viewport->ID;
auto& subContextes = s_gui->m_nvgSubContextes;
auto& subContextes = g_gui->m_nvgSubContextes;
auto subContext = subContextes.find(id);
if (subContext != subContextes.end()) {
nvgDeleteSubContextGL(subContext->second);
subContextes.erase(subContext);
}
if (s_gui->m_destroyWindowOldCallback) s_gui->m_destroyWindowOldCallback(viewport);
if (g_gui->m_destroyWindowOldCallback) g_gui->m_destroyWindowOldCallback(viewport);
};
glfwSetKeyCallback(m_window, glfwKeyCallbackTrampoline);
// Some bad GPU drivers (*cough* Intel) don't like mixed shaders versions,
Expand Down Expand Up @@ -2522,6 +2522,14 @@ void PCSX::GUI::magicOpen(const char* pathStr) {
g_emulator->m_cdrom->check();
}

bool PCSX::GUI::getSaveStateExists(uint32_t slot) {
if (slot >= 10) {
return false;
}
auto saveFilename = buildSaveStateFilename(slot);
return saveStateExists(saveFilename);
}

std::string PCSX::GUI::getSaveStatePrefix(bool includeSeparator) {
// the ID of the game. Every savestate is marked with the ID of the game it's from.
const auto gameID = g_emulator->m_cdrom->getCDRomID();
Expand All @@ -2545,22 +2553,28 @@ std::string PCSX::GUI::buildSaveStateFilename(int i) {
return fmt::format("{}{}{}", getSaveStatePrefix(false), getSaveStatePostfix(), i);
}

void PCSX::GUI::saveSaveState(std::filesystem::path filename) {
std::string PCSX::GUI::buildSaveStateFilename(std::string saveStateName) {
return fmt::format("{}{}{}", getSaveStatePrefix(true), saveStateName, getSaveStatePostfix());
}

bool PCSX::GUI::saveSaveState(std::filesystem::path filename) {
if (filename.is_relative()) {
filename = g_system->getPersistentDir() / filename;
}
// TODO: yeet this to libuv's threadpool.
ZWriter save(new UvFile(filename, FileOps::TRUNCATE), ZWriter::GZIP);
if (!save.failed()) save.writeString(SaveStates::save());
bool success = !save.failed();
if (success) save.writeString(SaveStates::save());
save.close();
return success;
}

void PCSX::GUI::loadSaveState(std::filesystem::path filename) {
bool PCSX::GUI::loadSaveState(std::filesystem::path filename) {
if (filename.is_relative()) {
filename = g_system->getPersistentDir() / filename;
}
ZReader save(new PosixFile(filename));
if (save.failed()) return;
if (save.failed()) return false;
std::ostringstream os;
constexpr unsigned buff_size = 1 << 16;
char* buff = new char[buff_size];
Expand All @@ -2580,8 +2594,22 @@ void PCSX::GUI::loadSaveState(std::filesystem::path filename) {
delete[] buff;

if (!error) SaveStates::load(os.str());
return !error;
}

bool PCSX::GUI::deleteSaveState(std::filesystem::path filename) {
if (filename.is_relative()) {
filename = g_system->getPersistentDir() / filename;
}
return std::remove(filename.string().c_str()) == 0;
}

bool PCSX::GUI::saveSaveStateSlot(uint32_t slot) { return saveSaveState(buildSaveStateFilename(slot)); }

bool PCSX::GUI::loadSaveStateSlot(uint32_t slot) { return loadSaveState(buildSaveStateFilename(slot)); }

bool PCSX::GUI::deleteSaveStateSlot(uint32_t slot) { return deleteSaveState(buildSaveStateFilename(slot)); }

bool PCSX::GUI::saveStateExists(std::filesystem::path filename) {
if (filename.is_relative()) {
filename = g_system->getPersistentDir() / filename;
Expand All @@ -2590,6 +2618,10 @@ bool PCSX::GUI::saveStateExists(std::filesystem::path filename) {
return !save.failed();
}

std::vector<std::pair<std::filesystem::path, std::string>> PCSX::GUI::getNamedSaveStates() {
return m_namedSaveStates.getNamedSaveStates(this);
}

void PCSX::GUI::byteRateToString(float rate, std::string& str) {
if (rate >= 1000000000) {
str = fmt::format("{:.2f} GB/s", rate / 1000000000);
Expand Down
Loading

0 comments on commit 3807824

Please sign in to comment.