From 3a21661f458711d9cf5ac7d094b92355e4bf98b2 Mon Sep 17 00:00:00 2001 From: offtkp Date: Mon, 24 Jul 2023 10:37:02 +0300 Subject: [PATCH] Refactor http server --- include/emulator.hpp | 5 +- include/httpserver.hpp | 67 ++++++++--- include/services/hid.hpp | 1 + src/emulator.cpp | 46 +------- src/httpserver.cpp | 245 ++++++++++++++++++++++++--------------- 5 files changed, 212 insertions(+), 152 deletions(-) diff --git a/include/emulator.hpp b/include/emulator.hpp index 040b93b2..d738aef3 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -63,6 +63,7 @@ class Emulator { #ifdef PANDA3DS_ENABLE_HTTP_SERVER HttpServer httpServer; + friend class HttpServer; #endif // Keep the handle for the ROM here to reload when necessary and to prevent deleting it @@ -93,8 +94,4 @@ class Emulator { bool loadELF(const std::filesystem::path& path); bool loadELF(std::ifstream& file); void initGraphicsContext(); - -#ifdef PANDA3DS_ENABLE_HTTP_SERVER - void pollHttpServer(); -#endif }; diff --git a/include/httpserver.hpp b/include/httpserver.hpp index 8e01cce7..f6329589 100644 --- a/include/httpserver.hpp +++ b/include/httpserver.hpp @@ -3,34 +3,75 @@ #include #include +#include #include +#include #include +#include +#include #include "helpers.hpp" -enum class HttpAction { None, Screenshot, PressKey, ReleaseKey }; +enum class HttpActionType { None, Screenshot, Key }; + +class Emulator; +namespace httplib { + class Server; + class Response; +} // namespace httplib + +// Wrapper for httplib::Response that allows the HTTP server to wait for the response to be ready +struct DeferredResponseWrapper { + DeferredResponseWrapper(httplib::Response& response) : inner_response(response) {} + + httplib::Response& inner_response; + std::mutex mutex; + std::condition_variable cv; + bool ready = false; +}; + +// Actions derive from this class and are used to communicate with the HTTP server +class HttpAction { + public: + HttpAction(HttpActionType type) : type(type) {} + virtual ~HttpAction() = default; + + HttpActionType getType() const { return type; } + + static std::unique_ptr createScreenshotAction(DeferredResponseWrapper& response); + static std::unique_ptr createKeyAction(uint32_t key, bool state); + + private: + HttpActionType type; +}; struct HttpServer { + HttpServer(Emulator* emulator); + ~HttpServer(); + + void processActions(); + + private: static constexpr const char* httpServerScreenshotPath = "screenshot.png"; - std::atomic_bool pendingAction = false; - HttpAction action = HttpAction::None; - std::mutex actionMutex = {}; - u32 pendingKey = 0; + Emulator* emulator; - HttpServer(); + std::unique_ptr server; - void startHttpServer(); - std::string status(); + std::thread httpServerThread; + std::queue> actionQueue; + std::mutex actionQueueMutex; -private: - std::map> keyMap; - std::array pressedKeys = {}; + std::map keyMap; bool paused = false; + void startHttpServer(); + void pushAction(std::unique_ptr action); + std::string status(); u32 stringToKey(const std::string& key_name); - bool getKeyState(const std::string& key_name); - void setKeyState(const std::string& key_name, bool state); + + HttpServer(const HttpServer&) = delete; + HttpServer& operator=(const HttpServer&) = delete; }; #endif // PANDA3DS_ENABLE_HTTP_SERVER \ No newline at end of file diff --git a/include/services/hid.hpp b/include/services/hid.hpp index 23a36ec6..a6cf2ec2 100644 --- a/include/services/hid.hpp +++ b/include/services/hid.hpp @@ -90,6 +90,7 @@ class HIDService { void pressKey(u32 mask) { newButtons |= mask; } void releaseKey(u32 mask) { newButtons &= ~mask; } + bool isPressed(u32 mask) { return (oldButtons & mask) != 0; } u32 getOldButtons() { return oldButtons; } s16 getCirclepadX() { return circlePadX; } diff --git a/src/emulator.cpp b/src/emulator.cpp index 98c4c67f..1547536a 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -13,7 +13,11 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1; Emulator::Emulator() : config(std::filesystem::current_path() / "config.toml"), kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory, config), - memory(cpu.getTicksRef()), cheats(memory, kernel.getServiceManager().getHID()) { + memory(cpu.getTicksRef()), cheats(memory, kernel.getServiceManager().getHID()) +#ifdef PANDA3DS_ENABLE_HTTP_SERVER + , httpServer(this) +#endif +{ if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { Helpers::panic("Failed to initialize SDL2"); } @@ -100,10 +104,6 @@ void Emulator::step() {} void Emulator::render() {} void Emulator::run() { -#ifdef PANDA3DS_ENABLE_HTTP_SERVER - httpServer.startHttpServer(); -#endif - while (running) { runFrame(); HIDService& hid = kernel.getServiceManager().getHID(); @@ -337,7 +337,7 @@ void Emulator::run() { void Emulator::runFrame() { if (romType != ROMType::None) { #ifdef PANDA3DS_ENABLE_HTTP_SERVER - pollHttpServer(); + httpServer.processActions(); #endif cpu.runFrame(); // Run 1 frame of instructions gpu.display(); // Display graphics @@ -448,37 +448,3 @@ bool Emulator::loadELF(std::ifstream& file) { // Reset our graphics context and initialize the GPU's graphics context void Emulator::initGraphicsContext() { gpu.initGraphicsContext(); } - -#ifdef PANDA3DS_ENABLE_HTTP_SERVER -void Emulator::pollHttpServer() { - std::scoped_lock lock(httpServer.actionMutex); - - HIDService& hid = kernel.getServiceManager().getHID(); - - if (httpServer.pendingAction) { - switch (httpServer.action) { - case HttpAction::Screenshot: gpu.screenshot(HttpServer::httpServerScreenshotPath); break; - - case HttpAction::PressKey: - if (httpServer.pendingKey != 0) { - hid.pressKey(httpServer.pendingKey); - httpServer.pendingKey = 0; - } - break; - - case HttpAction::ReleaseKey: - if (httpServer.pendingKey != 0) { - hid.releaseKey(httpServer.pendingKey); - httpServer.pendingKey = 0; - } - break; - - case HttpAction::None: break; - } - - httpServer.action = HttpAction::None; - httpServer.pendingAction = false; - httpServer.pendingAction.notify_all(); - } -} -#endif diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 6ae7af66..85e67d01 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -2,131 +2,186 @@ #include "httpserver.hpp" #include +#include #include #include #include -#include +#include "emulator.hpp" #include "httplib.h" -#include "services/hid.hpp" -HttpServer::HttpServer() : keyMap( - { - {"A", { HID::Keys::A, false } }, - {"B", { HID::Keys::B, false } }, - {"Select", { HID::Keys::Select, false } }, - {"Start", { HID::Keys::Start, false } }, - {"Right", { HID::Keys::Right, false } }, - {"Left", { HID::Keys::Left, false } }, - {"Up", { HID::Keys::Up, false } }, - {"Down", { HID::Keys::Down, false } }, - {"R", { HID::Keys::R, false } }, - {"L", { HID::Keys::L, false } }, - {"X", { HID::Keys::X, false } }, - {"Y", { HID::Keys::Y, false } }, +class HttpActionScreenshot : public HttpAction { + public: + HttpActionScreenshot(DeferredResponseWrapper& response) : HttpAction(HttpActionType::Screenshot), response(response) {} + + DeferredResponseWrapper& getResponse() { return response; } + + private: + DeferredResponseWrapper& response; +}; + +class HttpActionKey : public HttpAction { + public: + HttpActionKey(uint32_t key, bool state) : HttpAction(HttpActionType::Key), key(key), state(state) {} + + uint32_t getKey() const { return key; } + bool getState() const { return state; } + + private: + uint32_t key; + bool state; +}; + +std::unique_ptr HttpAction::createScreenshotAction(DeferredResponseWrapper& response) { + return std::make_unique(response); +} + +std::unique_ptr HttpAction::createKeyAction(uint32_t key, bool state) { return std::make_unique(key, state); } + +HttpServer::HttpServer(Emulator* emulator) + : emulator(emulator), server(std::make_unique()), keyMap({ + {"A", {HID::Keys::A}}, + {"B", {HID::Keys::B}}, + {"Select", {HID::Keys::Select}}, + {"Start", {HID::Keys::Start}}, + {"Right", {HID::Keys::Right}}, + {"Left", {HID::Keys::Left}}, + {"Up", {HID::Keys::Up}}, + {"Down", {HID::Keys::Down}}, + {"R", {HID::Keys::R}}, + {"L", {HID::Keys::L}}, + {"X", {HID::Keys::X}}, + {"Y", {HID::Keys::Y}}, + }) { + httpServerThread = std::thread(&HttpServer::startHttpServer, this); +} + +HttpServer::~HttpServer() { + printf("Stopping http server...\n"); + server->stop(); + if (httpServerThread.joinable()) { + httpServerThread.join(); } -) {} +} + +void HttpServer::pushAction(std::unique_ptr action) { + std::scoped_lock lock(actionQueueMutex); + actionQueue.push(std::move(action)); +} void HttpServer::startHttpServer() { - std::thread http_thread([this]() { - httplib::Server server; + server->Get("/ping", [](const httplib::Request&, httplib::Response& response) { response.set_content("pong", "text/plain"); }); - server.Get("/ping", [](const httplib::Request&, httplib::Response& response) { response.set_content("pong", "text/plain"); }); - - server.Get("/screen", [this](const httplib::Request&, httplib::Response& response) { - { - std::scoped_lock lock(actionMutex); - pendingAction = true; - action = HttpAction::Screenshot; - } - // wait until the screenshot is ready - pendingAction.wait(true); - std::ifstream image(httpServerScreenshotPath, std::ios::binary); - std::vector buffer(std::istreambuf_iterator(image), {}); - response.set_content(buffer.data(), buffer.size(), "image/png"); - }); - - server.Get("/input", [this](const httplib::Request& request, httplib::Response& response) { - bool ok = false; - for (auto& [keyStr, value] : request.params) { - auto key = stringToKey(keyStr); - printf("Param: %s\n", keyStr.c_str()); - - if (key != 0) { - std::scoped_lock lock(actionMutex); - pendingAction = true; - pendingKey = key; - ok = true; - if (value == "1") { - action = HttpAction::PressKey; - setKeyState(keyStr, true); - } else if (value == "0") { - action = HttpAction::ReleaseKey; - setKeyState(keyStr, false); - } else { - // Should not happen but just in case - pendingAction = false; - ok = false; - } - // Not supporting multiple keys at once for now (ever?) - break; - } - } - - if (ok) { - response.set_content("ok", "text/plain"); - } - }); - - server.Get("/step", [this](const httplib::Request&, httplib::Response& response) { - // TODO: implement /step - response.set_content("ok", "text/plain"); - }); - - server.Get("/status", [this](const httplib::Request&, httplib::Response& response) { - response.set_content(status(), "text/plain"); - }); - - // TODO: ability to specify host and port - printf("Starting HTTP server on port 1234\n"); - server.listen("localhost", 1234); + server->Get("/screen", [this](const httplib::Request&, httplib::Response& response) { + DeferredResponseWrapper wrapper(response); + // Lock the mutex before pushing the action to ensure that the condition variable is not notified before we wait on it + std::unique_lock lock(wrapper.mutex); + pushAction(HttpAction::createScreenshotAction(wrapper)); + wrapper.cv.wait(lock, [&wrapper] { return wrapper.ready; }); }); - http_thread.detach(); + server->Get("/input", [this](const httplib::Request& request, httplib::Response& response) { + bool ok = false; + for (auto& [keyStr, value] : request.params) { + u32 key = stringToKey(keyStr); + + if (key != 0) { + bool state = value == "1"; + + if (!state && value != "0") { + // Invalid state + ok = false; + break; + } + + pushAction(HttpAction::createKeyAction(key, state)); + ok = true; + } else { + // Invalid key + ok = false; + break; + } + } + + if (ok) { + response.set_content("ok", "text/plain"); + } else { + response.set_content("error", "text/plain"); + } + }); + + server->Get("/step", [this](const httplib::Request&, httplib::Response& response) { + // TODO: implement /step + response.set_content("ok", "text/plain"); + }); + + server->Get("/status", [this](const httplib::Request&, httplib::Response& response) { response.set_content(status(), "text/plain"); }); + + // TODO: ability to specify host and port + printf("Starting HTTP server on port 1234\n"); + server->listen("localhost", 1234); } std::string HttpServer::status() { + HIDService& hid = emulator->kernel.getServiceManager().getHID(); + std::stringstream stringStream; stringStream << "Panda3DS\n"; stringStream << "Status: " << (paused ? "Paused" : "Running") << "\n"; + for (auto& [keyStr, value] : keyMap) { - stringStream << keyStr << ": " << value.second << "\n"; + stringStream << keyStr << ": " << hid.isPressed(value) << "\n"; } return stringStream.str(); } +void HttpServer::processActions() { + std::scoped_lock lock(actionQueueMutex); + + HIDService& hid = emulator->kernel.getServiceManager().getHID(); + + while (!actionQueue.empty()) { + std::unique_ptr action = std::move(actionQueue.front()); + actionQueue.pop(); + + switch (action->getType()) { + case HttpActionType::Screenshot: { + HttpActionScreenshot* screenshotAction = static_cast(action.get()); + emulator->gpu.screenshot(httpServerScreenshotPath); + std::ifstream file(httpServerScreenshotPath, std::ios::binary); + std::vector buffer(std::istreambuf_iterator(file), {}); + + DeferredResponseWrapper& response = screenshotAction->getResponse(); + response.inner_response.set_content(buffer.data(), buffer.size(), "image/png"); + std::unique_lock lock(response.mutex); + response.ready = true; + response.cv.notify_one(); + break; + } + case HttpActionType::Key: { + HttpActionKey* keyAction = static_cast(action.get()); + if (keyAction->getState()) { + hid.pressKey(keyAction->getKey()); + } else { + hid.releaseKey(keyAction->getKey()); + } + break; + } + default: { + break; + } + } + } +} + u32 HttpServer::stringToKey(const std::string& key_name) { if (keyMap.find(key_name) != keyMap.end()) { - return keyMap[key_name].first; + return keyMap[key_name]; } return 0; } -bool HttpServer::getKeyState(const std::string& key_name) { - if (keyMap.find(key_name) != keyMap.end()) { - return keyMap[key_name].second; - } - - return false; -} - -void HttpServer::setKeyState(const std::string& key_name, bool state) { - if (keyMap.find(key_name) != keyMap.end()) { - keyMap[key_name].second = state; - } -} - #endif // PANDA3DS_ENABLE_HTTP_SERVER \ No newline at end of file