Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add save state and screen access options to the web server #1643

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/core/gpu.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/***************************************************************************

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

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

ℹ Getting worse: Lines of Code in a Single File

The lines of code increases from 1152 to 1153, improve code health by reducing it to 1000. The number of Lines of Code in a single file. More Lines of Code lowers the code health.
* Copyright (C) 2022 PCSX-Redux authors *
* *
* This program is free software; you can redistribute it and/or modify *
Expand Down Expand Up @@ -764,7 +764,8 @@
} 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.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to move the savestate code to the general UI class instead of the GUI one, but that's for another PR.

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.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically that's a 500 Internal Server Error :) The request was fine, the server didn't manage to handle it properly.

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should either be static, or moved as methods of the ScreenExecutor.

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
@@ -1,4 +1,4 @@
/***************************************************************************

Check notice on line 1 in src/gui/gui.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

ℹ Getting worse: Lines of Code in a Single File

The lines of code increases from 2017 to 2042, improve code health by reducing it to 1000. The number of Lines of Code in a single file. More Lines of Code lowers the code health.

Check notice on line 1 in src/gui/gui.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 12.89 to 10.95, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.

Check notice on line 1 in src/gui/gui.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ Getting better: Primitive Obsession

The ratio of primitive types in function arguments decreases from 42.22% to 39.22%, threshold = 30.0%. The functions in this file have too many primitive types (e.g. int, double, float) in their function argument lists. Using many primitive types lead to the code smell Primitive Obsession. Avoid adding more primitive arguments.
* Copyright (C) 2019 PCSX-Redux authors *
* *
* This program is free software; you can redistribute it and/or modify *
Expand Down Expand Up @@ -136,7 +136,7 @@
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 @@
};
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 @@
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 @@
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 @@
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 @@
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
Loading