From 76c3695f6e64981a644aa3e436cf01a794135671 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 10:35:27 +0100 Subject: [PATCH 1/9] log: support udp log server --- .gitmodules | 2 +- CMakeLists.txt | 7 +++ core/cfg/option.cpp | 1 + core/cfg/option.h | 1 + core/deps/asio | 2 +- core/log/LogManager.cpp | 72 +++++++++++++++++-------------- core/log/LogManager.h | 7 ++- core/log/NetworkListener.h | 88 ++++++++++++++++++++++++++++++++++++++ core/nullDC.cpp | 1 + core/ui/gui.cpp | 16 +++---- shell/switch/stubs.c | 32 ++++++++++++++ shell/switch/sys/uio.h | 28 ++++++++++++ shell/switch/sys/un.h | 50 ++++++++++++++++++++++ 13 files changed, 263 insertions(+), 44 deletions(-) create mode 100644 core/log/NetworkListener.h create mode 100644 shell/switch/sys/uio.h create mode 100644 shell/switch/sys/un.h diff --git a/.gitmodules b/.gitmodules index fbec754c66..ef8c708d74 100644 --- a/.gitmodules +++ b/.gitmodules @@ -43,4 +43,4 @@ url = https://github.com/google/googletest.git [submodule "core/deps/asio"] path = core/deps/asio - url = https://github.com/chriskohlhoff/asio.git + url = https://github.com/flyinghead/asio.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dd949e61e..40713fb286 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,6 +146,13 @@ if(NINTENDO_SWITCH) if(USE_GLES) target_compile_definitions(${PROJECT_NAME} PRIVATE GLES) endif() + # asio + target_compile_definitions(${PROJECT_NAME} PRIVATE + ASIO_DISABLE_LOCAL_SOCKETS + ASIO_DISABLE_SERIAL_PORT + ESHUTDOWN=110 + SA_RESTART=0 + SA_NOCLDWAIT=0) elseif(LIBRETRO) add_library(${PROJECT_NAME} SHARED core/emulator.cpp) diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index eb06c2509c..658e18c900 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -135,6 +135,7 @@ Option DiscordPresence("DiscordPresence", true); #if defined(__ANDROID__) && !defined(LIBRETRO) Option UseSafFilePicker("UseSafFilePicker", true); #endif +OptionString LogServer("LogServer", "", "log"); // Profiler Option ProfilerEnabled("Profiler.Enabled"); diff --git a/core/cfg/option.h b/core/cfg/option.h index c3ba527865..e0d94a63d1 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -495,6 +495,7 @@ extern Option DiscordPresence; #if defined(__ANDROID__) && !defined(LIBRETRO) extern Option UseSafFilePicker; #endif +extern OptionString LogServer; // Profiling extern Option ProfilerEnabled; diff --git a/core/deps/asio b/core/deps/asio index 03ae834edb..d3402006e8 160000 --- a/core/deps/asio +++ b/core/deps/asio @@ -1 +1 @@ -Subproject commit 03ae834edbace31a96157b89bf50e5ee464e5ef9 +Subproject commit d3402006e84efb6114ff93e4f2b8508412ed80d5 diff --git a/core/log/LogManager.cpp b/core/log/LogManager.cpp index dacaac7065..881c9724af 100644 --- a/core/log/LogManager.cpp +++ b/core/log/LogManager.cpp @@ -15,6 +15,7 @@ #include "ConsoleListener.h" #include "InMemoryListener.h" +#include "NetworkListener.h" #include "Log.h" #include "StringUtil.h" #include "cfg/cfg.h" @@ -118,8 +119,6 @@ LogManager::LogManager() m_log[LogTypes::SAVESTATE] = {"SAVESTATE", "Save States"}; m_log[LogTypes::SH4] = {"SH4", "SH4 Modules"}; - RegisterListener(LogListener::CONSOLE_LISTENER, new ConsoleListener()); - // Set up log listeners int verbosity = cfgLoadInt("log", "Verbosity", LogTypes::LDEBUG); @@ -128,47 +127,56 @@ LogManager::LogManager() verbosity = 1; if (verbosity > MAX_LOGLEVEL) verbosity = MAX_LOGLEVEL; - SetLogLevel(static_cast(verbosity)); - if (cfgLoadBool("log", "LogToFile", false)) - { -#if defined(__ANDROID__) || defined(__APPLE__) || defined(TARGET_UWP) - std::string logPath = get_writable_data_path("flycast.log"); -#else - std::string logPath = "flycast.log"; -#endif - FileLogListener *listener = new FileLogListener(logPath); - if (!listener->IsValid()) - { - const char *home = nowide::getenv("HOME"); - if (home != nullptr) - { - delete listener; - listener = new FileLogListener(home + ("/" + logPath)); - } - } - RegisterListener(LogListener::FILE_LISTENER, listener); - EnableListener(LogListener::FILE_LISTENER, true); - } + + RegisterListener(LogListener::CONSOLE_LISTENER, new ConsoleListener()); EnableListener(LogListener::CONSOLE_LISTENER, cfgLoadBool("log", "LogToConsole", true)); - // EnableListener(LogListener::LOG_WINDOW_LISTENER, Config::Get(LOGGER_WRITE_TO_WINDOW)); RegisterListener(LogListener::IN_MEMORY_LISTENER, new InMemoryListener()); EnableListener(LogListener::IN_MEMORY_LISTENER, true); for (LogContainer& container : m_log) - { container.m_enable = cfgLoadBool("log", container.m_short_name, true); - } m_path_cutoff_point = DeterminePathCutOffPoint(); + + UpdateConfig(); } -LogManager::~LogManager() +void LogManager::UpdateConfig() { - // The log window listener pointer is owned by the GUI code. - delete m_listeners[LogListener::CONSOLE_LISTENER]; - delete m_listeners[LogListener::FILE_LISTENER]; - delete m_listeners[LogListener::IN_MEMORY_LISTENER]; + bool logToFile = cfgLoadBool("log", "LogToFile", false); + if (logToFile != IsListenerEnabled(LogListener::FILE_LISTENER)) + { + if (!logToFile) { + m_listeners[LogListener::FILE_LISTENER].reset(); + } + else { +#if defined(__ANDROID__) || defined(__APPLE__) || defined(TARGET_UWP) + std::string logPath = get_writable_data_path("flycast.log"); +#else + std::string logPath = "flycast.log"; +#endif + FileLogListener *listener = new FileLogListener(logPath); + if (!listener->IsValid()) + { + const char *home = nowide::getenv("HOME"); + if (home != nullptr) + { + delete listener; + listener = new FileLogListener(home + ("/" + logPath)); + } + } + RegisterListener(LogListener::FILE_LISTENER, listener); + } + EnableListener(LogListener::FILE_LISTENER, logToFile); + } + std::string newLogServer = cfgLoadStr("log", "LogServer", ""); + if (logServer != newLogServer) + { + logServer = newLogServer; + RegisterListener(LogListener::NETWORK_LISTENER, new NetworkListener(logServer)); + EnableListener(LogListener::NETWORK_LISTENER, !logServer.empty()); + } } // Return the current time formatted as Minutes:Seconds:Milliseconds @@ -241,7 +249,7 @@ const char* LogManager::GetFullName(LogTypes::LOG_TYPE type) const void LogManager::RegisterListener(LogListener::LISTENER id, LogListener* listener) { - m_listeners[id] = listener; + m_listeners[id] = std::unique_ptr(listener); } void LogManager::EnableListener(LogListener::LISTENER id, bool enable) diff --git a/core/log/LogManager.h b/core/log/LogManager.h index 0cbd922559..5cafd3628f 100644 --- a/core/log/LogManager.h +++ b/core/log/LogManager.h @@ -6,6 +6,7 @@ #include #include +#include #include "BitSet.h" #include "Log.h" @@ -23,6 +24,7 @@ class LogListener CONSOLE_LISTENER, LOG_WINDOW_LISTENER, IN_MEMORY_LISTENER, + NETWORK_LISTENER, NUMBER_OF_LISTENERS // Must be last }; @@ -52,6 +54,7 @@ class LogManager void RegisterListener(LogListener::LISTENER id, LogListener* listener); void EnableListener(LogListener::LISTENER id, bool enable); bool IsListenerEnabled(LogListener::LISTENER id) const; + void UpdateConfig(); private: struct LogContainer @@ -66,7 +69,6 @@ class LogManager }; LogManager(); - ~LogManager(); LogManager(const LogManager&) = delete; LogManager& operator=(const LogManager&) = delete; @@ -75,7 +77,8 @@ class LogManager LogTypes::LOG_LEVELS m_level; std::array m_log{}; - std::array m_listeners{}; + std::array, LogListener::NUMBER_OF_LISTENERS> m_listeners{}; BitSet32 m_listener_ids; size_t m_path_cutoff_point = 0; + std::string logServer; }; diff --git a/core/log/NetworkListener.h b/core/log/NetworkListener.h new file mode 100644 index 0000000000..069adcde1d --- /dev/null +++ b/core/log/NetworkListener.h @@ -0,0 +1,88 @@ +/* + Copyright 2025 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once +#include +#include +#include "LogManager.h" + +class NetworkListener : public LogListener +{ +public: + NetworkListener(const std::string& dest) + { + if (dest.empty()) + return; + std::string host; + std::string port("31667"); + auto colon = dest.find(':'); + if (colon != std::string::npos) { + port = dest.substr(colon + 1); + host = dest.substr(0, colon); + } + else { + host = dest; + } + asio::ip::udp::resolver resolver(io_context); + asio::error_code ec; + auto it = resolver.resolve(host, port, ec); + if (ec || it.empty()) { + fprintf(stderr, "Unknown hostname %s: %s\n", host.c_str(), ec.message().c_str()); + } + else + { + asio::ip::udp::endpoint endpoint = *it.begin(); + socket.connect(endpoint, ec); + if (ec) + fprintf(stderr, "Connect to log server failed: %s\n", ec.message().c_str()); + } + } + + void Log(LogTypes::LOG_LEVELS level, const char* msg) override + { + if (!socket.is_open()) + return; + const char *reset_attr = "\x1b[0m"; + std::string color_attr; + + switch (level) + { + case LogTypes::LOG_LEVELS::LNOTICE: + // light green + color_attr = "\x1b[92m"; + break; + case LogTypes::LOG_LEVELS::LERROR: + // light red + color_attr = "\x1b[91m"; + break; + case LogTypes::LOG_LEVELS::LWARNING: + // light yellow + color_attr = "\x1b[93m"; + break; + default: + break; + } + std::string str = color_attr + msg + reset_attr; + asio::error_code ec; + socket.send(asio::buffer(str), 0, ec); + } + +private: + asio::io_context io_context; + asio::ip::udp::socket socket { io_context }; +}; diff --git a/core/nullDC.cpp b/core/nullDC.cpp index 8e60388a36..b86fbd7b1a 100644 --- a/core/nullDC.cpp +++ b/core/nullDC.cpp @@ -110,6 +110,7 @@ void SaveSettings() void SaveAndroidSettings(); SaveAndroidSettings(); #endif + LogManager::GetInstance()->UpdateConfig(); } void flycast_term() diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 9f68259f28..2279f41382 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -1609,6 +1609,8 @@ static void contentpath_warning_popup() } } +#if !defined(NDEBUG) || defined(DEBUGFAST) || FC_PROFILER + static void gui_debug_tab() { header("Logging"); @@ -1640,6 +1642,9 @@ static void gui_debug_tab() } ImGui::EndCombo(); } + ImGui::InputText("Log Server", &config::LogServer.get(), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::SameLine(); + ShowHelpMarker("Log to this hostname[:port] with UDP. Default port is 31667."); } #if FC_PROFILER ImGui::Spacing(); @@ -1663,6 +1668,7 @@ static void gui_debug_tab() } #endif } +#endif static void addContentPathCallback(const std::string& path) { @@ -2839,14 +2845,8 @@ static void gui_settings_advanced() "Dump all textures into data/texdump/"); bool logToFile = cfgLoadBool("log", "LogToFile", false); - bool newLogToFile = logToFile; - ImGui::Checkbox("Log to File", &newLogToFile); - if (logToFile != newLogToFile) - { - cfgSaveBool("log", "LogToFile", newLogToFile); - LogManager::Shutdown(); - LogManager::Init(); - } + if (ImGui::Checkbox("Log to File", &logToFile)) + cfgSaveBool("log", "LogToFile", logToFile); ImGui::SameLine(); ShowHelpMarker("Log debug information to flycast.log"); #ifdef SENTRY_UPLOAD diff --git a/shell/switch/stubs.c b/shell/switch/stubs.c index 7b096ecf5f..b4d28f5fd1 100644 --- a/shell/switch/stubs.c +++ b/shell/switch/stubs.c @@ -1,5 +1,8 @@ #include #include +#include +#include +#include // Seems to be missing in newlib, dumb stub (file permissions is not a thing on fat32 anyways) mode_t umask(mode_t mask) @@ -7,3 +10,32 @@ mode_t umask(mode_t mask) return mask; } +int pause() +{ + sleep(0xffffffff); + return -1; +} + +// FIXME always failing stub +int pthread_sigmask(int how, const sigset_t *set, sigset_t *oset) +{ + switch (how) + { + case SIG_BLOCK: + case SIG_UNBLOCK: + case SIG_SETMASK: + break; + default: + errno = EINVAL; + return -1; + } + errno = ENOSYS; + return -1; +} + +// Map an interface index into its name. +char *if_indextoname(unsigned ifindex, char *ifname) +{ + errno = ENXIO; + return NULL; +} diff --git a/shell/switch/sys/uio.h b/shell/switch/sys/uio.h new file mode 100644 index 0000000000..e8b7c96040 --- /dev/null +++ b/shell/switch/sys/uio.h @@ -0,0 +1,28 @@ +#ifndef SYS_UIO_H_ +#define SYS_UIO_H_ +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Read data from file descriptor FD, and put the result in the + buffers described by IOVEC, which is a vector of COUNT 'struct iovec's. + The buffers are filled in the order specified. + Operates just like 'read' (see ) except that data are + put in IOVEC instead of a contiguous buffer. */ +extern ssize_t readv (int __fd, const struct iovec *__iovec, int __count); + +/* Write data pointed by the buffers described by IOVEC, which + is a vector of COUNT 'struct iovec's, to file descriptor FD. + The data is written in the order specified. + Operates just like 'write' (see ) except that the data + are taken from IOVEC instead of a contiguous buffer. */ +extern ssize_t writev (int __fd, const struct iovec *__iovec, int __count); + +#ifdef __cplusplus +} +#endif + +#endif /* SYS_UIO_H_ */ diff --git a/shell/switch/sys/un.h b/shell/switch/sys/un.h new file mode 100644 index 0000000000..b86217d408 --- /dev/null +++ b/shell/switch/sys/un.h @@ -0,0 +1,50 @@ +/* Copyright (C) 1991-2020 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _SYS_UN_H +#define _SYS_UN_H 1 + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Structure describing the address of an AF_LOCAL (aka AF_UNIX) socket. */ +struct sockaddr_un +{ + sa_family_t sun_family; + char sun_path[108]; /* Path name. */ +}; + +/* Should be defined in sockets.h */ +struct ipv6_mreq +{ + struct in6_addr ipv6mr_multiaddr; + unsigned int ipv6mr_interface; +}; + +/* Should be declared in net/if.h */ +char* if_indextoname(unsigned int, char*); +unsigned int if_nametoindex(const char*); + +#ifdef __cplusplus +} +#endif + +#endif /* sys/un.h */ From 7478476f9cac14128862012c5d67d301312ea331 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 10:50:46 +0100 Subject: [PATCH 2/9] multithreading utilities (periodic, worker thread) --- CMakeLists.txt | 5 +- core/achievements/achievements.cpp | 74 +++++----------- core/rend/CustomTexture.cpp | 108 +++++++++--------------- core/rend/CustomTexture.h | 25 ++---- core/util/periodic_thread.h | 117 ++++++++++++++++++++++++++ core/util/tsqueue.h | 71 ++++++++++++++++ core/util/worker_thread.h | 95 +++++++++++++++++++++ tests/src/util/PeriodicThreadTest.cpp | 26 ++++++ tests/src/util/TsQueueTest.cpp | 97 +++++++++++++++++++++ tests/src/util/WorkerThreadTest.cpp | 81 ++++++++++++++++++ 10 files changed, 560 insertions(+), 139 deletions(-) create mode 100644 core/util/periodic_thread.h create mode 100644 core/util/tsqueue.h create mode 100644 core/util/worker_thread.h create mode 100644 tests/src/util/PeriodicThreadTest.cpp create mode 100644 tests/src/util/TsQueueTest.cpp create mode 100644 tests/src/util/WorkerThreadTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 40713fb286..777b9cc32f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1909,7 +1909,10 @@ if(BUILD_TESTING) tests/src/serialize_test.cpp tests/src/AicaArmTest.cpp tests/src/Sh4InterpreterTest.cpp - tests/src/MmuTest.cpp) + tests/src/MmuTest.cpp + tests/src/util/PeriodicThreadTest.cpp + tests/src/util/TsQueueTest.cpp + tests/src/util/WorkerThreadTest.cpp) endif() if(NINTENDO_SWITCH) diff --git a/core/achievements/achievements.cpp b/core/achievements/achievements.cpp index b2ff0ed634..0a48fa61ed 100644 --- a/core/achievements/achievements.cpp +++ b/core/achievements/achievements.cpp @@ -37,8 +37,9 @@ #include #include #include -#include #include +#include "util/worker_thread.h" +#include "util/periodic_thread.h" namespace achievements { @@ -74,10 +75,8 @@ class Achievements std::string getOrDownloadImage(const char *url); std::pair getCachedImage(const char *url); void diskChange(); - void asyncTask(std::function f); - void startThread(); - void stopThread(); - void backgroundThread(); + void asyncTask(std::function&& f); + void stopThreads(); static void clientLoginWithTokenCallback(int result, const char *error_message, rc_client_t *client, void *userdata); static void clientLoginWithPasswordCallback(int result, const char *error_message, rc_client_t *client, void *userdata); @@ -112,11 +111,12 @@ class Achievements std::string cachePath; std::unordered_map cacheMap; std::mutex cacheMutex; - std::vector> tasks; - std::mutex taskMutex; - std::thread taskThread; - cResetEvent resetEvent; - bool threadRunning = false; + WorkerThread taskThread {"RA-background"}; + + PeriodicThread idleThread { "RA-idle", [this]() { + if (active) + rc_client_idle(rc_client); + }}; }; bool init() { @@ -176,6 +176,7 @@ Achievements::Achievements() EventManager::listen(Event::Pause, emuEventCallback, this); EventManager::listen(Event::Resume, emuEventCallback, this); EventManager::listen(Event::DiskChange, emuEventCallback, this); + idleThread.setPeriod(1000); } Achievements::~Achievements() @@ -188,44 +189,13 @@ Achievements::~Achievements() term(); } -void Achievements::asyncTask(std::function f) -{ - { - std::lock_guard _(taskMutex); - tasks.emplace_back(f); - } - resetEvent.Set(); -} - -void Achievements::startThread() -{ - threadRunning = true; - taskThread = std::thread(&Achievements::backgroundThread, this); +void Achievements::asyncTask(std::function&& f) { + taskThread.run(std::move(f)); } -void Achievements::stopThread() -{ - threadRunning = false; - resetEvent.Set(); - if (taskThread.joinable()) - taskThread.join(); -} - -void Achievements::backgroundThread() -{ - ThreadName _("RA-background"); - while (threadRunning) - { - if (!resetEvent.Wait(1000) && active && paused) - rc_client_idle(rc_client); - std::vector> localTasks; - { - std::lock_guard _(taskMutex); - std::swap(tasks, localTasks); - } - for (auto& f : localTasks) - f(); - } +void Achievements::stopThreads() { + taskThread.stop(); + idleThread.stop(); } bool Achievements::init() @@ -243,7 +213,6 @@ bool Achievements::init() //rc_client_set_unofficial_enabled(rc_client, 0); //rc_client_set_spectator_mode_enabled(rc_client, 0); loadCache(); - startThread(); if (!config::AchievementsUserName.get().empty() && !config::AchievementsToken.get().empty()) { @@ -357,7 +326,7 @@ void Achievements::term() if (rc_client == nullptr) return; unloadGame(); - stopThread(); + stopThreads(); rc_client_destroy(rc_client); rc_client = nullptr; } @@ -813,8 +782,11 @@ std::string Achievements::getGameHash() return hash; } -void Achievements::pauseGame() { +void Achievements::pauseGame() +{ paused = true; + if (active) + idleThread.start(); } void Achievements::resumeGame() @@ -822,6 +794,7 @@ void Achievements::resumeGame() paused = false; if (config::EnableAchievements && !settings.naomi.slave) { + idleThread.stop(); loadGame(); if (settings.raHardcoreMode && !config::AchievementsHardcoreMode) { @@ -955,8 +928,7 @@ void Achievements::unloadGame() paused = false; EventManager::unlisten(Event::VBlank, emuEventCallback, this); // wait for all async tasks before unloading the game - stopThread(); - startThread(); + stopThreads(); rc_client_unload_game(rc_client); settings.raHardcoreMode = false; } diff --git a/core/rend/CustomTexture.cpp b/core/rend/CustomTexture.cpp index 1ff8444b24..8f8beda18e 100644 --- a/core/rend/CustomTexture.cpp +++ b/core/rend/CustomTexture.cpp @@ -22,6 +22,8 @@ #include "oslib/storage.h" #include "cfg/option.h" #include "oslib/oslib.h" +#include "stdclass.h" +#include "util/worker_thread.h" #include #define STB_IMAGE_IMPLEMENTATION @@ -32,58 +34,34 @@ #include CustomTexture custom_texture; +static WorkerThread loader_thread {"CustomTexLoader"}; -void CustomTexture::LoaderThread() +void CustomTexture::loadTexture(BaseTextureCacheData *texture) { - LoadMap(); - while (initialized) + texture->ComputeHash(); + if (texture->custom_image_data != nullptr) { + free(texture->custom_image_data); + texture->custom_image_data = nullptr; + } + if (!texture->dirty) { - BaseTextureCacheData *texture; - - do { - texture = nullptr; - { - std::unique_lock lock(work_queue_mutex); - if (!work_queue.empty()) - { - texture = work_queue.back(); - work_queue.pop_back(); - } - } - - if (texture != nullptr) - { - texture->ComputeHash(); - if (texture->custom_image_data != nullptr) - { - free(texture->custom_image_data); - texture->custom_image_data = nullptr; - } - if (!texture->dirty) - { - int width, height; - u8 *image_data = LoadCustomTexture(texture->texture_hash, width, height); - if (image_data == nullptr && texture->old_vqtexture_hash != 0) - image_data = LoadCustomTexture(texture->old_vqtexture_hash, width, height); - if (image_data == nullptr) - image_data = LoadCustomTexture(texture->old_texture_hash, width, height); - if (image_data != nullptr) - { - texture->custom_width = width; - texture->custom_height = height; - texture->custom_image_data = image_data; - } - } - texture->custom_load_in_progress--; - } - - } while (texture != nullptr); - - wakeup_thread.Wait(); + int width, height; + u8 *image_data = loadTexture(texture->texture_hash, width, height); + if (image_data == nullptr && texture->old_vqtexture_hash != 0) + image_data = loadTexture(texture->old_vqtexture_hash, width, height); + if (image_data == nullptr) + image_data = loadTexture(texture->old_texture_hash, width, height); + if (image_data != nullptr) + { + texture->custom_width = width; + texture->custom_height = height; + texture->custom_image_data = image_data; + } } + texture->custom_load_in_progress--; } -std::string CustomTexture::GetGameId() +std::string CustomTexture::getGameId() { std::string game_id(settings.content.gameId); const size_t str_end = game_id.find_last_not_of(' '); @@ -95,12 +73,12 @@ std::string CustomTexture::GetGameId() return game_id; } -bool CustomTexture::Init() +bool CustomTexture::init() { if (!initialized) { initialized = true; - std::string game_id = GetGameId(); + std::string game_id = getGameId(); if (game_id.length() > 0) { textures_path = hostfs::getTextureLoadPath(game_id); @@ -113,7 +91,9 @@ bool CustomTexture::Init() { NOTICE_LOG(RENDERER, "Found custom textures directory: %s", textures_path.c_str()); custom_textures_available = true; - loader_thread.Start(); + loader_thread.run([this]() { + loadMap(); + }); } } catch (const FlycastException& e) { } @@ -125,20 +105,12 @@ bool CustomTexture::Init() void CustomTexture::Terminate() { - if (initialized) - { - initialized = false; - { - std::unique_lock lock(work_queue_mutex); - work_queue.clear(); - } - wakeup_thread.Set(); - loader_thread.WaitToEnd(); - texture_map.clear(); - } + loader_thread.stop(); + texture_map.clear(); + initialized = false; } -u8* CustomTexture::LoadCustomTexture(u32 hash, int& width, int& height) +u8* CustomTexture::loadTexture(u32 hash, int& width, int& height) { auto it = texture_map.find(hash); if (it == texture_map.end()) @@ -156,15 +128,13 @@ u8* CustomTexture::LoadCustomTexture(u32 hash, int& width, int& height) void CustomTexture::LoadCustomTextureAsync(BaseTextureCacheData *texture_data) { - if (!Init()) + if (!init()) return; texture_data->custom_load_in_progress++; - { - std::unique_lock lock(work_queue_mutex); - work_queue.insert(work_queue.begin(), texture_data); - } - wakeup_thread.Set(); + loader_thread.run([this, texture_data]() { + loadTexture(texture_data); + }); } void CustomTexture::DumpTexture(u32 hash, int w, int h, TextureType textype, void *src_buffer) @@ -172,7 +142,7 @@ void CustomTexture::DumpTexture(u32 hash, int w, int h, TextureType textype, voi std::string base_dump_dir = hostfs::getTextureDumpPath(); if (!file_exists(base_dump_dir)) make_directory(base_dump_dir); - std::string game_id = GetGameId(); + std::string game_id = getGameId(); if (game_id.length() == 0) return; @@ -299,7 +269,7 @@ void CustomTexture::DumpTexture(u32 hash, int w, int h, TextureType textype, voi free(dst_buffer); } -void CustomTexture::LoadMap() +void CustomTexture::loadMap() { texture_map.clear(); hostfs::DirectoryTree tree(textures_path); diff --git a/core/rend/CustomTexture.h b/core/rend/CustomTexture.h index 3994b99256..df28e7692c 100644 --- a/core/rend/CustomTexture.h +++ b/core/rend/CustomTexture.h @@ -17,41 +17,30 @@ along with reicast. If not, see . */ #pragma once - #include "texconv.h" -#include "stdclass.h" - #include -#include #include -#include class BaseTextureCacheData; -class CustomTexture { +class CustomTexture +{ public: - CustomTexture() : loader_thread(loader_thread_func, this, "CustomTexLoader") {} ~CustomTexture() { Terminate(); } - u8* LoadCustomTexture(u32 hash, int& width, int& height); void LoadCustomTextureAsync(BaseTextureCacheData *texture_data); void DumpTexture(u32 hash, int w, int h, TextureType textype, void *src_buffer); void Terminate(); private: - bool Init(); - void LoaderThread(); - std::string GetGameId(); - void LoadMap(); - - static void *loader_thread_func(void *param) { ((CustomTexture *)param)->LoaderThread(); return NULL; } + bool init(); + u8* loadTexture(u32 hash, int& width, int& height); + void loadTexture(BaseTextureCacheData *texture); + std::string getGameId(); + void loadMap(); bool initialized = false; bool custom_textures_available = false; std::string textures_path; - cThread loader_thread; - cResetEvent wakeup_thread; - std::vector work_queue; - std::mutex work_queue_mutex; std::map texture_map; }; diff --git a/core/util/periodic_thread.h b/core/util/periodic_thread.h new file mode 100644 index 0000000000..690b5c7bd5 --- /dev/null +++ b/core/util/periodic_thread.h @@ -0,0 +1,117 @@ +/* + Copyright 2025 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once +#include "stdclass.h" +#include "oslib/oslib.h" +#include +#include +#include +#include +#include +#include "log/Log.h" + +class VPeriodicThread +{ +public: + virtual ~VPeriodicThread() { + stop(); + } + + void start() + { + LockGuard _(mutex); + if (thread.joinable()) + return; + running = true; + event.Reset(); + thread = std::thread([this]() { + ThreadName _(name); + try { + init(); + while (true) + { + if (period != 0) + event.Wait(period); + else + event.Wait(); + if (!running) + break; + doWork(); + } + term(); + } catch (const std::runtime_error& e) { + ERROR_LOG(COMMON, "PeriodicThread %s: runtime error %s", name, e.what()); + } catch (...) { + ERROR_LOG(COMMON, "PeriodicThread %s: uncaught unknown exception", name); + } + }); + } + + void stop() + { + LockGuard _(mutex); + running = false; + event.Set(); + if (thread.joinable()) + thread.join(); + } + + void setPeriod(int period) { + this->period = period; + } + + void notify() { + event.Set(); + } + +protected: + VPeriodicThread(const char *name, int periodMS = 0) + : name(name), period(periodMS) + { } + virtual void doWork() = 0; + virtual void init() {} + virtual void term() {} + +private: + using LockGuard = std::lock_guard; + const char *name; + int period; + cResetEvent event; + std::thread thread; + std::atomic running = false; + std::mutex mutex; +}; + +class PeriodicThread : public VPeriodicThread +{ +public: + template + PeriodicThread(const char *name, F&& f, Args&&... args) + : VPeriodicThread(name, 0) + { + work = std::bind(std::forward(f), std::forward(args)...); + } + +private: + void doWork() override { + work(); + } + + std::function work; +}; diff --git a/core/util/tsqueue.h b/core/util/tsqueue.h new file mode 100644 index 0000000000..21e3be01dc --- /dev/null +++ b/core/util/tsqueue.h @@ -0,0 +1,71 @@ +/* + Copyright 2025 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once +#include +#include +#include + +template +class TsQueue +{ +public: + void push(const T& t) + { + std::lock_guard _(mutex); + queue.emplace(t); + condVar.notify_one(); + } + void push(T&& t) + { + std::lock_guard _(mutex); + queue.push(std::move(t)); + condVar.notify_one(); + } + + T pop() + { + std::unique_lock lock(mutex); + condVar.wait(lock, [this]() { return !queue.empty(); }); + T t = std::move(queue.front()); + queue.pop(); + return t; + } + + size_t size() const { + std::lock_guard _(mutex); + return queue.size(); + } + bool empty() const { + std::lock_guard _(mutex); + return queue.empty(); + } + + void clear() + { + std::queue empty; + std::lock_guard _(mutex); + std::swap(queue, empty); + } + // TODO bool tryPop(T& t, std::chrono::duration timeout) ? + +private: + std::queue queue; + mutable std::mutex mutex; + std::condition_variable condVar; +}; diff --git a/core/util/worker_thread.h b/core/util/worker_thread.h new file mode 100644 index 0000000000..29cdc2351e --- /dev/null +++ b/core/util/worker_thread.h @@ -0,0 +1,95 @@ +/* + Copyright 2025 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once +#include "tsqueue.h" +#include "oslib/oslib.h" +#include +#include +#include +#include +#include + +class WorkerThread +{ +public: + using Function = std::function; + + WorkerThread(const char *name) : name(name) { + } + ~WorkerThread() { + stop(); + } + + void stop() + { + std::lock_guard _(mutex); + if (thread != nullptr && thread->joinable()) + { + queue.push(Exit()); + thread->join(); + thread.reset(); + } + } + + void run(Function&& task) { + start(); + queue.push(std::move(task)); + } + + template + auto runFuture(F&& f, Args&&... args) -> std::future::type> + { + using return_type = typename std::result_of::type; + auto task = std::make_shared>( + std::bind(std::forward(f), std::forward(args)...)); + + run([task]() { + (*task)(); + }); + return task->get_future(); + } + +private: + void start() + { + std::lock_guard _(mutex); + if (thread != nullptr && thread->joinable()) + return; + queue.clear(); + thread = std::make_unique([this]() + { + ThreadName _(name); + while (true) + { + Task t = queue.pop(); + if (std::get_if(&t) != nullptr) + break; + Function& func = std::get(t); + func(); + } + }); + } + + const char * const name; + using Exit = std::monostate; + using Task = std::variant; + TsQueue queue; + std::unique_ptr thread; + std::mutex mutex; +}; diff --git a/tests/src/util/PeriodicThreadTest.cpp b/tests/src/util/PeriodicThreadTest.cpp new file mode 100644 index 0000000000..11e14fd211 --- /dev/null +++ b/tests/src/util/PeriodicThreadTest.cpp @@ -0,0 +1,26 @@ +#include "gtest/gtest.h" +#include "util/periodic_thread.h" +#include + +class PeriodicThreadTest : public ::testing::Test +{ +}; + +TEST_F(PeriodicThreadTest, Basic) +{ + std::atomic counter = 0; + PeriodicThread thread = PeriodicThread("Test", [&]() { + counter++; + }); + thread.setPeriod(10); + thread.start(); + usleep(15'000); + ASSERT_LT(0, counter); + int copy = counter; + usleep(15'000); + ASSERT_LT(copy, counter); + thread.stop(); + copy = counter; + usleep(15'000); + ASSERT_EQ(copy, counter); +} diff --git a/tests/src/util/TsQueueTest.cpp b/tests/src/util/TsQueueTest.cpp new file mode 100644 index 0000000000..b5deb95c79 --- /dev/null +++ b/tests/src/util/TsQueueTest.cpp @@ -0,0 +1,97 @@ +#include "gtest/gtest.h" +#include "util/tsqueue.h" +#include +#include + +class TsQueueTest : public ::testing::Test +{ +}; + +TEST_F(TsQueueTest, Basic) +{ + TsQueue queue; + ASSERT_TRUE(queue.empty()); + ASSERT_EQ(0, queue.size()); + queue.push(42); + ASSERT_FALSE(queue.empty()); + ASSERT_EQ(1, queue.size()); + ASSERT_EQ(42, queue.pop()); + + queue.push(1); + queue.push(2); + queue.push(3); + ASSERT_FALSE(queue.empty()); + ASSERT_EQ(3, queue.size()); + ASSERT_EQ(1, queue.pop()); + ASSERT_EQ(2, queue.pop()); + ASSERT_EQ(3, queue.pop()); +} + +TEST_F(TsQueueTest, MultiThread) +{ + TsQueue queue; + std::atomic gotResult = false; + std::future future = std::async(std::launch::async, [&]() { + bool res = queue.pop(); + gotResult = true; + return res; + }); + usleep(500'000); + ASSERT_FALSE(gotResult); + ASSERT_EQ(std::future_status::timeout, future.wait_for(std::chrono::seconds(0))); + queue.push(true); + ASSERT_TRUE(future.get()); +} + +TEST_F(TsQueueTest, Class) +{ + struct T1 { + float f; + }; + TsQueue q1; + q1.push({ 3.14f }); + T1 r1 = q1.pop(); + ASSERT_EQ(3.14f, r1.f); + + class T2 + { + public: + std::string s; + }; + TsQueue q2; + q2.push({ "pi" }); + T2 r2 = q2.pop(); + ASSERT_EQ(std::string("pi"), r2.s); + + // Non copyable, but moveable + class T3 + { + public: + T3(const char *s) : s(s) {} + T3(const T3&) = delete; + T3(T3&& other) { + std::swap(s, other.s); + } + T3& operator=(const T3& other) = delete; + T3& operator=(T3&& other) { + std::swap(s, other.s); + return *this; + } + + const char *s; + }; + TsQueue q3; + q3.push(T3("pi")); + T3 r3 = q3.pop(); + ASSERT_EQ("pi", r3.s); +} + +TEST_F(TsQueueTest, Clear) +{ + TsQueue q; + q.push("a"); + q.push("b"); + q.clear(); + q.push("c"); + ASSERT_EQ(0, strcmp("c", q.pop())); +} diff --git a/tests/src/util/WorkerThreadTest.cpp b/tests/src/util/WorkerThreadTest.cpp new file mode 100644 index 0000000000..f7e096b5c0 --- /dev/null +++ b/tests/src/util/WorkerThreadTest.cpp @@ -0,0 +1,81 @@ +#include "gtest/gtest.h" +#include "util/worker_thread.h" +#include +#include + +class WorkerThreadTest : public ::testing::Test +{ +}; + +TEST_F(WorkerThreadTest, Basic) +{ + WorkerThread worker{"Test"}; + std::atomic done = false; + const auto& task = [&]() { + done = true; + }; + worker.run(task); + usleep(100'000); + ASSERT_TRUE(done); + + // test restart + worker.stop(); + done = false; + worker.run(task); + usleep(100'000); + ASSERT_TRUE(done); +} + +TEST_F(WorkerThreadTest, MultiThread) +{ + WorkerThread worker{"Test"}; + std::atomic counter = 0; + const auto& task = [&]() { + ++counter; + }; + const auto& consumer = [&]() { + for (int i = 0; i < 100; i++) + worker.run(task); + }; + std::future futures[4]; + for (auto& f : futures) + f = std::async(std::launch::async, consumer); + for (auto& f : futures) + f.get(); + worker.stop(); // force all tasks to be executed before stopping + ASSERT_EQ(std::size(futures) * 100, counter); +} + +// There's no guarantee that tasks submitted while the worker is being stopped will +// be executed. But it shouldn't crash. +TEST_F(WorkerThreadTest, StartStop) +{ + WorkerThread worker{"Test"}; + std::atomic counter = 0; + const auto& task = [&]() { + ++counter; + }; + const auto& consumer = [&]() { + for (int i = 0; i < 100; i++) + worker.run(task); + }; + std::future future = std::async(std::launch::async, consumer); + std::future future2 = std::async(std::launch::async, [&]() { + for (int i = 0; i < 100; i++) + worker.stop(); + }); + future.get(); + future2.get(); + worker.stop(); + //ASSERT_EQ(100, counter); +} + +TEST_F(WorkerThreadTest, Future) +{ + WorkerThread worker{"Test"}; + const auto& task = [](u32 v) -> u32 { + return v; + }; + std::future f = worker.runFuture(task, 42); + ASSERT_EQ(42, f.get()); +} From 0481f5464de0b18ab6310c95a478b15ec033b07f Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 10:54:19 +0100 Subject: [PATCH 3/9] misc non-functional changes --- core/hw/naomi/naomi_roms.cpp | 1 - core/hw/pvr/ta_ctx.cpp | 30 ++++++++++++------------------ core/stdclass.cpp | 1 - core/types.h | 5 +++++ 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/core/hw/naomi/naomi_roms.cpp b/core/hw/naomi/naomi_roms.cpp index d8a0fdeceb..1825ee526c 100644 --- a/core/hw/naomi/naomi_roms.cpp +++ b/core/hw/naomi/naomi_roms.cpp @@ -586,7 +586,6 @@ const Game Games[] = { "opr-23968.ic20", 0x2000002, 0x800000, 0x0000000, InterleavedWord }, { "opr-23969.ic21s", 0x3000000, 0x800000, 0x0000000, InterleavedWord }, { "opr-23970.ic22", 0x3000002, 0x800000, 0x0000000, InterleavedWord }, - { NULL, 0, 0 }, } }, // Soreike! Anpanman Popcorn Koujou 2 (Rev C) diff --git a/core/hw/pvr/ta_ctx.cpp b/core/hw/pvr/ta_ctx.cpp index dd64b05a44..304a330ae2 100644 --- a/core/hw/pvr/ta_ctx.cpp +++ b/core/hw/pvr/ta_ctx.cpp @@ -101,6 +101,7 @@ void FinishRender(TA_context* ctx) } static std::mutex mtx_pool; +using Lock = std::lock_guard; static std::vector ctx_pool; static std::vector ctx_list; @@ -108,17 +109,15 @@ static std::vector ctx_list; TA_context *tactx_Alloc() { TA_context *ctx = nullptr; - - mtx_pool.lock(); - if (!ctx_pool.empty()) { - ctx = ctx_pool.back(); - ctx_pool.pop_back(); + Lock _(mtx_pool); + if (!ctx_pool.empty()) { + ctx = ctx_pool.back(); + ctx_pool.pop_back(); + } } - mtx_pool.unlock(); - if (ctx == nullptr) - { + if (ctx == nullptr) { ctx = new TA_context(); ctx->Alloc(); } @@ -129,17 +128,14 @@ static void tactx_Recycle(TA_context* ctx) { if (ctx->nextContext != nullptr) tactx_Recycle(ctx->nextContext); - mtx_pool.lock(); - if (ctx_pool.size() > 3) - { + Lock _(mtx_pool); + if (ctx_pool.size() > 3) { delete ctx; } - else - { + else { ctx->Reset(); ctx_pool.push_back(ctx); } - mtx_pool.unlock(); } static TA_context *tactx_Find(u32 addr, bool allocnew) @@ -147,8 +143,7 @@ static TA_context *tactx_Find(u32 addr, bool allocnew) TA_context *oldCtx = nullptr; for (TA_context *ctx : ctx_list) { - if (ctx->Address == addr) - { + if (ctx->Address == addr) { ctx->lastFrameUsed = FrameCount; return ctx; } @@ -205,11 +200,10 @@ void tactx_Term() delete ctx; ctx_list.clear(); - mtx_pool.lock(); + Lock _(mtx_pool); for (TA_context *ctx : ctx_pool) delete ctx; ctx_pool.clear(); - mtx_pool.unlock(); } const u32 NULL_CONTEXT = ~0u; diff --git a/core/stdclass.cpp b/core/stdclass.cpp index 342ff90f3c..8538e2e0c1 100644 --- a/core/stdclass.cpp +++ b/core/stdclass.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #ifdef _WIN32 #include diff --git a/core/types.h b/core/types.h index b9aba61314..9f2ee7d1e6 100644 --- a/core/types.h +++ b/core/types.h @@ -12,6 +12,11 @@ #else #define DYNACALL #endif +#ifdef _MSC_VER +// conversion from 't1' to 't2', possible loss of data +#pragma warning(disable: 4267) +#pragma warning(disable: 4244) +#endif #include #include From 522505273e128cab2996ac9133952d931a398049 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 10:57:38 +0100 Subject: [PATCH 4/9] miniupnpc: minissdpc.c: fix memory allocation error backport https://github.com/miniupnp/miniupnp/commit/9698973600e639ddf0ceb0ee565d7297598486fe --- core/deps/miniupnpc/src/minissdpc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deps/miniupnpc/src/minissdpc.c b/core/deps/miniupnpc/src/minissdpc.c index edebb1600a..98c5b37463 100644 --- a/core/deps/miniupnpc/src/minissdpc.c +++ b/core/deps/miniupnpc/src/minissdpc.c @@ -338,7 +338,7 @@ receiveDevicesFromMiniSSDPD(int s, int * error) #ifdef DEBUG printf(" usnsize=%u\n", usnsize); #endif /* DEBUG */ - tmp = (struct UPNPDev *)malloc(sizeof(struct UPNPDev)+urlsize+stsize+usnsize); + tmp = (struct UPNPDev *)malloc(sizeof(struct UPNPDev)+urlsize+stsize+usnsize+3); if(tmp == NULL) { if (error) *error = MINISSDPC_MEMORY_ERROR; From 2bee6b1ce4233de618caaffc1fd768f0d4196810 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 11:01:58 +0100 Subject: [PATCH 5/9] picotcp: fix msvc build. reduce logging --- core/deps/picotcp/include/arch/pico_msvc.h | 11 +++++++++++ core/deps/picotcp/include/pico_stack.h | 2 +- core/deps/picotcp/modules/pico_socket_tcp.c | 1 - 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/deps/picotcp/include/arch/pico_msvc.h b/core/deps/picotcp/include/arch/pico_msvc.h index e695ef7ae5..0b854f2db5 100644 --- a/core/deps/picotcp/include/arch/pico_msvc.h +++ b/core/deps/picotcp/include/arch/pico_msvc.h @@ -7,6 +7,8 @@ #include #pragma pack(pop) +#define PICO_SUPPORT_THREADING + #if defined(_MSC_VER) || defined(_MSC_EXTENSIONS) #define DELTA_EPOCH_IN_MICROSECS 11644473600000000Ui64 #else @@ -54,6 +56,15 @@ static inline void PICO_IDLE(void) #define alloca _alloca +#ifdef PICO_SUPPORT_THREADING +#define PICO_SUPPORT_MUTEX +/* mutex implementations */ +extern void* pico_mutex_init(void); +extern void pico_mutex_lock(void* mux); +extern void pico_mutex_unlock(void* mux); +extern void pico_mutex_deinit(void* mux); +#endif + #endif /* PICO_SUPPORT_MSVC */ diff --git a/core/deps/picotcp/include/pico_stack.h b/core/deps/picotcp/include/pico_stack.h index 2444411f31..2706d9f39b 100644 --- a/core/deps/picotcp/include/pico_stack.h +++ b/core/deps/picotcp/include/pico_stack.h @@ -9,7 +9,7 @@ #include "pico_frame.h" #include "pico_constants.h" -#define PICO_MAX_TIMERS 20 +#define PICO_MAX_TIMERS 50 #define PICO_ETH_MRU (1514u) #define PICO_IP_MRU (1500u) diff --git a/core/deps/picotcp/modules/pico_socket_tcp.c b/core/deps/picotcp/modules/pico_socket_tcp.c index 569e4e8315..f5e56bf757 100644 --- a/core/deps/picotcp/modules/pico_socket_tcp.c +++ b/core/deps/picotcp/modules/pico_socket_tcp.c @@ -190,7 +190,6 @@ static int socket_tcp_do_deliver(struct pico_socket *s, struct pico_frame *f) return 0; } - dbg("TCP SOCKET> Not s.\n"); return -1; } From c7d030c2e6c1f4d6f269f5a491a545752e5b18b8 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 11:13:25 +0100 Subject: [PATCH 6/9] uwp: enable vmem. fix dynarec crash. windows: dll loading utility uwp: Fix crash in x64 dynarec when rewriting due to wrong unprotect range. uwp: load MapViewOfFileEx dynamically to map regions at a given address. dll load and function lookup utility. --- core/rec-x64/rec_x64.cpp | 10 +++--- core/rend/dx11/dx11context.cpp | 19 ++++------ core/rend/dx11/dx11context.h | 3 +- core/rend/dx9/d3d_shaders.cpp | 14 +++----- core/rend/dx9/d3d_shaders.h | 3 +- core/rend/dx9/dxcontext.cpp | 14 ++------ core/rend/dx9/dxcontext.h | 3 +- core/windows/dynlink.h | 64 ++++++++++++++++++++++++++++++++++ core/windows/fault_handler.cpp | 3 +- core/windows/win_vmem.cpp | 15 ++++++-- core/windows/winmain.cpp | 36 +++++++++++++------ 11 files changed, 129 insertions(+), 55 deletions(-) create mode 100755 core/windows/dynlink.h diff --git a/core/rec-x64/rec_x64.cpp b/core/rec-x64/rec_x64.cpp index 08e5df24e0..b78ffc9e4e 100644 --- a/core/rec-x64/rec_x64.cpp +++ b/core/rec-x64/rec_x64.cpp @@ -1371,11 +1371,11 @@ class X64Dynarec : public Sh4Dynarec if (codeBuffer == nullptr) // init() not called yet return false; - void* protStart = codeBuffer->get(); - size_t protSize = codeBuffer->getFreeSpace(); - virtmem::jit_set_exec(protStart, protSize, false); + u8 *retAddr = *(u8**)context.rsp - 5; + if (retAddr < (u8*)codeBuffer->getBase() || retAddr >= (u8*)codeBuffer->getBase() + codeBuffer->getSize()) + return false; + virtmem::jit_set_exec(retAddr, 16, false); - u8 *retAddr = *(u8 **)context.rsp - 5; BlockCompiler compiler(*sh4ctx, *codeBuffer, retAddr); bool rc = false; try { @@ -1383,7 +1383,7 @@ class X64Dynarec : public Sh4Dynarec } catch (const Xbyak::Error& e) { ERROR_LOG(DYNAREC, "Fatal xbyak error: %s", e.what()); } - virtmem::jit_set_exec(protStart, protSize, true); + virtmem::jit_set_exec(retAddr, 16, true); return rc; } diff --git a/core/rend/dx11/dx11context.cpp b/core/rend/dx11/dx11context.cpp index 808efac006..a2ae253610 100644 --- a/core/rend/dx11/dx11context.cpp +++ b/core/rend/dx11/dx11context.cpp @@ -216,11 +216,6 @@ void DX11Context::term() pDeviceContext.reset(); pDevice.reset(); d3dcompiler = nullptr; - if (d3dcompilerHandle != NULL) - { - FreeLibrary(d3dcompilerHandle); - d3dcompilerHandle = NULL; - } } void DX11Context::Present() @@ -359,15 +354,15 @@ const pD3DCompile DX11Context::getCompiler() if (d3dcompiler == nullptr) { #ifndef TARGET_UWP - d3dcompilerHandle = LoadLibraryA("d3dcompiler_47.dll"); - if (d3dcompilerHandle == NULL) - d3dcompilerHandle = LoadLibraryA("d3dcompiler_46.dll"); - if (d3dcompilerHandle == NULL) + if (!d3dcompilerLib.load("d3dcompiler_47.dll")) { - WARN_LOG(RENDERER, "Neither d3dcompiler_47.dll or d3dcompiler_46.dll can be loaded"); - return D3DCompile; + if (!d3dcompilerLib.load("d3dcompiler_46.dll")) + { + WARN_LOG(RENDERER, "Neither d3dcompiler_47.dll or d3dcompiler_46.dll can be loaded"); + return D3DCompile; + } } - d3dcompiler = (pD3DCompile)GetProcAddress(d3dcompilerHandle, "D3DCompile"); + d3dcompiler = d3dcompilerLib.getFunc("D3DCompile", d3dcompiler); #endif if (d3dcompiler == nullptr) d3dcompiler = D3DCompile; diff --git a/core/rend/dx11/dx11context.h b/core/rend/dx11/dx11context.h index 2602645459..1fae5cc772 100644 --- a/core/rend/dx11/dx11context.h +++ b/core/rend/dx11/dx11context.h @@ -26,6 +26,7 @@ #include #include #include "windows/comptr.h" +#include "windows/dynlink.h" #include "dx11_overlay.h" #include "wsi/context.h" @@ -97,7 +98,7 @@ class DX11Context : public GraphicsContext Samplers samplers; D3D_FEATURE_LEVEL featureLevel{}; bool supportedTexFormats[5] {}; // indexed by TextureType enum - HMODULE d3dcompilerHandle = NULL; + WinLibLoader d3dcompilerLib; pD3DCompile d3dcompiler = nullptr; static constexpr UINT VENDOR_INTEL = 0x8086; diff --git a/core/rend/dx9/d3d_shaders.cpp b/core/rend/dx9/d3d_shaders.cpp index 518d4a2fbf..74a4c89f34 100644 --- a/core/rend/dx9/d3d_shaders.cpp +++ b/core/rend/dx9/d3d_shaders.cpp @@ -457,19 +457,18 @@ void D3DShaders::init(const ComPtr& device) for (int ver = 43; ver >= 24; ver--) { std::string dllname = "d3dx9_" + std::to_string(ver) + ".dll"; - d3dx9Library = LoadLibraryA(dllname.c_str()); - if (d3dx9Library != NULL) { + if (d3dx9Library.load(dllname.c_str())) { DEBUG_LOG(RENDERER, "Loaded %s", dllname.c_str()); break; } } - if (d3dx9Library == NULL) { + if (!d3dx9Library.loaded()) { ERROR_LOG(RENDERER, "Cannot load d3dx9_??.dll"); throw FlycastException("Cannot load d3dx9_??.dll"); } - pD3DXCompileShader = (decltype(D3DXCompileShader) *)GetProcAddress(d3dx9Library, "D3DXCompileShader"); - pD3DXGetVertexShaderProfile = (decltype(D3DXGetVertexShaderProfile) *)GetProcAddress(d3dx9Library, "D3DXGetVertexShaderProfile"); - pD3DXGetPixelShaderProfile = (decltype(D3DXGetPixelShaderProfile) *)GetProcAddress(d3dx9Library, "D3DXGetPixelShaderProfile"); + pD3DXCompileShader = d3dx9Library.getFunc("D3DXCompileShader", pD3DXCompileShader); + pD3DXGetVertexShaderProfile = d3dx9Library.getFunc("D3DXGetVertexShaderProfile", pD3DXGetVertexShaderProfile); + pD3DXGetPixelShaderProfile = d3dx9Library.getFunc("D3DXGetPixelShaderProfile", pD3DXGetPixelShaderProfile); if (pD3DXCompileShader == nullptr || pD3DXGetVertexShaderProfile == nullptr || pD3DXGetPixelShaderProfile == nullptr) { ERROR_LOG(RENDERER, "Cannot find entry point in d3dx9_??.dll"); throw FlycastException("Cannot load d3dx9_??.dll"); @@ -484,7 +483,4 @@ void D3DShaders::term() for (auto& shader : modVolShaders) shader.reset(); device.reset(); - if (d3dx9Library != NULL) - FreeLibrary(d3dx9Library); - d3dx9Library = NULL; } diff --git a/core/rend/dx9/d3d_shaders.h b/core/rend/dx9/d3d_shaders.h index 6ce69d74fd..c2260c9687 100644 --- a/core/rend/dx9/d3d_shaders.h +++ b/core/rend/dx9/d3d_shaders.h @@ -19,6 +19,7 @@ #pragma once #include #include "dxcontext.h" +#include "windows/dynlink.h" #include class D3DShaders @@ -41,7 +42,7 @@ class D3DShaders std::unordered_map> shaders; ComPtr vertexShaders[4]; ComPtr modVolShaders[2]; - HMODULE d3dx9Library = NULL; + WinLibLoader d3dx9Library; decltype(D3DXCompileShader) *pD3DXCompileShader = nullptr; decltype(D3DXGetVertexShaderProfile) *pD3DXGetVertexShaderProfile = nullptr; decltype(D3DXGetPixelShaderProfile) *pD3DXGetPixelShaderProfile = nullptr; diff --git a/core/rend/dx9/dxcontext.cpp b/core/rend/dx9/dxcontext.cpp index 2fb35aa074..42a377c2e2 100644 --- a/core/rend/dx9/dxcontext.cpp +++ b/core/rend/dx9/dxcontext.cpp @@ -40,17 +40,10 @@ bool DXContext::init(bool keepCurrentWindow) } #endif - d3d9Library = LoadLibraryA("D3D9.DLL"); - if (d3d9Library == NULL) - { - ERROR_LOG(RENDERER, "Cannot load D3D9.DLL"); - term(); - return false; - } - decltype(Direct3DCreate9) *pDirect3DCreate9 = (decltype(Direct3DCreate9) *)GetProcAddress(d3d9Library, "Direct3DCreate9"); + decltype(Direct3DCreate9) *pDirect3DCreate9 = d3d9Library.getFunc("Direct3DCreate9", pDirect3DCreate9); if (pDirect3DCreate9 == nullptr) { - ERROR_LOG(RENDERER, "Cannot find entry point Direct3DCreate9"); + ERROR_LOG(RENDERER, "Cannot load D3D9.DLL"); term(); return false; } @@ -123,9 +116,6 @@ void DXContext::term() imguiDriver.reset(); pDevice.reset(); pD3D.reset(); - if (d3d9Library != NULL) - FreeLibrary(d3d9Library); - d3d9Library = NULL; deviceReady = false; } diff --git a/core/rend/dx9/dxcontext.h b/core/rend/dx9/dxcontext.h index b0ab23aed8..33a1121ef3 100644 --- a/core/rend/dx9/dxcontext.h +++ b/core/rend/dx9/dxcontext.h @@ -23,6 +23,7 @@ #include #include #include "windows/comptr.h" +#include "windows/dynlink.h" #include "d3d_overlay.h" #include "wsi/context.h" @@ -63,7 +64,7 @@ class DXContext : public GraphicsContext private: void resetDevice(); - HMODULE d3d9Library = NULL; + WinLibLoader d3d9Library{ "D3D9.DLL" }; ComPtr pD3D; ComPtr pDevice; D3DPRESENT_PARAMETERS d3dpp{}; diff --git a/core/windows/dynlink.h b/core/windows/dynlink.h new file mode 100755 index 0000000000..6b57b73c76 --- /dev/null +++ b/core/windows/dynlink.h @@ -0,0 +1,64 @@ +/* + Copyright 2025 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once +#include + +namespace detail +{ + template + struct ProxyTraits { + using funcType = Ret (*)(Args...); + }; +} + +class WinLibLoader +{ +public: + WinLibLoader(const char* name = nullptr) : name(name) { + } + ~WinLibLoader() { + if (hinst != NULL) + FreeLibrary(hinst); + } + + template + auto getFunc(const char* functionName, Ret(* const funcPtr)(Args...)) + { + using funcType = typename detail::ProxyTraits::funcType; + if (!loaded()) { + if (!load(name)) + return static_cast(nullptr); + } + return reinterpret_cast(GetProcAddress(hinst, functionName)); + } + + bool load(const char* name) + { + if (hinst != NULL) + FreeLibrary(hinst); + hinst = LoadLibraryA(name); + return hinst != NULL; + } + + bool loaded() const { return hinst != NULL; } + +private: + const char* name; + HINSTANCE hinst = NULL; +}; diff --git a/core/windows/fault_handler.cpp b/core/windows/fault_handler.cpp index babe160f8f..b4923ebbe1 100644 --- a/core/windows/fault_handler.cpp +++ b/core/windows/fault_handler.cpp @@ -24,6 +24,7 @@ static PVOID vectoredHandler; static LONG (WINAPI *prevExceptionHandler)(EXCEPTION_POINTERS *ep); +const char *getThreadName(); static void readContext(const EXCEPTION_POINTERS *ep, host_context_t &context) { @@ -145,7 +146,7 @@ static LONG WINAPI exceptionHandler(EXCEPTION_POINTERS *ep) } #endif - ERROR_LOG(COMMON, "[GPF] PC %p unhandled access to %p", (void *)context.pc, address); + ERROR_LOG(COMMON, "[GPF] Thread:%s PC %p unhandled access to %p", getThreadName(), (void *)context.pc, address); if (prevExceptionHandler != nullptr) prevExceptionHandler(ep); diff --git a/core/windows/win_vmem.cpp b/core/windows/win_vmem.cpp index 329c4affc4..f4da3259dd 100644 --- a/core/windows/win_vmem.cpp +++ b/core/windows/win_vmem.cpp @@ -3,6 +3,7 @@ #include "oslib/virtmem.h" #include +#include "dynlink.h" namespace virtmem { @@ -54,12 +55,22 @@ static std::vector mapped_regions; // Implement vmem initialization for RAM, ARAM, VRAM and SH4 context, fpcb etc. +#ifdef TARGET_UWP +static WinLibLoader kernel32("Kernel32.dll"); +static LPVOID(*MapViewOfFileEx)(HANDLE, DWORD, DWORD, DWORD, SIZE_T, LPVOID); +#endif + // Please read the POSIX implementation for more information. On Windows this is // rather straightforward. bool init(void **vmem_base_addr, void **sh4rcb_addr, size_t ramSize) { #ifdef TARGET_UWP - return false; + if (MapViewOfFileEx == nullptr) + { + MapViewOfFileEx = kernel32.getFunc("MapViewOfFileEx", MapViewOfFileEx); + if (MapViewOfFileEx == nullptr) + return false; + } #endif unmapped_regions.reserve(32); mapped_regions.reserve(32); @@ -112,7 +123,6 @@ void ondemand_page(void *address, unsigned size_bytes) { void create_mappings(const Mapping *vmem_maps, unsigned nummaps) { // Since this is tricky to get right in Windows (in posix one can just unmap sections and remap later) // we unmap the whole thing only to remap it later. -#ifndef TARGET_UWP // Unmap the whole section for (void *p : mapped_regions) UnmapViewOfFile(p); @@ -148,7 +158,6 @@ void create_mappings(const Mapping *vmem_maps, unsigned nummaps) { } } } -#endif } template diff --git a/core/windows/winmain.cpp b/core/windows/winmain.cpp index 2ab7a11188..35b61c3a27 100644 --- a/core/windows/winmain.cpp +++ b/core/windows/winmain.cpp @@ -37,6 +37,7 @@ #include "emulator.h" #include "ui/mainui.h" #include "oslib/directory.h" +#include "dynlink.h" #ifdef USE_BREAKPAD #include "breakpad/client/windows/handler/exception_handler.h" #include "version.h" @@ -432,24 +433,39 @@ void os_RunInstance(int argc, const char *argv[]) } } +static WinLibLoader kernelBaseLib("KernelBase.dll"); + void os_SetThreadName(const char *name) { -#ifndef TARGET_UWP nowide::wstackstring wname; if (wname.convert(name)) { - static HRESULT (*SetThreadDescription)(HANDLE, PCWSTR); - if (SetThreadDescription == nullptr) - { - // supported in Windows 10, version 1607 or Windows Server 2016 - HINSTANCE libh = LoadLibraryW(L"KernelBase.dll"); - if (libh != NULL) - SetThreadDescription = (HRESULT (*)(HANDLE, PCWSTR))GetProcAddress(libh, "SetThreadDescription"); - } + static HRESULT (*SetThreadDescription)(HANDLE, PCWSTR) = kernelBaseLib.getFunc("SetThreadDescription", SetThreadDescription); if (SetThreadDescription != nullptr) SetThreadDescription(GetCurrentThread(), wname.get()); } -#endif +} + +const char *getThreadName() +{ + static HRESULT (*GetThreadDescription)(HANDLE, PWSTR *) = kernelBaseLib.getFunc("GetThreadDescription", GetThreadDescription); + if (GetThreadDescription == nullptr) + return "?"; + PWSTR wname = nullptr; + if (SUCCEEDED(GetThreadDescription(GetCurrentThread(), &wname))) + { + nowide::stackstring stname; + thread_local std::string name; + if (stname.convert(wname)) + name = stname.get(); + else + name = "?"; + LocalFree(wname); + return name.c_str(); + } + else { + return "?"; + } } #ifdef VIDEO_ROUTING From 45f43781846a0da1a59dfff2677ee59f4276dabf Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 11:27:11 +0100 Subject: [PATCH 7/9] network: use asio for modem/bba. DirectPlay4 support DirectPlay4 support: StarLancer, PBA Bowling miniupnp: remove unused stuff --- core/network/dns.cpp | 87 ++- core/network/miniupnp.cpp | 10 +- core/network/miniupnp.h | 10 +- core/network/picoppp.cpp | 1521 +++++++++++++++++++++---------------- 4 files changed, 913 insertions(+), 715 deletions(-) diff --git a/core/network/dns.cpp b/core/network/dns.cpp index af98bfcb51..4d45c4a23d 100644 --- a/core/network/dns.cpp +++ b/core/network/dns.cpp @@ -29,12 +29,13 @@ extern "C" { #include #include #include +#ifdef _MSC_VER +#pragma pack(pop) +#endif } -void get_host_by_name(const char *name, struct pico_ip4 dnsaddr); -int get_dns_answer(struct pico_ip4 *address, struct pico_ip4 dnsaddr); -char *read_name(char *reader, char *buffer, int *count); -void set_non_blocking(sock_t fd); +u32 makeDnsQueryPacket(void *buf, const char *host); +pico_ip4 parseDnsResponsePacket(const void *buf, size_t len); static sock_t sock_fd = INVALID_SOCKET; static unsigned short qid = PICO_TIME_MS(); @@ -56,7 +57,15 @@ void get_host_by_name(const char *host, struct pico_ip4 dnsaddr) // DNS Packet header char buf[1024]; - pico_dns_packet *dns = (pico_dns_packet *)&buf; + u32 len = makeDnsQueryPacket(buf, host); + + if (sendto(sock_fd, buf, len, 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) + perror("DNS sendto failed"); +} + +u32 makeDnsQueryPacket(void *buf, const char *host) +{ + pico_dns_packet *dns = (pico_dns_packet *)buf; dns->id = qid++; dns->qr = PICO_DNS_QR_QUERY; @@ -72,18 +81,25 @@ void get_host_by_name(const char *host, struct pico_ip4 dnsaddr) dns->nscount = 0; dns->arcount = 0; - char *qname = &buf[sizeof(pico_dns_packet)]; + char *qname = (char *)buf + sizeof(pico_dns_packet); strcpy(qname + 1, host); pico_dns_name_to_dns_notation(qname, 128); qname_len = strlen(qname) + 1; - struct pico_dns_question_suffix *qinfo = (struct pico_dns_question_suffix *) &buf[sizeof(pico_dns_packet) + qname_len]; //fill it + pico_dns_question_suffix *qinfo = (pico_dns_question_suffix *)(qname + qname_len); //fill it qinfo->qtype = htons(PICO_DNS_TYPE_A); // Address record qinfo->qclass = htons(PICO_DNS_CLASS_IN); - if (sendto(sock_fd, buf, sizeof(pico_dns_packet) + qname_len + sizeof(struct pico_dns_question_suffix), 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) - perror("DNS sendto failed"); + return sizeof(pico_dns_packet) + qname_len + sizeof(pico_dns_question_suffix); +} + +static int dnsNameLen(const char *s) +{ + if ((uint8_t)s[0] & 0xC0) + return 2; + else + return strlen(s) + 1; } int get_dns_answer(struct pico_ip4 *address, struct pico_ip4 dnsaddr) @@ -102,50 +118,39 @@ int get_dns_answer(struct pico_ip4 *address, struct pico_ip4 dnsaddr) if (peer.sin_addr.s_addr != dnsaddr.addr) return -1; - pico_dns_packet *dns = (pico_dns_packet*) buf; + pico_ip4 addr = parseDnsResponsePacket(buf, r); + if (addr.addr == ~0u) + return -1; + address->addr = addr.addr; - // move to the first answer - char *reader = &buf[sizeof(pico_dns_packet) + qname_len + sizeof(struct pico_dns_question_suffix)]; + return 0; +} - int stop = 0; +pico_ip4 parseDnsResponsePacket(const void *buf, size_t len) +{ + const pico_dns_packet *dns = (const pico_dns_packet *)buf; + + // move to the first answer + const char *reader = (const char *)buf + sizeof(pico_dns_packet); + reader += strlen(reader) + 1 + sizeof(pico_dns_question_suffix); for (int i = 0; i < ntohs(dns->ancount); i++) { - // FIXME Check name? - free(read_name(reader, buf, &stop)); - reader = reader + stop; - - struct pico_dns_record_suffix *record = (struct pico_dns_record_suffix *)reader; - reader = reader + sizeof(struct pico_dns_record_suffix); + // TODO Check name? + reader += dnsNameLen(reader); + const pico_dns_record_suffix *record = (const pico_dns_record_suffix *)reader; + reader += sizeof(pico_dns_record_suffix); if (ntohs(record->rtype) == PICO_DNS_TYPE_A) // Address record { - memcpy(&address->addr, reader, 4); + pico_ip4 address; + memcpy(&address.addr, reader, 4); - return 0; + return address; } reader = reader + ntohs(record->rdlength); } - return -1; -} - -char *read_name(char *reader, char *buffer, int *count) -{ - char *name = (char *)malloc(128); - if ((uint8_t)reader[0] & 0xC0) - { - int offset = (((uint8_t)reader[0] & ~0xC0) << 8) + (uint8_t)reader[1]; - reader = &buffer[offset]; - *count = 2; - } - else - { - *count = strlen(reader) + 1; - } - pico_dns_notation_to_name(reader, 128); - strcpy(name, reader + 1); - - return name; + return { ~0u }; } #if !defined(_WIN32) && !defined(__SWITCH__) diff --git a/core/network/miniupnp.cpp b/core/network/miniupnp.cpp index 8f7be3301f..34874545bf 100644 --- a/core/network/miniupnp.cpp +++ b/core/network/miniupnp.cpp @@ -54,16 +54,13 @@ bool MiniUPnP::Init() WARN_LOG(NETWORK, "Internet Gateway not found: error %d", error); return false; } - wanAddress[0] = 0; initialized = true; - if (UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, wanAddress) != 0) - WARN_LOG(NETWORK, "Cannot determine external IP address"); - DEBUG_LOG(NETWORK, "MiniUPnP: public IP is %s", wanAddress); return true; } void MiniUPnP::Term() { + std::lock_guard _(mutex); if (!initialized) return; DEBUG_LOG(NETWORK, "MiniUPnP::Term"); @@ -92,7 +89,10 @@ bool MiniUPnP::AddPortMapping(int port, bool tcp) WARN_LOG(NETWORK, "Port %d redirection failed: error %d", port, error); return false; } - mappedPorts.emplace_back(portStr, tcp); + { + std::lock_guard _(mutex); + mappedPorts.emplace_back(portStr, tcp); + } DEBUG_LOG(NETWORK, "MiniUPnP: forwarding %s port %d", tcp ? "TCP" : "UDP", port); return true; } diff --git a/core/network/miniupnp.h b/core/network/miniupnp.h index 713c5efed8..db06fafca3 100644 --- a/core/network/miniupnp.h +++ b/core/network/miniupnp.h @@ -24,29 +24,28 @@ #include #include #include +#include class MiniUPnP { public: MiniUPnP() { lanAddress[0] = 0; - wanAddress[0] = 0; memset(&urls, 0, sizeof(urls)); memset(&data, 0, sizeof(data)); } bool Init(); void Term(); bool AddPortMapping(int port, bool tcp); - const char *localAddress() const { return lanAddress; } - const char *externalAddress() const { return wanAddress; } + bool isInitialized() const { return initialized; } private: UPNPUrls urls; IGDdatas data; char lanAddress[32]; - char wanAddress[32]; std::vector> mappedPorts; bool initialized = false; + std::mutex mutex; }; #else @@ -57,8 +56,7 @@ class MiniUPnP bool Init() { return true; } void Term() {} bool AddPortMapping(int port, bool tcp) { return true; } - const char *localAddress() const { return ""; } - const char *externalAddress() const { return ""; } + bool isInitialized() const { return false; } }; #endif diff --git a/core/network/picoppp.cpp b/core/network/picoppp.cpp index d9ee60b429..8bab7152c1 100644 --- a/core/network/picoppp.cpp +++ b/core/network/picoppp.cpp @@ -1,29 +1,27 @@ /* - Created on: Sep 15, 2018 + Copyright 2024 flyinghead - Copyright 2018 flyinghead + This file is part of Flycast. - This file is part of reicast. - - reicast is free software: you can redistribute it and/or modify + Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. - reicast is distributed in the hope that it will be useful, + Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with reicast. If not, see . + along with Flycast. If not, see . */ #ifdef _WIN32 #include #endif -#include "stdclass.h" +#include "types.h" //#define BBA_PCAPNG_DUMP @@ -39,139 +37,47 @@ extern "C" { #include #include #include +#ifdef _MSC_VER +#pragma pack(pop) +#endif } +#include #include "net_platform.h" - -#include "types.h" #include "picoppp.h" #include "miniupnp.h" #include "cfg/option.h" #include "emulator.h" #include "oslib/oslib.h" +#include "util/tsqueue.h" +#include "util/shared_this.h" -#include +#include #include -#include #include #define RESOLVER1_OPENDNS_COM "208.67.222.222" #define AFO_ORIG_IP 0x83f2fb3f // 63.251.242.131 in network order #define IGP_ORIG_IP 0xef2bd2cc // 204.210.43.239 in network order +constexpr int PICO_TICK_MS = 5; static pico_device *pico_dev; -static std::queue in_buffer; -static std::queue out_buffer; - -static std::mutex in_buffer_lock; -static std::mutex out_buffer_lock; +static TsQueue in_buffer; +static TsQueue out_buffer; static pico_ip4 dcaddr; static pico_ip4 dnsaddr; -static pico_socket *pico_tcp_socket, *pico_udp_socket; struct pico_ip4 public_ip; static pico_ip4 afo_ip; -struct socket_pair -{ - socket_pair() : pico_sock(nullptr), native_sock(INVALID_SOCKET) {} - socket_pair(pico_socket *pico_sock, sock_t native_sock) : pico_sock(pico_sock), native_sock(native_sock) {} - ~socket_pair() { - if (pico_sock != nullptr) - pico_socket_close(pico_sock); - if (native_sock != INVALID_SOCKET) - closesocket(native_sock); - } - socket_pair(socket_pair &&) = default; - socket_pair(const socket_pair&) = delete; - socket_pair& operator=(const socket_pair&) = delete; - - pico_socket *pico_sock; - sock_t native_sock; - std::vector in_buffer; - bool shutdown = false; - - void receive_native() - { - size_t len; - const char *data; - char buf[536]; - - if (!in_buffer.empty()) - { - len = in_buffer.size(); - data = &in_buffer[0]; - } - else - { - if (native_sock == INVALID_SOCKET) - { - if (!shutdown && pico_sock->q_out.size == 0) - { - pico_socket_shutdown(pico_sock, PICO_SHUT_RDWR); - shutdown = true; - } - return; - } - int r = (int)recv(native_sock, buf, sizeof(buf), 0); - if (r == 0) - { - INFO_LOG(MODEM, "Socket[%d] recv(%zd) returned 0 -> EOF", short_be(pico_sock->remote_port), sizeof(buf)); - closesocket(native_sock); - native_sock = INVALID_SOCKET; - return; - } - if (r < 0) - { - if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK) - { - perror("recv tcp socket"); - closesocket(native_sock); - native_sock = INVALID_SOCKET; - } - return; - } - len = r; - data = buf; - } - if (pico_sock->remote_port == short_be(5011) && len >= 5 && data[0] == 1) - // Visual Concepts sport games - memcpy((void *)&data[1], &pico_sock->local_addr.ip4.addr, 4); - - int r2 = pico_socket_send(pico_sock, data, (int)len); - if (r2 < 0) - INFO_LOG(MODEM, "error TCP sending: %s", strerror(pico_err)); - else if (r2 < (int)len) - { - if (r2 > 0 || in_buffer.empty()) - { - len -= r2; - std::vector remain(len); - memcpy(&remain[0], &data[r2], len); - std::swap(in_buffer, remain); - } - } - else - { - in_buffer.clear(); - } - } -}; - -// tcp sockets -static std::map tcp_sockets; -static std::map tcp_connecting_sockets; -// udp sockets: src port -> socket fd -static std::map udp_sockets; - struct GamePortList { const char *gameId[10]; uint16_t udpPorts[10]; uint16_t tcpPorts[10]; }; -static GamePortList GamesPorts[] = { +static const GamePortList GamesPorts[] = { { // Alien Front Online { "MK-51171" }, { 7980 }, @@ -182,8 +88,13 @@ static GamePortList GamesPorts[] = { { 9789 }, { }, }, - { // Daytona USA - { "MK-51037", "HDR-0106" }, + { + { + "MK-51037", "HDR-0106" // Daytona USA + "HDR-0073" // Sega Tetris + "GENERIC", "T44501M" // Golf Shiyouyo 2 + // (the dreamcastlive patched versions are id'ed as GENERIC) + }, { 12079, 20675 }, }, { // Dee Dee Planet @@ -225,26 +136,18 @@ static GamePortList GamesPorts[] = { }, { // PBA Tour Bowling 2001 { "T26702N" }, - { 2300, 6500, 47624, 13139 }, // FIXME 2300-2400 ? - { 2300, 47624 }, // FIXME 2300-2400 ? + { 6500, 47624, 13139 }, // +dynamic DirectPlay port 2300-2400 + { 47624 }, // +dynamic DirectPlay port 2300-2400 }, { // Planet Ring { "MK-5114864", "MK-5112550" }, { 7648, 1285, 1028 }, { }, }, - { - { - "HDR-0073" // Sega Tetris - "GENERIC", "T44501M" // Golf Shiyouyo 2 - // (the dreamcastlive patched versions are id'ed as GENERIC) - }, - { 20675, 12079 }, - }, { // StarLancer { "T40209N", "T17723D 05" }, - { 2300, 6500, 47624 }, // FIXME 2300-2400 ? - { 2300, 47624 }, // FIXME 2300-2400 ? + { 6500, 47624 }, // +dynamic DirectPlay port 2300-2400 + { 47624 }, // +dynamic DirectPlay port 2300-2400 }, { // World Series Baseball 2K2 { "MK-51152", "HDR-0198" }, @@ -264,29 +167,18 @@ static GamePortList GamesPorts[] = { }, }; -// listening port -> socket fd -static std::map tcp_listening_sockets; - static bool pico_thread_running = false; extern "C" int dont_reject_opt_vj_hack; -static void read_native_sockets(); -void get_host_by_name(const char *name, pico_ip4 dnsaddr); -int get_dns_answer(pico_ip4 *address, pico_ip4 dnsaddr); +u32 makeDnsQueryPacket(void *buf, const char *host); +pico_ip4 parseDnsResponsePacket(const void *buf, size_t len); static int modem_read(pico_device *dev, void *data, int len) { u8 *p = (u8 *)data; - int count = 0; - out_buffer_lock.lock(); - while (!out_buffer.empty() && count < len) - { - *p++ = out_buffer.front(); - out_buffer.pop(); - count++; - } - out_buffer_lock.unlock(); + for (; !out_buffer.empty() && count < len; count++) + *p++ = out_buffer.pop(); return count; } @@ -295,268 +187,679 @@ static int modem_write(pico_device *dev, const void *data, int len) { u8 *p = (u8 *)data; - in_buffer_lock.lock(); for (int i = 0; i < len; i++) { while (in_buffer.size() > 1024) { - in_buffer_lock.unlock(); if (!pico_thread_running) return 0; PICO_IDLE(); - in_buffer_lock.lock(); } in_buffer.push(*p++); } - in_buffer_lock.unlock(); return len; } -void write_pico(u8 b) -{ - out_buffer_lock.lock(); +void write_pico(u8 b) { out_buffer.push(b); - out_buffer_lock.unlock(); } int read_pico() { - in_buffer_lock.lock(); if (in_buffer.empty()) - { - in_buffer_lock.unlock(); return -1; - } else - { - u32 b = in_buffer.front(); - in_buffer.pop(); - in_buffer_lock.unlock(); - return b; - } + return in_buffer.pop(); } -int pico_available() -{ - in_buffer_lock.lock(); - int len = in_buffer.size(); - in_buffer_lock.unlock(); - - return len; +int pico_available() { + return in_buffer.size(); } -static void read_from_dc_socket(pico_socket *pico_sock, sock_t nat_sock) +class DirectPlay { - char buf[1510]; +public: + virtual ~DirectPlay() = default; + virtual void processOutPacket(const u8 *data, int len) = 0; +}; - int r = pico_socket_read(pico_sock, buf, sizeof(buf)); - if (r > 0) +class TcpSocket : public SharedThis +{ +public: + void connect(pico_socket *pico_sock) { - if (pico_sock->local_port == short_be(5011) && r >= 5 && buf[0] == 1) - // Visual Concepts sport games - memcpy(&buf[1], &public_ip.addr, 4); - if (send(nat_sock, buf, r, 0) < r) + this->pico_sock = pico_sock; + attachPicoSocket(); + u32 remoteIp = pico_sock->local_addr.ip4.addr; + if (remoteIp == AFO_ORIG_IP // Alien Front Online + || remoteIp == IGP_ORIG_IP) // Internet Game Pack { - perror("tcp_callback send"); - tcp_sockets.erase(pico_sock); + remoteIp = afo_ip.addr; // same ip for both for now } + pico.state = Established; + asio::ip::address_v4 addrv4(*(std::array *)&remoteIp); + asio::ip::tcp::endpoint endpoint(addrv4, htons(pico_sock->local_port)); + setName(endpoint); + socket.async_connect(endpoint, + std::bind(&TcpSocket::onConnect, shared_from_this(), asio::placeholders::error)); } -} -static void tcp_callback(uint16_t ev, pico_socket *s) -{ - if (ev & PICO_SOCK_EV_RD) + void start() { - auto it = tcp_sockets.find(s); - if (it == tcp_sockets.end()) + pico_sock = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_TCP, nullptr); + if (pico_sock == nullptr) { + INFO_LOG(NETWORK, "pico_socket_open failed: error %d", pico_err); + return; + } + attachPicoSocket(); + const auto& endpoint = socket.remote_endpoint(); + setName(endpoint); + memcpy(&pico_sock->local_addr.ip4.addr, endpoint.address().to_v4().to_bytes().data(), 4); + pico_sock->local_port = htons(endpoint.port()); + if (pico_socket_connect(pico_sock, &dcaddr.addr, htons(socket.local_endpoint().port())) != 0) { - if (tcp_connecting_sockets.find(s) == tcp_connecting_sockets.end()) - INFO_LOG(MODEM, "Unknown socket: remote port %d", short_be(s->remote_port)); + INFO_LOG(NETWORK, "pico_socket_connect failed: error %d", pico_err); + pico_socket_close(pico_sock); + return; } + asio.state = Established; + socket.set_option(asio::ip::tcp::no_delay(true)); + } + + asio::ip::tcp::socket& getSocket() { + return socket; + } + +private: + TcpSocket(asio::io_context& io_context, std::shared_ptr directPlay) + : io_context(io_context), socket(io_context), directPlay(directPlay) { + } + + void setName(const asio::ip::tcp::endpoint& endpoint) + { + // for logging + if (socket.is_open()) + name = std::to_string(socket.local_endpoint().port()) + + " -> " + endpoint.address().to_string() + ":" + std::to_string(endpoint.port()); else + name = "? -> " + endpoint.address().to_string() + ":" + std::to_string(endpoint.port()); + } + + void attachPicoSocket() + { + pico_sock->wakeup = [](uint16_t ev, pico_socket *picoSock) + { + if (picoSock == nullptr || picoSock->priv == nullptr) + ERROR_LOG(NETWORK, "Pico callback with null tcp socket"); + else + static_cast(picoSock->priv)->get()->picoCallback(ev); + }; + pico_sock->priv = new Ptr(shared_from_this()); + } + + void detachPicoSocket() + { + pico.state = Closed; + if (pico_sock != nullptr) { - read_from_dc_socket(it->first, it->second.native_sock); + pico_sock->wakeup = nullptr; + void *priv = pico_sock->priv; + pico_sock = nullptr; + delete static_cast(priv); + // Note: 'this' might have been deleted at this point } } - if (ev & PICO_SOCK_EV_CONN) + void closeAll() { - pico_ip4 orig; - uint16_t port; - char peer[30]; - int yes = 1; + asio.state = Closed; + asio::error_code ec; + socket.close(ec); + pico.state = Closed; + if (pico_sock != nullptr) + pico_socket_close(pico_sock); + } - pico_socket *sock_a = pico_socket_accept(s, &orig, &port); - if (sock_a == NULL) - { - // Also called for child sockets - if (tcp_sockets.find(s) == tcp_sockets.end()) - INFO_LOG(MODEM, "pico_socket_accept: %s\n", strerror(pico_err)); + void onConnect(const std::error_code& ec) + { + if (ec) { + INFO_LOG(NETWORK, "TcpSocket[%s] outbound_connect failed: %s", name.c_str(), ec.message().c_str()); + closeAll(); } else { - pico_ipv4_to_string(peer, sock_a->local_addr.ip4.addr); - //printf("Connection established from port %d to %s:%d\n", short_be(port), peer, short_be(sock_a->local_port)); - pico_socket_setoption(sock_a, PICO_TCP_NODELAY, &yes); - pico_tcp_set_linger(sock_a, 10000); - /* Set keepalive options */ - // uint32_t ka_val = 5; - // pico_socket_setoption(sock_a, PICO_SOCKET_OPT_KEEPCNT, &ka_val); - // ka_val = 30000; - // pico_socket_setoption(sock_a, PICO_SOCKET_OPT_KEEPIDLE, &ka_val); - // ka_val = 5000; - // pico_socket_setoption(sock_a, PICO_SOCKET_OPT_KEEPINTVL, &ka_val); - - sock_t sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (!VALID(sockfd)) + asio.state = Established; + socket.set_option(asio::ip::tcp::no_delay(true)); + setName(socket.remote_endpoint()); + DEBUG_LOG(NETWORK, "TcpSocket[%s] outbound connected", name.c_str()); + readAsync(); + picoCallback(0); + } + } + + void readAsync() + { + if (asio.readInProgress || asio.state != Established) + return; + verify(pico.pendingWrite == 0); + asio.readInProgress = true; + socket.async_read_some(asio::buffer(in_buffer), + std::bind(&TcpSocket::onRead, shared_from_this(), + asio::placeholders::error, + asio::placeholders::bytes_transferred)); + } + + void onRead(const std::error_code& ec, size_t len) + { + asio.readInProgress = false; + if (ec || len == 0) + { + if (ec && ec != asio::error::eof && ec != asio::error::operation_aborted) + INFO_LOG(NETWORK, "TcpSocket[%s] read error %s", name.c_str(), + ec.message().c_str()); + else + DEBUG_LOG(NETWORK, "TcpSocket[%s] asio EOF", name.c_str()); + if (pico_sock != nullptr) { - perror("socket"); + if (pico.state == Established) + pico_socket_shutdown(pico_sock, PICO_SHUT_WR); + else if (pico.state == Closed) + pico_socket_close(pico_sock); + } + asio.state = Closed; + return; + } + if (pico_sock == nullptr) + return; + DEBUG_LOG(NETWORK, "TcpSocket[%s] inbound %d bytes", name.c_str(), (int)len); + if (pico_sock->remote_port == short_be(5011) && len >= 5 && in_buffer[0] == 1) + // Visual Concepts sport games + memcpy((void *)&in_buffer[1], &pico_sock->local_addr.ip4.addr, 4); + pico.pendingWrite = len; + picoCallback(PICO_SOCK_EV_WR); + } + + void onWritten(const std::error_code& ec, size_t len) + { + asio.writeInProgress = false; + if (ec) { + INFO_LOG(NETWORK, "TcpSocket[%s] write error: %s", name.c_str(), ec.message().c_str()); + closeAll(); + } + else { + DEBUG_LOG(NETWORK, "TcpSocket[%s] outbound %d bytes", name.c_str(), (int)len); + picoCallback(0); + } + } + + void picoCallback(u16 ev) + { + ev |= pico.pendingEvent; + pico.pendingEvent = 0; + if (!socket.is_open()) + { + if (ev & PICO_SOCK_EV_DEL) { + detachPicoSocket(); + } + else { + if (ev != PICO_SOCK_EV_FIN) + INFO_LOG(NETWORK, "TcpSocket[%s] asio socket is closed (ev %x, pendingW %d)", name.c_str(), ev, pico.pendingWrite); + pico_socket_close(pico_sock); + } + return; + } + if (ev & PICO_SOCK_EV_RD) + { + verify(pico.state != Closed); + if (asio.state == Connecting || asio.writeInProgress) { + pico.pendingEvent |= PICO_SOCK_EV_RD; } else { - sockaddr_in serveraddr; - memset(&serveraddr, 0, sizeof(serveraddr)); - serveraddr.sin_family = AF_INET; - serveraddr.sin_addr.s_addr = sock_a->local_addr.ip4.addr; - if (serveraddr.sin_addr.s_addr == AFO_ORIG_IP // Alien Front Online - || serveraddr.sin_addr.s_addr == IGP_ORIG_IP) // Internet Game Pack + // This callback might be called recursively if FIN is received + pico.readInProgress = true; + int r = pico_socket_read(pico_sock, sendbuf, sizeof(sendbuf)); + pico.readInProgress = false; + DEBUG_LOG(NETWORK, "TcpSocket[%s] read event: pico.state %d, %d bytes", name.c_str(), pico.state, r); + if (r > 0) { - serveraddr.sin_addr.s_addr = afo_ip.addr; // same ip for both for now + if (pico_sock->local_port == short_be(5011) && r >= 5 && sendbuf[0] == 1) + // Visual Concepts sport games + memcpy(&sendbuf[1], &public_ip.addr, 4); + else + directPlay->processOutPacket((const u8 *)&sendbuf[0], r); + asio::async_write(socket, asio::buffer(sendbuf, r), + std::bind(&TcpSocket::onWritten, shared_from_this(), + asio::placeholders::error, + asio::placeholders::bytes_transferred)); + asio.writeInProgress = true; } - - serveraddr.sin_port = sock_a->local_port; - set_non_blocking(sockfd); - if (connect(sockfd, (sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) + else if (r < 0) { - if (get_last_error() != EINPROGRESS && get_last_error() != L_EWOULDBLOCK) + INFO_LOG(NETWORK, "TcpSocket[%s] pico read error: %s", name.c_str(), strerror(pico_err)); + if (socket.is_open()) { - pico_ipv4_to_string(peer, sock_a->local_addr.ip4.addr); - INFO_LOG(MODEM, "TCP connection to %s:%d failed: %s", peer, short_be(sock_a->local_port), strerror(get_last_error())); - closesocket(sockfd); + if (asio.state == Closed) + socket.close(); + else + socket.shutdown(asio::socket_base::shutdown_send); } - else - tcp_connecting_sockets[sock_a] = sockfd; + pico_socket_close(pico_sock); + pico.state = Closed; + } + } + } + + if (ev & PICO_SOCK_EV_WR) + { + if (pico.pendingWrite > 0) + { + DEBUG_LOG(NETWORK, "TcpSocket[%s] write event: pico.state %d, %d bytes", name.c_str(), pico.state, pico.pendingWrite); + if (pico.state == Connecting) { + pico.pendingEvent |= PICO_SOCK_EV_WR; } else { - set_tcp_nodelay(sockfd); - - tcp_sockets.try_emplace(sock_a, sock_a, sockfd); + int sent = pico_socket_write(pico_sock, &in_buffer[0], (int)pico.pendingWrite); + if (sent < 0) + { + INFO_LOG(NETWORK, "TcpSocket[%s] pico send error: %s", name.c_str(), strerror(pico_err)); + pico.pendingWrite = 0; + closeAll(); + } + else if (sent < (int)pico.pendingWrite) + { + if (sent > 0) + { + // FIXME how to handle partial pico writes if any? PICO_SOCK_EV_WR? + WARN_LOG(NETWORK, "TcpSocket[%s] Partial pico send: %d -> %d", name.c_str(), (int)pico.pendingWrite, sent); + asio.state = Closed; + } + } + else { + pico.pendingWrite = 0; + readAsync(); + } } } + else { + readAsync(); + } } - } - if (ev & PICO_SOCK_EV_FIN) { - auto it = tcp_sockets.find(s); - if (it != tcp_sockets.end()) + if (ev & PICO_SOCK_EV_CONN) { - tcp_sockets.erase(it); + DEBUG_LOG(NETWORK, "TcpSocket[%s] connect event", name.c_str()); + verify(pico.state == Connecting); + pico.state = Established; + readAsync(); } - else + + if (ev & PICO_SOCK_EV_CLOSE) // FIN received { - auto it2 = tcp_connecting_sockets.find(s); - if (it2 != tcp_connecting_sockets.end()) + DEBUG_LOG(NETWORK, "TcpSocket[%s] close event (pending ev %x, pico.reading %d, asio.writing %d)", name.c_str(), + pico.pendingEvent, pico.readInProgress, asio.writeInProgress); + if (pico.pendingEvent == 0 && !pico.readInProgress && !asio.writeInProgress) { - closesocket(it2->second); - tcp_connecting_sockets.erase(it2); + pico.state = Closed; + if (socket.is_open()) { + pico_socket_shutdown(pico_sock, PICO_SHUT_RD); + socket.shutdown(asio::socket_base::shutdown_send); + } + else { + pico_socket_close(pico_sock); + } + } + else { + pico.pendingEvent |= PICO_SOCK_EV_CLOSE; } + } + + if (ev & PICO_SOCK_EV_FIN) // Socket is in the closed state + { + DEBUG_LOG(NETWORK, "TcpSocket[%s] FIN event (pending ev %x, asio.writing %d, pico.reading %d)", name.c_str(), + pico.pendingEvent, asio.writeInProgress, pico.readInProgress); + if (pico.pendingEvent == 0 && !asio.writeInProgress && !pico.readInProgress) + closeAll(); else - INFO_LOG(MODEM, "PICO_SOCK_EV_FIN: Unknown socket: remote port %d", short_be(s->remote_port)); + pico.pendingEvent |= PICO_SOCK_EV_FIN; + } + + if (ev & PICO_SOCK_EV_ERR) { + INFO_LOG(MODEM, "TcpSocket[%s] Pico socket error received: %s", name.c_str(), strerror(pico_err)); + closeAll(); } + + if (ev & PICO_SOCK_EV_DEL) + detachPicoSocket(); } - if (ev & PICO_SOCK_EV_ERR) { - INFO_LOG(MODEM, "Socket error received: %s", strerror(pico_err)); - auto it = tcp_sockets.find(s); - if (it == tcp_sockets.end()) - INFO_LOG(MODEM, "PICO_SOCK_EV_ERR: Unknown socket: remote port %d", short_be(s->remote_port)); - else - tcp_sockets.erase(it); + asio::io_context& io_context; + asio::ip::tcp::socket socket; + std::shared_ptr directPlay; + pico_socket *pico_sock = nullptr; + std::array in_buffer; + char sendbuf[1510]; + enum State { Connecting, Established, Closed }; + struct { + State state = Connecting; + bool readInProgress = false; + bool writeInProgress = false; + } asio; + struct { + State state = Connecting; + u16 pendingEvent = 0; + u32 pendingWrite = 0; + bool readInProgress = false; + } pico; + std::string name; + friend super; +}; + +// Handles inbound tcp connections +class TcpAcceptor : public SharedThis +{ +public: + void start() + { + TcpSocket::Ptr newSock = TcpSocket::create(io_context, directPlay); + + acceptor.async_accept(newSock->getSocket(), + std::bind(&TcpAcceptor::onAccept, shared_from_this(), newSock, asio::placeholders::error)); + } + + void stop() { + acceptor.close(); } - if (ev & PICO_SOCK_EV_CLOSE) +private: + TcpAcceptor(asio::io_context& io_context, u16 port, std::shared_ptr directPlay) + : io_context(io_context), + acceptor(asio::ip::tcp::acceptor(io_context, + asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port))), + directPlay(directPlay) { - auto it = tcp_sockets.find(s); - if (it == tcp_sockets.end()) - { - INFO_LOG(MODEM, "PICO_SOCK_EV_CLOSE: Unknown socket: remote port %d", short_be(s->remote_port)); + } + + void onAccept(TcpSocket::Ptr newSock, const std::error_code& ec) + { + if (ec) { + if (ec != asio::error::operation_aborted) + INFO_LOG(NETWORK, "accept failed: %s", ec.message().c_str()); } else { - if (it->second.native_sock != INVALID_SOCKET) - shutdown(it->second.native_sock, SHUT_WR); - pico_socket_shutdown(s, PICO_SHUT_RD); + DEBUG_LOG(NETWORK, "Inbound TCP connection to port %d from %s:%d", acceptor.local_endpoint().port(), + newSock->getSocket().remote_endpoint().address().to_string().c_str(), newSock->getSocket().remote_endpoint().port()); + newSock->start(); + start(); } } -// if (ev & PICO_SOCK_EV_WR) -// { -// } -} + asio::io_context& io_context; + asio::ip::tcp::acceptor acceptor; + std::shared_ptr directPlay; + friend super; +}; -static sock_t find_udp_socket(uint16_t src_port) +// Handles outbound dc tcp sockets +class TcpSink { - auto it = udp_sockets.find(src_port); - if (it != udp_sockets.end()) - return it->second; +public: + TcpSink(asio::io_context& io_context, std::shared_ptr directPlay) + : io_context(io_context), directPlay(directPlay) + { + pico_sock = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_TCP, [](uint16_t ev, pico_socket *picoSock) { + if (picoSock == nullptr || picoSock->priv == nullptr) + WARN_LOG(NETWORK, "Pico callback with null tcp socket"); + else + static_cast(picoSock->priv)->picoCallback(ev); + }); + if (pico_sock == nullptr) + ERROR_LOG(MODEM, "error opening TCP socket: %s", strerror(pico_err)); + pico_sock->priv = this; + int yes = 1; + pico_socket_setoption(pico_sock, PICO_TCP_NODELAY, &yes); + pico_ip4 inaddr_any = {}; + uint16_t listen_port = 0; + int ret = pico_socket_bind(pico_sock, &inaddr_any, &listen_port); + if (ret < 0) + ERROR_LOG(MODEM, "error binding TCP socket to port %u: %s", short_be(listen_port), strerror(pico_err)); + else if (pico_socket_listen(pico_sock, 10) != 0) + ERROR_LOG(MODEM, "error listening on port %u", short_be(listen_port)); + } + + ~TcpSink() { + if (pico_sock != nullptr) + pico_sock->wakeup = nullptr; + } - sock_t sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (!VALID(sockfd)) + void stop() { - perror("socket"); - return -1; + if (pico_sock != nullptr) + pico_socket_close(pico_sock); + directPlay.reset(); } -#ifndef _WIN32 - fcntl(sockfd, F_SETFL, O_NONBLOCK); -#else - u_long optl = 1; - ioctlsocket(sockfd, FIONBIO, &optl); -#endif - int broadcastEnable = 1; - setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, (const char *)&broadcastEnable, sizeof(broadcastEnable)); - - // bind to same port if possible (Toy Racer) - sockaddr_in saddr; - socklen_t saddr_len = sizeof(saddr); - memset(&saddr, 0, sizeof(saddr)); - saddr.sin_family = AF_INET; - saddr.sin_addr.s_addr = INADDR_ANY; - saddr.sin_port = src_port; - if (::bind(sockfd, (sockaddr *)&saddr, saddr_len) < 0) - perror("bind"); - - // FIXME Need to clean up at some point? - udp_sockets[src_port] = sockfd; - - return sockfd; -} -static void udp_callback(uint16_t ev, pico_socket *s) -{ - if (ev & PICO_SOCK_EV_RD) +private: + void picoCallback(uint16_t ev) { - char buf[1510]; - pico_ip4 src_addr; - uint16_t src_port; - pico_msginfo msginfo; - int r = 0; - while (true) + if (ev & PICO_SOCK_EV_CONN) { - r = pico_socket_recvfrom_extended(s, buf, sizeof(buf), &src_addr.addr, &src_port, &msginfo); + pico_ip4 orig; + uint16_t port; - if (r <= 0) + pico_socket *sock_a = pico_socket_accept(pico_sock, &orig, &port); + if (sock_a == nullptr) { + // Also called for child sockets + INFO_LOG(NETWORK, "pico_socket_accept error: %s", strerror(pico_err)); + } + else { - if (r < 0) - INFO_LOG(MODEM, "error UDP recv: %s", strerror(pico_err)); - break; + char peer[30]; + int yes = 1; + pico_ipv4_to_string(peer, sock_a->local_addr.ip4.addr); + DEBUG_LOG(NETWORK, "TcpSink: Outbound from port %d to %s:%d", short_be(port), peer, short_be(sock_a->local_port)); + pico_socket_setoption(sock_a, PICO_TCP_NODELAY, &yes); + pico_tcp_set_linger(sock_a, 10000); + + TcpSocket::Ptr psock = TcpSocket::create(io_context, directPlay); + psock->connect(sock_a); } - sock_t sockfd = find_udp_socket(src_port); - if (VALID(sockfd)) + } + + if (ev & PICO_SOCK_EV_ERR) { + INFO_LOG(NETWORK, "TcpSink error: %s", strerror(pico_err)); + pico_socket_close(pico_sock); + } + + if (ev & PICO_SOCK_EV_FIN) + pico_socket_close(pico_sock); + + if (ev & (PICO_SOCK_EV_RD | PICO_SOCK_EV_WR)) + WARN_LOG(NETWORK, "TcpSink: R/W event %x", ev); + + if (ev & PICO_SOCK_EV_DEL) { + pico_sock->priv = nullptr; + pico_sock = nullptr; + } + } + + asio::io_context& io_context; + std::shared_ptr directPlay; + pico_socket *pico_sock; +}; + +// Handles inbound datagram to a given port +class UdpSocket : public SharedThis +{ +public: + void start() { + readAsync(); + } + + void sendto(const char *buf, size_t len, u32 addr, u16 port) + { + this->sendbuf.resize(len); + memcpy(this->sendbuf.data(), buf, len); + asio::ip::udp::endpoint destination(asio::ip::address_v4(addr), port); + DEBUG_LOG(NETWORK, "UdpSocket: outbound %d bytes from %d to %s:%d", (int)len, socket.local_endpoint().port(), + destination.address().to_string().c_str(), destination.port()); + socket.async_send_to(asio::buffer(this->sendbuf), destination, + std::bind(&UdpSocket::onSent, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); + } + + void close() { + asio::error_code ec; + socket.close(ec); + } + +private: + UdpSocket(asio::io_context& io_context, u16 port, pico_socket *pico_sock) + : io_context(io_context), + socket(io_context, asio::ip::udp::endpoint(asio::ip::udp::v4(), port)), + pico_sock(pico_sock) + { + asio::socket_base::broadcast option(true); + socket.set_option(option); + } + + void readAsync() { + socket.async_receive_from(asio::buffer(this->recvbuf), source, + std::bind(&UdpSocket::onReceived, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); + } + + void onSent(const std::error_code& ec, size_t len) { + if (ec) + INFO_LOG(NETWORK, "UDP sendto failed: %s", ec.message().c_str()); + } + + void onReceived(const std::error_code& ec, size_t len) + { + if (ec) { + INFO_LOG(NETWORK, "UDP recv_from failed: %s", ec.message().c_str()); + return; + } + DEBUG_LOG(NETWORK, "UdpSocket: received %d bytes to port %d from %s:%d", (int)len, + socket.local_endpoint().port(), source.address().to_string().c_str(), source.port()); + if (len == 0) + WARN_LOG(NETWORK, "Received empty datagram"); + + // filter out messages coming from ourselves (happens for broadcasts) + u32 srcAddr = htonl(source.address().to_v4().to_uint()); + if (socket.local_endpoint().port() != source.port() || !is_local_address(srcAddr)) + { + pico_msginfo msginfo; + msginfo.dev = pico_dev; + msginfo.tos = 0; + msginfo.ttl = 0; + msginfo.local_addr.ip4.addr = srcAddr; + msginfo.local_port = htons(source.port()); + + int r = pico_socket_sendto_extended(pico_sock, &recvbuf[0], len, &dcaddr, htons(socket.local_endpoint().port()), &msginfo); + if (r < (int)len) + INFO_LOG(MODEM, "error UDP sending to port %d: %s", socket.local_endpoint().port(), strerror(pico_err)); + } + readAsync(); + } + + asio::io_context& io_context; + asio::ip::udp::socket socket; + pico_socket *pico_sock; + std::vector sendbuf; + std::array recvbuf; + asio::ip::udp::endpoint source; // source endpoint when receiving packets + friend super; +}; + +// Handles all outbound datagrams +class UdpSink +{ +public: + UdpSink(asio::io_context& io_context) + : io_context(io_context) + { + pico_sock = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_UDP, [](u16 ev, pico_socket *picoSock) { + if (picoSock == nullptr || picoSock->priv == nullptr) + ERROR_LOG(NETWORK, "Pico callback with null udp sink"); + else + static_cast(picoSock->priv)->picoCallback(ev); + }); + if (pico_sock == nullptr) { + ERROR_LOG(NETWORK, "error opening UDP socket: %s", strerror(pico_err)); + return; + } + pico_sock->priv = this; + pico_ip4 inaddr_any = {0}; + uint16_t listen_port = 0; + int ret = pico_socket_bind(pico_sock, &inaddr_any, &listen_port); + if (ret < 0) + ERROR_LOG(NETWORK, "error binding UDP socket to port %u: %s", short_be(listen_port), strerror(pico_err)); + } + + ~UdpSink() { + if (pico_sock != nullptr) + pico_sock->wakeup = nullptr; + } + + void setDirectPlay(std::shared_ptr directPlay) { + this->directPlay = directPlay; + } + + UdpSocket::Ptr findSocket(u16 port) + { + auto it = sockets.find(port); + if (it != sockets.end()) + return it->second; + try { + UdpSocket::Ptr sock = UdpSocket::create(io_context, port, pico_sock); + sock->start(); + sockets[port] = sock; + return sock; + } catch (const std::system_error& e) { + WARN_LOG(NETWORK, "Server UDP socket on port %d: %s", port, e.what()); + return nullptr; + } + } + + void stop() + { + for (auto& [port,sock] : sockets) + sock->close(); + sockets.clear(); + if (pico_sock != nullptr) + pico_socket_close(pico_sock); + directPlay.reset(); + } + +private: + void picoCallback(u16 ev) + { + if (ev & PICO_SOCK_EV_RD) + { + char buf[1510]; + pico_ip4 src_addr; + uint16_t src_port; + pico_msginfo msginfo; + int r = 0; + while (true) { + src_port = 0; + src_addr = {}; + r = pico_socket_recvfrom_extended(pico_sock, buf, sizeof(buf), &src_addr.addr, &src_port, &msginfo); + + if (r < 0) { + INFO_LOG(NETWORK, "error UDP recv: %s", strerror(pico_err)); + break; + } + if (r == 0 && src_port == 0 && src_addr.addr == 0) + // No more packets + break; + if (r == 0) + WARN_LOG(NETWORK, "Sending empty datagram"); // Daytona USA - if (msginfo.local_port == 0x2F2F && buf[0] == 0x20 && buf[2] == 0x42) + if (msginfo.local_port == 0x2F2F && r >= 3 && buf[0] == 0x20 && buf[2] == 0x42) { if (buf[1] == 0x2b && r >= 37 + (int)sizeof(public_ip.addr)) { @@ -575,275 +878,184 @@ static void udp_callback(uint16_t ev, pico_socket *s) memcpy(p, &public_ip.addr, sizeof(public_ip.addr)); } } - sockaddr_in dst_addr; - socklen_t addr_len = sizeof(dst_addr); - memset(&dst_addr, 0, sizeof(dst_addr)); - dst_addr.sin_family = AF_INET; - dst_addr.sin_addr.s_addr = msginfo.local_addr.ip4.addr; - dst_addr.sin_port = msginfo.local_port; - if (sendto(sockfd, buf, r, 0, (const sockaddr *)&dst_addr, addr_len) < 0) - perror("sendto udp socket"); + else if (msginfo.local_port == htons(47624)) + directPlay->processOutPacket((const u8 *)buf, r); + UdpSocket::Ptr sock = findSocket(htons(src_port)); + if (sock) + sock->sendto(buf, r, htonl(msginfo.local_addr.ip4.addr), htons(msginfo.local_port)); } } + if (ev & PICO_SOCK_EV_DEL) { + pico_sock->wakeup = nullptr; + pico_sock = nullptr; + } } - if (ev & PICO_SOCK_EV_ERR) { - INFO_LOG(MODEM, "UDP Callback error received"); - } -} + asio::io_context& io_context; + pico_socket *pico_sock = nullptr; + std::unordered_map sockets; + std::shared_ptr directPlay; +}; -static void read_native_sockets() +class DirectPlayImpl : public DirectPlay, public SharedThis { - int r; - sockaddr_in src_addr; - socklen_t addr_len; - - // Accept incoming TCP connections - for (auto it = tcp_listening_sockets.begin(); it != tcp_listening_sockets.end(); it++) +public: + void processOutPacket(const u8 *data, int len) override { - addr_len = sizeof(src_addr); - memset(&src_addr, 0, addr_len); - sock_t sockfd = accept(it->second, (sockaddr *)&src_addr, &addr_len); - if (!VALID(sockfd)) + if (!isDirectPlay(data, len)) + return; + + u16 port = htons(*(u16 *)&data[6]); + if (port >= 2300 && port <= 2400 && port != this->port) { - if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK) - perror("accept"); - continue; + NOTICE_LOG(NETWORK, "DirectPlay4 local port is %d", port); + if (acceptor) { + acceptor->stop(); + acceptor.reset(); + } + forwardPorts(port, false); + this->port = port; + udpSink.findSocket(port); + try { + acceptor = TcpAcceptor::create(io_context, port, shared_from_this()); + acceptor->start(); + } catch (const std::system_error& e) { + WARN_LOG(NETWORK, "DirectPlay TCP socket on port %d: %s", port, e.what()); + } } - //printf("Incoming TCP connection from %08x to port %d\n", src_addr.sin_addr.s_addr, short_be(it->first)); - pico_socket *ps = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_TCP, &tcp_callback); - if (ps == NULL) - { - INFO_LOG(MODEM, "pico_socket_open failed: error %d", pico_err); - closesocket(sockfd); - continue; - } - ps->local_addr.ip4.addr = src_addr.sin_addr.s_addr; - ps->local_port = src_addr.sin_port; - if (pico_socket_connect(ps, &dcaddr.addr, it->first) != 0) - { - INFO_LOG(MODEM, "pico_socket_connect failed: error %d", pico_err); - closesocket(sockfd); - pico_socket_close(ps); - continue; - } - set_non_blocking(sockfd); - set_tcp_nodelay(sockfd); - - tcp_sockets.try_emplace(ps, ps, sockfd); - } - - // Check connecting outbound TCP sockets - fd_set write_fds{}; - fd_set error_fds{}; - FD_ZERO(&write_fds); - FD_ZERO(&error_fds); - int max_fd = -1; - for (auto it = tcp_connecting_sockets.begin(); it != tcp_connecting_sockets.end(); it++) - { - FD_SET(it->second, &write_fds); - FD_SET(it->second, &error_fds); - max_fd = std::max(max_fd, (int)it->second); - } - if (max_fd > -1) - { - timeval tv{}; - int rc = select(max_fd + 1, nullptr, &write_fds, &error_fds, &tv); - if (rc == -1) - perror("select"); - else if (rc > 0) + if (*(u16 *)&data[24] == 0x13) // Add Forward Request { - for (auto it = tcp_connecting_sockets.begin(); it != tcp_connecting_sockets.end(); ) + // This one is the guest game port, only UDP is used + u16 port = htons(*(u16 *)&data[0x72]); + if (port >= 2300 && port <= 2400 && port != this->gamePort) { - if (!FD_ISSET(it->second, &write_fds) && !FD_ISSET(it->second, &error_fds)) - { - it++; - continue; - } - int error; -#ifdef _WIN32 - char *value = (char *)&error; -#else - int *value = &error; -#endif - socklen_t l = sizeof(int); - if (getsockopt(it->second, SOL_SOCKET, SO_ERROR, value, &l) < 0 || error != 0) - { - char peer[30]; - pico_ipv4_to_string(peer, it->first->local_addr.ip4.addr); - INFO_LOG(MODEM, "TCP connection to %s:%d failed: %s", peer, short_be(it->first->local_port), strerror(get_last_error())); - pico_socket_close(it->first); - closesocket(it->second); - } - else - { - set_tcp_nodelay(it->second); - - tcp_sockets.try_emplace(it->first, it->first, it->second); - - read_from_dc_socket(it->first, it->second); - } - it = tcp_connecting_sockets.erase(it); + if (*(u16 *)&data[0x62] == this->port) + WARN_LOG(NETWORK, "DirectPlay4 AddForwardRequest expected port %d got %d", this->port, *(u16 *)&data[0x62]); + NOTICE_LOG(NETWORK, "DirectPlay4 game port is %d", port); + forwardPorts(port, true); + this->gamePort = port; + udpSink.findSocket(port); } } } - static char buf[1500]; - pico_msginfo msginfo; - - // Read UDP sockets - for (auto it = udp_sockets.begin(); it != udp_sockets.end(); it++) + ~DirectPlayImpl() { - if (!VALID(it->second)) - continue; + if (upnpCmd.valid()) + upnpCmd.get(); + if (acceptor) + acceptor->stop(); + } - addr_len = sizeof(src_addr); - memset(&src_addr, 0, addr_len); - r = (int)recvfrom(it->second, buf, sizeof(buf), 0, (sockaddr *)&src_addr, &addr_len); - // filter out messages coming from ourselves (happens for broadcasts) - if (r > 0 && (it->first != src_addr.sin_port || !is_local_address(src_addr.sin_addr.s_addr))) - { - msginfo.dev = pico_dev; - msginfo.tos = 0; - msginfo.ttl = 0; - msginfo.local_addr.ip4.addr = src_addr.sin_addr.s_addr; - msginfo.local_port = src_addr.sin_port; +private: + DirectPlayImpl(asio::io_context& io_context, UdpSink& udpSink, std::shared_ptr upnp) + : io_context(io_context), udpSink(udpSink), upnp(upnp) { + } - int r2 = pico_socket_sendto_extended(pico_udp_socket, buf, r, &dcaddr, it->first, &msginfo); - if (r2 < r) - INFO_LOG(MODEM, "error UDP sending to %d: %s", short_be(it->first), strerror(pico_err)); - } - else if (r < 0 && get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK) - { - perror("recvfrom udp socket"); - continue; - } + bool isDirectPlay(const u8 *data, int len) + { + return len >= 24 && (data[2] & 0xf0) == 0xb0 && data[3] == 0xfa + // DirectPlay4 signature + && !memcmp(&data[20], "play", 4); } - // Read TCP sockets - for (auto it = tcp_sockets.begin(); it != tcp_sockets.end(); ) + void forwardPorts(u16 port, bool udpOnly) { - it->second.receive_native(); - if (it->second.pico_sock == nullptr) - it = tcp_sockets.erase(it); - else - it++; + if (upnp && upnp->isInitialized()) + { + if (upnpCmd.valid()) + upnpCmd.get(); + upnpCmd = std::async(std::launch::async, [this, port, udpOnly]() + { + if (!upnp->AddPortMapping(port, false)) + WARN_LOG(MODEM, "UPNP AddPortMapping UDP %d failed", port); + if (!udpOnly && !upnp->AddPortMapping(port, true)) + WARN_LOG(MODEM, "UPNP AddPortMapping TCP %d failed", port); + }); + } } -} -static void close_native_sockets() + u16 port = 0; + u16 gamePort = 0; + TcpAcceptor::Ptr acceptor; + asio::io_context& io_context; + UdpSink& udpSink; + std::shared_ptr upnp; + std::future upnpCmd; + friend super; +}; + +class DnsResolver : public SharedThis { - for (const auto& pair : udp_sockets) - closesocket(pair.second); - udp_sockets.clear(); - for (auto& pair : tcp_sockets) +public: + void resolve(const char *host, pico_ip4 *result) { - pico_socket_del_imm(pair.second.pico_sock); - pair.second.pico_sock = nullptr; - closesocket(pair.second.native_sock); - pair.second.native_sock = INVALID_SOCKET; + // need to introduce a dns query object if concurrency is needed + verify(!busy); + busy = true; + u32 len = makeDnsQueryPacket(buf, host); + socket.async_send_to(asio::buffer(buf, len), nsEndpoint, + std::bind(&DnsResolver::querySent, shared_from_this(), + result, + asio::placeholders::error, + asio::placeholders::bytes_transferred)); } - tcp_sockets.clear(); - for (const auto& pair : tcp_connecting_sockets) + +private: + DnsResolver(asio::io_context& io_context, const char *nameServer) + : io_context(io_context), socket(io_context) { - pico_socket_del_imm(pair.first); - closesocket(pair.second); + using namespace asio::ip; + udp::resolver resolver(io_context); + nsEndpoint = *resolver.resolve(udp::v4(), nameServer, "53").begin(); + socket.open(udp::v4()); } - tcp_connecting_sockets.clear(); - for (const auto& pair : tcp_listening_sockets) - closesocket(pair.second); - tcp_listening_sockets.clear(); -} - -static int modem_set_speed(pico_device *dev, uint32_t speed) -{ - return 0; -} -static uint32_t dns_query_start; -static uint32_t dns_query_attempts; - -static void reset_dns_entries() -{ - dns_query_attempts = 0; - dns_query_start = 0; - public_ip.addr = 0; - afo_ip.addr = 0; -} - -static void check_dns_entries() -{ - if (public_ip.addr == 0) + void querySent(pico_ip4 *result, const std::error_code& ec, size_t len) { - u32 ip; - pico_string_to_ipv4(RESOLVER1_OPENDNS_COM, &ip); - pico_ip4 tmpdns { ip }; - if (dns_query_start == 0) - { - dns_query_start = PICO_TIME_MS(); - get_host_by_name("myip.opendns.com", tmpdns); - } - else if (get_dns_answer(&public_ip, tmpdns) == 0) + if (!ec) { - dns_query_attempts = 0; - dns_query_start = 0; - char myip[16]; - pico_ipv4_to_string(myip, public_ip.addr); - INFO_LOG(MODEM, "My IP is %s", myip); + socket.async_receive_from(asio::mutable_buffer(buf, sizeof(buf)), nsEndpoint, + std::bind(&DnsResolver::responseReceived, shared_from_this(), + result, + asio::placeholders::error, + asio::placeholders::bytes_transferred)); } - else - { - if (PICO_TIME_MS() - dns_query_start > 1000) - { - if (++dns_query_attempts >= 5) - { - public_ip.addr = 0xffffffff; // Bogus but not null - dns_query_attempts = 0; - dns_query_start = 0; - WARN_LOG(MODEM, "Can't resolve my IP"); - } - else - // Retry - dns_query_start = 0; - } + else { + busy = false; } } - else if (afo_ip.addr == 0) + + void responseReceived(pico_ip4 *result, const std::error_code& ec, size_t len) { - if (dns_query_start == 0) - { - dns_query_start = PICO_TIME_MS(); - get_host_by_name("auriga.segasoft.com", dnsaddr); // Alien Front Online server - } - else + if (!ec) { - if (get_dns_answer(&afo_ip, dnsaddr) == 0) - { - dns_query_attempts = 0; - dns_query_start = 0; - char afoip[16]; - pico_ipv4_to_string(afoip, afo_ip.addr); - INFO_LOG(MODEM, "AFO server IP is %s", afoip); - } - else - { - if (PICO_TIME_MS() - dns_query_start > 1000) - { - if (++dns_query_attempts >= 5) - { - u32 addr; - pico_string_to_ipv4("146.185.135.179", &addr); // Default address - memcpy(&afo_ip.addr, &addr, sizeof(addr)); - dns_query_attempts = 0; - WARN_LOG(MODEM, "Can't resolve auriga.segasoft.com. Using default 146.185.135.179"); - } - else - // Retry - dns_query_start = 0; - } - } + *result = parseDnsResponsePacket(buf, len); + DEBUG_LOG(NETWORK, "dns resolved: %s (using %s)", + asio::ip::address_v4(*(std::array *)result).to_string().c_str(), + nsEndpoint.address().to_string().c_str()); } + busy = false; } + + asio::io_context& io_context; + asio::ip::udp::endpoint nsEndpoint; + asio::ip::udp::socket socket; + char buf[1024]; + bool busy = false; + friend super; +}; + +static void resolveDns(asio::io_context& io_context) +{ + public_ip.addr = 0; + afo_ip.addr = 0; + DnsResolver::Ptr resolver = DnsResolver::create(io_context, RESOLVER1_OPENDNS_COM); + resolver->resolve("myip.opendns.com", &public_ip); + char str[16]; + pico_ipv4_to_string(str, dnsaddr.addr); + resolver = DnsResolver::create(io_context, str); + resolver->resolve("auriga.segasoft.com", &afo_ip); } static pico_device *pico_eth_create() @@ -921,8 +1133,7 @@ static void dumpFrame(const u8 *frame, u32 size) } static void closeDumpFile() { - if (pcapngDump != nullptr) - { + if (pcapngDump != nullptr) { fclose(pcapngDump); pcapngDump = nullptr; } @@ -934,28 +1145,56 @@ void pico_receive_eth_frame(const u8 *frame, u32 size) pico_stack_recv(pico_dev, (u8 *)frame, size); } -static int send_eth_frame(pico_device *dev, void *data, int len) -{ +static int send_eth_frame(pico_device *dev, void *data, int len) { dumpFrame((const u8 *)data, len); return pico_send_eth_frame((const u8 *)data, len); } -static void *pico_thread_func(void *) +static void picoTick(const std::error_code& ec, asio::steady_timer *timer) { - pico_stack_init(); -#ifdef _WIN32 + if (ec) { + ERROR_LOG(NETWORK, "picoTick timer error: %s", ec.message().c_str()); + return; + } + pico_stack_tick(); + timer->expires_at(timer->expiry() + asio::chrono::milliseconds(PICO_TICK_MS)); + timer->async_wait(std::bind(picoTick, asio::placeholders::error, timer)); +} + +class PicoThread +{ +public: + void start() { - static WSADATA wsaData; - if (wsaData.wVersion == 0) - { - if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0) - WARN_LOG(MODEM, "WSAStartup failed"); - } - } -#endif + verify(!thread.joinable()); + io_context = std::make_unique(); + thread = std::thread(&PicoThread::run, this); + } + + void stop() + { + if (!thread.joinable()) + return; + io_context->stop(); + thread.join(); + io_context.reset(); + } + +private: + void run(); - // Find the network ports for the current game const GamePortList *ports = nullptr; + std::shared_ptr upnp; + bool usingPPP = false; + std::thread thread; + std::unique_ptr io_context; +}; + +void PicoThread::run() +{ + ThreadName _("PicoTCP"); + // Find the network ports for the current game + ports = nullptr; for (u32 i = 0; i < std::size(GamesPorts) && ports == nullptr; i++) { const auto& game = GamesPorts[i]; @@ -975,40 +1214,32 @@ static void *pico_thread_func(void *) dont_reject_opt_vj_hack = settings.content.gameId == "6107117" || settings.content.gameId == "610-7390" || settings.content.gameId == "610-7391" ? 1 : 0; - std::future upnp = - std::async(std::launch::async, [ports]() { - // Initialize miniupnpc and map network ports - ThreadName _("UPNP-init"); - MiniUPnP upnp; - if (ports != nullptr && config::EnableUPnP) + std::future pnpFuture; + if (ports != nullptr && config::EnableUPnP) + { + upnp = std::make_shared(); + pnpFuture = std::move( + std::async(std::launch::async, [this]() { - if (!upnp.Init()) + // Initialize miniupnpc and map network ports + ThreadName _("UPNP-init"); + if (!upnp->Init()) WARN_LOG(MODEM, "UPNP Init failed"); else { for (u32 i = 0; i < std::size(ports->udpPorts) && ports->udpPorts[i] != 0; i++) - if (!upnp.AddPortMapping(ports->udpPorts[i], false)) + if (!upnp->AddPortMapping(ports->udpPorts[i], false)) WARN_LOG(MODEM, "UPNP AddPortMapping UDP %d failed", ports->udpPorts[i]); for (u32 i = 0; i < std::size(ports->tcpPorts) && ports->tcpPorts[i] != 0; i++) - if (!upnp.AddPortMapping(ports->tcpPorts[i], true)) + if (!upnp->AddPortMapping(ports->tcpPorts[i], true)) WARN_LOG(MODEM, "UPNP AddPortMapping TCP %d failed", ports->tcpPorts[i]); } - } - return upnp; - }); + })); + } // Empty queues - { - std::queue empty; - in_buffer_lock.lock(); - std::swap(in_buffer, empty); - in_buffer_lock.unlock(); - - std::queue empty2; - out_buffer_lock.lock(); - std::swap(out_buffer, empty2); - out_buffer_lock.unlock(); - } + in_buffer.clear(); + out_buffer.clear(); // Find DNS ip address { @@ -1016,10 +1247,13 @@ static void *pico_thread_func(void *) if (dnsName == "46.101.91.123") // override legacy default with current one dnsName = "dns.flyca.st"; - hostent *hp = gethostbyname(dnsName.c_str()); - if (hp != nullptr && hp->h_length > 0) + asio::ip::udp::resolver resolver(*io_context); + auto it = resolver.resolve(asio::ip::udp::v4(), dnsName, "53"); + if (!it.empty()) { - memcpy(&dnsaddr.addr, hp->h_addr_list[0], sizeof(dnsaddr.addr)); + asio::ip::udp::endpoint endpoint = *it.begin(); + + memcpy(&dnsaddr.addr, &endpoint.address().to_v4().to_bytes()[0], sizeof(dnsaddr.addr)); char s[17]; pico_ipv4_to_string(s, dnsaddr.addr); NOTICE_LOG(MODEM, "%s IP is %s", dnsName.c_str(), s); @@ -1032,17 +1266,19 @@ static void *pico_thread_func(void *) WARN_LOG(MODEM, "Can't resolve dns.flyca.st. Using default 46.101.91.123"); } } - reset_dns_entries(); + resolveDns(*io_context); + + pico_stack_init(); // Create ppp/eth device - const bool usingPPP = !config::EmulateBBA; + usingPPP = !config::EmulateBBA; u32 addr; if (usingPPP) { // PPP pico_dev = pico_ppp_create(); if (!pico_dev) - return NULL; + throw FlycastException("PicoTCP ppp creation failed"); pico_string_to_ipv4("192.168.167.2", &addr); memcpy(&dcaddr.addr, &addr, sizeof(addr)); pico_ppp_set_peer_ip(pico_dev, dcaddr); @@ -1054,7 +1290,7 @@ static void *pico_thread_func(void *) pico_ppp_set_serial_read(pico_dev, modem_read); pico_ppp_set_serial_write(pico_dev, modem_write); - pico_ppp_set_serial_set_speed(pico_dev, modem_set_speed); + pico_ppp_set_serial_set_speed(pico_dev, [](pico_device *dev, uint32_t speed) { return 0; }); pico_dev->proxied = 1; pico_ppp_connect(pico_dev); @@ -1064,7 +1300,7 @@ static void *pico_thread_func(void *) // Ethernet pico_dev = pico_eth_create(); if (pico_dev == nullptr) - return nullptr; + throw FlycastException("PicoTCP eth creation failed"); pico_dev->send = &send_eth_frame; pico_dev->proxied = 1; pico_queue_protect(pico_dev->q_in); @@ -1079,7 +1315,7 @@ static void *pico_thread_func(void *) // dreamcast IP pico_string_to_ipv4("192.168.169.2", &addr); memcpy(&dcaddr.addr, &addr, sizeof(addr)); - + pico_dhcp_server_setting dhcpSettings{ 0 }; dhcpSettings.dev = pico_dev; dhcpSettings.server_ip = ipaddr; @@ -1093,89 +1329,49 @@ static void *pico_thread_func(void *) WARN_LOG(MODEM, "DHCP server init failed"); } - pico_udp_socket = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_UDP, &udp_callback); - if (pico_udp_socket == NULL) { - INFO_LOG(MODEM, "error opening UDP socket: %s", strerror(pico_err)); - return nullptr; - } - int yes = 1; - pico_ip4 inaddr_any = {0}; - uint16_t listen_port = 0; - int ret = pico_socket_bind(pico_udp_socket, &inaddr_any, &listen_port); - if (ret < 0) - INFO_LOG(MODEM, "error binding UDP socket to port %u: %s", short_be(listen_port), strerror(pico_err)); - - pico_tcp_socket = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_TCP, &tcp_callback); - if (pico_tcp_socket == NULL) { - INFO_LOG(MODEM, "error opening TCP socket: %s", strerror(pico_err)); - } - pico_socket_setoption(pico_tcp_socket, PICO_TCP_NODELAY, &yes); - ret = pico_socket_bind(pico_tcp_socket, &inaddr_any, &listen_port); - if (ret < 0) { - INFO_LOG(MODEM, "error binding TCP socket to port %u: %s", short_be(listen_port), strerror(pico_err)); - } - else - { - if (pico_socket_listen(pico_tcp_socket, 10) != 0) - INFO_LOG(MODEM, "error listening on port %u", short_be(listen_port)); - } + // Create sinks + UdpSink udpSink(*io_context); + DirectPlayImpl::Ptr directPlay = DirectPlayImpl::create(*io_context, udpSink, upnp); + udpSink.setDirectPlay(directPlay); + TcpSink tcpSink(*io_context, directPlay); // Open listening sockets - sockaddr_in saddr; - socklen_t saddr_len = sizeof(saddr); - memset(&saddr, 0, sizeof(saddr)); - saddr.sin_family = AF_INET; - saddr.sin_addr.s_addr = INADDR_ANY; + std::vector acceptors; if (ports != nullptr) { for (u32 i = 0; i < std::size(ports->udpPorts) && ports->udpPorts[i] != 0; i++) - { - uint16_t port = short_be(ports->udpPorts[i]); - find_udp_socket(port); - // bind is done in find_udp_socket - } + udpSink.findSocket(ports->udpPorts[i]); for (u32 i = 0; i < std::size(ports->tcpPorts) && ports->tcpPorts[i] != 0; i++) - { - uint16_t port = short_be(ports->tcpPorts[i]); - saddr.sin_port = port; - sock_t sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (::bind(sockfd, (sockaddr *)&saddr, saddr_len) < 0) - { - perror("bind"); - closesocket(sockfd); - continue; + try { + auto acceptor = TcpAcceptor::create(*io_context, ports->tcpPorts[i], directPlay); + acceptor->start(); + acceptors.push_back(std::move(acceptor)); + } catch (const std::system_error& e) { + WARN_LOG(NETWORK, "Server TCP socket on port %d: %s", ports->tcpPorts[i], e.what()); } - if (listen(sockfd, 5) < 0) - { - perror("listen"); - closesocket(sockfd); - continue; - } - set_non_blocking(sockfd); - tcp_listening_sockets[port] = sockfd; - } } - while (pico_thread_running) - { - read_native_sockets(); - pico_stack_tick(); - check_dns_entries(); - PICO_IDLE(); - } - - close_native_sockets(); - pico_socket_del_imm(pico_tcp_socket); - pico_socket_del_imm(pico_udp_socket); + // pico stack timer + asio::steady_timer timer(*io_context); + picoTick({}, &timer); + + // main loop + io_context->run(); + + for (auto& acceptor : acceptors) + acceptor->stop(); + acceptors.clear(); + tcpSink.stop(); + udpSink.stop(); + pico_stack_tick(); pico_stack_tick(); pico_stack_tick(); if (pico_dev) { - if (usingPPP) - { + if (usingPPP) { pico_ppp_destroy(pico_dev); } else @@ -1187,14 +1383,13 @@ static void *pico_thread_func(void *) pico_dev = nullptr; } pico_stack_deinit(); - - if (ports != nullptr) - upnp.get().Term(); - - return NULL; + if (upnp) { + upnp->Term(); + upnp.reset(); + } } -static cThread pico_thread(pico_thread_func, nullptr, "PicoTCP"); +static PicoThread pico_thread; bool start_pico() { @@ -1202,7 +1397,7 @@ bool start_pico() if (pico_thread_running) return false; pico_thread_running = true; - pico_thread.Start(); + pico_thread.start(); return true; } @@ -1211,7 +1406,7 @@ void stop_pico() { emu.setNetworkState(false); pico_thread_running = false; - pico_thread.WaitToEnd(); + pico_thread.stop(); } // picotcp mutex implementation From a0e8e9a9dfa4b3ede571455b80cdf52db0f88260 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 12:11:45 +0100 Subject: [PATCH 8/9] libretro: windows build fix --- CMakeLists.txt | 3 +++ core/windows/fault_handler.cpp | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 777b9cc32f..1222688003 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -851,6 +851,9 @@ if(LIBRETRO) if(APPLE) target_sources(${PROJECT_NAME} PRIVATE shell/libretro/oslib_apple.mm) endif() + if(WIN32) + target_link_libraries(${PROJECT_NAME} PRIVATE mswsock) + endif() endif() target_sources(${PROJECT_NAME} PRIVATE diff --git a/core/windows/fault_handler.cpp b/core/windows/fault_handler.cpp index b4923ebbe1..ceed7f0057 100644 --- a/core/windows/fault_handler.cpp +++ b/core/windows/fault_handler.cpp @@ -24,7 +24,14 @@ static PVOID vectoredHandler; static LONG (WINAPI *prevExceptionHandler)(EXCEPTION_POINTERS *ep); +#ifndef LIBRETRO const char *getThreadName(); +#else +// TODO +static const char *getThreadName() { + return ""; +} +#endif static void readContext(const EXCEPTION_POINTERS *ep, host_context_t &context) { From 5a1c3f80f58827c0538efbfd647f19898345c57e Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 13:55:39 +0100 Subject: [PATCH 9/9] msvc32 build fix --- core/windows/dynlink.h | 60 ++++++++++++++++++++-------------------- core/windows/winmain.cpp | 4 +-- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/core/windows/dynlink.h b/core/windows/dynlink.h index 6b57b73c76..0149e4ee9c 100755 --- a/core/windows/dynlink.h +++ b/core/windows/dynlink.h @@ -21,44 +21,44 @@ namespace detail { - template - struct ProxyTraits { - using funcType = Ret (*)(Args...); - }; + template + struct ProxyTraits { + using funcType = Ret (WINAPI *)(Args...); + }; } class WinLibLoader { public: - WinLibLoader(const char* name = nullptr) : name(name) { - } - ~WinLibLoader() { - if (hinst != NULL) - FreeLibrary(hinst); - } + WinLibLoader(const char* name = nullptr) : name(name) { + } + ~WinLibLoader() { + if (hinst != NULL) + FreeLibrary(hinst); + } - template - auto getFunc(const char* functionName, Ret(* const funcPtr)(Args...)) - { - using funcType = typename detail::ProxyTraits::funcType; - if (!loaded()) { - if (!load(name)) - return static_cast(nullptr); - } - return reinterpret_cast(GetProcAddress(hinst, functionName)); - } + template + auto getFunc(const char* functionName, Ret(WINAPI * const funcPtr)(Args...)) + { + using funcType = typename detail::ProxyTraits::funcType; + if (!loaded()) { + if (!load(name)) + return static_cast(nullptr); + } + return reinterpret_cast(GetProcAddress(hinst, functionName)); + } - bool load(const char* name) - { - if (hinst != NULL) - FreeLibrary(hinst); - hinst = LoadLibraryA(name); - return hinst != NULL; - } + bool load(const char* name) + { + if (hinst != NULL) + FreeLibrary(hinst); + hinst = LoadLibraryA(name); + return hinst != NULL; + } - bool loaded() const { return hinst != NULL; } + bool loaded() const { return hinst != NULL; } private: - const char* name; - HINSTANCE hinst = NULL; + const char* name; + HINSTANCE hinst = NULL; }; diff --git a/core/windows/winmain.cpp b/core/windows/winmain.cpp index 35b61c3a27..82ffb7604d 100644 --- a/core/windows/winmain.cpp +++ b/core/windows/winmain.cpp @@ -440,7 +440,7 @@ void os_SetThreadName(const char *name) nowide::wstackstring wname; if (wname.convert(name)) { - static HRESULT (*SetThreadDescription)(HANDLE, PCWSTR) = kernelBaseLib.getFunc("SetThreadDescription", SetThreadDescription); + static HRESULT (WINAPI *SetThreadDescription)(HANDLE, PCWSTR) = kernelBaseLib.getFunc("SetThreadDescription", SetThreadDescription); if (SetThreadDescription != nullptr) SetThreadDescription(GetCurrentThread(), wname.get()); } @@ -448,7 +448,7 @@ void os_SetThreadName(const char *name) const char *getThreadName() { - static HRESULT (*GetThreadDescription)(HANDLE, PWSTR *) = kernelBaseLib.getFunc("GetThreadDescription", GetThreadDescription); + static HRESULT (WINAPI *GetThreadDescription)(HANDLE, PWSTR *) = kernelBaseLib.getFunc("GetThreadDescription", GetThreadDescription); if (GetThreadDescription == nullptr) return "?"; PWSTR wname = nullptr;