From 76c3695f6e64981a644aa3e436cf01a794135671 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 2 Jan 2025 10:35:27 +0100 Subject: [PATCH 01/76] 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 fbec754c6..ef8c708d7 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 8dd949e61..40713fb28 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 eb06c2509..658e18c90 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 c3ba52786..e0d94a63d 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 03ae834ed..d3402006e 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 dacaac706..881c9724a 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 0cbd92255..5cafd3628 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 @@ public: CONSOLE_LISTENER, LOG_WINDOW_LISTENER, IN_MEMORY_LISTENER, + NETWORK_LISTENER, NUMBER_OF_LISTENERS // Must be last }; @@ -52,6 +54,7 @@ public: 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 @@ private: }; LogManager(); - ~LogManager(); LogManager(const LogManager&) = delete; LogManager& operator=(const LogManager&) = delete; @@ -75,7 +77,8 @@ private: 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 000000000..069adcde1 --- /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 8e60388a3..b86fbd7b1 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 9f68259f2..2279f4138 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 7b096ecf5..b4d28f5fd 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 000000000..e8b7c9604 --- /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 000000000..b86217d40 --- /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 02/76] multithreading utilities (periodic, worker thread) --- CMakeLists.txt | 5 +- core/achievements/achievements.cpp | 74 +++++----------- core/rend/CustomTexture.cpp | 110 +++++++++--------------- 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, 561 insertions(+), 140 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 40713fb28..777b9cc32 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 b2ff0ed63..0a48fa61e 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 @@ private: 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 @@ private: 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::asyncTask(std::function&& f) { + taskThread.run(std::move(f)); } -void Achievements::startThread() -{ - threadRunning = true; - taskThread = std::thread(&Achievements::backgroundThread, this); -} - -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 1ff8444b2..8f8beda18 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) - { - 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(); + 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 = 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 3994b9925..df28e7692 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 000000000..690b5c7bd --- /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 000000000..21e3be01d --- /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 000000000..29cdc2351 --- /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 000000000..11e14fd21 --- /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 000000000..b5deb95c7 --- /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 000000000..f7e096b5c --- /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 03/76] 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 d8a0fdece..1825ee526 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 dd64b05a4..304a330ae 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 342ff90f3..8538e2e0c 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 b9aba6131..9f2ee7d1e 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 04/76] 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 edebb1600..98c5b3746 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 05/76] 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 e695ef7ae..0b854f2db 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 2444411f3..2706d9f39 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 569e4e831..f5e56bf75 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 06/76] 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 08e5df24e..b78ffc9e4 100644 --- a/core/rec-x64/rec_x64.cpp +++ b/core/rec-x64/rec_x64.cpp @@ -1371,11 +1371,11 @@ public: 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 @@ public: } 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 808efac00..a2ae25361 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 260264545..1fae5cc77 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 @@ private: 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 518d4a2fb..74a4c89f3 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 6ce69d74f..c2260c968 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 @@ private: 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 2fb35aa07..42a377c2e 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 b0ab23aed..33a1121ef 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 @@ public: 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 000000000..6b57b73c7 --- /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 babe160f8..b4923ebbe 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 329c4affc..f4da3259d 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 2ab7a1118..35b61c3a2 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 07/76] 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 | 1601 +++++++++++++++++++++---------------- 4 files changed, 953 insertions(+), 755 deletions(-) diff --git a/core/network/dns.cpp b/core/network/dns.cpp index af98bfcb5..4d45c4a23 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; + + return 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 - char *reader = &buf[sizeof(pico_dns_packet) + qname_len + sizeof(struct pico_dns_question_suffix)]; - - int stop = 0; + 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 8f7be3301..34874545b 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 713c5efed..db06fafca 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 @@ public: 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 d9ee60b42..8bab7152c 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()) - { - if (tcp_connecting_sockets.find(s) == tcp_connecting_sockets.end()) - INFO_LOG(MODEM, "Unknown socket: remote port %d", short_be(s->remote_port)); + 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) + { + 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 - { - read_from_dc_socket(it->first, it->second.native_sock); - } + name = "? -> " + endpoint.address().to_string() + ":" + std::to_string(endpoint.port()); } - if (ev & PICO_SOCK_EV_CONN) + void attachPicoSocket() { - pico_ip4 orig; - uint16_t port; - char peer[30]; - int yes = 1; - - 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)); - } - 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)) + pico_sock->wakeup = [](uint16_t ev, pico_socket *picoSock) { - perror("socket"); + 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) + { + 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 + } + } + + void closeAll() + { + asio.state = Closed; + asio::error_code ec; + socket.close(ec); + pico.state = Closed; + if (pico_sock != nullptr) + pico_socket_close(pico_sock); + } + + 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 + { + 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) + { + 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 - } - - serveraddr.sin_port = sock_a->local_port; - set_non_blocking(sockfd); - if (connect(sockfd, (sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) - { - if (get_last_error() != EINPROGRESS && get_last_error() != L_EWOULDBLOCK) - { - 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 (pico_sock->local_port == short_be(5011) && r >= 5 && sendbuf[0] == 1) + // Visual Concepts sport games + memcpy(&sendbuf[1], &public_ip.addr, 4); else - tcp_connecting_sockets[sock_a] = sockfd; + 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; + } + else if (r < 0) + { + INFO_LOG(NETWORK, "TcpSocket[%s] pico read error: %s", name.c_str(), strerror(pico_err)); + if (socket.is_open()) + { + if (asio.state == Closed) + socket.close(); + else + socket.shutdown(asio::socket_base::shutdown_send); + } + 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_CONN) + { + DEBUG_LOG(NETWORK, "TcpSocket[%s] connect event", name.c_str()); + verify(pico.state == Connecting); + pico.state = Established; + readAsync(); + } + + if (ev & PICO_SOCK_EV_CLOSE) // FIN received + { + 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) + { + 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 + 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_FIN) { - auto it = tcp_sockets.find(s); - if (it != tcp_sockets.end()) - { - 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(); + } + +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) + { + } + + 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 { - auto it2 = tcp_connecting_sockets.find(s); - if (it2 != tcp_connecting_sockets.end()) - { - closesocket(it2->second); - tcp_connecting_sockets.erase(it2); + 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(); + } + } + + asio::io_context& io_context; + asio::ip::tcp::acceptor acceptor; + std::shared_ptr directPlay; + friend super; +}; + +// Handles outbound dc tcp sockets +class TcpSink +{ +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; + } + + void stop() + { + if (pico_sock != nullptr) + pico_socket_close(pico_sock); + directPlay.reset(); + } + +private: + void picoCallback(uint16_t ev) + { + if (ev & PICO_SOCK_EV_CONN) + { + pico_ip4 orig; + uint16_t port; + + 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 - INFO_LOG(MODEM, "PICO_SOCK_EV_FIN: Unknown socket: remote port %d", short_be(s->remote_port)); - } - } - - 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); - } - - if (ev & PICO_SOCK_EV_CLOSE) - { - 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)); - } - else - { - if (it->second.native_sock != INVALID_SOCKET) - shutdown(it->second.native_sock, SHUT_WR); - pico_socket_shutdown(s, PICO_SHUT_RD); - } - } - -// if (ev & PICO_SOCK_EV_WR) -// { -// } -} - -static sock_t find_udp_socket(uint16_t src_port) -{ - auto it = udp_sockets.find(src_port); - if (it != udp_sockets.end()) - return it->second; - - sock_t sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (!VALID(sockfd)) - { - perror("socket"); - return -1; - } -#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) - { - char buf[1510]; - pico_ip4 src_addr; - uint16_t src_port; - pico_msginfo msginfo; - int r = 0; - while (true) - { - r = pico_socket_recvfrom_extended(s, buf, sizeof(buf), &src_addr.addr, &src_port, &msginfo); - - if (r <= 0) { - 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_ERR) { - INFO_LOG(MODEM, "UDP Callback error received"); - } -} - -static void read_native_sockets() -{ - 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++) - { - 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 (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK) - perror("accept"); - continue; + if (ev & PICO_SOCK_EV_DEL) { + pico_sock->wakeup = nullptr; + pico_sock = nullptr; } - //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++) + asio::io_context& io_context; + pico_socket *pico_sock = nullptr; + std::unordered_map sockets; + std::shared_ptr directPlay; +}; + +class DirectPlayImpl : public DirectPlay, public SharedThis +{ +public: + void processOutPacket(const u8 *data, int len) override { - 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 (!isDirectPlay(data, len)) + return; + + u16 port = htons(*(u16 *)&data[6]); + if (port >= 2300 && port <= 2400 && port != this->port) { - for (auto it = tcp_connecting_sockets.begin(); it != tcp_connecting_sockets.end(); ) + 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()); + } + } + if (*(u16 *)&data[24] == 0x13) // Add Forward Request + { + // 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) + bool isDirectPlay(const u8 *data, int len) + { + return len >= 24 && (data[2] & 0xf0) == 0xb0 && data[3] == 0xfa + // DirectPlay4 signature + && !memcmp(&data[20], "play", 4); + } + + void forwardPorts(u16 port, bool udpOnly) + { + if (upnp && upnp->isInitialized()) { - perror("recvfrom udp socket"); - continue; + 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); + }); } } - // Read TCP sockets - for (auto it = tcp_sockets.begin(); it != tcp_sockets.end(); ) - { - it->second.receive_native(); - if (it->second.pico_sock == nullptr) - it = tcp_sockets.erase(it); - else - it++; - } -} + 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; +}; -static void close_native_sockets() +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) + void querySent(pico_ip4 *result, const std::error_code& ec, size_t len) + { + if (!ec) + { + 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 { + busy = false; + } + } + + void responseReceived(pico_ip4 *result, const std::error_code& ec, size_t len) + { + if (!ec) + { + *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) { - 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) - { - 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) - { - 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); - } - 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 if (afo_ip.addr == 0) - { - 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 (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; - } - } - } - } + 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 - { - static WSADATA wsaData; - if (wsaData.wVersion == 0) - { - if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0) - WARN_LOG(MODEM, "WSAStartup failed"); - } - } -#endif + 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() + { + 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(); - } + // 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(); - close_native_sockets(); - pico_socket_del_imm(pico_tcp_socket); - pico_socket_del_imm(pico_udp_socket); 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 08/76] 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 777b9cc32..122268800 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 b4923ebbe..ceed7f005 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 09/76] 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 6b57b73c7..0149e4ee9 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 35b61c3a2..82ffb7604 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; From 2ca9187137c1dfab42c6e7e5ad946a33a8dcffec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Dec 2024 22:21:39 +0000 Subject: [PATCH 10/76] Fetch translations & Recreate libretro_core_options_intl.h --- shell/libretro/libretro_core_options_intl.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shell/libretro/libretro_core_options_intl.h b/shell/libretro/libretro_core_options_intl.h index 28f4b677a..86523fbcf 100644 --- a/shell/libretro/libretro_core_options_intl.h +++ b/shell/libretro/libretro_core_options_intl.h @@ -2115,15 +2115,15 @@ struct retro_core_options_v2 options_ar = { #define CORE_OPTION_NAME_INTERNAL_RESOLUTION_INFO_0_AST "Modifica la resolución de la renderización." #define OPTION_VAL_320X240_AST NULL #define OPTION_VAL_640X480_AST NULL -#define OPTION_VAL_800X600_AST NULL -#define OPTION_VAL_960X720_AST NULL -#define OPTION_VAL_1024X768_AST NULL +#define OPTION_VAL_800X600_AST "800x600 (x1,25)" +#define OPTION_VAL_960X720_AST "960x720 (x1,5)" +#define OPTION_VAL_1024X768_AST "1024x768 (x1,6)" #define OPTION_VAL_1280X960_AST NULL -#define OPTION_VAL_1440X1080_AST NULL -#define OPTION_VAL_1600X1200_AST NULL +#define OPTION_VAL_1440X1080_AST "1440x1080 (x2,25)" +#define OPTION_VAL_1600X1200_AST "1600x1200 (x2,5)" #define OPTION_VAL_1920X1440_AST NULL #define OPTION_VAL_2560X1920_AST NULL -#define OPTION_VAL_2880X2160_AST NULL +#define OPTION_VAL_2880X2160_AST "2880x2160 (x4,5)" #define OPTION_VAL_3200X2400_AST NULL #define OPTION_VAL_3840X2880_AST NULL #define OPTION_VAL_4480X3360_AST NULL From 0558c957e881e23f268aa4db0cb254c45efc19da Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 21 Dec 2024 12:25:30 +0100 Subject: [PATCH 11/76] libretro: set an SDL hint to keep flycast SIGSEGV handler Thanks to mrfixit2001 for helping troubleshoot the issue. --- shell/libretro/libretro.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/shell/libretro/libretro.cpp b/shell/libretro/libretro.cpp index 6c6fccb78..cc80eac56 100644 --- a/shell/libretro/libretro.cpp +++ b/shell/libretro/libretro.cpp @@ -27,6 +27,8 @@ #include #include #include "nswitch.h" +#elif defined(__linux__) || defined(__FreeBSD__) +#include #endif #include @@ -348,6 +350,11 @@ void retro_init() if (!addrspace::reserve()) ERROR_LOG(VMEM, "Cannot reserve memory space"); +#if defined(__linux__) || defined(__FreeBSD__) + // SDL evdev keyboard driver installs a SIGSEGV signal handler by default, which replaces flycast's one. + // Make sure to avoid this if SDL is initialized after the core (which happens). + setenv("SDL_NO_SIGNAL_HANDLERS", "1", 1); +#endif os_InstallFaultHandler(); MapleConfigMap::UpdateVibration = updateVibration; From 0606f3e63ed56b5bd82eab7db156c9a4ceb3377b Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 21 Dec 2024 12:26:16 +0100 Subject: [PATCH 12/76] ios: make the keyboard remappable --- shell/apple/emulator-ios/emulator/ios_keyboard.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/apple/emulator-ios/emulator/ios_keyboard.h b/shell/apple/emulator-ios/emulator/ios_keyboard.h index a91ebbbf0..9b15b8f3b 100644 --- a/shell/apple/emulator-ios/emulator/ios_keyboard.h +++ b/shell/apple/emulator-ios/emulator/ios_keyboard.h @@ -11,7 +11,7 @@ class API_AVAILABLE(ios(14.0)) IOSKeyboard : public KeyboardDevice { public: - IOSKeyboard(int port, GCKeyboard *keyboard) : KeyboardDevice(port, "iOS", false), gcKeyboard(keyboard) + IOSKeyboard(int port, GCKeyboard *keyboard) : KeyboardDevice(port, "iOS"), gcKeyboard(keyboard) { set_maple_port(port); loadMapping(); From a3902fc278fa7e48cdf4e113c989461c9bfb262b Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 26 Dec 2024 16:31:23 +0100 Subject: [PATCH 13/76] sdl: disable rumble when rumble power is set to 0 Issue #1766 Issue #1783 --- core/sdl/sdl_gamepad.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/core/sdl/sdl_gamepad.h b/core/sdl/sdl_gamepad.h index 46bbd0b45..d9820e5e4 100644 --- a/core/sdl/sdl_gamepad.h +++ b/core/sdl/sdl_gamepad.h @@ -303,8 +303,12 @@ public: SDL_JoystickSetPlayerIndex(sdl_joystick, port <= 3 ? port : -1); } - u16 getRumbleIntensity(float power) { - return (u16)std::min(power * 65535.f / std::pow(1.06f, 100.f - rumblePower), 65535.f); + u16 getRumbleIntensity(float power) + { + if (rumblePower == 0) + return 0; + else + return (u16)std::min(power * 65535.f / std::pow(1.06f, 100.f - rumblePower), 65535.f); } void doRumble(float power, u32 duration_ms) { @@ -389,6 +393,7 @@ public: SDL_HapticStopAll(haptic); if (hasAutocenter) SDL_HapticSetAutocenter(haptic, 0); + vib_inclination = 0; } if (!hapticRumble) rumble(0, 0, 0); From 45c700ab60e65cc35bc70e9fc6853cf34fb0df5c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Dec 2024 22:20:31 +0000 Subject: [PATCH 14/76] Fetch translations & Recreate libretro_core_options_intl.h --- shell/libretro/libretro_core_options_intl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/libretro/libretro_core_options_intl.h b/shell/libretro/libretro_core_options_intl.h index 86523fbcf..10ce93b13 100644 --- a/shell/libretro/libretro_core_options_intl.h +++ b/shell/libretro/libretro_core_options_intl.h @@ -80690,7 +80690,7 @@ struct retro_core_options_v2 options_val = { #define OPTION_VAL_2_VN NULL #define CORE_OPTION_NAME_DELAY_FRAME_SWAPPING_LABEL_VN NULL #define CORE_OPTION_NAME_DELAY_FRAME_SWAPPING_INFO_0_VN NULL -#define CORE_OPTION_NAME_DETECT_VSYNC_SWAP_INTERVAL_LABEL_VN NULL +#define CORE_OPTION_NAME_DETECT_VSYNC_SWAP_INTERVAL_LABEL_VN "Phát hiện sá»± thay đổi tốc độ khung hình" #define CORE_OPTION_NAME_DETECT_VSYNC_SWAP_INTERVAL_INFO_0_VN NULL #define CORE_OPTION_NAME_PVR2_FILTERING_LABEL_VN NULL #define CORE_OPTION_NAME_PVR2_FILTERING_INFO_0_VN NULL From 42752acef58982bf2aa94be438b064f97a7aed54 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 30 Dec 2024 12:20:29 +0100 Subject: [PATCH 15/76] Fix crash when reading 2448-byte sectors Fixes MINIDUMP-5J0, MINIDUMP-5M7, MINIDUMP-5M8, MINIDUMP-5MA, MINIDUMP-5MF --- core/imgread/common.cpp | 2 +- core/imgread/common.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/imgread/common.cpp b/core/imgread/common.cpp index 3a55a2b63..d0157b509 100644 --- a/core/imgread/common.cpp +++ b/core/imgread/common.cpp @@ -282,7 +282,7 @@ bool Disc::readSector(u32 FAD, u8 *dst, SectorFormat *sector_type, u8 *subcode, u32 Disc::ReadSectors(u32 FAD, u32 count, u8* dst, u32 fmt, bool stopOnMiss, LoadProgress *progress) { - u8 temp[2352]; + u8 temp[2448]; SectorFormat secfmt; SubcodeFormat subfmt; diff --git a/core/imgread/common.h b/core/imgread/common.h index fc740a518..8366d7fdc 100644 --- a/core/imgread/common.h +++ b/core/imgread/common.h @@ -161,7 +161,7 @@ struct Disc for (u32 j=tracks[i].StartFAD;j<=tracks[i].EndFAD;j++) { - u8 temp[2352]; + u8 temp[2448]; ReadSectors(j,1,temp,fmt); std::fwrite(temp, fmt, 1, fo); } From 95583583caee8c579c27e97e91f528aed9459559 Mon Sep 17 00:00:00 2001 From: scribam Date: Sun, 29 Dec 2024 11:38:20 +0100 Subject: [PATCH 16/76] ci: force ubuntu-22.04 on libretro-x86_64-pc-linux-gnu target --- .github/workflows/c-cpp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 79ec624ff..841689118 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -23,7 +23,7 @@ jobs: - {name: x86_64-pc-linux-gnu, os: ubuntu-20.04, shell: sh, cmakeArgs: -G Ninja -DUSE_DISCORD=ON -DUSE_LIBCDIO=ON, destDir: linux, buildType: RelWithDebInfo} - {name: x86_64-pc-windows-msvc, os: windows-latest, shell: cmd, arch: x64, cmakeArgs: -G Ninja -DUSE_DISCORD=ON, buildType: Release} - {name: x86_64-w64-mingw32, os: windows-latest, shell: 'msys2 {0}', cmakeArgs: -G Ninja -DUSE_DISCORD=ON -DUSE_LIBCDIO=ON, destDir: win, buildType: RelWithDebInfo} - - {name: libretro-x86_64-pc-linux-gnu, os: ubuntu-latest, shell: sh, cmakeArgs: -DLIBRETRO=ON -DUSE_LIBCDIO=ON -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE -G Ninja, buildType: Release} + - {name: libretro-x86_64-pc-linux-gnu, os: ubuntu-22.04, shell: sh, cmakeArgs: -DLIBRETRO=ON -DUSE_LIBCDIO=ON -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE -G Ninja, buildType: Release} - {name: libretro-x86_64-w64-mingw32, os: windows-latest, shell: 'msys2 {0}', cmakeArgs: -DLIBRETRO=ON -DUSE_LIBCDIO=ON -G Ninja, buildType: Release} steps: From c94deaee9d83ea64dc758f6dc146bad0617d1cc2 Mon Sep 17 00:00:00 2001 From: scribam Date: Sun, 29 Dec 2024 16:59:57 +0100 Subject: [PATCH 17/76] build: drop unsupported architectures --- core/build.h | 10 ++-------- core/linux/context.cpp | 4 ---- core/oslib/host_context.h | 2 -- core/ui/gui.cpp | 4 ---- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/core/build.h b/core/build.h index 183669cba..184be8e55 100755 --- a/core/build.h +++ b/core/build.h @@ -15,12 +15,8 @@ //HOST_CPU #define CPU_X86 0x20000001 #define CPU_ARM 0x20000002 -#define CPU_MIPS 0x20000003 +#define CPU_ARM64 0x20000003 #define CPU_X64 0x20000004 -#define CPU_GENERIC 0x20000005 //used for pnacl, emscripten, etc -#define CPU_PPC 0x20000006 -#define CPU_PPC64 0x20000007 -#define CPU_ARM64 0x20000008 //FEAT_SHREC, FEAT_AREC, FEAT_DSPREC #define DYNAREC_NONE 0x40000001 @@ -36,10 +32,8 @@ #define HOST_CPU CPU_ARM #elif defined(__aarch64__) || defined(_M_ARM64) #define HOST_CPU CPU_ARM64 -#elif defined(__mips__) - #define HOST_CPU CPU_MIPS #else - #define HOST_CPU CPU_GENERIC + #error Unsupported architecture #endif #if defined(TARGET_IPHONE) && !defined(__aarch64__) diff --git a/core/linux/context.cpp b/core/linux/context.cpp index c61d504e7..7ab2d8e90 100644 --- a/core/linux/context.cpp +++ b/core/linux/context.cpp @@ -125,10 +125,6 @@ static void context_segfault(host_context_t* hostctx, void* segfault_ctx) #else #error "Unsupported OS" #endif -#elif HOST_CPU == CPU_MIPS - bicopy(hostctx->pc, MCTX(.pc)); -#elif HOST_CPU == CPU_GENERIC - //nothing! #else #error Unsupported HOST_CPU #endif diff --git a/core/oslib/host_context.h b/core/oslib/host_context.h index c19c09800..535207f4a 100644 --- a/core/oslib/host_context.h +++ b/core/oslib/host_context.h @@ -2,9 +2,7 @@ #include "types.h" struct host_context_t { -#if HOST_CPU != CPU_GENERIC uintptr_t pc; -#endif #if HOST_CPU == CPU_X86 u32 eax; diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 2279f4138..533f5fbd8 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -2899,12 +2899,8 @@ static void gui_settings_about() "x86" #elif HOST_CPU == CPU_ARM "ARM" -#elif HOST_CPU == CPU_MIPS - "MIPS" #elif HOST_CPU == CPU_X64 "x86/64" -#elif HOST_CPU == CPU_GENERIC - "Generic" #elif HOST_CPU == CPU_ARM64 "ARM64" #else From 8dc55721d0e9019e09549fe1a0f1eab4fc5e4481 Mon Sep 17 00:00:00 2001 From: Edward Li Date: Sun, 29 Dec 2024 04:03:32 +0800 Subject: [PATCH 18/76] Generate Xcode project which can support both iOS and iOS Simulator --- CMakeLists.txt | 1 + core/build.h | 8 +++++++- core/types.h | 3 +-- .../apple/emulator-ios/emulator/FlycastViewController.mm | 7 +++++++ shell/apple/generate_xcode_project.command | 6 +++++- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 122268800..7f1318b5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,7 @@ if(APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum macOS deployment version") set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "") endif() + set(ZLIB_LIBRARY "-lz" CACHE STRING "Use generic linker flag for Xcode to support multiple SDKs") endif() if(LIBRETRO) diff --git a/core/build.h b/core/build.h index 184be8e55..748b88f1f 100755 --- a/core/build.h +++ b/core/build.h @@ -36,10 +36,16 @@ #error Unsupported architecture #endif -#if defined(TARGET_IPHONE) && !defined(__aarch64__) +#if defined(__APPLE__) +#include "TargetConditionals.h" +#if defined(TARGET_OS_SIMULATOR) // iOS simulator #define TARGET_NO_REC #endif +#if defined(TARGET_MAC) && HOST_CPU == CPU_ARM64 +#define TARGET_ARM_MAC +#endif +#endif #if defined(TARGET_NO_REC) #define FEAT_SHREC DYNAREC_NONE diff --git a/core/types.h b/core/types.h index 9f2ee7d1e..f33a8caa0 100644 --- a/core/types.h +++ b/core/types.h @@ -56,8 +56,7 @@ int darw_printf(const char* Text,...); #endif #ifndef TARGET_IPHONE -#if defined(__APPLE__) && defined(__MACH__) && HOST_CPU == CPU_ARM64 -#define TARGET_ARM_MAC +#if defined(TARGET_ARM_MAC) #include inline static void JITWriteProtect(bool enabled) { if (__builtin_available(macOS 11.0, *)) diff --git a/shell/apple/emulator-ios/emulator/FlycastViewController.mm b/shell/apple/emulator-ios/emulator/FlycastViewController.mm index c70ed41cf..bff71e870 100644 --- a/shell/apple/emulator-ios/emulator/FlycastViewController.mm +++ b/shell/apple/emulator-ios/emulator/FlycastViewController.mm @@ -323,6 +323,13 @@ static void updateAudioSession(Event event, void *) } settings.display.dpi = 160.f * scale; initRenderApi(); +#if defined(TARGET_OS_SIMULATOR) && HOST_CPU == CPU_ARM64 + if (config::RendererType.get() == RenderType::OpenGL) + { + NSLog(@"🚨🚨🚨 OpenGL renderer is not supported in Apple Silicon Mac 🚨🚨🚨\n"); + raise(SIGTRAP); + } +#endif mainui_init(); [self altKitStart]; diff --git a/shell/apple/generate_xcode_project.command b/shell/apple/generate_xcode_project.command index 3a992acaa..f96ea65c6 100755 --- a/shell/apple/generate_xcode_project.command +++ b/shell/apple/generate_xcode_project.command @@ -6,7 +6,11 @@ echo "2) iOS" read -p "Choose your target platform: " x if [ $x -eq 2 ]; then - option="-DCMAKE_SYSTEM_NAME=iOS" + if [ "$(uname -m)" = "arm64" ]; then + option="-DCMAKE_SYSTEM_NAME=iOS" + else + option="-DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64;x86_64" + fi lldbinitfolder="emulator-ios" echo 'Building iOS xcodeproj for debugging' echo 'Remove CODE_SIGNING_ALLOWED=NO in Build Settings if you are using your Apple Developer Certificate for signing' From 1bd894f941aec6130bde481448fcc3a44c1b6105 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 31 Dec 2024 18:22:07 +0100 Subject: [PATCH 19/76] rend: reset scissoring before drawing final modvol quad See d618abc3205fe185b26afe58eb5472b800ae0b42 Issue #1797 --- core/rend/dx11/dx11_renderer.cpp | 1 + core/rend/dx9/d3d_renderer.cpp | 3 ++- core/rend/gles/gldraw.cpp | 1 + core/rend/vulkan/drawer.cpp | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/rend/dx11/dx11_renderer.cpp b/core/rend/dx11/dx11_renderer.cpp index ebb15ede2..4363d04a2 100644 --- a/core/rend/dx11/dx11_renderer.cpp +++ b/core/rend/dx11/dx11_renderer.cpp @@ -886,6 +886,7 @@ void DX11Renderer::drawModVols(int first, int count) setCullMode(0); //enable color writes deviceContext->OMSetBlendState(blendStates.getState(true, 4, 5), nullptr, 0xffffffff); + deviceContext->RSSetScissorRects(1, &scissorRect); //black out any stencil with '1' //only pixels that are Modvol enabled, and in area 1 diff --git a/core/rend/dx9/d3d_renderer.cpp b/core/rend/dx9/d3d_renderer.cpp index f3554a56d..7d2e55028 100644 --- a/core/rend/dx9/d3d_renderer.cpp +++ b/core/rend/dx9/d3d_renderer.cpp @@ -697,6 +697,7 @@ void D3DRenderer::drawModVols(int first, int count) devCache.SetRenderState(D3DRS_COLORWRITEENABLE, 0); int mod_base = -1; + int clip_rect[4] = {}; for (int cmv = 0; cmv < count; cmv++) { @@ -715,7 +716,6 @@ void D3DRenderer::drawModVols(int first, int count) else setMVS_Mode(Xor, param.isp); // XOR'ing (closed volume) - int clip_rect[4] = {}; setTileClip(param.tileclip, clip_rect); //TODO inside clipping @@ -733,6 +733,7 @@ void D3DRenderer::drawModVols(int first, int count) devCache.SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); //enable color writes devCache.SetRenderState(D3DRS_COLORWRITEENABLE, D3DCOLORWRITEENABLE_ALPHA | D3DCOLORWRITEENABLE_RED | D3DCOLORWRITEENABLE_GREEN | D3DCOLORWRITEENABLE_BLUE); + setTileClip(0, clip_rect); //black out any stencil with '1' devCache.SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE); diff --git a/core/rend/gles/gldraw.cpp b/core/rend/gles/gldraw.cpp index d79d8e824..38239508e 100644 --- a/core/rend/gles/gldraw.cpp +++ b/core/rend/gles/gldraw.cpp @@ -570,6 +570,7 @@ void DrawModVols(int first, int count) SetCull(0); //enable color writes glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE); + SetBaseClipping(); //black out any stencil with '1' glcache.Enable(GL_BLEND); diff --git a/core/rend/vulkan/drawer.cpp b/core/rend/vulkan/drawer.cpp index 60b60363d..abfb99338 100644 --- a/core/rend/vulkan/drawer.cpp +++ b/core/rend/vulkan/drawer.cpp @@ -297,6 +297,7 @@ void Drawer::DrawModVols(const vk::CommandBuffer& cmdBuffer, int first, int coun int mod_base = -1; vk::Pipeline pipeline; + vk::Rect2D scissorRect; for (int cmv = 0; cmv < count; cmv++) { @@ -317,7 +318,6 @@ void Drawer::DrawModVols(const vk::CommandBuffer& cmdBuffer, int first, int coun cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); descriptorSets.bindPerPolyDescriptorSets(cmdBuffer, param, first + cmv, curMainBuffer, offsets.naomi2ModVolOffset); - vk::Rect2D scissorRect; SetTileClip(cmdBuffer, param.tileclip, scissorRect); // TODO inside clipping @@ -333,6 +333,7 @@ void Drawer::DrawModVols(const vk::CommandBuffer& cmdBuffer, int first, int coun } } cmdBuffer.bindVertexBuffers(0, curMainBuffer, {0}); + SetTileClip(cmdBuffer, 0, scissorRect); const std::array pushConstants = { 1 - FPU_SHAD_SCALE.scale_factor / 256.f, 0, 0, 0, 0, 0 }; cmdBuffer.pushConstants(pipelineManager->GetPipelineLayout(), vk::ShaderStageFlagBits::eFragment, 0, pushConstants); From 3cf480803374cd2067de557b400072e079ef61d6 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sun, 5 Jan 2025 10:21:54 +0100 Subject: [PATCH 20/76] fix perf regression on macOS and iOS. Add Silent Scope (JP) identifier. Regression introduced in f3a3364 CopyToVRAM wasn't enabled for Silent Scope (JP). Issue #1796 --- core/build.h | 2 +- core/emulator.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/build.h b/core/build.h index 748b88f1f..45e067472 100755 --- a/core/build.h +++ b/core/build.h @@ -38,7 +38,7 @@ #if defined(__APPLE__) #include "TargetConditionals.h" -#if defined(TARGET_OS_SIMULATOR) +#if TARGET_OS_SIMULATOR // iOS simulator #define TARGET_NO_REC #endif diff --git a/core/emulator.cpp b/core/emulator.cpp index 0e716e770..2364b9da5 100644 --- a/core/emulator.cpp +++ b/core/emulator.cpp @@ -148,6 +148,8 @@ static void loadSpecialSettings() || prod_id == "T9507N" // Silent Scope (EU) || prod_id == "T9505D" + // Silent Scope (JP) + || prod_id == "T9513M" // Pro Pinball - Trilogy (EU) || prod_id == "T30701D 50") { From 160d3e7ef35b87d9811f50421fe1e7cfe1718468 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 13 Oct 2024 22:14:12 -0700 Subject: [PATCH 21/76] cmake: C++20 enablement Enables the C++ 20 standard --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f1318b5e..eeda931cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ else() project(flycast) endif() -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) if(NINTENDO_SWITCH) set(CMAKE_CXX_EXTENSIONS ON) From b2c1d134e1eb1b4b9502be88816ebb02cd997d8a Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 13 Oct 2024 22:47:33 -0700 Subject: [PATCH 22/76] deps: Update luabridge to 2.8 From 2.6 to 2.8. Includes some C++20 fixes. --- core/deps/luabridge | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deps/luabridge b/core/deps/luabridge index fab7b33b8..5d21e3563 160000 --- a/core/deps/luabridge +++ b/core/deps/luabridge @@ -1 +1 @@ -Subproject commit fab7b33b896a42dcc865ba5ecdbacd9f409137f8 +Subproject commit 5d21e35633a1f87ed08af115b07d3386096f792b From 9344856c7eab92a273fd7462b457d0feeb156524 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 13 Oct 2024 23:12:10 -0700 Subject: [PATCH 23/76] ggpo: Fix malformed template constructor This form is not needed for a template-class's constructor. Fix for C++20 builds --- core/deps/ggpo/lib/ggpo/ring_buffer.h | 2 +- core/deps/ggpo/lib/ggpo/static_buffer.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/deps/ggpo/lib/ggpo/ring_buffer.h b/core/deps/ggpo/lib/ggpo/ring_buffer.h index 1e51f7bb1..c27f95da5 100644 --- a/core/deps/ggpo/lib/ggpo/ring_buffer.h +++ b/core/deps/ggpo/lib/ggpo/ring_buffer.h @@ -13,7 +13,7 @@ template class RingBuffer { public: - RingBuffer() : + RingBuffer() : _head(0), _tail(0), _size(0) { diff --git a/core/deps/ggpo/lib/ggpo/static_buffer.h b/core/deps/ggpo/lib/ggpo/static_buffer.h index 8ff52d76a..592e18adb 100644 --- a/core/deps/ggpo/lib/ggpo/static_buffer.h +++ b/core/deps/ggpo/lib/ggpo/static_buffer.h @@ -13,7 +13,7 @@ template class StaticBuffer { public: - StaticBuffer() : + StaticBuffer() : _size(0) { } From 22bff1b01766ad9af506766a08f29da53c034621 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Mon, 14 Oct 2024 00:11:16 -0700 Subject: [PATCH 24/76] cmake: Use `set_target_properties` to set C++ standard This stops dependencies from inheriting a global C++20 standard setting and instead designates the flycast target alone to be a C++20 project. --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index eeda931cb..ec5972fc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ else() project(flycast) endif() -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) if(NINTENDO_SWITCH) set(CMAKE_CXX_EXTENSIONS ON) @@ -227,6 +227,10 @@ else() add_executable(${PROJECT_NAME} core/emulator.cpp) endif() +set_target_properties(${PROJECT_NAME} PROPERTIES + CMAKE_CXX_STANDARD 20 + CMAKE_CXX_STANDARD_REQUIRED ON) + if(WINDOWS_STORE) set(USE_OPENGL OFF) set(USE_VULKAN OFF) From a5a338d40849b2c13425ca63b294d74273b14c34 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 9 Jan 2025 18:48:29 +0100 Subject: [PATCH 25/76] experimental DCNet cloud service --- CMakeLists.txt | 6 +- core/cfg/option.cpp | 1 + core/cfg/option.h | 1 + core/hw/bba/bba.cpp | 12 +- core/hw/bba/bba.h | 1 + core/hw/modem/modem.cpp | 12 +- core/network/alienfnt_modem.cpp | 24 +-- core/network/dcnet.cpp | 256 ++++++++++++++++++++++++++++++++ core/network/dcnet.h | 38 +++++ core/network/netservice.cpp | 68 +++++++++ core/network/netservice.h | 48 ++++++ core/network/picoppp.cpp | 42 +++++- core/network/picoppp.h | 23 ++- core/ui/gui.cpp | 1 + 14 files changed, 493 insertions(+), 40 deletions(-) create mode 100644 core/network/dcnet.cpp create mode 100644 core/network/dcnet.h create mode 100644 core/network/netservice.cpp create mode 100644 core/network/netservice.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ec5972fc8..aedfbb961 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1194,7 +1194,11 @@ target_sources(${PROJECT_NAME} PRIVATE core/network/output.cpp core/network/output.h core/network/picoppp.cpp - core/network/picoppp.h) + core/network/picoppp.h + core/network/netservice.cpp + core/network/netservice.h + core/network/dcnet.cpp + core/network/dcnet.h) if(ANDROID) target_sources(${PROJECT_NAME} PRIVATE diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index 658e18c90..63b54716a 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -162,6 +162,7 @@ Option GGPOChatTimeout("GGPOChatTimeout", 10, "network"); Option NetworkOutput("NetworkOutput", false, "network"); Option MultiboardSlaves("MultiboardSlaves", 1, "network"); Option BattleCableEnable("BattleCable", false, "network"); +Option UseDCNet("DCNet", false, "network"); #ifdef USE_OMX Option OmxAudioLatency("audio_latency", 100, "omx"); diff --git a/core/cfg/option.h b/core/cfg/option.h index e0d94a63d..843e071d9 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -522,6 +522,7 @@ extern Option GGPOChatTimeout; extern Option NetworkOutput; extern Option MultiboardSlaves; extern Option BattleCableEnable; +extern Option UseDCNet; #ifdef USE_OMX extern Option OmxAudioLatency; diff --git a/core/hw/bba/bba.cpp b/core/hw/bba/bba.cpp index be071d252..6deb09dbf 100644 --- a/core/hw/bba/bba.cpp +++ b/core/hw/bba/bba.cpp @@ -19,7 +19,7 @@ #include "bba.h" #include "rtl8139c.h" #include "hw/holly/holly_intc.h" -#include "network/picoppp.h" +#include "network/netservice.h" #include "serialize.h" static RTL8139State *rtl8139device; @@ -79,7 +79,7 @@ void bba_Term() { if (rtl8139device != nullptr) { - stop_pico(); + net::modbba::stop(); rtl8139_destroy(rtl8139device); rtl8139device = nullptr; } @@ -181,7 +181,7 @@ void bba_WriteMem(u32 addr, u32 data, u32 sz) { DEBUG_LOG(NETWORK, "GAPS reset"); rtl8139_reset(rtl8139device); - start_pico(); + net::modbba::start(); } break; @@ -208,12 +208,12 @@ void bba_WriteMem(u32 addr, u32 data, u32 sz) ssize_t qemu_send_packet(RTL8139State *s, const uint8_t *buf, int size) { - pico_receive_eth_frame(buf, size); + net::modbba::receiveEthFrame(buf, size); return size; } -int pico_send_eth_frame(const u8 *data, u32 len) +int bba_recv_frame(const u8 *data, u32 len) { if (!rtl8139_can_receive(rtl8139device)) return 0; @@ -249,7 +249,7 @@ void bba_Deserialize(Deserializer& deser) deser >> interruptPending; // returns true if the receiver is enabled and the network stack must be started if (rtl8139_deserialize(rtl8139device, deser)) - start_pico(); + net::modbba::start(); } #define POLYNOMIAL_BE 0x04c11db6 diff --git a/core/hw/bba/bba.h b/core/hw/bba/bba.h index e20f8ad21..021e5437d 100644 --- a/core/hw/bba/bba.h +++ b/core/hw/bba/bba.h @@ -26,3 +26,4 @@ u32 bba_ReadMem(u32 addr, u32 sz); void bba_WriteMem(u32 addr, u32 data, u32 sz); void bba_Serialize(Serializer& ser); void bba_Deserialize(Deserializer& deser); +int bba_recv_frame(const u8 *data, u32 len); diff --git a/core/hw/modem/modem.cpp b/core/hw/modem/modem.cpp index f3d6e904d..b52e7310a 100644 --- a/core/hw/modem/modem.cpp +++ b/core/hw/modem/modem.cpp @@ -25,7 +25,7 @@ #include "modem_regs.h" #include "hw/holly/holly_intc.h" #include "hw/sh4/sh4_sched.h" -#include "network/picoppp.h" +#include "network/netservice.h" #include "serialize.h" #include "cfg/option.h" #include "stdclass.h" @@ -263,7 +263,7 @@ static int modem_sched_func(int tag, int cycles, int jitter, void *arg) dspram[0x208] = 0xff; // 2.4 - 19.2 kpbs supported dspram[0x209] = 0xbf; // 21.6 - 33.6 kpbs supported, asymmetric supported - start_pico(); + net::modbba::start(); connect_state = CONNECTED; callback_cycles = SH4_MAIN_CLOCK / 1000000 * 238; // 238 us data_sent = false; @@ -291,7 +291,7 @@ static int modem_sched_func(int tag, int cycles, int jitter, void *arg) // Let WinCE send data first to avoid choking it if (!modem_regs.reg1e.RDBF && data_sent) { - int c = read_pico(); + int c = net::modbba::readModem(); if (c >= 0) { //LOG("pppd received %02x", c); @@ -328,7 +328,7 @@ void ModemInit() void ModemReset() { - stop_pico(); + net::modbba::stop(); } void ModemTerm() @@ -441,7 +441,7 @@ static void modem_reset(u32 v) { if (state == MS_RESET) { - stop_pico(); + net::modbba::stop();; memset(&modem_regs, 0, sizeof(modem_regs)); state = MS_RESETING; ControllerTestStart(); @@ -528,7 +528,7 @@ static void ModemNormalWrite(u32 reg, u32 data) if (sent_fp) fputc(data, sent_fp); #endif - write_pico(data); + net::modbba::writeModem(data); modem_regs.reg1e.TDBE = 0; } break; diff --git a/core/network/alienfnt_modem.cpp b/core/network/alienfnt_modem.cpp index 05cb9157f..e9b1ede38 100644 --- a/core/network/alienfnt_modem.cpp +++ b/core/network/alienfnt_modem.cpp @@ -19,7 +19,7 @@ #include "alienfnt_modem.h" #include "hw/sh4/sh4_sched.h" #include "hw/sh4/modules/modules.h" -#include "picoppp.h" +#include "netservice.h" #include #include @@ -34,7 +34,7 @@ struct ModemEmu : public SerialPort::Pipe ~ModemEmu() override { sh4_sched_unregister(schedId); - stop_pico(); + net::modbba::stop(); SCIFSerialPort::Instance().setPipe(nullptr); } @@ -47,7 +47,7 @@ struct ModemEmu : public SerialPort::Pipe return c; } else if (dataMode) - return read_pico(); + return net::modbba::readModem(); else return 0; } @@ -57,7 +57,7 @@ struct ModemEmu : public SerialPort::Pipe if (!toSend.empty()) return toSend.size(); else if (dataMode) - return pico_available(); + return net::modbba::modemAvailable(); else return 0; } @@ -76,10 +76,10 @@ struct ModemEmu : public SerialPort::Pipe } else { - write_pico('+'); - write_pico('+'); - write_pico('+'); - write_pico(data); + net::modbba::writeModem('+'); + net::modbba::writeModem('+'); + net::modbba::writeModem('+'); + net::modbba::writeModem(data); } pluses = 0; plusTime = 0; @@ -92,10 +92,10 @@ struct ModemEmu : public SerialPort::Pipe else { while (pluses > 0) { - write_pico('+'); + net::modbba::writeModem('+'); pluses--; } - write_pico(data); + net::modbba::writeModem(data); } } else if (data == '\r' || data == '\n') @@ -113,13 +113,13 @@ private: recvBuf.clear(); if (line.substr(0, 4) == "ATDT") { send("CONNECT 14400"); - start_pico(); + net::modbba::start(); dataMode = true; sh4_sched_request(schedId, SH4_MAIN_CLOCK / 60); } if (line.substr(0, 3) == "ATH") { - stop_pico(); + net::modbba::stop(); send("OK"); } else if (line.substr(0, 2) == "AT") diff --git a/core/network/dcnet.cpp b/core/network/dcnet.cpp new file mode 100644 index 000000000..a59f3004a --- /dev/null +++ b/core/network/dcnet.cpp @@ -0,0 +1,256 @@ +/* + 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 . + */ +#include "types.h" +#include "netservice.h" +#include "util/tsqueue.h" +#include "oslib/oslib.h" +#include +#include +#include +#include +#ifndef __ANDROID__ +//#define DEBUG_PPP 1 +#endif +#ifdef DEBUG_PPP +#include +#endif + +namespace net::modbba +{ + +static TsQueue in_buffer; +static TsQueue out_buffer; + +class DCNetService : public Service +{ +public: + bool start() override; + void stop() override; + + void writeModem(u8 b) override; + int readModem() override; + int modemAvailable() override; + + void receiveEthFrame(const u8 *frame, u32 size) override; +}; + +class Socket +{ +public: + Socket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint) + : socket(io_context), timer(io_context) + { + socket.connect(endpoint); + os_notify("Connected to DCNet cloud", 5000); + receive(); + sendIfAny({}); + } + + ~Socket() { + if (dumpfp != nullptr) + fclose(dumpfp); + } + +private: + void receive() { + socket.async_read_some(asio::buffer(recvBuffer), + std::bind(&Socket::onRecv, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); + } + void onRecv(asio::error_code ec, size_t len) + { + if (ec) { + ERROR_LOG(NETWORK, "onRecv error: %s", ec.message().c_str()); + return; + } + pppdump(recvBuffer.data(), len, false); + for (size_t i = 0; i < len; i++) + in_buffer.push(recvBuffer[i]); + receive(); + } + + void sendIfAny(const std::error_code& ec) + { + if (ec) { + ERROR_LOG(NETWORK, "sendIfAny timer error: %s", ec.message().c_str()); + return; + } + if (!sending) + { + for (; !out_buffer.empty() && sendBufSize < sendBuffer.size(); sendBufSize++) + { + sendBuffer[sendBufSize] = out_buffer.pop(); + if (sendBufSize != 0 && sendBuffer[sendBufSize] == 0x7e) { + sendBufSize++; + break; + } + } + if ((sendBufSize > 1 && sendBuffer[sendBufSize - 1] == 0x7e) + || sendBufSize == sendBuffer.size()) + { + pppdump(sendBuffer.data(), sendBufSize, true); + asio::async_write(socket, asio::buffer(sendBuffer, sendBufSize), + std::bind(&Socket::onSent, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); + sending = true; + } + } + + timer.expires_at(timer.expiry() + asio::chrono::milliseconds(5)); + timer.async_wait(std::bind(&Socket::sendIfAny, this, asio::placeholders::error)); + } + + void onSent(asio::error_code ec, size_t len) + { + sending = false; + sendBufSize = 0; + if (ec) { + ERROR_LOG(NETWORK, "onRecv error: %s", ec.message().c_str()); + return; + } + } + + void pppdump(uint8_t *buf, int len, bool egress) + { +#ifdef DEBUG_PPP + if (!len) + return; + const auto& timems = []() -> time_t { + timeval tv; + gettimeofday(&tv, nullptr); + return tv.tv_sec * 1000 + tv.tv_usec / 1000; + }; + if (dumpfp == nullptr) + { + dumpfp = fopen("ppp.dump", "a"); + if (dumpfp == nullptr) + return; + time_t now; + time(&now); + u32 reset_time = ntohl((u32)now); + fputc(7, dumpfp); // Reset time + fwrite(&reset_time, sizeof(reset_time), 1, dumpfp); + dump_last_time_ms = timems(); + } + + u32 delta = (timems() - dump_last_time_ms) / 100; + if (delta < 256) { + fputc(6, dumpfp); // Time step (short) + fwrite(&delta, 1, 1, dumpfp); + } + else + { + delta = ntohl(delta); + fputc(5, dumpfp); // Time step (long) + fwrite(&delta, sizeof(delta), 1, dumpfp); + } + dump_last_time_ms = timems(); + + fputc(egress ? 1 : 2, dumpfp); // Sent/received data + + uint16_t slen = htons(len); + fwrite(&slen, 2, 1, dumpfp); + + fwrite(buf, 1, len, dumpfp); +#endif + } + + asio::ip::tcp::socket socket; + asio::steady_timer timer; + std::array recvBuffer; + std::array sendBuffer; + u32 sendBufSize = 0; + bool sending = false; + + FILE *dumpfp = nullptr; + time_t dump_last_time_ms; +}; + +class DCNetThread +{ +public: + void start() + { + verify(!thread.joinable()); + io_context = std::make_unique(); + thread = std::thread(&DCNetThread::run, this); + } + + void stop() + { + if (!thread.joinable()) + return; + io_context->stop(); + thread.join(); + io_context.reset(); + } + +private: + void run(); + + std::thread thread; + std::unique_ptr io_context; +}; +static DCNetThread thread; + +bool DCNetService::start() { + thread.start(); + return true; +} + +void DCNetService::stop() { + thread.stop(); +} + +void DCNetService::writeModem(u8 b) { + out_buffer.push(b); +} + +int DCNetService::readModem() +{ + if (in_buffer.empty()) + return -1; + else + return in_buffer.pop(); +} + +int DCNetService::modemAvailable() { + return in_buffer.size(); +} + +void DCNetService::receiveEthFrame(unsigned char const*, unsigned int) { + // TODO +} + + +void DCNetThread::run() +{ + try { + asio::ip::tcp::resolver resolver(*io_context); + auto it = resolver.resolve("dcnet.flyca.st", "7654"); + if (it.empty()) + throw std::runtime_error("Can't find dcnet host"); + asio::ip::tcp::endpoint endpoint = *it.begin(); + + Socket socket(*io_context, endpoint); + io_context->run(); + } catch (const std::runtime_error& e) { + ERROR_LOG(NETWORK, "DCNetThread::run error: %s", e.what()); + } +} + +} diff --git a/core/network/dcnet.h b/core/network/dcnet.h new file mode 100644 index 000000000..995ee618e --- /dev/null +++ b/core/network/dcnet.h @@ -0,0 +1,38 @@ +/* + 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 "netservice.h" + +namespace net::modbba +{ + +class DCNetService : public Service +{ +public: + bool start() override; + void stop() override; + + void writeModem(u8 b) override; + int readModem() override; + int modemAvailable() override; + + void receiveEthFrame(const u8 *frame, u32 size) override; +}; + +} diff --git a/core/network/netservice.cpp b/core/network/netservice.cpp new file mode 100644 index 000000000..41998c5db --- /dev/null +++ b/core/network/netservice.cpp @@ -0,0 +1,68 @@ +/* + 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 . + */ +#include "netservice.h" +#include "picoppp.h" +#include "dcnet.h" +#include "emulator.h" +#include "cfg/option.h" + +namespace net::modbba +{ + +static Service *service; +static bool usingDCNet; + +bool start() +{ + if (service == nullptr || usingDCNet != config::UseDCNet) + { + delete service; + if (config::UseDCNet) + service = new DCNetService(); + else + service = new PicoTcpService(); + usingDCNet = config::UseDCNet; + } + return service->start(); +} + +void stop() { + if (service != nullptr) + service->stop(); +} + +void writeModem(u8 b) { + verify(service != nullptr); + service->writeModem(b); +} +int readModem() { + verify(service != nullptr); + return service->readModem(); +} +int modemAvailable() { + verify(service != nullptr); + return service->modemAvailable(); +} + +void receiveEthFrame(const u8 *frame, u32 size) { + verify(service != nullptr); + service->receiveEthFrame(frame, size); +} + +} diff --git a/core/network/netservice.h b/core/network/netservice.h new file mode 100644 index 000000000..a9adb9158 --- /dev/null +++ b/core/network/netservice.h @@ -0,0 +1,48 @@ +/* + 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 "types.h" + +namespace net::modbba +{ + +bool start(); +void stop(); + +void writeModem(u8 b); +int readModem(); +int modemAvailable(); + +void receiveEthFrame(const u8 *frame, u32 size); + +class Service +{ +public: + virtual ~Service() = default; + virtual bool start() = 0; + virtual void stop() = 0; + + virtual void writeModem(u8 b) = 0; + virtual int readModem() = 0; + virtual int modemAvailable() = 0; + + virtual void receiveEthFrame(const u8 *frame, u32 size) = 0; +}; + +} diff --git a/core/network/picoppp.cpp b/core/network/picoppp.cpp index 8bab7152c..72bd48dea 100644 --- a/core/network/picoppp.cpp +++ b/core/network/picoppp.cpp @@ -51,6 +51,7 @@ extern "C" { #include "oslib/oslib.h" #include "util/tsqueue.h" #include "util/shared_this.h" +#include "hw/bba/bba.h" #include #include @@ -201,11 +202,11 @@ static int modem_write(pico_device *dev, const void *data, int len) return len; } -void write_pico(u8 b) { +static void write_pico(u8 b) { out_buffer.push(b); } -int read_pico() +static int read_pico() { if (in_buffer.empty()) return -1; @@ -213,7 +214,7 @@ int read_pico() return in_buffer.pop(); } -int pico_available() { +static int pico_available() { return in_buffer.size(); } @@ -1138,7 +1139,7 @@ static void closeDumpFile() pcapngDump = nullptr; } } -void pico_receive_eth_frame(const u8 *frame, u32 size) +static void pico_receive_eth_frame(const u8 *frame, u32 size) { dumpFrame(frame, size); if (pico_dev != nullptr) @@ -1147,7 +1148,7 @@ void pico_receive_eth_frame(const u8 *frame, u32 size) 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); + return bba_recv_frame((const u8 *)data, len); } static void picoTick(const std::error_code& ec, asio::steady_timer *timer) @@ -1391,7 +1392,7 @@ void PicoThread::run() static PicoThread pico_thread; -bool start_pico() +static bool start_pico() { emu.setNetworkState(true); if (pico_thread_running) @@ -1402,7 +1403,7 @@ bool start_pico() return true; } -void stop_pico() +static void stop_pico() { emu.setNetworkState(false); pico_thread_running = false; @@ -1429,3 +1430,30 @@ void pico_mutex_deinit(void *mux) { } } + +namespace net::modbba +{ + +bool PicoTcpService::start() { + return start_pico(); +} +void PicoTcpService::stop() { + stop_pico(); +} + +void PicoTcpService::writeModem(u8 b) { + write_pico(b); +} +int PicoTcpService::readModem() { + return read_pico(); +} +int PicoTcpService::modemAvailable() { + return pico_available(); +} + +void PicoTcpService::receiveEthFrame(const u8 *frame, u32 size) { + pico_receive_eth_frame(frame, size); +} + +} + diff --git a/core/network/picoppp.h b/core/network/picoppp.h index e09a04178..24430922c 100644 --- a/core/network/picoppp.h +++ b/core/network/picoppp.h @@ -19,13 +19,20 @@ along with reicast. If not, see . */ #pragma once +#include "netservice.h" -bool start_pico(); -void stop_pico(); -void write_pico(u8 b); -int read_pico(); -int pico_available(); +namespace net::modbba +{ -void pico_receive_eth_frame(const u8 *frame, u32 size); -// implemented in bba -int pico_send_eth_frame(const u8 *data, u32 len); +class PicoTcpService : public Service +{ +public: + bool start() override; + void stop() override; + void writeModem(u8 b) override; + int readModem() override; + int modemAvailable() override; + void receiveEthFrame(const u8 *frame, u32 size) override; +}; + +} diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 533f5fbd8..31a0b724e 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -2795,6 +2795,7 @@ static void gui_settings_network() OptionCheckbox("Broadband Adapter Emulation", config::EmulateBBA, "Emulate the Ethernet Broadband Adapter (BBA) instead of the Modem"); } + OptionCheckbox("Use DCNet (Experimental)", config::UseDCNet, "Connect to the experimental DCNet cloud service."); } #ifdef NAOMI_MULTIBOARD ImGui::Spacing(); From bb4ba3ed89521c9859e3948512e7d648e50004e0 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 9 Jan 2025 19:01:50 +0100 Subject: [PATCH 26/76] dcnet: libretro support --- shell/libretro/libretro_core_options.h | 14 ++++++++++++++ shell/libretro/option.cpp | 1 + 2 files changed, 15 insertions(+) diff --git a/shell/libretro/libretro_core_options.h b/shell/libretro/libretro_core_options.h index d60c61dae..6891c44cc 100644 --- a/shell/libretro/libretro_core_options.h +++ b/shell/libretro/libretro_core_options.h @@ -245,6 +245,20 @@ struct retro_core_option_v2_definition option_defs_us[] = { }, "enabled", }, + { + CORE_OPTION_NAME "_dcnet", + "Use DCNet (Experimental)", + NULL, + "Connect to the experimental DCNet cloud service.", + NULL, + "system", + { + { "disabled", NULL }, + { "enabled", NULL }, + { NULL, NULL }, + }, + "disabled", + }, { CORE_OPTION_NAME "_internal_resolution", diff --git a/shell/libretro/option.cpp b/shell/libretro/option.cpp index 0e2569abd..e8061ff32 100644 --- a/shell/libretro/option.cpp +++ b/shell/libretro/option.cpp @@ -122,6 +122,7 @@ Option GGPOAnalogAxes("", 0); Option NetworkOutput(CORE_OPTION_NAME "_network_output", false); Option MultiboardSlaves("", 0); Option BattleCableEnable("", false); +Option UseDCNet(CORE_OPTION_NAME "_dcnet", false); // Maple From b98e833c570eb1487657ec0dc307b65266a40fb2 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 17 Jan 2025 17:46:49 +0100 Subject: [PATCH 27/76] uwp: define _WIN32_WINNT to avoid compile issues with asio --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index aedfbb961..2e1181f25 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -266,6 +266,7 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE $<$:NOMINMAX> $<$:TEST_AUTOMATION> $<$:NOCRYPT> + $<$:_WIN32_WINNT=0x0A00> $<$,$>:_USE_MATH_DEFINES>) if(UNIX AND NOT ANDROID AND NOT APPLE) From 5efdf58e85e1dced44676a8f6d714190d562e70f Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 21 Jan 2025 18:12:10 +0100 Subject: [PATCH 28/76] custom texture: skip updating a texture if its hash hasn't changed Some textures are frequently updated because of their location in vram, close to a hotspot. This causes the custom texture to never be loaded. This change will skip the unneeded texture update, allowing the custom tex to replace it when available. Issue #1812 --- core/rend/CustomTexture.cpp | 1 - core/rend/TexCache.cpp | 11 +++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/rend/CustomTexture.cpp b/core/rend/CustomTexture.cpp index 8f8beda18..2df7fbf06 100644 --- a/core/rend/CustomTexture.cpp +++ b/core/rend/CustomTexture.cpp @@ -38,7 +38,6 @@ static WorkerThread loader_thread {"CustomTexLoader"}; void CustomTexture::loadTexture(BaseTextureCacheData *texture) { - texture->ComputeHash(); if (texture->custom_image_data != nullptr) { free(texture->custom_image_data); texture->custom_image_data = nullptr; diff --git a/core/rend/TexCache.cpp b/core/rend/TexCache.cpp index fe73022ed..90cd47100 100644 --- a/core/rend/TexCache.cpp +++ b/core/rend/TexCache.cpp @@ -496,7 +496,18 @@ bool BaseTextureCacheData::Update() } } if (config::CustomTextures) + { + u32 oldHash = texture_hash; + ComputeHash(); + if (Updates > 1 && oldHash == texture_hash) + { + // Texture hasn't changed so skip the update. + protectVRam(); + size = originalSize; + return true; + } custom_texture.LoadCustomTextureAsync(this); + } void *temp_tex_buffer = NULL; u32 upscaled_w = width; From 5b513cac5b467640f76e9b806e1a7834eb55ed5f Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 21 Jan 2025 18:18:05 +0100 Subject: [PATCH 29/76] gl: use textureProj for palette texture sampling when possible/available Fixes UV precision issue when doing the Z-divide manually. Issue #1804 --- core/rend/gles/gles.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/rend/gles/gles.cpp b/core/rend/gles/gles.cpp index d2d7b7ffd..59e3fc8ea 100644 --- a/core/rend/gles/gles.cpp +++ b/core/rend/gles/gles.cpp @@ -202,10 +202,12 @@ lowp vec4 getPaletteEntry(highp float colorIndex) lowp vec4 palettePixel(highp vec3 coords) { -#if TARGET_GL != GLES2 && TARGET_GL != GL2 && DIV_POS_Z != 1 - coords.xy /= coords.z; -#endif +#if TARGET_GL == GLES2 || TARGET_GL == GL2 || DIV_POS_Z == 1 return getPaletteEntry(texture(tex, coords.xy).FOG_CHANNEL); +#else + return getPaletteEntry(textureProj(tex, coords).FOG_CHANNEL); +#endif + } #elif pp_Palette == 2 // Bi-linear filtering From 536be34853a7febc0de046a6ad93285a8669d47b Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 21 Jan 2025 18:19:39 +0100 Subject: [PATCH 30/76] dcnet: update emu network state when connected --- core/network/dcnet.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/network/dcnet.cpp b/core/network/dcnet.cpp index a59f3004a..b1c436e9c 100644 --- a/core/network/dcnet.cpp +++ b/core/network/dcnet.cpp @@ -20,6 +20,7 @@ #include "netservice.h" #include "util/tsqueue.h" #include "oslib/oslib.h" +#include "emulator.h" #include #include #include @@ -207,13 +208,16 @@ private: }; static DCNetThread thread; -bool DCNetService::start() { +bool DCNetService::start() +{ + emu.setNetworkState(true); thread.start(); return true; } void DCNetService::stop() { thread.stop(); + emu.setNetworkState(false); } void DCNetService::writeModem(u8 b) { From 18656f5986250e147b8f47bcb3d36965bb98ddcf Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 21 Jan 2025 18:21:47 +0100 Subject: [PATCH 31/76] picoppp: terminate MiniUPnP in a background thread --- core/network/picoppp.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/network/picoppp.cpp b/core/network/picoppp.cpp index 72bd48dea..3ec8cad7b 100644 --- a/core/network/picoppp.cpp +++ b/core/network/picoppp.cpp @@ -1384,8 +1384,12 @@ void PicoThread::run() pico_dev = nullptr; } pico_stack_deinit(); - if (upnp) { - upnp->Term(); + if (upnp) + { + std::thread pnpTerm([upnp = this->upnp]() { + upnp->Term(); + }); + pnpTerm.detach(); upnp.reset(); } } From 5cc15429cd0bbbcc170320c5def63fb65d580fe0 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 22 Jan 2025 18:26:29 +0100 Subject: [PATCH 32/76] Force Native Depth Interpolation on UWP/Xbox. UWP build fix --- core/cfg/option.cpp | 4 ++++ core/network/dcnet.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index 63b54716a..bc87f33fb 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -105,7 +105,11 @@ Option TextureFiltering("rend.TextureFiltering", 0); // Default Option ThreadedRendering("rend.ThreadedRendering", true); Option DupeFrames("rend.DupeFrames", false); Option PerPixelLayers("rend.PerPixelLayers", 32); +#ifdef TARGET_UWP +Option NativeDepthInterpolation("rend.NativeDepthInterpolation", true); +#else Option NativeDepthInterpolation("rend.NativeDepthInterpolation", false); +#endif Option EmulateFramebuffer("rend.EmulateFramebuffer", false); Option FixUpscaleBleedingEdge("rend.FixUpscaleBleedingEdge", true); Option CustomGpuDriver("rend.CustomGpuDriver", false); diff --git a/core/network/dcnet.cpp b/core/network/dcnet.cpp index b1c436e9c..37bfb598f 100644 --- a/core/network/dcnet.cpp +++ b/core/network/dcnet.cpp @@ -16,12 +16,12 @@ You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ +#include #include "types.h" #include "netservice.h" #include "util/tsqueue.h" #include "oslib/oslib.h" #include "emulator.h" -#include #include #include #include From a0e1eb00050bfe22b8eec22c386ed0855ef9b38b Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 5 Jan 2025 18:36:55 -0800 Subject: [PATCH 33/76] vulkan: Allow native `RGB8` screenshot framebuffers when available Some notable devices support native RGB8 color-attachments: https://vulkan.gpuinfo.org/listdevicescoverage.php?optimaltilingformat=R8G8B8_UNORM&featureflagbit=COLOR_ATTACHMENT This removes the need to do a manual RGBA->RGB format conversion on the CPU in favor of a direct memcpy from the downloaded texture, when available. --- core/rend/vulkan/vulkan_context.cpp | 41 ++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/core/rend/vulkan/vulkan_context.cpp b/core/rend/vulkan/vulkan_context.cpp index 557d6bb11..11795a788 100644 --- a/core/rend/vulkan/vulkan_context.cpp +++ b/core/rend/vulkan/vulkan_context.cpp @@ -1340,9 +1340,22 @@ bool VulkanContext::GetLastFrame(std::vector& data, int& width, int& height) else width = w; } + + vk::Format imageFormat = vk::Format::eR8G8B8A8Unorm; + const vk::ImageUsageFlags imageUsage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc; + + // Test if RGB8 is natively supported to avoid having to do a format conversion + bool nativeRgb8 = false; + vk::ImageFormatProperties rgb8Properties{}; + if (physicalDevice.getImageFormatProperties(vk::Format::eR8G8B8Unorm, vk::ImageType::e2D, vk::ImageTiling::eOptimal, imageUsage, {}, &rgb8Properties) == vk::Result::eSuccess) + { + nativeRgb8 = true; + imageFormat = vk::Format::eR8G8B8Unorm; + } + // color attachment FramebufferAttachment attachment(physicalDevice, *device); - attachment.Init(width, height, vk::Format::eR8G8B8A8Unorm, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc, "screenshot"); + attachment.Init(width, height, imageFormat, imageUsage, "screenshot"); // command buffer vk::UniqueCommandBuffer commandBuffer = std::move(device->allocateCommandBuffersUnique( vk::CommandBufferAllocateInfo(*commandPools.back(), vk::CommandBufferLevel::ePrimary, 1)).front()); @@ -1352,7 +1365,7 @@ bool VulkanContext::GetLastFrame(std::vector& data, int& width, int& height) CommandBufferDebugScope _(commandBuffer.get(), "GetLastFrame", scopeColor); // render pass - vk::AttachmentDescription attachmentDescription = vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), vk::Format::eR8G8B8A8Unorm, vk::SampleCountFlagBits::e1, + vk::AttachmentDescription attachmentDescription = vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), imageFormat, vk::SampleCountFlagBits::e1, vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eStore, vk::AttachmentLoadOp::eDontCare, vk::AttachmentStoreOp::eDontCare, vk::ImageLayout::eUndefined, vk::ImageLayout::eTransferSrcOptimal); vk::AttachmentReference colorReference(0, vk::ImageLayout::eColorAttachmentOptimal); @@ -1417,15 +1430,25 @@ bool VulkanContext::GetLastFrame(std::vector& data, int& width, int& height) const u8 *img = (const u8 *)attachment.GetBufferData()->MapMemory(); data.clear(); - data.reserve(width * height * 3); - for (int y = 0; y < height; y++) + if (nativeRgb8) { - for (int x = 0; x < width; x++) + // Format is already RGB, can be directly copied + data.resize(width * height * 3); + std::memcpy(data.data(), img, width* height * 3); + } + else + { + data.reserve(width * height * 3); + // RGBA -> RGB + for (int y = 0; y < height; y++) { - data.push_back(*img++); - data.push_back(*img++); - data.push_back(*img++); - img++; + for (int x = 0; x < width; x++) + { + data.push_back(*img++); + data.push_back(*img++); + data.push_back(*img++); + img++; + } } } attachment.GetBufferData()->UnmapMemory(); From da9f030a0e8921f4a9281454e7ab4a5bf5594ead Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sun, 13 Oct 2024 16:24:40 -0700 Subject: [PATCH 34/76] vk: Refactor swapchain format selection Use a series of stable-partitions to sort the list of available formats to find the best candidate surface-format/color-space that is a non-sRGB format being presented in an sRGB color-space. Vulkan mandates that all surface formats that have SRGB forms must also support a UNORM form. This is basically just RGBA8/BGRA8 on all platforms still, but in a way that is still capable of falling back to secondary formats in a stable way in the case that the primary choice is not available. Mobile devices especially have a LOT of secondary HDR surface formats and other weird formats that can be used to present such as RGBA16 or RGBA565. With stable partitions, if we can't get our best option then there is always a "next best thing" to fall back on rather than relying on the driver-order. --- core/rend/vulkan/vulkan_context.cpp | 42 ++++++++++++++++------------- core/rend/vulkan/vulkan_context.h | 2 +- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/core/rend/vulkan/vulkan_context.cpp b/core/rend/vulkan/vulkan_context.cpp index 11795a788..bc12873ef 100644 --- a/core/rend/vulkan/vulkan_context.cpp +++ b/core/rend/vulkan/vulkan_context.cpp @@ -42,6 +42,7 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE #endif #include +#include void ReInitOSD(); @@ -632,23 +633,28 @@ void VulkanContext::CreateSwapChain() for (auto& img : imageViews) img.reset(); - // get the supported VkFormats - std::vector formats = physicalDevice.getSurfaceFormatsKHR(GetSurface()); - assert(!formats.empty()); - for (const auto& f : formats) - { - DEBUG_LOG(RENDERER, "Supported surface format: %s", vk::to_string(f.format).c_str()); - // Try to find an non-sRGB color format - if (f.format == vk::Format::eB8G8R8A8Unorm || f.format == vk::Format::eR8G8B8A8Unorm) + // Determine surface format and color-space + std::vector surfaceFormats = physicalDevice.getSurfaceFormatsKHR(GetSurface()); + + // Prefer a non-sRGB image format + std::stable_partition(surfaceFormats.begin(), surfaceFormats.end(), + [](const vk::SurfaceFormatKHR& surfaceFormat) -> bool { - colorFormat = f.format; - break; + return std::string_view("SRGB").compare(vk::componentNumericFormat(surfaceFormat.format, 0)) != 0; } - } - if (colorFormat == vk::Format::eUndefined) - { - colorFormat = (formats[0].format == vk::Format::eUndefined) ? vk::Format::eB8G8R8A8Unorm : formats[0].format; - } + ); + + // Prefer an sRGB presentation color-space + std::stable_partition(surfaceFormats.begin(), surfaceFormats.end(), + [](const vk::SurfaceFormatKHR& surfaceFormat) -> bool + { + return surfaceFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear; + } + ); + + // Top of the list is the best candidate surface format/color-space + const vk::SurfaceFormatKHR& targetSurfaceFormat = surfaceFormats[0]; + presentFormat = targetSurfaceFormat.format; int tries = 0; do { @@ -708,7 +714,7 @@ void VulkanContext::CreateSwapChain() // for final screenshot or Syphon usage |= vk::ImageUsageFlagBits::eTransferSrc; #endif - vk::SwapchainCreateInfoKHR swapChainCreateInfo(vk::SwapchainCreateFlagsKHR(), GetSurface(), imageCount, colorFormat, vk::ColorSpaceKHR::eSrgbNonlinear, + vk::SwapchainCreateInfoKHR swapChainCreateInfo(vk::SwapchainCreateFlagsKHR(), GetSurface(), imageCount, targetSurfaceFormat.format, targetSurfaceFormat.colorSpace, swapchainExtent, 1, usage, vk::SharingMode::eExclusive, 0, nullptr, preTransform, vk::CompositeAlphaFlagBitsKHR::eOpaque, swapchainPresentMode, true, nullptr); u32 queueFamilyIndices[2] = { graphicsQueueIndex, presentQueueIndex }; @@ -744,7 +750,7 @@ void VulkanContext::CreateSwapChain() u32 imageIdx = 0; for (auto image : swapChainImages) { - vk::ImageViewCreateInfo imageViewCreateInfo(vk::ImageViewCreateFlags(), image, vk::ImageViewType::e2D, colorFormat, componentMapping, subResourceRange); + vk::ImageViewCreateInfo imageViewCreateInfo(vk::ImageViewCreateFlags(), image, vk::ImageViewType::e2D, presentFormat, componentMapping, subResourceRange); imageViews[imageIdx++] = device->createImageViewUnique(imageViewCreateInfo); // create a UniqueCommandPool to allocate a CommandBuffer from @@ -757,7 +763,7 @@ void VulkanContext::CreateSwapChain() depthFormat = findDepthFormat(physicalDevice); // Render pass - vk::AttachmentDescription attachmentDescription = vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), colorFormat, vk::SampleCountFlagBits::e1, + vk::AttachmentDescription attachmentDescription = vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), presentFormat, vk::SampleCountFlagBits::e1, vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eStore, vk::AttachmentLoadOp::eDontCare, vk::AttachmentStoreOp::eDontCare, vk::ImageLayout::eUndefined, vk::ImageLayout::ePresentSrcKHR); diff --git a/core/rend/vulkan/vulkan_context.h b/core/rend/vulkan/vulkan_context.h index 4c6a45056..db4e16444 100644 --- a/core/rend/vulkan/vulkan_context.h +++ b/core/rend/vulkan/vulkan_context.h @@ -240,7 +240,7 @@ private: vk::UniqueSwapchainKHR swapChain; std::vector imageViews; u32 currentImage = 0; - vk::Format colorFormat = vk::Format::eUndefined; + vk::Format presentFormat = vk::Format::eUndefined; vk::Queue graphicsQueue; vk::Queue presentQueue; From aa65b81f892010da0ce00908d1bc59f9244c1030 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 23 Jan 2025 18:34:58 +0100 Subject: [PATCH 35/76] switch build fix --- core/network/dcnet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/network/dcnet.cpp b/core/network/dcnet.cpp index 37bfb598f..c70c37764 100644 --- a/core/network/dcnet.cpp +++ b/core/network/dcnet.cpp @@ -16,8 +16,8 @@ You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ -#include #include "types.h" +#include #include "netservice.h" #include "util/tsqueue.h" #include "oslib/oslib.h" From 1b6bf25e7abd54d6fde2a94f8e60de861ed2ab82 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 23 Jan 2025 18:40:24 +0100 Subject: [PATCH 36/76] naomi: broadcast digital outputs even when using FFB drive The first byte is for lamps, the next two are for FFB. Fixes missing lamp notifications when broadcasting digital outputs (F355). --- core/hw/maple/maple_jvs.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/hw/maple/maple_jvs.cpp b/core/hw/maple/maple_jvs.cpp index 2cd83cc9d..d0b16938f 100644 --- a/core/hw/maple/maple_jvs.cpp +++ b/core/hw/maple/maple_jvs.cpp @@ -627,6 +627,7 @@ protected: in = (in & 0xAA) >> 1 | (in & 0x55) << 1; out = process(in); + jvs_837_13844::write_digital_out(1, data); // The first byte is for lamps } virtual u8 process(u8 in) = 0; From bb733323ce0021d055e289473acc958d8906842b Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 24 Jan 2025 18:13:56 +0100 Subject: [PATCH 37/76] dcnet utility for dreampi --- tools/dreampi/Makefile | 17 + tools/dreampi/dcnet.c | 308 +++++ tools/dreampi/dcnet.rpi | Bin 0 -> 14088 bytes tools/dreampi/dreampi.py | 1106 +++++++++++++++++ {gdtool => tools/gdtool}/gdtool.sln | 0 {gdtool => tools/gdtool}/gdtool.vcxproj | 0 .../gdtool}/gdtool.vcxproj.filters | 0 {gdtool => tools/gdtool}/gdtool.vcxproj.user | 0 {gdtool => tools/gdtool}/src/cdromfs.cpp | 0 {gdtool => tools/gdtool}/src/cdromfs.h | 0 {gdtool => tools/gdtool}/src/cdromfs_imp.h | 0 {gdtool => tools/gdtool}/src/main.cpp | 0 12 files changed, 1431 insertions(+) create mode 100644 tools/dreampi/Makefile create mode 100644 tools/dreampi/dcnet.c create mode 100755 tools/dreampi/dcnet.rpi create mode 100755 tools/dreampi/dreampi.py rename {gdtool => tools/gdtool}/gdtool.sln (100%) rename {gdtool => tools/gdtool}/gdtool.vcxproj (100%) rename {gdtool => tools/gdtool}/gdtool.vcxproj.filters (100%) rename {gdtool => tools/gdtool}/gdtool.vcxproj.user (100%) rename {gdtool => tools/gdtool}/src/cdromfs.cpp (100%) rename {gdtool => tools/gdtool}/src/cdromfs.h (100%) rename {gdtool => tools/gdtool}/src/cdromfs_imp.h (100%) rename {gdtool => tools/gdtool}/src/main.cpp (100%) diff --git a/tools/dreampi/Makefile b/tools/dreampi/Makefile new file mode 100644 index 000000000..6313e63b3 --- /dev/null +++ b/tools/dreampi/Makefile @@ -0,0 +1,17 @@ +# +# Utility to connect to DCNet using dreampi. +# To install: +# make install +# Make a copy of /home/pi/dreampi/dreampi.py +# Copy the included dreampi.py into /home/pi/dreampi +# +CFLAGS=-O3 -Wall -Wconversion + +dcnet: dcnet.c + $(CC) $(CFLAGS) -c -o $@ $< + +clean: + rm -f dcnet.o dcnet + +install: dcnet + cp dcnet /home/pi/ diff --git a/tools/dreampi/dcnet.c b/tools/dreampi/dcnet.c new file mode 100644 index 000000000..cf16a89d4 --- /dev/null +++ b/tools/dreampi/dcnet.c @@ -0,0 +1,308 @@ +/* + 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 . + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEFAULT_TTY "/dev/ttyACM0" +#define DEFAULT_HOST "dcnet.flyca.st" +#define DEFAULT_PORT 7654 + +char ttyName[512] = DEFAULT_TTY; +int useUdp; +char hostName[64] = DEFAULT_HOST; +uint16_t port = DEFAULT_PORT; +struct termios tbufsave; + +int setNonBlocking(int fd) +{ + int flags = fcntl(fd, F_GETFL, 0); + if (flags == -1) + flags = 0; + flags |= O_NONBLOCK; + if (fcntl(fd, F_SETFL, flags) != 0) { + perror("fcntl(O_NONBLOCK)"); + return -1; + } + return 0; +} + +int configureTty(int fd, int local) +{ + if (tcgetattr(fd, &tbufsave) == -1) + perror("tcgetattr"); + struct termios tbuf; + memcpy(&tbuf, &tbufsave, sizeof(tbuf)); + /* 8-bit, one stop bit, no parity, carrier detect, no hang up on close, + disable RTS/CTS flow control. + */ + tbuf.c_cflag &= ~(CSIZE | CSTOPB | PARENB | CLOCAL | HUPCL | CRTSCTS); + tbuf.c_cflag |= CS8 | CREAD; + if (local) + // ignore CD + tbuf.c_cflag |= CLOCAL; + + /* don't translate NL to CR or CR to NL on input, get all 8 bits of input + disable xon/xoff flow control on output, no interrupt on break signal, + ignore parity, ignore break + */ + tbuf.c_iflag = IGNBRK | IGNPAR; + /* disable all output processing */ + tbuf.c_oflag = 0; + /* non-canonical, ignore signals and no echoing on output */ + tbuf.c_lflag = 0; + tbuf.c_cc[VMIN] = 1; + tbuf.c_cc[VTIME] = 0; + /* set the parameters associated with the terminal port */ + if (tcsetattr(fd, TCSANOW, &tbuf) == -1) { + perror("tcsetattr"); + return 1; + } + return 0; +} + +int main(int argc, char *argv[]) +{ + int opt; + while ((opt = getopt(argc, argv, "t:h:p:")) != -1) + { + switch (opt) + { + /* + case 'u': + useUdp = 1; + break; + */ + case 't': + strcpy(ttyName, optarg); + break; + case 'h': + strcpy(hostName, optarg); + break; + case 'p': + port = (uint16_t)atoi(optarg); + break; + default: + fprintf(stderr, "Usage: %s [-t ] [-h ]\n", argv[0]); + fprintf(stderr, "Default tty is %s. Default host:port is %s:%d\n", DEFAULT_TTY, DEFAULT_HOST, DEFAULT_PORT); + exit(1); + } + } + + fprintf(stderr, "DCNet starting\n"); + /* Resolve server name */ + struct addrinfo hints, *result; + memset(&hints, 0, sizeof (hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = useUdp ? SOCK_DGRAM : SOCK_STREAM; + hints.ai_flags |= AI_CANONNAME; + int errcode = getaddrinfo(hostName, NULL, &hints, &result); + if (errcode != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(errcode)); + return -1; + } + if (result == NULL) { + fprintf(stderr, "%s: host not found\n", hostName); + return -1; + } + char s[100]; + struct sockaddr_in *serverAddress = (struct sockaddr_in *)result->ai_addr; + inet_ntop(result->ai_family, &serverAddress->sin_addr, s, 100); + printf("%s is %s (%s)\n", hostName, s, result->ai_canonname); + + /* TTY */ + int ttyfd = open(ttyName, O_RDWR); + if (ttyfd == -1) { + perror("Can't open tty"); + return 1; + } + if (configureTty(ttyfd, 0)) + return 1; + + /* SOCKET */ + int sockfd = socket(AF_INET, useUdp ? SOCK_DGRAM : SOCK_STREAM, 0); + if (sockfd == -1) { + fprintf(stderr, "socket() failed: %d\n", errno); + return 1; + } + serverAddress->sin_port = htons(port); + if (connect(sockfd, (struct sockaddr *)serverAddress, sizeof(*serverAddress)) != 0) { + fprintf(stderr, "connect() failed: %d\n", errno); + return 1; + } + freeaddrinfo(result); + + if (setNonBlocking(sockfd) || setNonBlocking(ttyfd)) + return -1; + + char outbuf[1504]; + ssize_t outbuflen = 0; + char inbuf[1504]; + ssize_t inbuflen = 0; + for (;;) + { +#ifdef DEBUG + ssize_t old_olen = outbuflen; + ssize_t old_ilen = inbuflen; +#endif + fd_set readfds; + FD_ZERO(&readfds); + if (inbuflen < sizeof(inbuf)) + FD_SET(sockfd, &readfds); + if (outbuflen < sizeof(outbuf)) + FD_SET(ttyfd, &readfds); + + fd_set writefds; + FD_ZERO(&writefds); + ssize_t outbufReady = 0; + if (outbuflen > 0) + { + outbufReady = outbuflen; + /* + if (outbuf[0] != 0x7e) { + outbufReady = outbuflen; + } + else { + for (int i = 1; i < outbuflen; i++) + { + if (outbuf[i] == 0x7e) { + outbufReady = i + 1; + break; + } + } + } + */ + if (outbufReady != 0) + FD_SET(sockfd, &writefds); + } + if (inbuflen > 0) + FD_SET(ttyfd, &writefds); + int nfds = (sockfd > ttyfd ? sockfd : ttyfd) + 1; + if (select(nfds, &readfds, &writefds, NULL, NULL) == -1) + { + if (errno == EINTR) + continue; + fprintf(stderr, "select() failed: %d\n", errno); + close(sockfd); + return 1; + } + if (FD_ISSET(ttyfd, &readfds)) + { + ssize_t ret = read(ttyfd, outbuf + outbuflen, sizeof(outbuf) - (size_t)outbuflen); + if (ret < 0) { + if (errno != EINTR && errno != EWOULDBLOCK) { + fprintf(stderr, "read from tty failed: %d\n", errno); + break; + } + ret = 0; + } + else if (ret == 0) { + fprintf(stderr, "modem hang up\n"); + break; + } + if (ret > 0) { + outbuflen += ret; + FD_SET(sockfd, &writefds); + } + } + if (FD_ISSET(sockfd, &readfds)) + { + ssize_t ret = read(sockfd, inbuf + inbuflen, sizeof(inbuf) - (size_t)inbuflen); + if (ret < 0) { + if (errno != EINTR && errno != EWOULDBLOCK) { + fprintf(stderr, "read from socket failed: %d\n", errno); + break; + } + ret = 0; + } + else if (ret == 0) { + fprintf(stderr, "socket read EOF\n"); + break; + } + if (ret > 0) { + inbuflen += ret; + FD_SET(ttyfd, &writefds); + } + } + if (FD_ISSET(ttyfd, &writefds)) + { + ssize_t ret = write(ttyfd, inbuf, (size_t)inbuflen); + if (ret < 0) { + if (errno != EINTR && errno != EWOULDBLOCK) { + fprintf(stderr, "write to tty failed: %d\n", errno); + break; + } + ret = 0; + } + if (ret > 0) + { + inbuflen -= ret; + if (inbuflen > 0) + memmove(inbuf, inbuf + ret, (size_t)inbuflen); + } + } + if (FD_ISSET(sockfd, &writefds)) + { + ssize_t ret = write(sockfd, outbuf, (size_t)outbufReady); + if (ret < 0) { + if (errno == EINTR && errno != EWOULDBLOCK) { + fprintf(stderr, "write to socket failed: %d\n", errno); + break; + } + ret = 0; + } + if (ret > 0) + { + outbuflen -= ret; + if (outbuflen > 0) + memmove(outbuf, outbuf + ret, (size_t)outbuflen); + } + } +#ifdef DEBUG + printf("OUT %03zd%c IN %03zd%c\r", + outbuflen, outbuflen > old_olen ? '+' : outbuflen < old_olen ? '-' : ' ', + inbuflen, inbuflen > old_ilen ? '+' : inbuflen < old_ilen ? '-' : ' '); + fflush(stdout); +#endif + } + close(ttyfd); + ttyfd = open(ttyName, O_RDWR); + if (ttyfd == -1) { + perror("Can't reopen tty"); + } + else { + tcsetattr(ttyfd, TCSANOW, &tbufsave); + close(ttyfd); + } + close(sockfd); + fprintf(stderr, "DCNet stopped\n"); + return 0; +} + diff --git a/tools/dreampi/dcnet.rpi b/tools/dreampi/dcnet.rpi new file mode 100755 index 0000000000000000000000000000000000000000..0eb751899dbbce94b4f302a720f2dbb113e05982 GIT binary patch literal 14088 zcmeHO4|G)3nZIvl0wDx5K#+)0UL|zn0+R_wifq&g1Vn|9Vz9QQP9~X`WMnckXXb@J zF5O0;qAOXb6jr;^j2@3mdvH%w+{0S2vBk=9Wo3_wy6fuv;n@guPxQFfZq?b}@4dO< z@uWT5Jw1E&?BT+f`~AD$cmICx-FNSNXX%P%ilPXaT%uNx+EJm12IRNR#~+UN{qbQ}BTw$ zg)aijQ%arknFN;!&%t0N3ezv;F8r;6yb5x{2xtQ3vW^KDNx(Ou8GM42ebvyzAit1U z-V0a_x!wOq;1O7!b*qa;I_jd~dC^F$Z{xgBvbU$pmx}wYg2>dN4eH&{vW6_d{$QP% zXah|(d6c*P_`uKZO)WTf+Vx9q`#t|~#T$Qm=jdd}sW1Dz%oBL9G*I$$m-?gX(uMtB zDix~mlzNofj{`#Z#V~d0gZdKy<$y_mivYC6G!1YuU^)q;D!>fDrGU!-|G0N|^^tu4 zr5y)C>Gf~?&1V0hEt6hZx&GB-$F^(l$a-~QF)itGPk@8(~SRHE_2RI#)z$R$R|4JXB_nX zpx?BWf5RdFX9xYa4tkD*e#$}L?V!^RdXa+`ST~(qDGNj@-nmZGMM_V0CN>FeV?-C- zp-2$4mQ2QzB4K5n@mNgj)I}tw>A{#DPl%KjB_q@kPnyN`cvN(2dMF%DMq*uY(W~{Q zU^p0rfml2kjdzChNIWL=POw9|o)k$f6lRflLKlEgvK!S1LtS)jNJex`bVlPT4I(Wj zx)QLeccC>=zf-`2Xx5BP8Ps;|x<}B!cCRC_pCCX4NI80q5MQ#6;vAsKnwg2mfMddYNP4 zS2C|gEMShnP|bX@5Gr#7lp5v;61B_`B<3WW$GS_A^_6{25$5-Y=BAr!Q)7ik#^4x^S zm*>W*>1>HNogFXI*%u3i(|9W9>xC%m5rNz!(V7<@4E{A9=clSCC}QsK0gprWQK=$M z0vf(NXej=4Uih~d`9k58@IGji!Ph?cdKYw=3Yixm9yGoU-jFz&eXFQ95qi79dmZ%X z(m_M0dyF&Fkc;mdgW#XM+QKulk^2XXw;_8QyaymtL^{6<`619l+MrQEKmOf@LSKP& z_E5>7u^w`@=)Xn`^# z+)%@yF&<^TXy1_k)$CK(|Ma~l{V!&d(8DUvKL9(b*k`N-UDdkJw6~ppARB;;W3-jD zzcuF`PG_spzuV2%k~R66{_o~W{ON~<+LxQDx^goUC74$otA+SRerDh=b7cX@ym#hi zdKJjaa!Nn?Qw^>D`TTUYY6DyoUtQ-DH#5b~NPj>D>%vJdF zG;PVi&l6?zwd<}-d3!or-irC_-#!L2atZu3p$c^`J z{oZinggq0*n(T06<(^WL_Ux%L`(aJq+dA+E%kWJ5aKn6Re-pcdn1&Vq{3x%>fW)9@Muu+A&jKYubw&oD7pLrfKrFCgWh&B1= z3Wa~U&wCyU4?^brJg6TuYe4_n>@rV4v|wQTN0< zX#eqn-0053BR>JJ0s1}gjrnV?LDq}){X57$L|vamF6(+P+UJ3Oqtr)zABCL0hmpSr z{mU;K$d!8UGGiCpv7hD9Kl9N)KVtv1FGK&_ntu%abNRK<#W=`58mFeSQRpLnoRYF} z=)c>2T{-&bpe?L{D`Be*{lM`O?RRHjBa5*&{tD&T=OtztEl2-rieowV>alQ zDMO!?pq-0B-wk;fa{Krn5QD}n$Rdy}hpb&4G#+9()bVQYH-WzkI@5vo08~hA=lX>Ac6g@8l%3*Q4ScMa(n2!-Ixr zc%D&?I+ml}AAVLieJSedM;+GU`F4CV?15jY4jOL%&WualjqtlVe|e!!IrUlLRIOJL z+zVAhry4ede@SK>o)M6L&kG)(7u20vsLQ^By1xuys__mQHPrVuWtt|Zvk%X_EAzZ} zC`(;uIhK*-ym-&53eeAMrfqNk;VL|5Pb+53I%0qxc^mq*W?gofI&e25hrX{W*5|jF z7qoK~>f{X^9{E?uDzTSmA|LZ1??!A8fpm5l{ow)5*x{PfKh-rS?Pk1q27Im~_$w`i zuLn#zLwY2Dz8i%dtc7ed-boIktpaiVpP)lKSVv{)%%Cx8n9sPje1&*!q@*4G5^3NE zMo5<;-Vk>+9Wmu^v>eF~1-7~GYuPrH{PE(4&>1jL$NYG)V}$e999KMF@cxq@Z9kSR zhy3EeF=Kk`F=KtvH|mZN6=ltgU#Kg`ECLzJOCIM!7(Bk0aK5#pJ#B2$4d|OhI=j4m z&{*C&XpC#$nJH-nARa0G>8xv4I@^eH%R)oh(d~yzSvMuHE$8v?IXOYavk-L~4?pa8 z51wVeM%;$pNbMv=r~r(8mhaJghvt_SbBj~ED%i5BW$}ttOKwKY5@&Zq#O1yUPm5Hj zTU(^gPO0CV*Y_>_(bYy>-*g?(9+4{kX~{}I=S8#D73z!XDp+bH1wTGjvU=hveG%Y>P1l#lDOu5*k}O_qT=b zU@l)s7kc1A4_xSh3q5e52QKu$g&w%j0~dPW|6C7j#y+_Xz{I}%r3z#j%Kaj{A9`5PuE)0&>=K6!~7ze`wR9p1q8WXJxyCJ;(G90e_=d>jHlrNQ zZ4u`znm0snZaYTfkQRyeewf_F6Xu>Vn$gONkIda>)Iz)~#u&F5y%6KZXhu8ho(^-j zjEabd7R?(eLiCT(jEdH|HFKPGFdIv5Gdhovkk2TNh{f}W4+_hH^3IQyT;gURyz6A8 zNrH0OK1OQN&pPC?KZK|-<+6UfJ%e+Yu|$8gf7DifkBxSrziI!ZO-_A2Z>%)Yte+eY z-q?AhC}Ae;f8@|#SR60vzkeS6gT-eYZvoN&CpMb?Kef@6AK}+XGfgtCFa@ile6?F`>d?!&54^DuV z?W6pC(0pHFQgCC9{l|9}E0vr6OUIw3DKEFt1&G)_M&2nH&kEU9CXE&$M9cN8Vm&`@ zg>2J2uF8lnLI0&<|9UaLb&m4aJLuI8`VP=MUm|-l?B5GSon{mPb?;6lNQ)B=0&RW1GSHW9lqgkIIXvQnfE7m^-TCUH@uIb{>9s2iy zW_-5nSBM`tTcEjK+Kc|*cgRbuM>wRcAI28*%RU~{pf8^< ztp6L1{+f>RcKw;4&yIhUm;;*e){jR&%ljSvmOJR6gVr7N-Js<;9sBQo(Dplkk2>`8 zsK0z3O~LNvX@~q54*D$zE$aGGN%KN%cV}lE?!p#t#dgQ~_}bOeRkxtd=PUj{pSUxr z*Zbn4CM;?~e+*+080!p0qrnYYXkD-?_Q$XpgH7uNwo{gT9x(XM%9`r@}hb=m5s zl}m$5TbfZ#tIiTXr(jqFo4?-DwDQI!(6UL)gEJAlVa2M&O)G+{mMyzwX`e8U_2q?5+{g?B&R%s420K!zu`7@~m$FNXcPH&E`#n%QpQn2wsD(p%sCWz1 zF16ffu`t@mbG#Px6%XRNBHevStqsR@II=UD*40vbS?l_zonzeuwR6VKIqgjA`l_9S z0Z09;=E}}Fdm+@W*_YDRgcHy?=ZYwg+U#05Ty2TR7ULT0Ivia>JDiFKaT*woYUZt3 zyKeBtRd5`R#DcIbuejP3inm$qEWM+zD;2`!Rr8$JF0qX)=K@@xd!yE_YhAjvbD(Vw zg>CeOc_r3eWLX$T88)yhmcVD6AzD!iNK7&DXT}#(8?EyGS7z`a1d|_{l&dEPSMT zLa83%3vY_So|WTnu_?h3d5Ra7?L-i=q!y*Vg(RZ7@R`Hn)3uGj87v@*o2$duw0flv zcdEl1g-`3TB88=H1qn-C;&*hyIxL5JBc1RXhc(Lz)~)a%R`nwM2-7BSKu0?IbVRoQ zZsK<|zOC`G$bEDh2!3Oei87{H0PcRNXZ>yi&rRSm$;VDT7*db>?p-iYBbAU#y=LGa z@H*hqFZqOZ0Dj{m&i(#tP*8!}{el#C0=Vm^9={WQ1U>GGr9HNx z6tERQy~O~2A1H^R3~=gkmo4>n0B2q4htK^pK&-C>U?5SCX*)pbS>FtCA6qsOGU^fT zx9RacW-Ti7ruCTulC$=37j(=X-)|f6GR1n!B>nyn47y=^`JUGZy&4k5B=sHx4ZXw` zmsJIE7d@ncC+H@$VECle9M(v|X %s",ip[0],ip[1]) + rules.append(rule) + return rules + +def remove_dnat_rule(drule): + if drule: + table = iptc.Table(iptc.Table.NAT) + chain = iptc.Chain(table, "PREROUTING") + chain.delete_rule(drule) + logger.info("DNAT rule removed") + +def start_afo_patching(): + + def fetch_replacement_ip(): + url = "http://dreamcast.online/afo.txt" + try: + r = requests.get(url) + r.raise_for_status() + afo_IP = r.text.strip() + return afo_IP + except requests.exceptions.HTTPError: + return None + + replacement = fetch_replacement_ip() + + if replacement is None: + logger.warning("Not starting AFO patch as couldn't get IP from server") + return + + table = iptc.Table(iptc.Table.NAT) + chain = iptc.Chain(table, "PREROUTING") + + rule = iptc.Rule() + rule.protocol = "tcp" + rule.dst = "63.251.242.131" + rule.create_target("DNAT") + rule.target.to_destination = replacement + + chain.append_rule(rule) + + logger.info("AFO routing enabled") + return rule + + +def stop_afo_patching(afo_patcher_rule): + if afo_patcher_rule: + table = iptc.Table(iptc.Table.NAT) + chain = iptc.Chain(table, "PREROUTING") + chain.delete_rule(afo_patcher_rule) + logger.info("AFO routing disabled") + +def start_service(name): + try: + logger.info("Starting {} process - Thanks ShuoumaDC!".format(name)) + with open(os.devnull, "wb") as devnull: + subprocess.check_call(["sudo", "service", name, "start"], stdout=devnull) + except (subprocess.CalledProcessError, IOError): + logging.warning("Unable to start the {} process".format(name)) + + +def stop_service(name): + try: + logger.info("Stopping {} process".format(name)) + with open(os.devnull, "wb") as devnull: + subprocess.check_call(["sudo", "service", name, "stop"], stdout=devnull) + except (subprocess.CalledProcessError, IOError): + logging.warning("Unable to stop the {} process".format(name)) + + +def get_default_iface_name_linux(): + route = "/proc/net/route" + with open(route) as f: + for line in f.readlines(): + try: + iface, dest, _, flags, _, _, _, _, _, _, _, = line.strip().split() + if dest != "00000000" or not int(flags, 16) & 2: + continue + return iface + except: + continue + + +def ip_exists(ip, iface): + command = ["arp", "-a", "-i", iface] + output = subprocess.check_output(command).decode() + if ("(%s)" % ip) in output: + logger.info("IP existed at %s", ip) + return True + else: + logger.info("Free IP at %s", ip) + return False + + +def find_next_unused_ip(start): + interface = get_default_iface_name_linux() + + parts = [int(x) for x in start.split(".")] + current_check = parts[-1] - 1 + + while current_check: + test_ip = ".".join([str(x) for x in parts[:3] + [current_check]]) + if not ip_exists(test_ip, interface): + return test_ip + current_check -= 1 + + raise Exception("Unable to find a free IP on the network") + + +def autoconfigure_ppp(device, speed): + """ + Every network is different, this function runs on boot and tries + to autoconfigure PPP as best it can by detecting the subnet and gateway + we're running on. + + Returns the IP allocated to the Dreamcast + """ + + gateway_ip = subprocess.check_output( + "route -n | grep 'UG[ \t]' | awk '{print $2}'", shell=True + ).decode() + subnet = gateway_ip.split(".")[:3] + + PEERS_TEMPLATE = "{device}\n" "{device_speed}\n" "{this_ip}:{dc_ip}\n" "auth\n" + + OPTIONS_TEMPLATE = "debug\n" "ms-dns {this_ip}\n" "proxyarp\n" "ktune\n" "noccp\n" + + PAP_SECRETS_TEMPLATE = "# Modded from dreampi.py\n" "# INBOUND connections\n" '* * "" *' "\n" + + this_ip = find_next_unused_ip(".".join(subnet) + ".100") + dreamcast_ip = find_next_unused_ip(this_ip) + + logger.info("Dreamcast IP: {}".format(dreamcast_ip)) + + peers_content = PEERS_TEMPLATE.format( + device=device, device_speed=speed, this_ip=this_ip, dc_ip=dreamcast_ip + ) + + with open("/etc/ppp/peers/dreamcast", "w") as f: + f.write(peers_content) + + options_content = OPTIONS_TEMPLATE.format(this_ip=this_ip) + + with open("/etc/ppp/options", "w") as f: + f.write(options_content) + + pap_secrets_content = PAP_SECRETS_TEMPLATE + + with open("/etc/ppp/pap-secrets", "w") as f: + f.write(pap_secrets_content) + + return dreamcast_ip + + +ENABLE_SPEED_DETECTION = ( + False +) # Set this to true if you want to use wvdialconf for device detection + + +def detect_device_and_speed(): + MAX_SPEED = 57600 + + if not ENABLE_SPEED_DETECTION: + # By default we don't detect the speed or device as it's flakey in later + # Pi kernels. But it might be necessary for some people so that functionality + # can be enabled by setting the flag above to True + return ("/dev/ttyACM0", MAX_SPEED) + + command = ["wvdialconf", "/dev/null"] + + try: + output = subprocess.check_output(command, stderr=subprocess.STDOUT).decode() + + lines = output.split("\n") + + for line in lines: + match = re.match(r"(.+):\sSpeed\s(\d+);", line.strip()) + if match: + device = match.group(1) + speed = int(match.group(2)) + logger.info("Detected device {} with speed {}".format(device, speed)) + + # Many modems report speeds higher than they can handle so we cap + # to 56k + return device, min(speed, MAX_SPEED) + else: + logger.info("No device detected") + + except: + logger.exception("Unable to detect modem. Falling back to ttyACM0") + return ("/dev/ttyACM0", MAX_SPEED) + + +class Daemon(object): + def __init__(self, pidfile, process): + self.pidfile = pidfile + self.process = process + + def daemonize(self): + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + + except OSError: + sys.exit(1) + + os.chdir("/") + os.setsid() + os.umask(0) + + try: + pid = os.fork() + if pid > 0: + sys.exit(0) + except OSError: + sys.exit(1) + + atexit.register(self.delete_pid) + pid = str(os.getpid()) + with open(self.pidfile, "w+") as f: + f.write("%s\n" % pid) + + def delete_pid(self): + os.remove(self.pidfile) + + def _read_pid_from_pidfile(self): + try: + with open(self.pidfile, "r") as pf: + pid = int(pf.read().strip()) + except IOError: + pid = None + return pid + + def start(self): + pid = self._read_pid_from_pidfile() + + if pid: + logger.info("Daemon already running, exiting") + sys.exit(1) + + logger.info("Starting daemon") + self.daemonize() + self.run() + + def stop(self): + pid = self._read_pid_from_pidfile() + + if not pid: + logger.info("pidfile doesn't exist, deamon must not be running") + return + + try: + while True: + os.kill(pid, signal.SIGTERM) + time.sleep(0.1) + + except OSError: + if os.path.exists(self.pidfile): + os.remove(self.pidfile) + else: + sys.exit(1) + + def restart(self): + self.stop() + self.start() + + def run(self): + self.process() + + +class Modem(object): + def __init__(self, device, speed, send_dial_tone=True): + self._device, self._speed = device, speed + self._serial = None + self._sending_tone = False + + if send_dial_tone: + self._dial_tone_wav = self._read_dial_tone() + else: + self._dial_tone_wav = None + + self._time_since_last_dial_tone = None + self._dial_tone_counter = 0 + + @property + def device_speed(self): + return self._speed + + @property + def device_name(self): + return self._device + + def _read_dial_tone(self): + this_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) + dial_tone_wav = os.path.join(this_dir, "dial-tone.wav") + + with open(dial_tone_wav, "rb") as f: + dial_tone = f.read() # Read the entire wav file + dial_tone = dial_tone[44:] # Strip the header (44 bytes) + + return dial_tone + + def connect(self): + if self._serial: + self.disconnect() + + logger.info("Opening serial interface to {}".format(self._device)) + self._serial = serial.Serial( + self._device, self._speed, timeout=0 + ) + return self._serial + + def connect_netlink(self,speed = 115200, timeout = 0.01, rtscts = False): #non-blocking + if self._serial: + self.disconnect() + logger.info("Opening serial interface to {}".format(self._device)) + self._serial = serial.Serial( + self._device, speed, timeout=timeout, rtscts = rtscts + ) + + def disconnect(self): + if self._serial and self._serial.isOpen(): + self._serial.flush() + self._serial.close() + self._serial = None + logger.info("Serial interface terminated") + + def reset(self): + while True: + try: + self.send_command("ATZ0",timeout=3) # Send reset command + time.sleep(1) + self.send_command("AT&F0") + self.send_command("ATE0W2") # Don't echo our responses + return + except IOError: + self.shake_it_off() # modem isn't responding. Try a harder reset + + def start_dial_tone(self): + if not self._dial_tone_wav: + return + i = 0 + while i < 3: + try: + self.reset() + self.send_command(b"AT+FCLASS=8") # Enter voice mode + self.send_command(b"AT+VLS=1") # Go off-hook + self.send_command(b"AT+VSM=1,8000") # 8 bit unsigned PCM + self.send_command(b"AT+VTX") # Voice transmission mode + logger.info("") + break + except IOError: + time.sleep(0.5) + i+=1 + pass + + self._sending_tone = True + + self._time_since_last_dial_tone = datetime.now() - timedelta(seconds=100) + + self._dial_tone_counter = 0 + + def stop_dial_tone(self): + if not self._sending_tone: + return + if self._serial is None: + raise Exception("Not connected") + + self._serial.write(b"\x00\x10\x03\r\n") + self.send_escape() + self.send_command(b"ATH0") # Go on-hook + self.reset() # Reset the modem + self._sending_tone = False + + def answer(self): + self.reset() + # When we send ATA we only want to look for CONNECT. Some modems respond OK then CONNECT + # and that messes everything up + self.send_command(b"ATA", ignore_responses=[b"OK"]) + time.sleep(5) + logger.info("Call answered!") + #logger.info(subprocess.check_output(["pon", "dreamcast"]).decode()) + self.disconnect() + subprocess.check_call(["/home/pi/dcnet"]) + logger.info("Connection terminated") + self.connect() + + def netlink_answer(self): + self.reset() + # When we send ATA we only want to look for CONNECT. Some modems respond OK then CONNECT + # and that messes everything up + self.send_command(b"ATA", ignore_responses=[b"OK"]) + # time.sleep(5) + logger.info("Call answered!") + logger.info("Connected") + + def query_modem(self, command, timeout=3, response = "OK"): #this function assumes we're being passed a non-blocking modem + if isinstance(command, bytes): + final_command = command + b'\r\n' + else: + final_command = ("%s\r\n" % command).encode() + self._serial.write(final_command) + logger.info(final_command.decode()) + + start = time.time() + + line = b"" + while True: + new_data = self._serial.readline().strip() + + if not new_data: #non-blocking modem will end up here when timeout reached, try until this function's timeout is reached. + if time.time() - start < timeout: + continue + raise IOError() + + line = line + new_data + + if response.encode() in line: + if response != "OK": + logger.info(line.decode()) + return # Valid response + + def send_command( + self, command, timeout=60, ignore_responses = None + ): + if self._serial is None: + raise Exception("Not connected") + if ignore_responses is None: + ignore_responses = [] + + VALID_RESPONSES = [b"OK", b"ERROR", b"CONNECT", b"VCON"] + + for ignore in ignore_responses: + VALID_RESPONSES.remove(ignore) + + if isinstance(command, bytes): + final_command = command + b'\r\n' + else: + final_command = ("%s\r\n" % command).encode() + + self._serial.write(final_command) + logger.info('Command: %s' % final_command.decode()) + + start = time.time() + line = b"" + while True: + new_data = self._serial.readline().strip() + + if not new_data: + if time.time() - start < timeout: + continue + raise IOError("There was a timeout while waiting for a response from the modem") + + line = line + new_data + for resp in VALID_RESPONSES: + if resp in line: + if resp != b"OK": + logger.info('Response: %s' % line.decode()) + if resp == b"ERROR": + raise IOError("Command returned an error") + # logger.info(line[line.find(resp) :].decode()) + return # We are done + + + def send_escape(self): + if self._serial is None: + raise Exception("Not connected") + time.sleep(1.0) + self._serial.write(b"+++") + time.sleep(1.0) + + def shake_it_off(self): #sometimes the modem gets stuck in data mode + for i in range(3): + self._serial.write(b'+') + time.sleep(0.2) + time.sleep(4) + self.send_command('ATH0') #make sure we're on hook + logger.info("Shook it off") + + + def update(self): + now = datetime.now() + if self._sending_tone: + # Keep sending dial tone + BUFFER_LENGTH = 1000 + TIME_BETWEEN_UPLOADS_MS = (1000.0 / 8000.0) * BUFFER_LENGTH + + if self._dial_tone_wav is None: + raise Exception("Dial tone wav not loaded") + if self._serial is None: + raise Exception("Not connected") + + if ( + not self._time_since_last_dial_tone + or ((now - (self._time_since_last_dial_tone)).microseconds * 1000) + >= TIME_BETWEEN_UPLOADS_MS + ): + byte = self._dial_tone_wav[ + self._dial_tone_counter : self._dial_tone_counter + BUFFER_LENGTH + ] + self._dial_tone_counter += BUFFER_LENGTH + if self._dial_tone_counter >= len(self._dial_tone_wav): + self._dial_tone_counter = 0 + self._serial.write(byte) + self._time_since_last_dial_tone = now + + +class GracefulKiller(object): + def __init__(self): + self.kill_now = False + signal.signal(signal.SIGINT, self.exit_gracefully) + signal.signal(signal.SIGTERM, self.exit_gracefully) + + def exit_gracefully(self, signum, frame): + logging.warning("Received signal: %s", signum) + self.kill_now = True + +def do_netlink(side,dial_string,modem,saturn=True): + # ser = serial.Serial(device_and_speed[0], device_and_speed[1], timeout=0.005) + state, opponent = netlink.netlink_setup(side,dial_string,modem) + if state == "failed": + for i in range(3): + modem._serial.write(b'+') + time.sleep(0.2) + time.sleep(4) + modem.send_command(b'ATH0') + return + if saturn == False: + netlink.kddi_exchange(side,state,opponent,ser=modem._serial) + else: + netlink.netlink_exchange(side,state,opponent,ser=modem._serial) + + +def process(): + + xbandnums = ["18002071194","19209492263","0120717360","0355703001"] + + xbandMatching = False + xbandTimer = None + xbandInit = False + openXband = False + + killer = GracefulKiller() + + dial_tone_enabled = "--disable-dial-tone" not in sys.argv + + # Make sure pppd isn't running + with open(os.devnull, "wb") as devnull: + subprocess.call(["sudo", "killall", "pppd"], stderr=devnull) + + device_and_speed, internet_connected = None, False + # Startup checks, make sure that we don't do anything until + # we have a modem and internet connection + while True: + logger.info("Detecting connection and modem...") + internet_connected = check_internet_connection() + device_and_speed = detect_device_and_speed() + + if internet_connected and device_and_speed: + logger.info("Internet connected and device found!") + break + + elif not internet_connected: + logger.warn("Unable to detect an internet connection. Waiting...") + elif not device_and_speed: + logger.warn("Unable to find a modem device. Waiting...") + + time.sleep(5) + + modem = Modem(device_and_speed[0], device_and_speed[1], dial_tone_enabled) + + dreamcast_ip = autoconfigure_ppp(modem.device_name, modem.device_speed) + + # Get a port forwarding object, now that we know the DC IP. + if "--enable-port-forwarding" in sys.argv: + port_forwarding = PortForwarding(dreamcast_ip, logger) + port_forwarding.forward_all() + else: + port_forwarding = None + + mode = "LISTENING" + + modem.connect() + if dial_tone_enabled: + modem.start_dial_tone() + + time_digit_heard = None + global saturn + saturn = True + dcnow = DreamcastNowService() + while True: + if killer.kill_now: + break + + now = datetime.now() + + if mode == "LISTENING": + + if xbandMatching == True: + if xbandInit == False: + xband.xbandInit() + xbandInit = True + if time.time() - xbandTimer > 900: #Listen for incoming connections for 15 minutes + xbandMatching = False + xband.closeXband() + openXband = False + continue + if openXband == False: + xband.openXband() + openXband = True + xbandResult,opponent = xband.xbandListen(modem) + if xbandResult == "connected": + xband.netlink_exchange("waiting","connected",opponent,ser=modem._serial) + logger.info("Xband Disconnected") + mode = "LISTENING" + modem.connect() + modem.start_dial_tone() + xbandMatching = False + xband.closeXband() + openXband = False + + + modem.update() + char = modem._serial.read(1) + char = char.strip() + if not char: + continue + + if ord(char) == 16: + # DLE character + try: + parsed = netlink.digit_parser(modem) + if parsed == "nada": + pass + elif isinstance(parsed,dict): + client = parsed['client'] + dial_string = parsed['dial_string'] + side = parsed['side'] + logger.info("Heard: %s" % dial_string) + + if dial_string in xbandnums: + logger.info("Calling Xband server") + client = "xband" + mode = "XBAND ANSWERING" + + elif dial_string == "00": + side = "waiting" + client = "direct_dial" + saturn = False + elif dial_string[0:3] == "859": + try: + kddi_opponent = dial_string + kddi_lookup = "https://dial.redreamcast.net/?phoneNumber=%s" % kddi_opponent + response = requests.get(kddi_lookup) + response.raise_for_status() + ip = response.text + if len(ip) == 0: + pass + else: + dial_string = ip + logger.info(dial_string) + saturn = False + side = "calling" + client = "direct_dial" + time.sleep(7) + except requests.exceptions.HTTPError: + pass + elif len(dial_string.split('*')) == 5 and dial_string.split('*')[-1] == "1": + oppIP = '.'.join(dial_string.split('*')[0:4]) + client = "xband" + mode = "NETLINK ANSWERING" + side = "calling" + + + if client == "direct_dial": + mode = "NETLINK ANSWERING" + elif client == "xband": + pass + else: + mode = "ANSWERING" + modem.stop_dial_tone() + time_digit_heard = now + except (TypeError, ValueError): + pass + + elif mode == "XBAND ANSWERING": + # print("xband answering") + if (now - time_digit_heard).total_seconds() > 8.0: + time_digit_heard = None + modem.query_modem("ATA", timeout=60, response = "CONNECT") + xband.xbandServer(modem) + mode = "LISTENING" + modem.connect() + modem.start_dial_tone() + xbandMatching = True + xbandTimer = time.time() + + elif mode == "ANSWERING": + if time_digit_heard is None: + raise Exception("Impossible code path") + if (now - time_digit_heard).total_seconds() > 8.0: + time_digit_heard = None + modem.answer() +# modem.disconnect() +# mode = "CONNECTED" + mode = "LISTENING" + modem.start_dial_tone() + + elif mode == "NETLINK ANSWERING": + if (now - time_digit_heard).total_seconds() > 8.0: + time_digit_heard = None + + try: + if client == "xband": + xband.init_xband(modem) + result = xband.ringPhone(oppIP,modem) + if result == "hangup": + mode = "LISTENING" + modem.connect() + modem.start_dial_tone() + else: + mode = "NETLINK_CONNECTED" + else: + modem.connect_netlink(speed=57600,timeout=0.01,rtscts = True) #non-blocking version + modem.query_modem(b"AT%E0\V1") + if saturn: + modem.query_modem(b'AT%C0\N3') + modem.query_modem(b'AT+MS=V32b,1,14400,14400,14400,14400') + modem.query_modem(b"ATA", timeout=120, response = "CONNECT") + mode = "NETLINK_CONNECTED" + except IOError: + modem.connect() + mode = "LISTENING" + modem.start_dial_tone() + elif mode == "CONNECTED": + dcnow.go_online(dreamcast_ip) + + for line in sh.tail("-f", "/var/log/messages", "-n", "1", _iter=True): + if "pppd" in line and "Exit" in line:#wait for pppd to execute the ip-down script + logger.info("Detected modem hang up, going back to listening") + break + dcnow.go_offline() #changed dcnow to wait 15 seconds for event instead of sleeping. Should be faster. + mode = "LISTENING" + # modem = Modem(device_and_speed[0], device_and_speed[1], dial_tone_enabled) + modem.connect() + if dial_tone_enabled: + modem.start_dial_tone() + elif mode == "NETLINK_CONNECTED": + if client == "xband": + xband.netlink_exchange("calling","connected",oppIP,ser=modem._serial) + else: + do_netlink(side,dial_string,modem,saturn=saturn) + logger.info("Netlink Disconnected") + mode = "LISTENING" + modem.connect() + modem.start_dial_tone() + if port_forwarding is not None: + port_forwarding.delete_all() + return 0 + + +def enable_prom_mode_on_wlan0(): + """ + The Pi wifi firmware seems broken, we can only get it to work by enabling + promiscuous mode. + + This is a hack, we just enable it for wlan0 and ignore errors + """ + + try: + subprocess.check_call("sudo ifconfig wlan0 promisc".split()) + logging.info("Promiscuous mode set on wlan0") + except subprocess.CalledProcessError: + logging.info("Attempted to set promiscuous mode on wlan0 but was unsuccessful") + logging.info("Probably no wifi connected, or using a different device name") + + +def main(): + afo_patcher_rule = None + ttl_rule = None + dnat_rules = [] + + try: + # Don't do anything until there is an internet connection + while not check_internet_connection(): + logger.info("Waiting for internet connection...") + time.sleep(3) + + #try auto updates /disabled for now + updater() + global xband + global netlink + try: + import xband as xband + import netlink as netlink + except ImportError: + logger.info("couldn't import xband or netlink modules") + + + # Dreampi local update check + dreampi_py_local_update() + + # Try to update the DNS configuration + update_dns_file() + + # Hack around dodgy Raspberry Pi things + enable_prom_mode_on_wlan0() + + # Just make sure everything is fine + restart_dnsmasq() + + config_server.start() + afo_patcher_rule = start_afo_patching() + dnat_rules = start_dnat_rules() + ttl_rule = add_increased_ttl() + start_service("dcvoip") + start_service("dcgamespy") + start_service("dc2k2") + start_service("dcdaytona") + return process() + except: + logger.exception("Something went wrong...") + return 1 + finally: + stop_service("dc2k2") + stop_service("dcgamespy") + stop_service("dcvoip") + stop_service("dcdaytona") + if afo_patcher_rule is not None: + stop_afo_patching(afo_patcher_rule) + if ttl_rule is not None: + remove_increased_ttl(ttl_rule) + if dnat_rules is not None: + for drule in dnat_rules: + remove_dnat_rule(drule) + + config_server.stop() + logger.info("Dreampi quit successfully") + + +if __name__ == "__main__": + logger.setLevel(logging.INFO) + syslog_handler = logging.handlers.SysLogHandler(address="/dev/log") + syslog_handler.setFormatter( + logging.Formatter("%(name)s[%(process)d]: %(levelname)s %(message)s") + ) + logger.addHandler(syslog_handler) + + if len(sys.argv) > 1 and "--no-daemon" in sys.argv: + # logger.addHandler(logging.StreamHandler()) + sys.exit(main()) + + daemon = Daemon("/tmp/dreampi.pid", main) + + if len(sys.argv) == 2: + if sys.argv[1] == "start": + daemon.start() + elif sys.argv[1] == "stop": + daemon.stop() + elif sys.argv[1] == "restart": + daemon.restart() + else: + sys.exit(2) + sys.exit(0) + else: + print(("Usage: %s start|stop|restart" % sys.argv[0])) + sys.exit(2) diff --git a/gdtool/gdtool.sln b/tools/gdtool/gdtool.sln similarity index 100% rename from gdtool/gdtool.sln rename to tools/gdtool/gdtool.sln diff --git a/gdtool/gdtool.vcxproj b/tools/gdtool/gdtool.vcxproj similarity index 100% rename from gdtool/gdtool.vcxproj rename to tools/gdtool/gdtool.vcxproj diff --git a/gdtool/gdtool.vcxproj.filters b/tools/gdtool/gdtool.vcxproj.filters similarity index 100% rename from gdtool/gdtool.vcxproj.filters rename to tools/gdtool/gdtool.vcxproj.filters diff --git a/gdtool/gdtool.vcxproj.user b/tools/gdtool/gdtool.vcxproj.user similarity index 100% rename from gdtool/gdtool.vcxproj.user rename to tools/gdtool/gdtool.vcxproj.user diff --git a/gdtool/src/cdromfs.cpp b/tools/gdtool/src/cdromfs.cpp similarity index 100% rename from gdtool/src/cdromfs.cpp rename to tools/gdtool/src/cdromfs.cpp diff --git a/gdtool/src/cdromfs.h b/tools/gdtool/src/cdromfs.h similarity index 100% rename from gdtool/src/cdromfs.h rename to tools/gdtool/src/cdromfs.h diff --git a/gdtool/src/cdromfs_imp.h b/tools/gdtool/src/cdromfs_imp.h similarity index 100% rename from gdtool/src/cdromfs_imp.h rename to tools/gdtool/src/cdromfs_imp.h diff --git a/gdtool/src/main.cpp b/tools/gdtool/src/main.cpp similarity index 100% rename from gdtool/src/main.cpp rename to tools/gdtool/src/main.cpp From 7ef35eaf687265b37ac11a71f3da1689a1534d66 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 24 Jan 2025 18:26:44 +0100 Subject: [PATCH 38/76] dreampi: fix paths --- tools/dreampi/Makefile | 3 ++- tools/dreampi/dreampi.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/dreampi/Makefile b/tools/dreampi/Makefile index 6313e63b3..bcf2a5e03 100644 --- a/tools/dreampi/Makefile +++ b/tools/dreampi/Makefile @@ -14,4 +14,5 @@ clean: rm -f dcnet.o dcnet install: dcnet - cp dcnet /home/pi/ + cp dcnet /home/pi/dreampi/dcnet.rpi + diff --git a/tools/dreampi/dreampi.py b/tools/dreampi/dreampi.py index 9335152b1..5a3d887e6 100755 --- a/tools/dreampi/dreampi.py +++ b/tools/dreampi/dreampi.py @@ -620,7 +620,7 @@ class Modem(object): logger.info("Call answered!") #logger.info(subprocess.check_output(["pon", "dreamcast"]).decode()) self.disconnect() - subprocess.check_call(["/home/pi/dcnet"]) + subprocess.check_call(["/home/pi/dreampi/dcnet.rpi"]) logger.info("Connection terminated") self.connect() From 944af44c7f88d1558d3734ff71928646c3888252 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 25 Jan 2025 17:00:48 +0100 Subject: [PATCH 39/76] naomi: some digital outputs still missing for f355 Lamps use 14 bits of output in f355. --- core/hw/maple/maple_jvs.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/hw/maple/maple_jvs.cpp b/core/hw/maple/maple_jvs.cpp index d0b16938f..8a60a39d8 100644 --- a/core/hw/maple/maple_jvs.cpp +++ b/core/hw/maple/maple_jvs.cpp @@ -627,7 +627,9 @@ protected: in = (in & 0xAA) >> 1 | (in & 0x55) << 1; out = process(in); - jvs_837_13844::write_digital_out(1, data); // The first byte is for lamps + // The rest of the bits are for lamps + u8 lamps[2] = { data[0], (u8)(data[1] & 0xfc) }; + jvs_837_13844::write_digital_out(2, lamps); } virtual u8 process(u8 in) = 0; From 77636391fc230d3876319d39d71ed73b5dbc2cb6 Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Thu, 16 Jan 2025 11:04:00 +0100 Subject: [PATCH 40/76] Enabled the usage of VMUs and Rumble Packs with Original DC Controllers --- core/hw/maple/maple_devs.cpp | 2 +- core/sdl/dreamconn.cpp | 271 ++++++++++++++++++++++++++++++----- core/sdl/dreamconn.h | 21 ++- core/sdl/sdl.cpp | 2 +- 4 files changed, 250 insertions(+), 46 deletions(-) diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index 33e76b772..1602f746f 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -2106,7 +2106,7 @@ maple_device* maple_Create(MapleDeviceType type) return nullptr; } -#if defined(_WIN32) && !defined(TARGET_UWP) && defined(USE_SDL) && !defined(LIBRETRO) +#if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) && defined(USE_SDL) && !defined(LIBRETRO) #include "sdl/dreamconn.h" struct DreamConnVmu : public maple_sega_vmu diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index c379042fb..12cec51f4 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -18,7 +18,7 @@ */ #include "dreamconn.h" -#ifdef USE_DREAMCONN +#ifdef USE_DREAMCASTCONTROLLER #include "hw/maple/maple_devs.h" #include "ui/gui.h" #include @@ -27,9 +27,17 @@ #include #include +#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) +#include +#endif + +#if defined(_WIN32) +#include +#endif + void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); -static bool sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream) +static asio::error_code sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream, asio::serial_port& serial_handler, int dreamcastControllerType) { std::ostringstream s; s.fill('0'); @@ -42,93 +50,280 @@ static bool sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream) for (u32 i = 0; i < sz; i++) s << " " << std::setw(2) << (u32)msg.data[i]; s << "\r\n"; - - asio::ip::tcp::socket& sock = static_cast(stream.socket()); + asio::error_code ec; - asio::write(sock, asio::buffer(s.str()), ec); - return !ec; + + if (dreamcastControllerType == TYPE_DREAMCONN) + { + if (!stream) + return asio::error::not_connected; + asio::ip::tcp::socket& sock = static_cast(stream.socket()); + asio::write(sock, asio::buffer(s.str()), ec); + } + else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB) + { + if (!serial_handler.is_open()) + return asio::error::not_connected; + asio::async_write(serial_handler, asio::buffer(s.str()), asio::transfer_exactly(s.str().size()), [ &serial_handler](const asio::error_code& error, size_t bytes_transferred) + { + if (error) { + serial_handler.cancel(); + } + }); + } + + return ec; } -static bool receiveMsg(MapleMsg& msg, std::istream& stream) +static bool receiveMsg(MapleMsg& msg, std::istream& stream, asio::serial_port& serial_handler, int dreamcastControllerType) { std::string response; - if (!std::getline(stream, response)) - return false; - sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); - if ((msg.getDataSize() - 1) * 3 + 13 >= response.length()) - return false; - for (unsigned i = 0; i < msg.getDataSize(); i++) - sscanf(&response[i * 3 + 12], "%hhx", &msg.data[i]); - return !stream.fail(); + + if (dreamcastControllerType == TYPE_DREAMCONN) + { + if (!std::getline(stream, response)) + return false; + sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); + if ((msg.getDataSize() - 1) * 3 + 13 >= response.length()) + return false; + for (unsigned i = 0; i < msg.getDataSize(); i++) + sscanf(&response[i * 3 + 12], "%hhx", &msg.data[i]); + return !stream.fail(); + } + else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB) + { + asio::error_code ec; + + char c; + for (int i = 0; i < 2; ++i) + { + // discard the first message as we are interested in the second only which returns the controller configuration + response = ""; + while (serial_handler.read_some(asio::buffer(&c, 1), ec) > 0) + { + if (!serial_handler.is_open()) + return false; + if (c == '\n') + break; + response += c; + } + response.pop_back(); + } + + sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); + + if (!ec && serial_handler.is_open()) + return true; + else + return false; + } +} + +static std::string getFirstSerialDevice() { + std::string device_prefix = ""; + +#if defined(_WIN32) + device_prefix = "\\\\.\\COM"; + for (int i = 1; i <= 256; ++i) { + std::string comPort = device_prefix + std::to_string(i); + HANDLE hCom = CreateFile(comPort.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + if (hCom != INVALID_HANDLE_VALUE) { + CloseHandle(hCom); + return "COM" + std::to_string(i); + } + } + return ""; +#endif + +#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) + +#if defined(__linux__) + device_prefix = "ttyACM"; +#elif (defined(__APPLE__) && defined(TARGET_OS_MAC)) + device_prefix = "tty.usbmodem"; +#endif + std::string path = "/dev/"; + DIR *dir; + struct dirent *ent; + if ((dir = opendir(path.c_str())) != NULL) { + while ((ent = readdir(dir)) != NULL) { + std::string device = ent->d_name; + if (device.find(device_prefix) != std::string::npos) { + closedir(dir); + return path + device; + } + } + closedir(dir); + } + return ""; +#endif } void DreamConn::connect() { - iostream = asio::ip::tcp::iostream("localhost", std::to_string(BASE_PORT + bus)); - if (!iostream) { - WARN_LOG(INPUT, "DreamConn[%d] connection failed: %s", bus, iostream.error().message().c_str()); - disconnect(); - return; + maple_io_connected = false; + + asio::error_code ec; + + switch (dreamcastControllerType) { + case TYPE_DREAMCONN: + { +#if !defined(_WIN32) + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ Controller supported on Windows only", bus); + return; +#endif + iostream = asio::ip::tcp::iostream("localhost", std::to_string(BASE_PORT + bus)); + if (!iostream) { + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, iostream.error().message().c_str()); + disconnect(); + return; + } + iostream.expires_from_now(std::chrono::seconds(1)); + break; + } + case TYPE_DREAMCASTCONTROLLERUSB: + { + // the serial port isn't ready at this point, so we need to sleep briefly + // we probably should have a better way to handle this +#if defined(_WIN32) + Sleep(500); +#elif defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) + usleep(500000); +#endif + + serial_handler = asio::serial_port(io_service); + + // select first available serial device + std::string serial_device = getFirstSerialDevice(); + + serial_handler.open(serial_device, ec); + + if (ec || !serial_handler.is_open()) { + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); + disconnect(); + return; + } + break; + } + default: + { + return; + } } - iostream.expires_from_now(std::chrono::seconds(1)); + // Now get the controller configuration MapleMsg msg; msg.command = MDCF_GetCondition; msg.destAP = (bus << 6) | 0x20; msg.originAP = bus << 6; msg.setData(MFID_0_Input); - if (!sendMsg(msg, iostream)) + + ec = sendMsg(msg, iostream, serial_handler, dreamcastControllerType); + if (ec) { - WARN_LOG(INPUT, "DreamConn[%d] communication failed", bus); + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); disconnect(); return; } - if (!receiveMsg(msg, iostream)) { - WARN_LOG(INPUT, "DreamConn[%d] read timeout", bus); + if (!receiveMsg(msg, iostream, serial_handler, dreamcastControllerType)) { + WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus); disconnect(); return; } - iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow + if (dreamcastControllerType == TYPE_DREAMCONN) + iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow + expansionDevs = msg.originAP & 0x1f; - NOTICE_LOG(INPUT, "Connected to DreamConn[%d]: VMU:%d, Rumble Pack:%d", bus, hasVmu(), hasRumble()); + config::MapleExpansionDevices[bus][0] = hasVmu() ? MDT_SegaVMU : MDT_None; config::MapleExpansionDevices[bus][1] = hasRumble() ? MDT_PurupuruPack : MDT_None; + + if (hasVmu() || hasRumble()) + { + NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, dreamcastControllerType == 1 ? "DreamConn+ Controller" : "Dreamcast Controller USB", hasVmu(), hasRumble()); + maple_io_connected = true; + } + else + { + WARN_LOG(INPUT, "DreamcastController[%d] connection: no VMU or Rumble Pack connected", bus); + disconnect(); + return; + } } void DreamConn::disconnect() { - if (iostream) { - iostream.close(); - NOTICE_LOG(INPUT, "Disconnected from DreamConn[%d]", bus); + if (dreamcastControllerType == TYPE_DREAMCONN) + { + if (iostream) + iostream.close(); } + else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB) + { + if (serial_handler.is_open()) + serial_handler.cancel(); + serial_handler.close(); + } + + maple_io_connected = false; + + NOTICE_LOG(INPUT, "Disconnected from DreamcastController[%d]", bus); } bool DreamConn::send(const MapleMsg& msg) { - if (!iostream) + asio::error_code ec; + + if (maple_io_connected) + ec = sendMsg(msg, iostream, serial_handler, dreamcastControllerType); + else return false; - if (!sendMsg(msg, iostream)) { - WARN_LOG(INPUT, "DreamConn[%d] send failed: %s", bus, iostream.error().message().c_str()); + if (ec) { + maple_io_connected = false; + WARN_LOG(INPUT, "DreamcastController[%d] send failed: %s", bus, ec.message().c_str()); + disconnect(); return false; } return true; } -bool DreamConnGamepad::isDreamConn(int deviceIndex) +bool DreamConnGamepad::isDreamcastController(int deviceIndex) { char guid_str[33] {}; SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(deviceIndex), guid_str, sizeof(guid_str)); INFO_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str, guid_str[10], guid_str[11], guid_str[8], guid_str[9], guid_str[18], guid_str[19], guid_str[16], guid_str[17]); + // DreamConn VID:4457 PID:4443 - return memcmp("5744000043440000", guid_str + 8, 16) == 0; + // Dreamcast Controller USB VID:1209 PID:2f07 + if (memcmp("5744000043440000", guid_str + 8, 16) == 0 || memcmp("09120000072f0000", guid_str + 8, 16) == 0) + { + return true; + } + return false; } DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) : SDLGamepad(maple_port, joystick_idx, sdl_joystick) { - _name = "DreamConn+ Controller"; + char guid_str[33] {}; + + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(joystick_idx), guid_str, sizeof(guid_str)); + + // DreamConn VID:4457 PID:4443 + // Dreamcast Controller USB VID:1209 PID:2f07 + if (memcmp("5744000043440000", guid_str + 8, 16) == 0) + { + dreamcastControllerType = TYPE_DREAMCONN; + _name = "DreamConn+ Controller"; + } + else if (memcmp("09120000072f0000", guid_str + 8, 16) == 0) + { + dreamcastControllerType = TYPE_DREAMCASTCONTROLLERUSB; + _name = "Dreamcast Controller USB"; + } + EventManager::listen(Event::Start, handleEvent, this); EventManager::listen(Event::LoadState, handleEvent, this); } @@ -145,7 +340,7 @@ void DreamConnGamepad::set_maple_port(int port) } else if (dreamconn == nullptr || dreamconn->getBus() != port) { dreamconn.reset(); - dreamconn = std::make_shared(port); + dreamconn = std::make_shared(port, dreamcastControllerType); } SDLGamepad::set_maple_port(port); } @@ -205,7 +400,7 @@ void DreamConn::connect() { void DreamConn::disconnect() { } -bool DreamConnGamepad::isDreamConn(int deviceIndex) { +bool DreamConnGamepad::isDreamcastController(int deviceIndex) { return false; } DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index bf2e32680..572fcf427 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -20,8 +20,10 @@ #include "types.h" #include "emulator.h" #include "sdl_gamepad.h" -#if defined(_WIN32) && !defined(TARGET_UWP) -#define USE_DREAMCONN 1 +#if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) +#define USE_DREAMCASTCONTROLLER 1 +#define TYPE_DREAMCONN 1 +#define TYPE_DREAMCASTCONTROLLERUSB 2 #include #endif @@ -48,14 +50,17 @@ static_assert(sizeof(MapleMsg) == 1028); class DreamConn { const int bus; -#ifdef USE_DREAMCONN + const int dreamcastControllerType; asio::ip::tcp::iostream iostream; -#endif + asio::io_context io_context; + asio::io_service io_service; + asio::serial_port serial_handler{io_context}; + bool maple_io_connected; u8 expansionDevs = 0; static constexpr u16 BASE_PORT = 37393; public: - DreamConn(int bus) : bus(bus) { + DreamConn(int bus, int dreamcastControllerType) : bus(bus), dreamcastControllerType(dreamcastControllerType) { connect(); } ~DreamConn() { @@ -67,6 +72,9 @@ public: int getBus() const { return bus; } + int getDreamcastControllerType() const { + return dreamcastControllerType; + } bool hasVmu() { return expansionDevs & 1; } @@ -88,7 +96,7 @@ public: void set_maple_port(int port) override; bool gamepad_btn_input(u32 code, bool pressed) override; bool gamepad_axis_input(u32 code, int value) override; - static bool isDreamConn(int deviceIndex); + static bool isDreamcastController(int deviceIndex); private: static void handleEvent(Event event, void *arg); @@ -98,4 +106,5 @@ private: bool ltrigPressed = false; bool rtrigPressed = false; bool startPressed = false; + int dreamcastControllerType; }; diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index a93c19e3c..455634742 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -83,7 +83,7 @@ static void sdl_open_joystick(int index) std::shared_ptr gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); #else std::shared_ptr gamepad; - if (DreamConnGamepad::isDreamConn(index)) + if (DreamConnGamepad::isDreamcastController(index)) gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); else gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); From aee932808e01d886a8b14c7ae4356590c326952d Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Mon, 20 Jan 2025 15:12:36 +0100 Subject: [PATCH 41/76] Removed unused function getDreamcastControllerType() --- core/sdl/dreamconn.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index 572fcf427..c82729dc3 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -72,9 +72,6 @@ public: int getBus() const { return bus; } - int getDreamcastControllerType() const { - return dreamcastControllerType; - } bool hasVmu() { return expansionDevs & 1; } From 160828a4c76b741fac7df9ca7f8b46aece478f7d Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Mon, 20 Jan 2025 15:16:23 +0100 Subject: [PATCH 42/76] Overrode default mapping for DreamcastControllerUsb on MacOS/Windows --- core/sdl/dreamconn.cpp | 21 +++++-- core/sdl/sdl_gamepad.h | 121 ++++++++++++++++++++++++++++++++--------- 2 files changed, 113 insertions(+), 29 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 12cec51f4..01968697f 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -169,7 +169,7 @@ void DreamConn::connect() case TYPE_DREAMCONN: { #if !defined(_WIN32) - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ Controller supported on Windows only", bus); + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ / DreamConn S Controller supported on Windows only", bus); return; #endif iostream = asio::ip::tcp::iostream("localhost", std::to_string(BASE_PORT + bus)); @@ -240,7 +240,7 @@ void DreamConn::connect() if (hasVmu() || hasRumble()) { - NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, dreamcastControllerType == 1 ? "DreamConn+ Controller" : "Dreamcast Controller USB", hasVmu(), hasRumble()); + NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, dreamcastControllerType == 1 ? "DreamConn+ / DreamcConn S Controller" : "Dreamcast Controller USB", hasVmu(), hasRumble()); maple_io_connected = true; } else @@ -316,7 +316,7 @@ DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystic if (memcmp("5744000043440000", guid_str + 8, 16) == 0) { dreamcastControllerType = TYPE_DREAMCONN; - _name = "DreamConn+ Controller"; + _name = "DreamConn+ / DreamConn S Controller"; } else if (memcmp("09120000072f0000", guid_str + 8, 16) == 0) { @@ -372,14 +372,27 @@ bool DreamConnGamepad::gamepad_axis_input(u32 code, int value) { if (!is_detecting_input()) { + // For DreamcastControllerUsb, we need to diff between Linux which exposes the UDB HID event codes as expected, and MacOS/Windows which require a dedicated mapping + // Change is temporary for testing - will be superseded by an extension of SDL providing default mappings for DreamcastControllerUsb +#if defined(_WIN32) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) + if ((code == leftTrigger && dreamcastControllerType == TYPE_DREAMCONN) || (code == 2 && dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB)) { + ltrigPressed = value > 0; + checkKeyCombo(); + } + if ((code == rightTrigger && dreamcastControllerType == TYPE_DREAMCONN) || (code == 5 && dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB)) { + rtrigPressed = value > 0; + checkKeyCombo(); + } +#elif defined(__linux__) if (code == leftTrigger) { ltrigPressed = value > 0; checkKeyCombo(); } - else if (code == rightTrigger) { + if (code == rightTrigger) { rtrigPressed = value > 0; checkKeyCombo(); } +#endif } else { ltrigPressed = false; diff --git a/core/sdl/sdl_gamepad.h b/core/sdl/sdl_gamepad.h index d9820e5e4..6ef0ba095 100644 --- a/core/sdl/sdl_gamepad.h +++ b/core/sdl/sdl_gamepad.h @@ -4,28 +4,73 @@ #include "stdclass.h" #include "sdl.h" -template +template class DefaultInputMapping : public InputMapping { public: DefaultInputMapping() { - name = "Default"; - set_button(DC_BTN_Y, 0); - set_button(DC_BTN_B, 1); - set_button(DC_BTN_A, 2); - set_button(DC_BTN_X, 3); - set_button(DC_BTN_START, 9); + if (!DreamcastControllerUsb) + { + name = "Default"; + set_button(DC_BTN_Y, 0); + set_button(DC_BTN_B, 1); + set_button(DC_BTN_A, 2); + set_button(DC_BTN_X, 3); + set_button(DC_BTN_START, 9); - set_axis(0, DC_AXIS_LEFT, 0, false); - set_axis(0, DC_AXIS_RIGHT, 0, true); - set_axis(0, DC_AXIS_UP, 1, false); - set_axis(0, DC_AXIS_DOWN, 1, true); - set_axis(0, DC_AXIS2_LEFT, 2, false); - set_axis(0, DC_AXIS2_RIGHT, 2, true); - set_axis(0, DC_AXIS2_UP, 3, false); - set_axis(0, DC_AXIS2_DOWN, 3, true); - dirty = false; + set_axis(0, DC_AXIS_LEFT, 0, false); + set_axis(0, DC_AXIS_RIGHT, 0, true); + set_axis(0, DC_AXIS_UP, 1, false); + set_axis(0, DC_AXIS_DOWN, 1, true); + set_axis(0, DC_AXIS2_LEFT, 2, false); + set_axis(0, DC_AXIS2_RIGHT, 2, true); + set_axis(0, DC_AXIS2_UP, 3, false); + set_axis(0, DC_AXIS2_DOWN, 3, true); + dirty = false; + } + else + { + // For DreamcastControllerUsb, we need to diff between Linux which exposes the UDB HID event codes as expected, and MacOS/Windows which require a dedicated mapping + // Change is temporary for testing - will be superseded by an extension of SDL providing default mappings for DreamcastControllerUsb + name = "Dreamcast Controller USB"; +#if defined(__linux__) + set_button(DC_BTN_Y, 0); + set_button(DC_BTN_B, 1); + set_button(DC_BTN_A, 2); + set_button(DC_BTN_X, 3); + set_button(DC_BTN_START, 9); + + set_axis(0, DC_AXIS_LEFT, 0, false); + set_axis(0, DC_AXIS_RIGHT, 0, true); + set_axis(0, DC_AXIS_UP, 1, false); + set_axis(0, DC_AXIS_DOWN, 1, true); + set_axis(0, DC_AXIS2_LEFT, 2, false); + set_axis(0, DC_AXIS2_RIGHT, 2, true); + set_axis(0, DC_AXIS2_UP, 3, false); + set_axis(0, DC_AXIS2_DOWN, 3, true); + dirty = false; +#elif defined(_WIN32) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) + set_button(DC_DPAD_UP, 256); + set_button(DC_DPAD_DOWN, 257); + set_button(DC_DPAD_LEFT, 258); + set_button(DC_DPAD_RIGHT, 259); + + set_button(DC_BTN_Y, 4); + set_button(DC_BTN_B, 1); + set_button(DC_BTN_A, 0); + set_button(DC_BTN_X, 3); + set_button(DC_BTN_START, 11); + + set_axis(0, DC_AXIS_LEFT, 0, false); + set_axis(0, DC_AXIS_RIGHT, 0, true); + set_axis(0, DC_AXIS_UP, 1, false); + set_axis(0, DC_AXIS_DOWN, 1, true); + set_axis(0, DC_AXIS_LT, 2, true); + set_axis(0, DC_AXIS_RT, 5, true); + dirty = false; +#endif + } } DefaultInputMapping(SDL_GameController *sdlController) : DefaultInputMapping() @@ -157,7 +202,10 @@ public: dirty = false; } else - INFO_LOG(INPUT, "using default mapping"); + if (DreamcastControllerUsb) + INFO_LOG(INPUT, "using default mapping for Dreamcast Controller USB"); + else + INFO_LOG(INPUT, "using default mapping"); } }; @@ -197,7 +245,18 @@ public: } if (!find_mapping()) - input_mapper = std::make_shared>(sdl_controller); + { + // We explicitely check for Dreamcast Controller USB with VID:1209 PID:2f07 to override the default button mapping + // Change is temporary for testing - will be superseded by an extension of SDL providing default mappings for DreamcastControllerUsb + char guid_str[33] {}; + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(joystick_idx), guid_str, sizeof(guid_str)); + if (memcmp("09120000072f0000", guid_str + 8, 16) == 0) + { + input_mapper = std::make_shared>(sdl_controller); + } + else + input_mapper = std::make_shared>(sdl_controller); + } else INFO_LOG(INPUT, "using custom mapping '%s'", input_mapper->name.c_str()); @@ -607,16 +666,28 @@ public: void resetMappingToDefault(bool arcade, bool gamepad) override { - NOTICE_LOG(INPUT, "Resetting SDL gamepad to default: %d %d", arcade, gamepad); - if (arcade) + // We explicitely check for Dreamcast Controller USB with VID:1209 PID:2f07 to override the default button mapping + // Change is temporary for testing - will be superseded by an extension of SDL providing default mappings for DreamcastControllerUsb + char guid_str[33] {}; + SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(this->sdl_joystick), guid_str, sizeof(guid_str)); + if (memcmp("09120000072f0000", guid_str + 8, 16) == 0) { - if (gamepad) - input_mapper = std::make_shared>(sdl_controller); - else - input_mapper = std::make_shared>(sdl_controller); + NOTICE_LOG(INPUT, "Resetting SDL gamepad to default for Dreamcast Controller USB"); + input_mapper = std::make_shared>(sdl_controller); } else - input_mapper = std::make_shared>(sdl_controller); + { + NOTICE_LOG(INPUT, "Resetting SDL gamepad to default: %d %d", arcade, gamepad); + if (arcade) + { + if (gamepad) + input_mapper = std::make_shared>(sdl_controller); + else + input_mapper = std::make_shared>(sdl_controller); + } + else + input_mapper = std::make_shared>(sdl_controller); + } } From 28f5dd983e6c6d123132862f5bbafcf1b7d5db6c Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Mon, 20 Jan 2025 15:58:19 +0100 Subject: [PATCH 43/76] Now properly setting and checking left/right Trigger --- core/sdl/dreamconn.cpp | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 01968697f..5d0a6f00a 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -322,6 +322,8 @@ DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystic { dreamcastControllerType = TYPE_DREAMCASTCONTROLLERUSB; _name = "Dreamcast Controller USB"; + leftTrigger = 2; + rightTrigger = 5; } EventManager::listen(Event::Start, handleEvent, this); @@ -372,27 +374,14 @@ bool DreamConnGamepad::gamepad_axis_input(u32 code, int value) { if (!is_detecting_input()) { - // For DreamcastControllerUsb, we need to diff between Linux which exposes the UDB HID event codes as expected, and MacOS/Windows which require a dedicated mapping - // Change is temporary for testing - will be superseded by an extension of SDL providing default mappings for DreamcastControllerUsb -#if defined(_WIN32) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) - if ((code == leftTrigger && dreamcastControllerType == TYPE_DREAMCONN) || (code == 2 && dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB)) { - ltrigPressed = value > 0; - checkKeyCombo(); - } - if ((code == rightTrigger && dreamcastControllerType == TYPE_DREAMCONN) || (code == 5 && dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB)) { - rtrigPressed = value > 0; - checkKeyCombo(); - } -#elif defined(__linux__) if (code == leftTrigger) { ltrigPressed = value > 0; checkKeyCombo(); } - if (code == rightTrigger) { + else if (code == rightTrigger) { rtrigPressed = value > 0; checkKeyCombo(); } -#endif } else { ltrigPressed = false; From cb2d3e3776227696bcdb236607ef2c9f43426d2b Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Tue, 21 Jan 2025 17:39:37 +0100 Subject: [PATCH 44/76] Maple Message to Dreamcast Controller USB now prefixed for compatibility with newest version --- core/sdl/dreamconn.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 5d0a6f00a..e9365135e 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -41,6 +41,12 @@ static asio::error_code sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& st { std::ostringstream s; s.fill('0'); + if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB) + { + // Messages to Dreamcast Controller USB need to be prefixed to trigger the correct parser + s << "X "; + } + s << std::hex << std::uppercase << std::setw(2) << (u32)msg.command << " " << std::setw(2) << (u32)msg.destAP << " " From 32c274f407be19250316c0a2e0c72f446871c4ce Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Tue, 21 Jan 2025 17:41:58 +0100 Subject: [PATCH 45/76] Updated serial device search on Windows to select only matching VID/PID --- core/sdl/dreamconn.cpp | 53 +++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index e9365135e..96f10e795 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -32,7 +32,8 @@ #endif #if defined(_WIN32) -#include +#include +#include #endif void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); @@ -123,24 +124,53 @@ static bool receiveMsg(MapleMsg& msg, std::istream& stream, asio::serial_port& s else return false; } + + return false; } static std::string getFirstSerialDevice() { - std::string device_prefix = ""; - + + // On Windows, we get the first serial device matching our VID/PID #if defined(_WIN32) - device_prefix = "\\\\.\\COM"; - for (int i = 1; i <= 256; ++i) { - std::string comPort = device_prefix + std::to_string(i); - HANDLE hCom = CreateFile(comPort.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); - if (hCom != INVALID_HANDLE_VALUE) { - CloseHandle(hCom); - return "COM" + std::to_string(i); + HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES); + if (deviceInfoSet == INVALID_HANDLE_VALUE) { + return ""; + } + + SP_DEVINFO_DATA deviceInfoData; + deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); + + for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) { + DWORD dataType, bufferSize = 0; + SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, NULL, 0, &bufferSize); + + if (bufferSize > 0) { + std::vector buffer(bufferSize); + if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)buffer.data(), bufferSize, NULL)) { + std::string hardwareId(buffer.begin(), buffer.end()); + if (hardwareId.find("VID_1209") != std::string::npos && hardwareId.find("PID_2F07") != std::string::npos) { + HKEY deviceKey = SetupDiOpenDevRegKey(deviceInfoSet, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); + if (deviceKey != INVALID_HANDLE_VALUE) { + char portName[256]; + DWORD portNameSize = sizeof(portName); + if (RegQueryValueEx(deviceKey, "PortName", NULL, NULL, (LPBYTE)portName, &portNameSize) == ERROR_SUCCESS) { + RegCloseKey(deviceKey); + SetupDiDestroyDeviceInfoList(deviceInfoSet); + return std::string(portName); + } + RegCloseKey(deviceKey); + } + } + } } } + + SetupDiDestroyDeviceInfoList(deviceInfoSet); return ""; #endif - + + // On MacOS/Linux, we get the first serial device matching the device prefix + std::string device_prefix = ""; #if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) #if defined(__linux__) @@ -148,6 +178,7 @@ static std::string getFirstSerialDevice() { #elif (defined(__APPLE__) && defined(TARGET_OS_MAC)) device_prefix = "tty.usbmodem"; #endif + std::string path = "/dev/"; DIR *dir; struct dirent *ent; From b3fa453d75f054af02aaf26eafbba437e29139db Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Thu, 23 Jan 2025 20:01:54 +0100 Subject: [PATCH 46/76] dreamconn: asio::serial_port isn't defined on all platforms --- core/sdl/dreamconn.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index c82729dc3..53dadf0b9 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -51,10 +51,12 @@ class DreamConn { const int bus; const int dreamcastControllerType; +#ifdef USE_DREAMCASTCONTROLLER asio::ip::tcp::iostream iostream; asio::io_context io_context; asio::io_service io_service; asio::serial_port serial_handler{io_context}; +#endif bool maple_io_connected; u8 expansionDevs = 0; static constexpr u16 BASE_PORT = 37393; From 4389a3ffa662c93324756614bcf19ad8c8a694d1 Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Fri, 24 Jan 2025 10:55:21 +0100 Subject: [PATCH 47/76] Reverted Dreamcast Controller USB mapping workaround --- core/sdl/dreamconn.cpp | 2 - core/sdl/sdl_gamepad.h | 121 +++++++++-------------------------------- 2 files changed, 25 insertions(+), 98 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 96f10e795..f323a33c9 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -359,8 +359,6 @@ DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystic { dreamcastControllerType = TYPE_DREAMCASTCONTROLLERUSB; _name = "Dreamcast Controller USB"; - leftTrigger = 2; - rightTrigger = 5; } EventManager::listen(Event::Start, handleEvent, this); diff --git a/core/sdl/sdl_gamepad.h b/core/sdl/sdl_gamepad.h index 6ef0ba095..d9820e5e4 100644 --- a/core/sdl/sdl_gamepad.h +++ b/core/sdl/sdl_gamepad.h @@ -4,73 +4,28 @@ #include "stdclass.h" #include "sdl.h" -template +template class DefaultInputMapping : public InputMapping { public: DefaultInputMapping() { - if (!DreamcastControllerUsb) - { - name = "Default"; - set_button(DC_BTN_Y, 0); - set_button(DC_BTN_B, 1); - set_button(DC_BTN_A, 2); - set_button(DC_BTN_X, 3); - set_button(DC_BTN_START, 9); + name = "Default"; + set_button(DC_BTN_Y, 0); + set_button(DC_BTN_B, 1); + set_button(DC_BTN_A, 2); + set_button(DC_BTN_X, 3); + set_button(DC_BTN_START, 9); - set_axis(0, DC_AXIS_LEFT, 0, false); - set_axis(0, DC_AXIS_RIGHT, 0, true); - set_axis(0, DC_AXIS_UP, 1, false); - set_axis(0, DC_AXIS_DOWN, 1, true); - set_axis(0, DC_AXIS2_LEFT, 2, false); - set_axis(0, DC_AXIS2_RIGHT, 2, true); - set_axis(0, DC_AXIS2_UP, 3, false); - set_axis(0, DC_AXIS2_DOWN, 3, true); - dirty = false; - } - else - { - // For DreamcastControllerUsb, we need to diff between Linux which exposes the UDB HID event codes as expected, and MacOS/Windows which require a dedicated mapping - // Change is temporary for testing - will be superseded by an extension of SDL providing default mappings for DreamcastControllerUsb - name = "Dreamcast Controller USB"; -#if defined(__linux__) - set_button(DC_BTN_Y, 0); - set_button(DC_BTN_B, 1); - set_button(DC_BTN_A, 2); - set_button(DC_BTN_X, 3); - set_button(DC_BTN_START, 9); - - set_axis(0, DC_AXIS_LEFT, 0, false); - set_axis(0, DC_AXIS_RIGHT, 0, true); - set_axis(0, DC_AXIS_UP, 1, false); - set_axis(0, DC_AXIS_DOWN, 1, true); - set_axis(0, DC_AXIS2_LEFT, 2, false); - set_axis(0, DC_AXIS2_RIGHT, 2, true); - set_axis(0, DC_AXIS2_UP, 3, false); - set_axis(0, DC_AXIS2_DOWN, 3, true); - dirty = false; -#elif defined(_WIN32) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) - set_button(DC_DPAD_UP, 256); - set_button(DC_DPAD_DOWN, 257); - set_button(DC_DPAD_LEFT, 258); - set_button(DC_DPAD_RIGHT, 259); - - set_button(DC_BTN_Y, 4); - set_button(DC_BTN_B, 1); - set_button(DC_BTN_A, 0); - set_button(DC_BTN_X, 3); - set_button(DC_BTN_START, 11); - - set_axis(0, DC_AXIS_LEFT, 0, false); - set_axis(0, DC_AXIS_RIGHT, 0, true); - set_axis(0, DC_AXIS_UP, 1, false); - set_axis(0, DC_AXIS_DOWN, 1, true); - set_axis(0, DC_AXIS_LT, 2, true); - set_axis(0, DC_AXIS_RT, 5, true); - dirty = false; -#endif - } + set_axis(0, DC_AXIS_LEFT, 0, false); + set_axis(0, DC_AXIS_RIGHT, 0, true); + set_axis(0, DC_AXIS_UP, 1, false); + set_axis(0, DC_AXIS_DOWN, 1, true); + set_axis(0, DC_AXIS2_LEFT, 2, false); + set_axis(0, DC_AXIS2_RIGHT, 2, true); + set_axis(0, DC_AXIS2_UP, 3, false); + set_axis(0, DC_AXIS2_DOWN, 3, true); + dirty = false; } DefaultInputMapping(SDL_GameController *sdlController) : DefaultInputMapping() @@ -202,10 +157,7 @@ public: dirty = false; } else - if (DreamcastControllerUsb) - INFO_LOG(INPUT, "using default mapping for Dreamcast Controller USB"); - else - INFO_LOG(INPUT, "using default mapping"); + INFO_LOG(INPUT, "using default mapping"); } }; @@ -245,18 +197,7 @@ public: } if (!find_mapping()) - { - // We explicitely check for Dreamcast Controller USB with VID:1209 PID:2f07 to override the default button mapping - // Change is temporary for testing - will be superseded by an extension of SDL providing default mappings for DreamcastControllerUsb - char guid_str[33] {}; - SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(joystick_idx), guid_str, sizeof(guid_str)); - if (memcmp("09120000072f0000", guid_str + 8, 16) == 0) - { - input_mapper = std::make_shared>(sdl_controller); - } - else - input_mapper = std::make_shared>(sdl_controller); - } + input_mapper = std::make_shared>(sdl_controller); else INFO_LOG(INPUT, "using custom mapping '%s'", input_mapper->name.c_str()); @@ -666,28 +607,16 @@ public: void resetMappingToDefault(bool arcade, bool gamepad) override { - // We explicitely check for Dreamcast Controller USB with VID:1209 PID:2f07 to override the default button mapping - // Change is temporary for testing - will be superseded by an extension of SDL providing default mappings for DreamcastControllerUsb - char guid_str[33] {}; - SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(this->sdl_joystick), guid_str, sizeof(guid_str)); - if (memcmp("09120000072f0000", guid_str + 8, 16) == 0) + NOTICE_LOG(INPUT, "Resetting SDL gamepad to default: %d %d", arcade, gamepad); + if (arcade) { - NOTICE_LOG(INPUT, "Resetting SDL gamepad to default for Dreamcast Controller USB"); - input_mapper = std::make_shared>(sdl_controller); + if (gamepad) + input_mapper = std::make_shared>(sdl_controller); + else + input_mapper = std::make_shared>(sdl_controller); } else - { - NOTICE_LOG(INPUT, "Resetting SDL gamepad to default: %d %d", arcade, gamepad); - if (arcade) - { - if (gamepad) - input_mapper = std::make_shared>(sdl_controller); - else - input_mapper = std::make_shared>(sdl_controller); - } - else - input_mapper = std::make_shared>(sdl_controller); - } + input_mapper = std::make_shared>(sdl_controller); } From ce0060126e8e9410360ec0f06af9f447707cf6ba Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Fri, 24 Jan 2025 10:55:59 +0100 Subject: [PATCH 48/76] Now adding mappings for Dreamcast Controller USB during input_sdl_init() --- core/sdl/sdl.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index 455634742..44d269507 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -266,6 +266,15 @@ void input_sdl_init() if (settings.input.keyboardLangId == KeyboardLayout::US) settings.input.keyboardLangId = detectKeyboardLayout(); barcode.clear(); + + // Add MacOS and Windows mappings for Dreamcast Controller USB + // Linux mappings are OK by default + // Can be removed once mapping is merged into SDL, see https://github.com/libsdl-org/SDL/pull/12039 +#if (defined(__APPLE__) && defined(TARGET_OS_MAC)) + SDL_GameControllerAddMapping("0300000009120000072f000000010000,OrangeFox86 Dreamcast Controller USB,crc:3cef,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:a2,righttrigger:a5,start:b11"); +#elif defined(_WIN32) + SDL_GameControllerAddMapping("0300000009120000072f000000000000,OrangeFox86 Dreamcast Controller USB,crc:baa5,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:-a2,righttrigger:-a5,start:b11"); +#endif } void input_sdl_quit() From 26d386f83300a0af4b94054a5cc309f35de027a7 Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Fri, 24 Jan 2025 16:00:45 +0100 Subject: [PATCH 49/76] Fixed handling of asio::io_context --- core/sdl/dreamconn.cpp | 12 ++++++++---- core/sdl/dreamconn.h | 1 - 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index f323a33c9..9f56ef560 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -38,7 +38,7 @@ void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); -static asio::error_code sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream, asio::serial_port& serial_handler, int dreamcastControllerType) +static asio::error_code sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream, asio::io_context& io_context, asio::serial_port& serial_handler, int dreamcastControllerType) { std::ostringstream s; s.fill('0'); @@ -69,6 +69,9 @@ static asio::error_code sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& st } else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB) { + io_context.run(); + io_context.reset(); + if (!serial_handler.is_open()) return asio::error::not_connected; asio::async_write(serial_handler, asio::buffer(s.str()), asio::transfer_exactly(s.str().size()), [ &serial_handler](const asio::error_code& error, size_t bytes_transferred) @@ -228,7 +231,7 @@ void DreamConn::connect() usleep(500000); #endif - serial_handler = asio::serial_port(io_service); + serial_handler = asio::serial_port(io_context); // select first available serial device std::string serial_device = getFirstSerialDevice(); @@ -255,7 +258,7 @@ void DreamConn::connect() msg.originAP = bus << 6; msg.setData(MFID_0_Input); - ec = sendMsg(msg, iostream, serial_handler, dreamcastControllerType); + ec = sendMsg(msg, iostream, io_context, serial_handler, dreamcastControllerType); if (ec) { WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); @@ -300,6 +303,7 @@ void DreamConn::disconnect() if (serial_handler.is_open()) serial_handler.cancel(); serial_handler.close(); + io_context.stop(); } maple_io_connected = false; @@ -312,7 +316,7 @@ bool DreamConn::send(const MapleMsg& msg) asio::error_code ec; if (maple_io_connected) - ec = sendMsg(msg, iostream, serial_handler, dreamcastControllerType); + ec = sendMsg(msg, iostream, io_context, serial_handler, dreamcastControllerType); else return false; if (ec) { diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index 53dadf0b9..d883c61fc 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -54,7 +54,6 @@ class DreamConn #ifdef USE_DREAMCASTCONTROLLER asio::ip::tcp::iostream iostream; asio::io_context io_context; - asio::io_service io_service; asio::serial_port serial_handler{io_context}; #endif bool maple_io_connected; From b92ac1544eac841336e8800ba12b8664f3e4b02a Mon Sep 17 00:00:00 2001 From: scribam Date: Sat, 25 Jan 2025 12:38:32 +0100 Subject: [PATCH 50/76] android: update project --- shell/android-studio/gradle/libs.versions.toml | 2 +- shell/android-studio/gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/android-studio/gradle/libs.versions.toml b/shell/android-studio/gradle/libs.versions.toml index 64a4f0144..289690912 100644 --- a/shell/android-studio/gradle/libs.versions.toml +++ b/shell/android-studio/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.7.0" +agp = "8.8.0" appcompat = "1.3.1" commonsLang3 = "3.12.0" documentfile = "1.0.1" diff --git a/shell/android-studio/gradle/wrapper/gradle-wrapper.properties b/shell/android-studio/gradle/wrapper/gradle-wrapper.properties index 8a516d151..7dd873057 100644 --- a/shell/android-studio/gradle/wrapper/gradle-wrapper.properties +++ b/shell/android-studio/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Oct 12 11:37:50 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 57a3e4ca53c6bd5f8f448dadf7fc1c3d4cf7667c Mon Sep 17 00:00:00 2001 From: scribam Date: Sat, 25 Jan 2025 12:38:32 +0100 Subject: [PATCH 51/76] ci: update netbsd version --- .github/workflows/bsd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bsd.yml b/.github/workflows/bsd.yml index 7beecc583..a0f53a031 100644 --- a/.github/workflows/bsd.yml +++ b/.github/workflows/bsd.yml @@ -16,7 +16,7 @@ jobs: version: '14.2' pkginstall: sudo pkg install -y alsa-lib ccache cmake evdev-proto git libao libevdev libudev-devd libzip lua54 miniupnpc ninja pkgconf pulseaudio sdl2 libcdio - operating_system: netbsd - version: '10.0' + version: '10.1' pkginstall: sudo pkgin update && sudo pkgin -y install alsa-lib ccache cmake gcc12 git libao libzip lua54 miniupnpc ninja-build pkgconf pulseaudio SDL2 libcdio && export PATH=/usr/pkg/gcc12/bin:$PATH - operating_system: openbsd version: '7.6' @@ -36,7 +36,7 @@ jobs: key: ccache-${{ matrix.operating_system }}-${{ matrix.architecture }}-${{ github.sha }} restore-keys: ccache-${{ matrix.operating_system }}-${{ matrix.architecture }}- - - uses: cross-platform-actions/action@v0.26.0 + - uses: cross-platform-actions/action@v0.27.0 with: operating_system: ${{ matrix.operating_system }} architecture: ${{ matrix.architecture }} From ccdd53ee0e41eef6be845d9b13714e52c5980ee1 Mon Sep 17 00:00:00 2001 From: scribam Date: Sat, 25 Jan 2025 12:38:32 +0100 Subject: [PATCH 52/76] deps: update sdl to version 2.30.11 --- core/deps/SDL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deps/SDL b/core/deps/SDL index 9c821dc21..fa24d868a 160000 --- a/core/deps/SDL +++ b/core/deps/SDL @@ -1 +1 @@ -Subproject commit 9c821dc21ccbd69b2bda421fdb35cb4ae2da8f5e +Subproject commit fa24d868ac2f8fd558e4e914c9863411245db8fd From b70c2791b256453135b33f612a53ef944a915def Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 27 Jan 2025 17:30:06 +0100 Subject: [PATCH 53/76] dynarec: proper ftrc implem. (arm32/64) Use double for fipr/ftrv Use double for canonical FIPR and FTRV on all platforms. Fix interpreter implementation of FTRC. Fix canonical implementation of cvt_f2i (FTRC). arm32: use Vfma instead of Vmla for FMAC. Vfma does not a fused muliply-add, Vmla doesn't. arm32: Use canonical implementations of FIPR and FTRV. arm32,arm64: Correct implemetation of cvt_f2i (FTRC) Fixes desync with NBA 2k1/2k2 online games. --- core/hw/sh4/dyna/shil_canonical.h | 46 +++++++++++-------------------- core/hw/sh4/interpr/sh4_fpu.cpp | 33 +++++++++++++--------- core/rec-ARM/rec_arm.cpp | 22 +++++++++++---- core/rec-ARM64/rec_arm64.cpp | 14 +++++++++- 4 files changed, 65 insertions(+), 50 deletions(-) diff --git a/core/hw/sh4/dyna/shil_canonical.h b/core/hw/sh4/dyna/shil_canonical.h index 21e1a8230..fd9918c22 100644 --- a/core/hw/sh4/dyna/shil_canonical.h +++ b/core/hw/sh4/dyna/shil_canonical.h @@ -141,19 +141,11 @@ shil_compile( \ template static inline float innerProduct(const float *f1, const float *f2) { -#if HOST_CPU == CPU_X86 || HOST_CPU == CPU_X64 || HOST_CPU == CPU_ARM64 const double f = (double)f1[0] * f2[Stride * 0] + (double)f1[1] * f2[Stride * 1] + (double)f1[2] * f2[Stride * 2] + (double)f1[3] * f2[Stride * 3]; return fixNaN((float)f); -#else - const float f = f1[0] * f2[Stride * 0] - + f1[1] * f2[Stride * 1] - + f1[2] * f2[Stride * 2] - + f1[3] * f2[Stride * 3]; - return fixNaN(f); -#endif } #endif @@ -723,33 +715,27 @@ shil_opc_end() //shop_cvt_f2i_t //float to integer : truncate shil_opc(cvt_f2i_t) -#if HOST_CPU == CPU_X86 || HOST_CPU == CPU_X64 shil_canonical ( u32,f1,(f32 f1), - if (f1 > 2147483520.0f) // IEEE 754: 0x4effffff - return 0x7fffffff; - else - { - s32 res = (s32)f1; - - // Fix result sign for Intel CPUs - if ((u32)res == 0x80000000 && f1 == f1 && *(s32 *)&f1 > 0) - res = 0x7fffffff; - - return res; + s32 res; + if (f1 > 2147483520.0f) { // IEEE 754: 0x4effffff + res = 0x7fffffff; } -) -#else -shil_canonical -( -u32,f1,(f32 f1), - if (f1 > 2147483520.0f) // IEEE 754: 0x4effffff - return 0x7fffffff; - else - return (s32)f1; -) + else { + res = (s32)f1; +#if HOST_CPU == CPU_X86 || HOST_CPU == CPU_X64 + // Fix result sign for Intel CPUs + if ((u32)res == 0x80000000 && f1 > 0) + res = 0x7fffffff; +#elif HOST_CPU == CPU_ARM || HOST_CPU == CPU_ARM64 + // conversion of NaN returns 0 on ARM + if (std::isnan(f1)) + res = 0x80000000; #endif + } + return res; +) shil_compile ( diff --git a/core/hw/sh4/interpr/sh4_fpu.cpp b/core/hw/sh4/interpr/sh4_fpu.cpp index 231f45fe3..c05951102 100644 --- a/core/hw/sh4/interpr/sh4_fpu.cpp +++ b/core/hw/sh4/interpr/sh4_fpu.cpp @@ -519,28 +519,35 @@ sh4op(i1111_nnnn_0011_1101) if (ctx->fpscr.PR == 0) { u32 n = GetN(op); - ctx->fpul = (u32)(s32)ctx->fr[n]; - - if ((s32)ctx->fpul > 0x7fffff80) - ctx->fpul = 0x7fffffff; - // Intel CPUs convert out of range float numbers to 0x80000000. Manually set the correct sign - else if (ctx->fpul == 0x80000000 && ctx->fr[n] == ctx->fr[n]) + if (std::isnan(ctx->fr[n])) { + ctx->fpul = 0x80000000; + } + else { - if (*(int *)&ctx->fr[n] > 0) // Using integer math to avoid issues with Inf and NaN + ctx->fpul = (u32)(s32)ctx->fr[n]; + if ((s32)ctx->fpul > 0x7fffff80) + ctx->fpul = 0x7fffffff; +#if HOST_CPU == CPU_X86 || HOST_CPU == CPU_X64 + // Intel CPUs convert out of range float numbers to 0x80000000. Manually set the correct sign + else if (ctx->fpul == 0x80000000 && ctx->fr[n] > 0) ctx->fpul--; +#endif } } else { f64 f = getDRn(ctx, op); - ctx->fpul = (u32)(s32)f; - - // TODO saturate - // Intel CPUs convert out of range float numbers to 0x80000000. Manually set the correct sign - if (ctx->fpul == 0x80000000 && f == f) + if (std::isnan(f)) { + ctx->fpul = 0x80000000; + } + else { - if (*(s64 *)&f > 0) // Using integer math to avoid issues with Inf and NaN + ctx->fpul = (u32)(s32)f; +#if HOST_CPU == CPU_X86 || HOST_CPU == CPU_X64 + // Intel CPUs convert out of range float numbers to 0x80000000. Manually set the correct sign + if (ctx->fpul == 0x80000000 && f > 0) ctx->fpul--; +#endif } } } diff --git a/core/rec-ARM/rec_arm.cpp b/core/rec-ARM/rec_arm.cpp index 4552a3c5a..878a8a2fc 100644 --- a/core/rec-ARM/rec_arm.cpp +++ b/core/rec-ARM/rec_arm.cpp @@ -1901,7 +1901,6 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op unaryFpOp(op, &MacroAssembler::Vsqrt); break; - case shop_fmac: { SRegister rd = reg.mapFReg(op->rd); @@ -1945,7 +1944,7 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op } if (!rd.Is(rs1)) Vmov(rd, rs1); - Vmla(rd, rs2, rs3); + Vfma(rd, rs2, rs3); } break; @@ -2001,7 +2000,7 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op Vstr(d0, MemOperand(r8, op->rd.reg_nofs())); } break; - + /* fall back to the canonical implementations for better precision case shop_fipr: { QRegister _r1 = q0; @@ -2098,7 +2097,7 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op #endif } break; - + */ case shop_frswap: Sub(r0, r8, -op->rs1.reg_nofs()); Sub(r1, r8, -op->rd.reg_nofs()); @@ -2111,8 +2110,19 @@ void Arm32Assembler::compileOp(RuntimeBlockInfo* block, shil_opcode* op, bool op break; case shop_cvt_f2i_t: - Vcvt(S32, F32, s0, reg.mapFReg(op->rs1)); - Vmov(reg.mapReg(op->rd), s0); + { + SRegister from = reg.mapFReg(op->rs1); + Register to = reg.mapReg(op->rd); + Vcvt(S32, F32, s0, from); + Vmov(to, s0); + Mvn(r0, 127); + Sub(r0, r0, 0x80000000); + Cmp(to, r0); + Mvn(gt, to, 0xf8000000); + Vcmp(from, from); + Vmrs(RegisterOrAPSR_nzcv(APSR_nzcv), FPSCR); + Mov(ne, to, 0x80000000); + } break; case shop_cvt_i2f_n: // may be some difference should be made ? diff --git a/core/rec-ARM64/rec_arm64.cpp b/core/rec-ARM64/rec_arm64.cpp index c9fbe7f81..9fff1759e 100644 --- a/core/rec-ARM64/rec_arm64.cpp +++ b/core/rec-ARM64/rec_arm64.cpp @@ -952,8 +952,20 @@ public: break; case shop_cvt_f2i_t: - Fcvtzs(regalloc.MapRegister(op.rd), regalloc.MapVRegister(op.rs1)); + { + const VRegister& from = regalloc.MapVRegister(op.rs1); + const Register& to = regalloc.MapRegister(op.rd); + Fcvtzs(to, from); + Mov(w0, 0x7FFFFF80); + Cmp(to, w0); + Mov(w0, 0x7FFFFFF); + Csel(to, w0, to, gt); + Fcmp(from, from); + Mov(w0, 0x80000000); + Csel(to, to, w0, vc); + } break; + case shop_cvt_i2f_n: case shop_cvt_i2f_z: Scvtf(regalloc.MapVRegister(op.rd), regalloc.MapRegister(op.rs1)); From 34104724a6409486db9daa4273dc2875ac0d9da4 Mon Sep 17 00:00:00 2001 From: Mike Kosek Date: Sun, 26 Jan 2025 11:03:32 +0100 Subject: [PATCH 54/76] Added config file override for DreamcastControllerUsb Serial Device --- core/sdl/dreamconn.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 9f56ef560..54829e1b8 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -233,8 +233,18 @@ void DreamConn::connect() serial_handler = asio::serial_port(io_context); - // select first available serial device - std::string serial_device = getFirstSerialDevice(); + std::string serial_device = ""; + + // use user-configured serial device if available, fallback to first available + if (cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default") != "default") { + serial_device = cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default"); + INFO_LOG(INPUT, "DreamcastController[%d] connecting to user-configured serial device: %s", bus, serial_device.c_str()); + } + else + { + serial_device = getFirstSerialDevice(); + INFO_LOG(INPUT, "DreamcastController[%d] connecting to autoselected serial device: %s", bus, serial_device.c_str()); + } serial_handler.open(serial_device, ec); From 1513c6cab87f6dbf434f75d3cfbbdbd43ada9d37 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 27 Jan 2025 17:47:22 +0100 Subject: [PATCH 55/76] msvc build fix --- core/hw/sh4/dyna/shil_canonical.h | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/core/hw/sh4/dyna/shil_canonical.h b/core/hw/sh4/dyna/shil_canonical.h index fd9918c22..a1be200cf 100644 --- a/core/hw/sh4/dyna/shil_canonical.h +++ b/core/hw/sh4/dyna/shil_canonical.h @@ -715,6 +715,7 @@ shil_opc_end() //shop_cvt_f2i_t //float to integer : truncate shil_opc(cvt_f2i_t) +#if HOST_CPU == CPU_X86 || HOST_CPU == CPU_X64 shil_canonical ( u32,f1,(f32 f1), @@ -724,18 +725,29 @@ u32,f1,(f32 f1), } else { res = (s32)f1; -#if HOST_CPU == CPU_X86 || HOST_CPU == CPU_X64 // Fix result sign for Intel CPUs if ((u32)res == 0x80000000 && f1 > 0) res = 0x7fffffff; -#elif HOST_CPU == CPU_ARM || HOST_CPU == CPU_ARM64 - // conversion of NaN returns 0 on ARM - if (std::isnan(f1)) - res = 0x80000000; -#endif } return res; ) +#elif HOST_CPU == CPU_ARM || HOST_CPU == CPU_ARM64 +shil_canonical +( +u32,f1,(f32 f1), + s32 res; + if (f1 > 2147483520.0f) { // IEEE 754: 0x4effffff + res = 0x7fffffff; + } + else { + res = (s32)f1; + // conversion of NaN returns 0 on ARM + if (std::isnan(f1)) + res = 0x80000000; + } + return res; +) +#endif shil_compile ( From d84108de3201ba4e1cf96a9b2b6066fc6bfcd4f7 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 28 Jan 2025 17:26:22 +0100 Subject: [PATCH 56/76] maple: yet another maple timing change Use 2 Mb/s for console and 740 Kb/s for devices. Fixes Silent Scope vmu load issue at boot. Issue #1796 D+Vine has issues detecting A and B presses with one controller connected but it does work eventually. No issue with more than 1 controller. Issue #1279 Power Drift only boots with one controller connected. Crashes with 2 or more. --- core/hw/maple/maple_if.cpp | 22 ++++++++++++++++------ core/hw/sh4/sh4_sched.h | 4 ++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/core/hw/maple/maple_if.cpp b/core/hw/maple/maple_if.cpp index e446057a9..5ca716637 100644 --- a/core/hw/maple/maple_if.cpp +++ b/core/hw/maple/maple_if.cpp @@ -163,7 +163,8 @@ static void maple_DoDma() } const bool swap_msb = (SB_MMSEL == 0); - u32 xfer_count = 0; + u32 xferOut = 0; + u32 xferIn = 0; bool last = false; while (!last) { @@ -226,7 +227,8 @@ static void maple_DoDma() inlen = (inlen + 1) * 4; u32 outbuf[1024 / 4]; u32 outlen = MapleDevices[bus][port]->RawDma(&p_data[0], inlen, outbuf); - xfer_count += inlen + 3 + outlen + 3; // start, parity and stop bytes + xferIn += inlen + 3; // start, parity and stop bytes + xferOut += outlen + 3; #ifdef STRICT_MODE if (!check_mdapro(header_2 + outlen - 1)) { @@ -258,7 +260,7 @@ static void maple_DoDma() u32 bus = (header_1 >> 16) & 3; if (MapleDevices[bus][5]) { SDCKBOccupied = SDCKBOccupied || MapleDevices[bus][5]->get_lightgun_pos(); - xfer_count++; + xferIn++; } addr += 1 * 4; } @@ -271,7 +273,7 @@ static void maple_DoDma() case MP_Reset: addr += 1 * 4; - xfer_count++; + xferIn++; break; case MP_NOP: @@ -285,9 +287,17 @@ static void maple_DoDma() } // Maple bus max speed: 2 Mb/s, actual speed: 1 Mb/s - //printf("Maple XFER size %d bytes - %.2f ms\n", xfer_count, xfer_count * 1000.0f / (128 * 1024)); + // actual measured speed with protocol analyzer for devices (vmu?) is 724-738Kb/s + // See https://github.com/OrangeFox86/DreamcastControllerUsbPico/blob/main/measurements/Dreamcast-Power-Up-Digital-and-Analog-Player1-Controller-VMU-JumpPack.sal if (!SDCKBOccupied) - sh4_sched_request(maple_schid, std::min((u64)xfer_count * (SH4_MAIN_CLOCK / (256 * 1024)), (u64)SH4_MAIN_CLOCK)); + { + // 2 Mb/s from console + u32 cycles = sh4CyclesForXfer(xferIn, 2'000'000 / 8); + // 740 Kb/s from devices + cycles += sh4CyclesForXfer(xferOut, 740'000 / 8); + cycles = std::min(cycles, SH4_MAIN_CLOCK); + sh4_sched_request(maple_schid, cycles); + } } static int maple_schd(int tag, int cycles, int jitter, void *arg) diff --git a/core/hw/sh4/sh4_sched.h b/core/hw/sh4/sh4_sched.h index b40368972..6e4ed32b7 100644 --- a/core/hw/sh4/sh4_sched.h +++ b/core/hw/sh4/sh4_sched.h @@ -53,3 +53,7 @@ void sh4_sched_serialize(Serializer& ser); void sh4_sched_deserialize(Deserializer& deser); void sh4_sched_serialize(Serializer& ser, int id); void sh4_sched_deserialize(Deserializer& deser, int id); + +constexpr u32 sh4CyclesForXfer(u32 sizeUnit, u32 unitPerSecond) { + return (u64)SH4_MAIN_CLOCK * sizeUnit / unitPerSecond; +} From a35e8214df5ced173593cd6dda71a73fa23423fc Mon Sep 17 00:00:00 2001 From: James Date: Tue, 28 Jan 2025 09:27:26 -0700 Subject: [PATCH 57/76] Serial comm update (#1820) * Broke apart DreamConn and DreamcastControllerUsb implementations; added serial timeouts; process serial in thread; removed crc from DreamcastControllerUsb sdl definition * Added missing include statements * Changed INFO_LOG to NOTICE_LOG on a couple of lines * Clear the read queue once string is pulled off of it --- core/sdl/dreamconn.cpp | 768 +++++++++++++++++++++++++++++------------ core/sdl/dreamconn.h | 15 +- core/sdl/sdl.cpp | 14 +- 3 files changed, 554 insertions(+), 243 deletions(-) diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 54829e1b8..76d5ed06e 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -26,6 +26,11 @@ #include #include #include +#include +#include +#include +#include +#include #if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) #include @@ -38,256 +43,568 @@ void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); -static asio::error_code sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream, asio::io_context& io_context, asio::serial_port& serial_handler, int dreamcastControllerType) +class DreamcastControllerConnection { - std::ostringstream s; - s.fill('0'); - if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB) - { - // Messages to Dreamcast Controller USB need to be prefixed to trigger the correct parser - s << "X "; - } - - s << std::hex << std::uppercase - << std::setw(2) << (u32)msg.command << " " - << std::setw(2) << (u32)msg.destAP << " " - << std::setw(2) << (u32)msg.originAP << " " - << std::setw(2) << (u32)msg.size; - const u32 sz = msg.getDataSize(); - for (u32 i = 0; i < sz; i++) - s << " " << std::setw(2) << (u32)msg.data[i]; - s << "\r\n"; - - asio::error_code ec; - - if (dreamcastControllerType == TYPE_DREAMCONN) - { - if (!stream) - return asio::error::not_connected; - asio::ip::tcp::socket& sock = static_cast(stream.socket()); - asio::write(sock, asio::buffer(s.str()), ec); - } - else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB) - { - io_context.run(); - io_context.reset(); - - if (!serial_handler.is_open()) - return asio::error::not_connected; - asio::async_write(serial_handler, asio::buffer(s.str()), asio::transfer_exactly(s.str().size()), [ &serial_handler](const asio::error_code& error, size_t bytes_transferred) - { - if (error) { - serial_handler.cancel(); - } - }); - } - - return ec; -} +protected: + //! The maple bus index [0,3] + const int bus; -static bool receiveMsg(MapleMsg& msg, std::istream& stream, asio::serial_port& serial_handler, int dreamcastControllerType) -{ - std::string response; - - if (dreamcastControllerType == TYPE_DREAMCONN) +public: + DreamcastControllerConnection(const DreamcastControllerConnection&) = delete; + DreamcastControllerConnection() = delete; + + explicit DreamcastControllerConnection(int bus) : bus(bus) + {} + + std::optional connect(){ + if (!establishConnection()) { + return std::nullopt; + } + + // Now get the controller configuration + MapleMsg msg; + msg.command = MDCF_GetCondition; + msg.destAP = (bus << 6) | 0x20; + msg.originAP = bus << 6; + msg.setData(MFID_0_Input); + + asio::error_code ec = sendMsg(msg); + if (ec) + { + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); + disconnect(); + return std::nullopt; + } + if (!receiveMsg(msg)) { + WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus); + disconnect(); + return std::nullopt; + } + + onConnectComplete(); + + return msg; + } + + virtual void disconnect() = 0; + virtual asio::error_code sendMsg(const MapleMsg& msg) = 0; + virtual bool receiveMsg(MapleMsg& msg) = 0; + +protected: + virtual bool establishConnection() = 0; + virtual void onConnectComplete() = 0; + + std::string msgToString(const MapleMsg& msg, const std::string& delim = " ") { - if (!std::getline(stream, response)) + std::ostringstream s; + s.fill('0'); + + s << std::hex << std::uppercase + << std::setw(2) << (u32)msg.command << " " + << std::setw(2) << (u32)msg.destAP << " " + << std::setw(2) << (u32)msg.originAP << " " + << std::setw(2) << (u32)msg.size; + const u32 sz = msg.getDataSize(); + for (u32 i = 0; i < sz; i++) + s << " " << std::setw(2) << (u32)msg.data[i]; + s << "\r\n"; + + return s.str(); + } +}; + +class DreamConnConnection : public DreamcastControllerConnection +{ + //! Base port of communication to DreamConn + static constexpr u16 BASE_PORT = 37393; + //! Stream to a DreamConn device + asio::ip::tcp::iostream iostream; + +public: + //! DreamConn VID:4457 PID:4443 + static constexpr const char* VID_PID_GUID = "5744000043440000"; + +public: + DreamConnConnection(const DreamConnConnection&) = delete; + DreamConnConnection() = delete; + + explicit DreamConnConnection(int bus) : DreamcastControllerConnection(bus) + {} + + ~DreamConnConnection() { + disconnect(); + } + + bool establishConnection() override { +#if !defined(_WIN32) + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ / DreamConn S Controller supported on Windows only", bus); + return false; +#else + iostream = asio::ip::tcp::iostream("localhost", std::to_string(BASE_PORT + bus)); + if (!iostream) { + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, iostream.error().message().c_str()); + disconnect(); + return false; + } + iostream.expires_from_now(std::chrono::seconds(1)); + return true; +#endif + } + + void onConnectComplete() override { + iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow + } + + void disconnect() override + { + if (iostream) { + iostream.close(); + } + } + + asio::error_code sendMsg(const MapleMsg& msg) override + { + const std::string msgStr = msgToString(msg); + asio::error_code ec; + + if (!iostream) { + return asio::error::not_connected; + } + asio::ip::tcp::socket& sock = static_cast(iostream.socket()); + asio::write(sock, asio::buffer(msgStr), ec); + + return ec; + } + + bool receiveMsg(MapleMsg& msg) override + { + std::string response; + + if (!std::getline(iostream, response)) return false; sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); if ((msg.getDataSize() - 1) * 3 + 13 >= response.length()) return false; for (unsigned i = 0; i < msg.getDataSize(); i++) sscanf(&response[i * 3 + 12], "%hhx", &msg.data[i]); - return !stream.fail(); + return !iostream.fail(); + + return false; } - else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB) +}; + +//! See: https://github.com/OrangeFox86/DreamcastControllerUsbPico +class DreamcastControllerUsbPicoConnection : public DreamcastControllerConnection +{ + //! Asynchronous context for serial_handler + asio::io_context io_context; + //! Output buffer data for serial_handler + std::string serial_out_data; + //! Handles communication to DreamcastControllerUsbPico + asio::serial_port serial_handler{io_context}; + //! Set to true while an async write is in progress with serial_handler + bool serial_write_in_progress = false; + //! Signaled when serial_write_in_progress transitions to false + std::condition_variable write_cv; + //! Mutex for write_cv and serializes access to serial_write_in_progress + std::mutex write_cv_mutex; + //! Input stream buffer from serial_handler + asio::streambuf serial_read_buffer; + //! Thread which runs the io_context + std::unique_ptr io_context_thread; + //! Contains queue of incoming lines from serial + std::list read_queue; + //! Signaled when data is in read_queue + std::condition_variable read_cv; + //! Mutex for read_cv and serializes access to read_queue + std::mutex read_cv_mutex; + //! Current timeout in milliseconds + std::chrono::milliseconds timeout_ms; + +public: + //! Dreamcast Controller USB VID:1209 PID:2f07 + static constexpr const char* VID_PID_GUID = "09120000072f0000"; + +public: + DreamcastControllerUsbPicoConnection(const DreamcastControllerUsbPicoConnection&) = delete; + DreamcastControllerUsbPicoConnection() = delete; + + explicit DreamcastControllerUsbPicoConnection(int bus) : DreamcastControllerConnection(bus) + {} + + ~DreamcastControllerUsbPicoConnection(){ + disconnect(); + } + + bool establishConnection() override { + asio::error_code ec; + + // Timeout is 1 second while establishing connection + timeout_ms = std::chrono::seconds(1); + + // the serial port isn't ready at this point, so we need to sleep briefly + // we probably should have a better way to handle this + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + serial_handler = asio::serial_port(io_context); + io_context.reset(); + + std::string serial_device = ""; + + // use user-configured serial device if available, fallback to first available + if (cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default") != "default") { + serial_device = cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default"); + NOTICE_LOG(INPUT, "DreamcastController[%d] connecting to user-configured serial device: %s", bus, serial_device.c_str()); + } + else + { + serial_device = getFirstSerialDevice(); + NOTICE_LOG(INPUT, "DreamcastController[%d] connecting to autoselected serial device: %s", bus, serial_device.c_str()); + } + + serial_handler.open(serial_device, ec); + + if (ec || !serial_handler.is_open()) { + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); + disconnect(); + return false; + } + + // This must be done before the io_context is run because it will keep io_context from returning immediately + startSerialRead(); + + io_context_thread = std::make_unique([this](){contextThreadEnty();}); + + return true; + } + + void onConnectComplete() override { + // Timeout is extended to 5 seconds for all other communication after connection + timeout_ms = std::chrono::seconds(5); + } + + void disconnect() override + { + io_context.stop(); + + if (serial_handler.is_open()) { + try + { + serial_handler.cancel(); + } + catch(const asio::system_error&) + { + // Ignore cancel errors + } + } + + try + { + serial_handler.close(); + } + catch(const asio::system_error&) + { + // Ignore closing errors + } + } + + void contextThreadEnty() + { + // This context should never exit until disconnect due to read handler automatically rearming + io_context.run(); + } + + asio::error_code sendMsg(const MapleMsg& msg) override { asio::error_code ec; - - char c; - for (int i = 0; i < 2; ++i) - { - // discard the first message as we are interested in the second only which returns the controller configuration - response = ""; - while (serial_handler.read_some(asio::buffer(&c, 1), ec) > 0) - { - if (!serial_handler.is_open()) - return false; - if (c == '\n') - break; - response += c; - } - response.pop_back(); + + if (!serial_handler.is_open()) { + return asio::error::not_connected; } - - sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); - - if (!ec && serial_handler.is_open()) - return true; - else + + // Wait for last write to complete + std::unique_lock lock(write_cv_mutex); + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + if (!write_cv.wait_until(lock, expiration, [this](){return (!serial_write_in_progress || !serial_handler.is_open());})) + { + return asio::error::timed_out; + } + + // Check again before continuing + if (!serial_handler.is_open()) { + return asio::error::not_connected; + } + + // Clear out the read buffer before writing next command + read_queue.clear(); + + serial_write_in_progress = true; + // Messages to Dreamcast Controller USB need to be prefixed to trigger the correct parser + serial_out_data = std::string("X ") + msgToString(msg); + asio::async_write(serial_handler, asio::buffer(serial_out_data), asio::transfer_exactly(serial_out_data.size()), [this](const asio::error_code& error, size_t bytes_transferred) + { + std::unique_lock lock(write_cv_mutex); + if (error) { + try + { + serial_handler.cancel(); + } + catch(const asio::system_error&) + { + // Ignore cancel errors + } + } + serial_write_in_progress = false; + write_cv.notify_all(); + }); + + return ec; + } + + bool receiveMsg(MapleMsg& msg) override + { + std::string response; + + // Wait for at least 2 lines to be received (first line is echo back) + std::unique_lock lock(read_cv_mutex); + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + if (!read_cv.wait_until(lock, expiration, [this](){return ((read_queue.size() >= 2) || !serial_handler.is_open());})) + { + // Timeout return false; - } - - return false; -} + } -static std::string getFirstSerialDevice() { - - // On Windows, we get the first serial device matching our VID/PID + if (read_queue.size() < 2) { + // Connection was closed before data could be received + return false; + } + + // discard the first message as we are interested in the second only which returns the controller configuration + response = std::move(read_queue.back()); + read_queue.clear(); + + sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); + + if (serial_handler.is_open()) { + return true; + } + else { + return false; + } + + return false; + } + +private: + static std::string getFirstSerialDevice() { + + // On Windows, we get the first serial device matching our VID/PID #if defined(_WIN32) - HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES); - if (deviceInfoSet == INVALID_HANDLE_VALUE) { - return ""; - } + HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES); + if (deviceInfoSet == INVALID_HANDLE_VALUE) { + return ""; + } - SP_DEVINFO_DATA deviceInfoData; - deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); + SP_DEVINFO_DATA deviceInfoData; + deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); - for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) { - DWORD dataType, bufferSize = 0; - SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, NULL, 0, &bufferSize); + for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) { + DWORD dataType, bufferSize = 0; + SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, NULL, 0, &bufferSize); - if (bufferSize > 0) { - std::vector buffer(bufferSize); - if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)buffer.data(), bufferSize, NULL)) { - std::string hardwareId(buffer.begin(), buffer.end()); - if (hardwareId.find("VID_1209") != std::string::npos && hardwareId.find("PID_2F07") != std::string::npos) { - HKEY deviceKey = SetupDiOpenDevRegKey(deviceInfoSet, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); - if (deviceKey != INVALID_HANDLE_VALUE) { - char portName[256]; - DWORD portNameSize = sizeof(portName); - if (RegQueryValueEx(deviceKey, "PortName", NULL, NULL, (LPBYTE)portName, &portNameSize) == ERROR_SUCCESS) { + if (bufferSize > 0) { + std::vector buffer(bufferSize); + if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)buffer.data(), bufferSize, NULL)) { + std::string hardwareId(buffer.begin(), buffer.end()); + if (hardwareId.find("VID_1209") != std::string::npos && hardwareId.find("PID_2F07") != std::string::npos) { + HKEY deviceKey = SetupDiOpenDevRegKey(deviceInfoSet, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); + if (deviceKey != INVALID_HANDLE_VALUE) { + char portName[256]; + DWORD portNameSize = sizeof(portName); + if (RegQueryValueEx(deviceKey, "PortName", NULL, NULL, (LPBYTE)portName, &portNameSize) == ERROR_SUCCESS) { + RegCloseKey(deviceKey); + SetupDiDestroyDeviceInfoList(deviceInfoSet); + return std::string(portName); + } RegCloseKey(deviceKey); - SetupDiDestroyDeviceInfoList(deviceInfoSet); - return std::string(portName); } - RegCloseKey(deviceKey); } } } } - } - - SetupDiDestroyDeviceInfoList(deviceInfoSet); - return ""; + + SetupDiDestroyDeviceInfoList(deviceInfoSet); + return ""; #endif - + +#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) // On MacOS/Linux, we get the first serial device matching the device prefix std::string device_prefix = ""; -#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) - + #if defined(__linux__) - device_prefix = "ttyACM"; + device_prefix = "ttyACM"; #elif (defined(__APPLE__) && defined(TARGET_OS_MAC)) - device_prefix = "tty.usbmodem"; + device_prefix = "tty.usbmodem"; #endif - - std::string path = "/dev/"; - DIR *dir; - struct dirent *ent; - if ((dir = opendir(path.c_str())) != NULL) { - while ((ent = readdir(dir)) != NULL) { - std::string device = ent->d_name; - if (device.find(device_prefix) != std::string::npos) { - closedir(dir); - return path + device; + + std::string path = "/dev/"; + DIR *dir; + struct dirent *ent; + if ((dir = opendir(path.c_str())) != NULL) { + while ((ent = readdir(dir)) != NULL) { + std::string device = ent->d_name; + if (device.find(device_prefix) != std::string::npos) { + closedir(dir); + return path + device; + } + } + closedir(dir); + } + return ""; +#endif + } + + void startSerialRead() + { + serialReadHandler(asio::error_code(), 0); + // Just to make sure initial data is cleared off of incoming buffer + io_context.poll_one(); + read_queue.clear(); + } + + void serialReadHandler(const asio::error_code& error, std::size_t size) + { + if (error) { + std::lock_guard lock(read_cv_mutex); + try + { + serial_handler.cancel(); + } + catch(const asio::system_error&) + { + // Ignore cancel errors + } + read_cv.notify_all(); + } + else { + // Rearm the read + asio::async_read_until( + serial_handler, + serial_read_buffer, + '\n', + [this](const asio::error_code& error, std::size_t size) -> void { + if (size > 0) + { + // Lock access to read_queue + std::lock_guard lock(read_cv_mutex); + // Consume the received data + if (consumeReadBuffer() > 0) + { + // New lines available + read_cv.notify_all(); + } + } + // Auto reload read - io_context will always have work to do + serialReadHandler(error, size); + } + ); + } + } + + int consumeReadBuffer() { + if (serial_read_buffer.size() <= 0) { + return 0; + } + + int numberOfLines = 0; + while (true) + { + char c = '\0'; + std::string line; + + // Consume characters until buffers are empty or \n found + asio::const_buffers_1 data = serial_read_buffer.data(); + std::size_t consumed = 0; + for (const asio::const_buffer& buff : data) + { + const char* buffDat = static_cast(buff.data()); + for (std::size_t i = 0; i < buff.size(); ++i) + { + c = *buffDat++; + ++consumed; + + if (c == '\n') { + // Stop reading now + break; + } + + line += c; + } + + if (c == '\n') { + // Stop reading now + break; + } + } + + if (c == '\n') { + serial_read_buffer.consume(consumed); + + // Remove carriage return if found and add this line to queue + if (line.size() > 0 && line[line.size() - 1] == '\r') { + line.pop_back(); + } + read_queue.push_back(std::move(line)); + + ++numberOfLines; + } + else { + // Ran out of data to consume + return numberOfLines; } } - closedir(dir); } - return ""; -#endif +}; + +DreamConn::DreamConn(int bus, int dreamcastControllerType) : bus(bus), dreamcastControllerType(dreamcastControllerType) { + switch (dreamcastControllerType) + { + case TYPE_DREAMCONN: + dcConnection = std::make_unique(bus); + break; + + case TYPE_DREAMCASTCONTROLLERUSB: + dcConnection = std::make_unique(bus); + break; + } + + connect(); +} + +DreamConn::~DreamConn() { + disconnect(); } void DreamConn::connect() { maple_io_connected = false; - - asio::error_code ec; - - switch (dreamcastControllerType) { - case TYPE_DREAMCONN: - { -#if !defined(_WIN32) - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ / DreamConn S Controller supported on Windows only", bus); - return; -#endif - iostream = asio::ip::tcp::iostream("localhost", std::to_string(BASE_PORT + bus)); - if (!iostream) { - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, iostream.error().message().c_str()); - disconnect(); - return; - } - iostream.expires_from_now(std::chrono::seconds(1)); - break; - } - case TYPE_DREAMCASTCONTROLLERUSB: - { - // the serial port isn't ready at this point, so we need to sleep briefly - // we probably should have a better way to handle this -#if defined(_WIN32) - Sleep(500); -#elif defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) - usleep(500000); -#endif + expansionDevs = 0; - serial_handler = asio::serial_port(io_context); - - std::string serial_device = ""; - - // use user-configured serial device if available, fallback to first available - if (cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default") != "default") { - serial_device = cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default"); - INFO_LOG(INPUT, "DreamcastController[%d] connecting to user-configured serial device: %s", bus, serial_device.c_str()); - } - else - { - serial_device = getFirstSerialDevice(); - INFO_LOG(INPUT, "DreamcastController[%d] connecting to autoselected serial device: %s", bus, serial_device.c_str()); - } - - serial_handler.open(serial_device, ec); - - if (ec || !serial_handler.is_open()) { - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); - disconnect(); - return; - } - break; - } - default: - { - return; - } + if (!dcConnection) { + return; } - - // Now get the controller configuration - MapleMsg msg; - msg.command = MDCF_GetCondition; - msg.destAP = (bus << 6) | 0x20; - msg.originAP = bus << 6; - msg.setData(MFID_0_Input); - - ec = sendMsg(msg, iostream, io_context, serial_handler, dreamcastControllerType); - if (ec) + + std::optional msg = dcConnection->connect(); + if (!msg) { - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); - disconnect(); return; } - if (!receiveMsg(msg, iostream, serial_handler, dreamcastControllerType)) { - WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus); - disconnect(); - return; - } - if (dreamcastControllerType == TYPE_DREAMCONN) - iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow - expansionDevs = msg.originAP & 0x1f; - + expansionDevs = msg->originAP & 0x1f; + config::MapleExpansionDevices[bus][0] = hasVmu() ? MDT_SegaVMU : MDT_None; config::MapleExpansionDevices[bus][1] = hasRumble() ? MDT_PurupuruPack : MDT_None; - + if (hasVmu() || hasRumble()) { NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, dreamcastControllerType == 1 ? "DreamConn+ / DreamcConn S Controller" : "Dreamcast Controller USB", hasVmu(), hasRumble()); @@ -303,30 +620,27 @@ void DreamConn::connect() void DreamConn::disconnect() { - if (dreamcastControllerType == TYPE_DREAMCONN) - { - if (iostream) - iostream.close(); + if (!dcConnection) { + return; } - else if (dreamcastControllerType == TYPE_DREAMCASTCONTROLLERUSB) - { - if (serial_handler.is_open()) - serial_handler.cancel(); - serial_handler.close(); - io_context.stop(); - } - + + dcConnection->disconnect(); + maple_io_connected = false; - + NOTICE_LOG(INPUT, "Disconnected from DreamcastController[%d]", bus); } bool DreamConn::send(const MapleMsg& msg) { + if (!dcConnection) { + return false; + } + asio::error_code ec; if (maple_io_connected) - ec = sendMsg(msg, iostream, io_context, serial_handler, dreamcastControllerType); + ec = dcConnection->sendMsg(msg); else return false; if (ec) { @@ -342,14 +656,16 @@ bool DreamConnGamepad::isDreamcastController(int deviceIndex) { char guid_str[33] {}; SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(deviceIndex), guid_str, sizeof(guid_str)); - INFO_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str, + NOTICE_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str, guid_str[10], guid_str[11], guid_str[8], guid_str[9], guid_str[18], guid_str[19], guid_str[16], guid_str[17]); - + // DreamConn VID:4457 PID:4443 // Dreamcast Controller USB VID:1209 PID:2f07 - if (memcmp("5744000043440000", guid_str + 8, 16) == 0 || memcmp("09120000072f0000", guid_str + 8, 16) == 0) + if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0 || + memcmp(DreamcastControllerUsbPicoConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { + NOTICE_LOG(INPUT, "Dreamcast controller found!"); return true; } return false; @@ -359,22 +675,22 @@ DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystic : SDLGamepad(maple_port, joystick_idx, sdl_joystick) { char guid_str[33] {}; - + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(joystick_idx), guid_str, sizeof(guid_str)); - + // DreamConn VID:4457 PID:4443 // Dreamcast Controller USB VID:1209 PID:2f07 - if (memcmp("5744000043440000", guid_str + 8, 16) == 0) + if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { dreamcastControllerType = TYPE_DREAMCONN; _name = "DreamConn+ / DreamConn S Controller"; } - else if (memcmp("09120000072f0000", guid_str + 8, 16) == 0) + else if (memcmp(DreamcastControllerUsbPicoConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { dreamcastControllerType = TYPE_DREAMCASTCONTROLLERUSB; _name = "Dreamcast Controller USB"; } - + EventManager::listen(Event::Start, handleEvent, this); EventManager::listen(Event::LoadState, handleEvent, this); } @@ -444,7 +760,7 @@ void DreamConnGamepad::checkKeyCombo() { gui_open_settings(); } -#else +#else // USE_DREAMCASTCONTROLLER void DreamConn::connect() { } diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index d883c61fc..90200615a 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -26,6 +26,7 @@ #define TYPE_DREAMCASTCONTROLLERUSB 2 #include #endif +#include struct MapleMsg { @@ -52,21 +53,15 @@ class DreamConn const int bus; const int dreamcastControllerType; #ifdef USE_DREAMCASTCONTROLLER - asio::ip::tcp::iostream iostream; - asio::io_context io_context; - asio::serial_port serial_handler{io_context}; + std::unique_ptr dcConnection; #endif bool maple_io_connected; u8 expansionDevs = 0; - static constexpr u16 BASE_PORT = 37393; public: - DreamConn(int bus, int dreamcastControllerType) : bus(bus), dreamcastControllerType(dreamcastControllerType) { - connect(); - } - ~DreamConn() { - disconnect(); - } + DreamConn(int bus, int dreamcastControllerType); + + ~DreamConn(); bool send(const MapleMsg& msg); diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index 44d269507..794c2f315 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -271,9 +271,9 @@ void input_sdl_init() // Linux mappings are OK by default // Can be removed once mapping is merged into SDL, see https://github.com/libsdl-org/SDL/pull/12039 #if (defined(__APPLE__) && defined(TARGET_OS_MAC)) - SDL_GameControllerAddMapping("0300000009120000072f000000010000,OrangeFox86 Dreamcast Controller USB,crc:3cef,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:a2,righttrigger:a5,start:b11"); + SDL_GameControllerAddMapping("0300000009120000072f000000010000,OrangeFox86 Dreamcast Controller USB,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:a2,righttrigger:a5,start:b11"); #elif defined(_WIN32) - SDL_GameControllerAddMapping("0300000009120000072f000000000000,OrangeFox86 Dreamcast Controller USB,crc:baa5,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:-a2,righttrigger:-a5,start:b11"); + SDL_GameControllerAddMapping("0300000009120000072f000000000000,OrangeFox86 Dreamcast Controller USB,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:-a2,righttrigger:-a5,start:b11"); #endif } @@ -547,7 +547,7 @@ void input_sdl_handle() case SDL_JOYDEVICEREMOVED: sdl_close_joystick((SDL_JoystickID)event.jdevice.which); break; - + case SDL_DROPFILE: gui_start_game(event.drop.file); break; @@ -602,7 +602,7 @@ static inline void get_window_state() windowPos.h /= hdpiScaling; SDL_GetWindowPosition(window, &windowPos.x, &windowPos.y); } - + } #if defined(_WIN32) && !defined(TARGET_UWP) @@ -629,14 +629,14 @@ bool sdl_recreate_window(u32 flags) PROCESS_SYSTEM_DPI_AWARE = 1, PROCESS_PER_MONITOR_DPI_AWARE = 2 } PROCESS_DPI_AWARENESS; - + HRESULT(WINAPI *SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS dpiAwareness); // Windows 8.1 and later void* shcoreDLL = SDL_LoadObject("SHCORE.DLL"); if (shcoreDLL) { SetProcessDpiAwareness = (HRESULT(WINAPI *)(PROCESS_DPI_AWARENESS)) SDL_LoadFunction(shcoreDLL, "SetProcessDpiAwareness"); if (SetProcessDpiAwareness) { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); - + if (SDL_GetDisplayDPI(0, &settings.display.dpi, NULL, NULL) != -1){ //SDL_WINDOWPOS_UNDEFINED is Display 0 //When using HiDPI mode, set correct DPI scaling hdpiScaling = settings.display.dpi / 96.f; @@ -645,7 +645,7 @@ bool sdl_recreate_window(u32 flags) SDL_UnloadObject(shcoreDLL); } #endif - + #ifdef __SWITCH__ AppletOperationMode om = appletGetOperationMode(); if (om == AppletOperationMode_Handheld) From c143cae09ba8936024e5585c18185b094421f45a Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 31 Jan 2025 15:32:18 +0100 Subject: [PATCH 58/76] disable Daytona USA (US) widescreen cheat breaks online features --- core/cheats.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/cheats.cpp b/core/cheats.cpp index 999be2002..8ffcbab99 100644 --- a/core/cheats.cpp +++ b/core/cheats.cpp @@ -83,7 +83,7 @@ const WidescreenCheat CheatManager::widescreen_cheats[] = { "T30006M", nullptr, { 0x4CF42C, 0x4CF45C, 0x3E1A36, 0x3E1A34, 0x3E1A3C, 0x3E1A54, 0x3E1A5C }, { 0x43F00000, 0x3F400000, 0x08010000, 0, 0, 0, 0 } }, { "MK-5103750", nullptr, { 0x1FE270 }, { 0x43700000 } }, // Daytona USA (PAL) - { "MK-51037", nullptr, { 0x1FC6D0 }, { 0x43700000 } }, // Daytona USA (USA) + // breaks online connection { "MK-51037", nullptr, { 0x1FC6D0 }, { 0x43700000 } }, // Daytona USA (USA) { "T9501N-50", nullptr, { 0x9821D4 }, { 0x3F400000 } }, // Deadly Skies (PAL) { "T8116D 50", nullptr, { 0x2E5530 }, { 0x43700000 } }, // Dead or Alive 2 (PAL) { "T3601N", nullptr, { 0x2F0670 }, { 0x43700000 } }, // Dead or Alive 2 (USA) From 914fa29d314f07086e92d3c98ba7a3121215e32f Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Fri, 31 Jan 2025 18:09:33 +0100 Subject: [PATCH 59/76] gdrom: increase dma delay for small DMA transfers Fixes WSB 2K2 freezes. Issue #1038 --- core/hw/gdrom/gdromv3.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/hw/gdrom/gdromv3.cpp b/core/hw/gdrom/gdromv3.cpp index 494f8adec..adaff8e8a 100644 --- a/core/hw/gdrom/gdromv3.cpp +++ b/core/hw/gdrom/gdromv3.cpp @@ -1209,9 +1209,12 @@ static int getGDROMTicks() return 512; u32 len = SB_GDLEN == 0 ? 0x02000000 : SB_GDLEN; if (len - SB_GDLEND > 10240) - return 1100000; // Large transfers: GD-ROM transfer rate 1.8 MB/s + // Large transfers: GD-ROM transfer rate 1.8 MB/s + return sh4CyclesForXfer(10240, 1'800'000); else - return std::min((u32)10240, len - SB_GDLEND) * 2; // Small transfers: Max G1 bus rate: 50 MHz x 16 bits + // Small transfers: Max G1 bus rate: 50 MHz x 16 bits + // ...slowed down to 25 MB/s for wsb2k2 + return sh4CyclesForXfer(std::min(10240, len - SB_GDLEND), 25'000'000); } else return 0; From 6a15e7a3aa8ffefff1b2f6cfd6917b5ffa19eaa0 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sun, 2 Feb 2025 17:11:22 +0100 Subject: [PATCH 60/76] rend: LoD0 of VQ YUV textures is invalid. Use LoD1 instead. Fixes background magenta textures in WSB2K2. Issue #1038 --- core/rend/TexCache.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/rend/TexCache.cpp b/core/rend/TexCache.cpp index 90cd47100..46743b57a 100644 --- a/core/rend/TexCache.cpp +++ b/core/rend/TexCache.cpp @@ -555,7 +555,10 @@ bool BaseTextureCacheData::Update() { PixelBuffer pb0; pb0.init(2, 2 ,false); - texconv32(&pb0, (u8*)&vram[vram_addr], 2, 2); + if (tcw.PixelFmt == PixelYUV) + // Use higher LoD mipmap + vram_addr = startAddress + VQMipPoint[1]; + texconv32(&pb0, &vram[vram_addr], 2, 2); *pb32.data() = *pb0.data(1, 1); continue; } From 3db0fce9b684a9012454e6ab9404a5f78c523b75 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sun, 2 Feb 2025 17:12:54 +0100 Subject: [PATCH 61/76] bypass dricas auth in aero dancing i --- core/cheats.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/cheats.cpp b/core/cheats.cpp index 8ffcbab99..fa7f2471a 100644 --- a/core/cheats.cpp +++ b/core/cheats.cpp @@ -489,6 +489,10 @@ void CheatManager::reset(const std::string& gameId) // force logging on to use more cycles cheats.emplace_back(Cheat::Type::setValue, "enable logging", true, 32, 0x00314228, 1, true); } + else if (gameId == "T6807M") { // Aero Dancing i + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 16, 0x0004b7a0, 0x000b, true); // rts + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 16, 0x0004b7a2, 0xe000, true); // mov #0, r0 + } if (cheats.size() > cheatCount) setActive(true); From 7cb0fe53d7bd0948428db217043878304311023a Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 3 Feb 2025 18:18:28 +0100 Subject: [PATCH 62/76] add cheats to bypass dricas authentication Aero Dancing i Daytona USA (JP) Sega Tetris Golf Shiyou Yo 2 Hundred Swords --- core/cheats.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/core/cheats.cpp b/core/cheats.cpp index fa7f2471a..19f2b3dde 100644 --- a/core/cheats.cpp +++ b/core/cheats.cpp @@ -489,9 +489,21 @@ void CheatManager::reset(const std::string& gameId) // force logging on to use more cycles cheats.emplace_back(Cheat::Type::setValue, "enable logging", true, 32, 0x00314228, 1, true); } + // Dricas auth bypass else if (gameId == "T6807M") { // Aero Dancing i - cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 16, 0x0004b7a0, 0x000b, true); // rts - cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 16, 0x0004b7a2, 0xe000, true); // mov #0, r0 + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0004b7a0, 0xe000000b, true); // rts, _mov #0, r0 + } + else if (gameId == "HDR-0106") { // Daytona USA (JP) + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0003ad30, 0xe000000b, true); // rts, _mov #0, r0 + } + else if (gameId == "HDR-0073") { // Sega Tetris + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x000a56f8, 0xe000000b, true); // rts, _mov #0, r0 + } + else if (gameId == "T44501M") { // Golf Shiyou Yo 2 + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0013f150, 0xe000000b, true); // rts, _mov #0, r0 + } + else if (gameId == "HDR-0124") { // Hundred Swords + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x006558ac, 0xe000000b, true); // rts, _mov #0, r0 } if (cheats.size() > cheatCount) From 77ed9c20de1c37dff0b4e2bd7e11dcae1b96ad7b Mon Sep 17 00:00:00 2001 From: James Date: Mon, 3 Feb 2025 10:19:22 -0700 Subject: [PATCH 63/76] Connect to DreamConn only once registered (#1828) --- core/input/gamepad_device.cpp | 5 +++- core/input/gamepad_device.h | 5 +++- core/sdl/dreamconn.cpp | 56 +++++++++++++++++++++++++---------- core/sdl/dreamconn.h | 12 ++++---- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/core/input/gamepad_device.cpp b/core/input/gamepad_device.cpp index 5dcb38b56..e7efadd7d 100644 --- a/core/input/gamepad_device.cpp +++ b/core/input/gamepad_device.cpp @@ -329,7 +329,7 @@ bool GamepadDevice::gamepad_axis_input(u32 code, int value) int threshold = 16384; if (code == leftTrigger || code == rightTrigger ) threshold = 100; - + if (std::abs(v) < threshold) kcode[port] |= key; // button released else @@ -554,6 +554,9 @@ void GamepadDevice::Register(const std::shared_ptr& gamepad) Lock _(_gamepads_mutex); _gamepads.push_back(gamepad); MapleConfigMap::UpdateVibration = updateVibration; + + gamepad->_is_registered = true; + gamepad->registered(); } void GamepadDevice::Unregister(const std::shared_ptr& gamepad) diff --git a/core/input/gamepad_device.h b/core/input/gamepad_device.h index 4cf560f68..17a92311b 100644 --- a/core/input/gamepad_device.h +++ b/core/input/gamepad_device.h @@ -40,7 +40,7 @@ public: virtual bool gamepad_btn_input(u32 code, bool pressed); virtual bool gamepad_axis_input(u32 code, int value); virtual ~GamepadDevice() = default; - + void detect_btn_input(input_detected_cb button_pressed); void detect_axis_input(input_detected_cb axis_moved); void detectButtonOrAxisInput(input_detected_cb input_changed); @@ -91,6 +91,7 @@ public: save_mapping(); } } + bool is_registered() const { return _is_registered; } static void Register(const std::shared_ptr& gamepad); static void Unregister(const std::shared_ptr& gamepad); @@ -145,6 +146,7 @@ protected: u32 rightTrigger = ~0; private: + virtual void registered() {} bool handleButtonInput(int port, DreamcastKey key, bool pressed); std::string make_mapping_filename(bool instance, int system, bool perGame = false); @@ -189,6 +191,7 @@ private: u64 _detection_start_time = 0; input_detected_cb _input_detected; bool _remappable; + bool _is_registered = false; u32 digitalToAnalogState[4]; std::map lastAxisValue[4]; bool perGameMapping = false; diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 76d5ed06e..e2d76bc8c 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -566,7 +566,19 @@ private: } }; -DreamConn::DreamConn(int bus, int dreamcastControllerType) : bus(bus), dreamcastControllerType(dreamcastControllerType) { +DreamConn::DreamConn(int bus, int dreamcastControllerType, const std::string& name) : + bus(bus), dreamcastControllerType(dreamcastControllerType), name(name) +{ + change_bus(bus); +} + +DreamConn::~DreamConn() { + disconnect(); +} + +void DreamConn::change_bus(int bus) { + disconnect(); + dcConnection.reset(); switch (dreamcastControllerType) { case TYPE_DREAMCONN: @@ -577,16 +589,14 @@ DreamConn::DreamConn(int bus, int dreamcastControllerType) : bus(bus), dreamcast dcConnection = std::make_unique(bus); break; } - - connect(); -} - -DreamConn::~DreamConn() { - disconnect(); } void DreamConn::connect() { + if (maple_io_connected) { + disconnect(); + } + maple_io_connected = false; expansionDevs = 0; @@ -607,7 +617,7 @@ void DreamConn::connect() if (hasVmu() || hasRumble()) { - NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, dreamcastControllerType == 1 ? "DreamConn+ / DreamcConn S Controller" : "Dreamcast Controller USB", hasVmu(), hasRumble()); + NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, name.c_str(), hasVmu(), hasRumble()); maple_io_connected = true; } else @@ -682,13 +692,13 @@ DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystic // Dreamcast Controller USB VID:1209 PID:2f07 if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { - dreamcastControllerType = TYPE_DREAMCONN; _name = "DreamConn+ / DreamConn S Controller"; + dreamconn = std::make_shared(maple_port, TYPE_DREAMCONN, _name); } else if (memcmp(DreamcastControllerUsbPicoConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { - dreamcastControllerType = TYPE_DREAMCASTCONTROLLERUSB; _name = "Dreamcast Controller USB"; + dreamconn = std::make_shared(maple_port, TYPE_DREAMCASTCONTROLLERUSB, _name); } EventManager::listen(Event::Start, handleEvent, this); @@ -702,16 +712,28 @@ DreamConnGamepad::~DreamConnGamepad() { void DreamConnGamepad::set_maple_port(int port) { - if (port < 0 || port >= 4) { - dreamconn.reset(); - } - else if (dreamconn == nullptr || dreamconn->getBus() != port) { - dreamconn.reset(); - dreamconn = std::make_shared(port, dreamcastControllerType); + if (dreamconn) { + if (port < 0 || port >= 4) { + dreamconn->disconnect(); + } + else if (dreamconn->getBus() != port) { + dreamconn->change_bus(port); + if (is_registered()) { + dreamconn->connect(); + } + } } SDLGamepad::set_maple_port(port); } +void DreamConnGamepad::registered() +{ + if (dreamconn) + { + dreamconn->connect(); + } +} + void DreamConnGamepad::handleEvent(Event event, void *arg) { DreamConnGamepad *gamepad = static_cast(arg); @@ -778,6 +800,8 @@ DreamConnGamepad::~DreamConnGamepad() { void DreamConnGamepad::set_maple_port(int port) { SDLGamepad::set_maple_port(port); } +void DreamConnGamepad::registered() { +} bool DreamConnGamepad::gamepad_btn_input(u32 code, bool pressed) { return SDLGamepad::gamepad_btn_input(code, pressed); } diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index 90200615a..8a0f98513 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -50,16 +50,17 @@ static_assert(sizeof(MapleMsg) == 1028); class DreamConn { - const int bus; + int bus = -1; const int dreamcastControllerType; + const std::string name; #ifdef USE_DREAMCASTCONTROLLER std::unique_ptr dcConnection; #endif - bool maple_io_connected; + bool maple_io_connected = false; u8 expansionDevs = 0; public: - DreamConn(int bus, int dreamcastControllerType); + DreamConn(int bus, int dreamcastControllerType, const std::string& name); ~DreamConn(); @@ -75,7 +76,8 @@ public: return expansionDevs & 2; } -private: + void change_bus(int bus); + void connect(); void disconnect(); }; @@ -87,6 +89,7 @@ public: ~DreamConnGamepad(); void set_maple_port(int port) override; + void registered() override; bool gamepad_btn_input(u32 code, bool pressed) override; bool gamepad_axis_input(u32 code, int value) override; static bool isDreamcastController(int deviceIndex); @@ -99,5 +102,4 @@ private: bool ltrigPressed = false; bool rtrigPressed = false; bool startPressed = false; - int dreamcastControllerType; }; From a99a72589bc6189fd002500d27dea9a4a6b3b8bb Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 4 Feb 2025 17:14:01 +0100 Subject: [PATCH 64/76] better cheats for dricas auth --- core/cheats.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/cheats.cpp b/core/cheats.cpp index 19f2b3dde..da8dede07 100644 --- a/core/cheats.cpp +++ b/core/cheats.cpp @@ -491,18 +491,23 @@ void CheatManager::reset(const std::string& gameId) } // Dricas auth bypass else if (gameId == "T6807M") { // Aero Dancing i + cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0004b7a0, 0x2fd62fe6, true); cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0004b7a0, 0xe000000b, true); // rts, _mov #0, r0 } else if (gameId == "HDR-0106") { // Daytona USA (JP) + cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0003ad30, 0x2fd62fe6, true); cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0003ad30, 0xe000000b, true); // rts, _mov #0, r0 } else if (gameId == "HDR-0073") { // Sega Tetris + cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x000a56f8, 0x2fd62fe6, true); cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x000a56f8, 0xe000000b, true); // rts, _mov #0, r0 } else if (gameId == "T44501M") { // Golf Shiyou Yo 2 + cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0013f150, 0x2fd62fe6, true); cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0013f150, 0xe000000b, true); // rts, _mov #0, r0 } else if (gameId == "HDR-0124") { // Hundred Swords + cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x006558ac, 0x1f414f22, true); cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x006558ac, 0xe000000b, true); // rts, _mov #0, r0 } From 5b9dd96064588d2a28d9c0990428c22d3c18ff6d Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 4 Feb 2025 17:17:38 +0100 Subject: [PATCH 65/76] option to set the ISP username in flash rom --- core/cfg/option.cpp | 1 + core/cfg/option.h | 1 + core/hw/flashrom/nvmem.cpp | 34 ++++++++++++++++++++++++++++------ core/ui/gui.cpp | 4 ++++ shell/libretro/option.cpp | 1 + 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index bc87f33fb..d46613ce5 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -167,6 +167,7 @@ Option NetworkOutput("NetworkOutput", false, "network"); Option MultiboardSlaves("MultiboardSlaves", 1, "network"); Option BattleCableEnable("BattleCable", false, "network"); Option UseDCNet("DCNet", false, "network"); +OptionString ISPUsername("ISPUsername", "flycast1", "network"); #ifdef USE_OMX Option OmxAudioLatency("audio_latency", 100, "omx"); diff --git a/core/cfg/option.h b/core/cfg/option.h index 843e071d9..81703e728 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -523,6 +523,7 @@ extern Option NetworkOutput; extern Option MultiboardSlaves; extern Option BattleCableEnable; extern Option UseDCNet; +extern OptionString ISPUsername; #ifdef USE_OMX extern Option OmxAudioLatency; diff --git a/core/hw/flashrom/nvmem.cpp b/core/hw/flashrom/nvmem.cpp index e6cfa0d50..c24ad36c9 100644 --- a/core/hw/flashrom/nvmem.cpp +++ b/core/hw/flashrom/nvmem.cpp @@ -49,7 +49,7 @@ static std::string getRomPrefix() } } -static void add_isp_to_nvmem(DCFlashChip *flash) +static void add_isp_to_nvmem(DCFlashChip *flash, const char *username) { u8 block[64]; if (!flash->ReadBlock(FLASH_PT_USER, FLASH_USER_INET, block)) @@ -79,11 +79,14 @@ static void add_isp_to_nvmem(DCFlashChip *flash) flash_isp1_block isp1{}; isp1._unknown[3] = 1; memcpy(isp1.sega, "SEGA", 4); - strcpy(isp1.username, "flycast1"); + if (username == nullptr || username[0] == '\0') + strcpy(isp1.username, "flycast1"); + else + strncpy(isp1.username, username, sizeof(isp1.username) - 1); strcpy(isp1.password, "password"); strcpy(isp1.phone, "1234567"); if (flash->WriteBlock(FLASH_PT_USER, FLASH_USER_ISP1, &isp1) != 1) - WARN_LOG(FLASHROM, "Failed to save ISP information to flash RAM"); + WARN_LOG(FLASHROM, "Failed to save ISP1 information to flash RAM"); memset(block, 0, sizeof(block)); flash->WriteBlock(FLASH_PT_USER, FLASH_USER_ISP1 + 1, block); @@ -97,11 +100,14 @@ static void add_isp_to_nvmem(DCFlashChip *flash) flash_isp2_block isp2{}; memcpy(isp2.sega, "SEGA", 4); - strcpy(isp2.username, "flycast2"); + if (username == nullptr || username[0] == '\0') + strcpy(isp2.username, "flycast2"); + else + strncpy(isp2.username, username, sizeof(isp2.username) - 1); strcpy(isp2.password, "password"); strcpy(isp2.phone, "1234567"); if (flash->WriteBlock(FLASH_PT_USER, FLASH_USER_ISP2, &isp2) != 1) - WARN_LOG(FLASHROM, "Failed to save ISP information to flash RAM"); + WARN_LOG(FLASHROM, "Failed to save ISP2 information to flash RAM"); u8 block[64]; memset(block, 0, sizeof(block)); for (u32 i = FLASH_USER_ISP2 + 1; i <= 0xEA; i++) @@ -113,6 +119,22 @@ static void add_isp_to_nvmem(DCFlashChip *flash) flash->WriteBlock(FLASH_PT_USER, i, block); } } + else if (username != nullptr && username[0] != '\0') + { + flash_isp1_block isp1{}; + flash->ReadBlock(FLASH_PT_USER, FLASH_USER_ISP1, &isp1); + memset(isp1.username, 0, sizeof(isp1.username)); + strncpy(isp1.username, username, sizeof(isp1.username) - 1); + if (flash->WriteBlock(FLASH_PT_USER, FLASH_USER_ISP1, &isp1) != 1) + WARN_LOG(FLASHROM, "Failed to save ISP1 information to flash RAM"); + + flash_isp2_block isp2{}; + flash->ReadBlock(FLASH_PT_USER, FLASH_USER_ISP2, &isp2); + memset(isp2.username, 0, sizeof(isp2.username)); + strncpy(isp2.username, username, sizeof(isp2.username) - 1); + if (flash->WriteBlock(FLASH_PT_USER, FLASH_USER_ISP2, &isp2) != 1) + WARN_LOG(FLASHROM, "Failed to save ISP2 information to flash RAM"); + } } static void fixUpDCFlash() @@ -162,7 +184,7 @@ static void fixUpDCFlash() if (static_cast(sys_nvmem)->WriteBlock(FLASH_PT_USER, FLASH_USER_SYSCFG, &syscfg) != 1) WARN_LOG(FLASHROM, "Failed to save time and language to flash RAM"); - add_isp_to_nvmem(static_cast(sys_nvmem)); + add_isp_to_nvmem(static_cast(sys_nvmem), config::ISPUsername.get().c_str()); // Check the console ID used by some network games (chuchu rocket) u8 *console_id = &sys_nvmem->data[0x1A058]; diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index 31a0b724e..f9f162509 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -2796,6 +2796,10 @@ static void gui_settings_network() "Emulate the Ethernet Broadband Adapter (BBA) instead of the Modem"); } OptionCheckbox("Use DCNet (Experimental)", config::UseDCNet, "Connect to the experimental DCNet cloud service."); + ImGui::InputText("ISP User Name", &config::ISPUsername.get(), ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CallbackCharFilter, + [](ImGuiInputTextCallbackData *data) { return static_cast(data->EventChar <= ' ' || data->EventChar > '~'); }, nullptr); + ImGui::SameLine(); + ShowHelpMarker("The ISP user name stored in the console Flash RAM. Used by some online games as the player name. Leave blank to keep the current Flash RAM value."); } #ifdef NAOMI_MULTIBOARD ImGui::Spacing(); diff --git a/shell/libretro/option.cpp b/shell/libretro/option.cpp index e8061ff32..7f9799a36 100644 --- a/shell/libretro/option.cpp +++ b/shell/libretro/option.cpp @@ -123,6 +123,7 @@ Option NetworkOutput(CORE_OPTION_NAME "_network_output", false); Option MultiboardSlaves("", 0); Option BattleCableEnable("", false); Option UseDCNet(CORE_OPTION_NAME "_dcnet", false); +OptionString ISPUsername("", "flycast1"); // Maple From a682e20bbdcee89094513e6168d54d5573fb7a87 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 4 Feb 2025 17:20:15 +0100 Subject: [PATCH 66/76] modem: modem must hang up as soon as reg20 is written some games don't do a full reset and the modem would stay connected. Remove debug file dumps. --- core/hw/modem/modem.cpp | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/core/hw/modem/modem.cpp b/core/hw/modem/modem.cpp index b52e7310a..6be776fdd 100644 --- a/core/hw/modem/modem.cpp +++ b/core/hw/modem/modem.cpp @@ -127,8 +127,6 @@ static bool data_sent; static u64 last_comm_stats; static int sent_bytes; static int recvd_bytes; -static FILE *recv_fp; -static FILE *sent_fp; #endif static int modem_sched_func(int tag, int cycles, int jitter, void *arg) @@ -436,12 +434,12 @@ static void modem_reset(u32 v) memset(&modem_regs, 0, sizeof(modem_regs)); state = MS_RESET; LOG("Modem reset start ..."); + net::modbba::stop(); } else { if (state == MS_RESET) { - net::modbba::stop();; memset(&modem_regs, 0, sizeof(modem_regs)); state = MS_RESETING; ControllerTestStart(); @@ -470,13 +468,6 @@ static u8 download_crc; static void ModemNormalWrite(u32 reg, u32 data) { -#ifndef NDEBUG - if (recv_fp == NULL) - { - recv_fp = fopen("ppp_recv.dump", "w"); - sent_fp = fopen("ppp_sent.dump", "w"); - } -#endif //if (!module_download && reg != 0x10) // LOG("ModemNormalWrite : %03X=%X", reg,data); u32 old = modem_regs.ptr[reg]; @@ -525,8 +516,6 @@ static void ModemNormalWrite(u32 reg, u32 data) data_sent = true; #ifndef NDEBUG sent_bytes++; - if (sent_fp) - fputc(data, sent_fp); #endif net::modbba::writeModem(data); modem_regs.reg1e.TDBE = 0; @@ -686,10 +675,6 @@ u32 ModemReadMem_A0_006(u32 addr, u32 size) modem_regs.reg1e.RDBF = 0; SET_STATUS_BIT(0x0c, modem_regs.reg0c.RXFNE, 0); SET_STATUS_BIT(0x01, modem_regs.reg01.RXHF, 0); -#ifndef NDEBUG - if (connect_state == CONNECTED && recv_fp) - fputc(data, recv_fp); -#endif update_interrupt(); } else if (reg == 0x16 || reg == 0x17) From bf1d4a56aed368f83106472a4317868dcfc54677 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 4 Feb 2025 17:27:47 +0100 Subject: [PATCH 67/76] gdrom: fix savestate compatibility issue consequence of c860807fefd958cc49edeff3e8e9270cda40abcb --- core/hw/gdrom/gdromv3.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/hw/gdrom/gdromv3.cpp b/core/hw/gdrom/gdromv3.cpp index adaff8e8a..a249e9004 100644 --- a/core/hw/gdrom/gdromv3.cpp +++ b/core/hw/gdrom/gdromv3.cpp @@ -140,6 +140,10 @@ void DmaBuffer::deserialize(Deserializer& deser) { deser >> index; deser >> size; + if (index >= sizeof(cache)) + clear(); + else + size = std::min(size, sizeof(cache) - index); deser >> cache; deser.skip(2352 * 16); } From b9df7d3469f32c143ec01356f3fe2464f87e6d2c Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 4 Feb 2025 17:46:02 +0100 Subject: [PATCH 68/76] network digital outputs: check config option when game starts Avoid the need to restart when enabling or disabling this option Send game name to newly connected sockets ignore SIGPIPE (telnet) Log network errors --- core/hw/naomi/naomi.cpp | 2 -- core/linux/common.cpp | 3 +++ core/network/net_platform.h | 4 ++-- core/network/output.h | 34 +++++++++++++++++++++++++--------- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/core/hw/naomi/naomi.cpp b/core/hw/naomi/naomi.cpp index 9a6fa8fbd..ea76f9304 100644 --- a/core/hw/naomi/naomi.cpp +++ b/core/hw/naomi/naomi.cpp @@ -201,8 +201,6 @@ static void Naomi_DmaEnable(u32 addr, u32 data) void naomi_reg_Init() { - networkOutput.init(); - static const u8 romSerialData[0x84] = { 0x19, 0x00, 0xaa, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/core/linux/common.cpp b/core/linux/common.cpp index c1f9d8b77..76c3f0a75 100644 --- a/core/linux/common.cpp +++ b/core/linux/common.cpp @@ -87,6 +87,9 @@ void os_InstallFaultHandler() #ifndef __SWITCH__ struct sigaction act; memset(&act, 0, sizeof(act)); + act.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &act, nullptr); + act.sa_sigaction = fault_handler; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO; diff --git a/core/network/net_platform.h b/core/network/net_platform.h index f4eab1ed1..62236c988 100644 --- a/core/network/net_platform.h +++ b/core/network/net_platform.h @@ -55,7 +55,7 @@ typedef int sock_t; #define L_EINPROGRESS EINPROGRESS #define get_last_error() (errno) #define INVALID_SOCKET (-1) -#define perror(s) do { INFO_LOG(NETWORK, "%s: %s", (s) != NULL ? (s) : "", strerror(get_last_error())); } while (false) +#define perror(s) do { ERROR_LOG(NETWORK, "%s: %s", (s) != NULL ? (s) : "", strerror(get_last_error())); } while (false) #else typedef SOCKET sock_t; #define VALID(s) ((s) != INVALID_SOCKET) @@ -63,7 +63,7 @@ typedef SOCKET sock_t; #define L_EAGAIN WSAEWOULDBLOCK #define L_EINPROGRESS WSAEINPROGRESS #define get_last_error() (WSAGetLastError()) -#define perror(s) do { INFO_LOG(NETWORK, "%s: Winsock error: %d", (s) != NULL ? (s) : "", WSAGetLastError()); } while (false) +#define perror(s) do { ERROR_LOG(NETWORK, "%s: Winsock error: %d", (s) != NULL ? (s) : "", WSAGetLastError()); } while (false) #ifndef SHUT_WR #define SHUT_WR SD_SEND #endif diff --git a/core/network/output.h b/core/network/output.h index db57148ee..c97da5cdd 100644 --- a/core/network/output.h +++ b/core/network/output.h @@ -27,10 +27,14 @@ class NetworkOutput { -public: void init() { - if (!config::NetworkOutput || settings.naomi.slave || settings.naomi.drivingSimSlave == 1) + if (!config::NetworkOutput || settings.naomi.slave || settings.naomi.drivingSimSlave == 1) { + term(); + return; + } + if (server != INVALID_SOCKET) + // already done return; server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); @@ -44,13 +48,13 @@ public: saddr.sin_port = htons(8000 + settings.naomi.drivingSimSlave); if (::bind(server, (sockaddr *)&saddr, saddr_len) < 0) { - perror("bind"); + perror("Network output: bind failed"); term(); return; } if (listen(server, 5) < 0) { - perror("listen"); + perror("Network output: listen failed"); term(); return; } @@ -58,27 +62,27 @@ public: EventManager::listen(Event::VBlank, vblankCallback, this); } +public: void term() { EventManager::unlisten(Event::VBlank, vblankCallback, this); for (sock_t sock : clients) closesocket(sock); clients.clear(); - if (server != INVALID_SOCKET) - { + if (server != INVALID_SOCKET) { closesocket(server); server = INVALID_SOCKET; } } - void reset() - { + void reset() { + init(); gameNameSent = false; } void output(const char *name, u32 value) { - if (!config::NetworkOutput || clients.empty()) + if (clients.empty()) return; if (!gameNameSent) { @@ -104,6 +108,18 @@ private: if (sockfd != INVALID_SOCKET) { set_non_blocking(sockfd); + if (gameNameSent) + { + std::string msg = "game = " + settings.content.gameId + "\n"; + if (::send(sockfd, msg.c_str(), msg.length(), 0) < 0) + { + int error = get_last_error(); + if (error != L_EWOULDBLOCK && error != L_EAGAIN) { + closesocket(sockfd); + return; + } + } + } clients.push_back(sockfd); } } From d6f3ca986d58f58a1278868144fc27f9814142a4 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 5 Feb 2025 18:28:12 +0100 Subject: [PATCH 69/76] fix from jscrivani to work around the initial failed connection --- tools/dreampi/dreampi.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/dreampi/dreampi.py b/tools/dreampi/dreampi.py index 5a3d887e6..6178da67e 100755 --- a/tools/dreampi/dreampi.py +++ b/tools/dreampi/dreampi.py @@ -79,7 +79,7 @@ DNS_FILE = "https://dreamcast.online/dreampi/dreampi_dns.conf" logger = logging.getLogger("dreampi") - +first_run = 1 def check_internet_connection(): """ Returns True if there's a connection """ @@ -578,9 +578,15 @@ class Modem(object): def start_dial_tone(self): if not self._dial_tone_wav: return + global first_run i = 0 while i < 3: try: + if first_run: + first_run = 0 + subprocess.Popen("/home/pi/dreampi/dcnet.rpi") + time.sleep(2) + subprocess.call("sudo killall dcnet.rpi".split()) self.reset() self.send_command(b"AT+FCLASS=8") # Enter voice mode self.send_command(b"AT+VLS=1") # Go off-hook From f3c838d19d4eb9b49c5e1004ab7b75dc032e7863 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Wed, 5 Feb 2025 18:29:56 +0100 Subject: [PATCH 70/76] dricas auth cheat for Aero Dancing i - Jikai Saku Made Matemasen --- core/cheats.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/cheats.cpp b/core/cheats.cpp index da8dede07..d55347e7f 100644 --- a/core/cheats.cpp +++ b/core/cheats.cpp @@ -494,6 +494,10 @@ void CheatManager::reset(const std::string& gameId) cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0004b7a0, 0x2fd62fe6, true); cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0004b7a0, 0xe000000b, true); // rts, _mov #0, r0 } + else if (gameId == "T6809M") { // Aero Dancing i - Jikai Saku Made Matemasen + cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0004b940, 0x2fd62fe6, true); + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0004b940, 0xe000000b, true); // rts, _mov #0, r0 + } else if (gameId == "HDR-0106") { // Daytona USA (JP) cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0003ad30, 0x2fd62fe6, true); cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0003ad30, 0xe000000b, true); // rts, _mov #0, r0 From c7499df3a89e223cbec7e89f88e4a472c48c1040 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 8 Feb 2025 16:17:15 +0100 Subject: [PATCH 71/76] cheats for Aero Dancing i & i2 in BBA mode --- core/cheats.cpp | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/core/cheats.cpp b/core/cheats.cpp index d55347e7f..8dde4af9c 100644 --- a/core/cheats.cpp +++ b/core/cheats.cpp @@ -490,29 +490,49 @@ void CheatManager::reset(const std::string& gameId) cheats.emplace_back(Cheat::Type::setValue, "enable logging", true, 32, 0x00314228, 1, true); } // Dricas auth bypass - else if (gameId == "T6807M") { // Aero Dancing i + else if (gameId == "T6807M") // Aero Dancing i + { + // modem cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0004b7a0, 0x2fd62fe6, true); - cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0004b7a0, 0xe000000b, true); // rts, _mov #0, r0 + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0004b7a0, 0xe000000b, true); // rts, _mov #0, r0 + // BBA + cheats.emplace_back(Cheat::Type::runNextIfEq, "bba bypass auth ifeq", true, 32, 0x0004af5c, 0x2fd62fe6, true); + cheats.emplace_back(Cheat::Type::setValue, "bba bypass dricas auth", true, 32, 0x0004af5c, 0xe000000b, true); + // IP check + cheats.emplace_back(Cheat::Type::runNextIfEq, "ip check ifeq", true, 32, 0x00020860, 0x4f222fe6, true); + cheats.emplace_back(Cheat::Type::setValue, "ip check ok", true, 32, 0x00020860, 0xe000000b, true); } - else if (gameId == "T6809M") { // Aero Dancing i - Jikai Saku Made Matemasen + else if (gameId == "T6809M") // Aero Dancing i - Jikai Saku Made Matemasen + { + // modem cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0004b940, 0x2fd62fe6, true); - cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0004b940, 0xe000000b, true); // rts, _mov #0, r0 + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0004b940, 0xe000000b, true); + // BBA + cheats.emplace_back(Cheat::Type::runNextIfEq, "bba bypass auth ifeq", true, 32, 0x0004f848, 0x2fd62fe6, true); + cheats.emplace_back(Cheat::Type::setValue, "bba bypass dricas auth", true, 32, 0x0004f848, 0xe000000b, true); + // IP check + cheats.emplace_back(Cheat::Type::runNextIfEq, "ip check ifeq", true, 32, 0x00020980, 0x4f222fe6, true); + cheats.emplace_back(Cheat::Type::setValue, "ip check ok", true, 32, 0x00020980, 0xe000000b, true); } else if (gameId == "HDR-0106") { // Daytona USA (JP) cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0003ad30, 0x2fd62fe6, true); - cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0003ad30, 0xe000000b, true); // rts, _mov #0, r0 + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0003ad30, 0xe000000b, true); } else if (gameId == "HDR-0073") { // Sega Tetris cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x000a56f8, 0x2fd62fe6, true); - cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x000a56f8, 0xe000000b, true); // rts, _mov #0, r0 + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x000a56f8, 0xe000000b, true); } else if (gameId == "T44501M") { // Golf Shiyou Yo 2 cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x0013f150, 0x2fd62fe6, true); - cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0013f150, 0xe000000b, true); // rts, _mov #0, r0 + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x0013f150, 0xe000000b, true); } - else if (gameId == "HDR-0124") { // Hundred Swords + else if (gameId == "HDR-0124") { // Hundred Swords cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x006558ac, 0x1f414f22, true); - cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x006558ac, 0xe000000b, true); // rts, _mov #0, r0 + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x006558ac, 0xe000000b, true); + } + else if (gameId == "T43903M") { // Culdcept II + cheats.emplace_back(Cheat::Type::runNextIfEq, "bypass auth ifeq", true, 32, 0x00800524, 0x2fd62fe6, true); + cheats.emplace_back(Cheat::Type::setValue, "bypass dricas auth", true, 32, 0x00800524, 0xe000000b, true); } if (cheats.size() > cheatCount) From 84356e3b26006f660f8f69a4c1c39ef288d0cd31 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sat, 8 Feb 2025 16:20:47 +0100 Subject: [PATCH 72/76] dreampi.py: shake off the modem on first run (from scrivani) --- tools/dreampi/dreampi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/dreampi/dreampi.py b/tools/dreampi/dreampi.py index 6178da67e..41d424e73 100755 --- a/tools/dreampi/dreampi.py +++ b/tools/dreampi/dreampi.py @@ -587,6 +587,7 @@ class Modem(object): subprocess.Popen("/home/pi/dreampi/dcnet.rpi") time.sleep(2) subprocess.call("sudo killall dcnet.rpi".split()) + self.shake_it_off() self.reset() self.send_command(b"AT+FCLASS=8") # Enter voice mode self.send_command(b"AT+VLS=1") # Go off-hook From 5187258f4bb360fea8245e101fc3f6ef027afdda Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Mon, 10 Feb 2025 18:12:24 +0100 Subject: [PATCH 73/76] dcnet: BBA support Stop network service when bba is reset. Start it on first frame out. dcnet: close connection on dhcp release picoppp: close all sockets and acceptors when stopping. --- core/hw/bba/bba.cpp | 4 +- core/hw/bba/rtl8139c.cpp | 3 +- core/network/dcnet.cpp | 302 +++++++++++++++++++++++++++++++----- core/network/netservice.cpp | 2 +- core/network/picoppp.cpp | 32 +++- 5 files changed, 296 insertions(+), 47 deletions(-) diff --git a/core/hw/bba/bba.cpp b/core/hw/bba/bba.cpp index 6deb09dbf..2b1b49d4a 100644 --- a/core/hw/bba/bba.cpp +++ b/core/hw/bba/bba.cpp @@ -179,9 +179,9 @@ void bba_WriteMem(u32 addr, u32 data, u32 sz) case GAPS_RESET: if (data & 1) { - DEBUG_LOG(NETWORK, "GAPS reset"); + INFO_LOG(NETWORK, "BBA: GAPS reset"); rtl8139_reset(rtl8139device); - net::modbba::start(); + net::modbba::stop(); } break; diff --git a/core/hw/bba/rtl8139c.cpp b/core/hw/bba/rtl8139c.cpp index 6a509387b..51ea904a1 100644 --- a/core/hw/bba/rtl8139c.cpp +++ b/core/hw/bba/rtl8139c.cpp @@ -1728,8 +1728,7 @@ static void rtl8139_io_writeb(void *opaque, uint8_t addr, uint32_t val) break; default: - INFO_LOG(NETWORK, "not implemented write(b) addr=0x%x val=0x%02x", addr, - val); + DEBUG_LOG(NETWORK, "not implemented write(b) addr=0x%x val=0x%02x", addr, val); break; } } diff --git a/core/network/dcnet.cpp b/core/network/dcnet.cpp index c70c37764..c3055788f 100644 --- a/core/network/dcnet.cpp +++ b/core/network/dcnet.cpp @@ -22,21 +22,22 @@ #include "util/tsqueue.h" #include "oslib/oslib.h" #include "emulator.h" +#include "hw/bba/bba.h" +#include "cfg/option.h" +#include "stdclass.h" + #include #include #include #ifndef __ANDROID__ -//#define DEBUG_PPP 1 -#endif -#ifdef DEBUG_PPP -#include +//#define WIRESHARK_DUMP 1 #endif namespace net::modbba { -static TsQueue in_buffer; -static TsQueue out_buffer; +static TsQueue toModem; +static TsQueue fromModem; class DCNetService : public Service { @@ -51,19 +52,20 @@ public: void receiveEthFrame(const u8 *frame, u32 size) override; }; -class Socket +template +class PPPSocket { public: - Socket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint) + PPPSocket(asio::io_context& io_context, const typename SocketT::endpoint_type& endpoint) : socket(io_context), timer(io_context) { socket.connect(endpoint); - os_notify("Connected to DCNet cloud", 5000); + os_notify("Connected to DCNet with modem", 5000); receive(); sendIfAny({}); } - ~Socket() { + ~PPPSocket() { if (dumpfp != nullptr) fclose(dumpfp); } @@ -71,7 +73,7 @@ public: private: void receive() { socket.async_read_some(asio::buffer(recvBuffer), - std::bind(&Socket::onRecv, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); + std::bind(&PPPSocket::onRecv, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); } void onRecv(asio::error_code ec, size_t len) { @@ -81,7 +83,7 @@ private: } pppdump(recvBuffer.data(), len, false); for (size_t i = 0; i < len; i++) - in_buffer.push(recvBuffer[i]); + toModem.push(recvBuffer[i]); receive(); } @@ -93,9 +95,9 @@ private: } if (!sending) { - for (; !out_buffer.empty() && sendBufSize < sendBuffer.size(); sendBufSize++) + for (; !fromModem.empty() && sendBufSize < sendBuffer.size(); sendBufSize++) { - sendBuffer[sendBufSize] = out_buffer.pop(); + sendBuffer[sendBufSize] = fromModem.pop(); if (sendBufSize != 0 && sendBuffer[sendBufSize] == 0x7e) { sendBufSize++; break; @@ -106,13 +108,13 @@ private: { pppdump(sendBuffer.data(), sendBufSize, true); asio::async_write(socket, asio::buffer(sendBuffer, sendBufSize), - std::bind(&Socket::onSent, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); + std::bind(&PPPSocket::onSent, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); sending = true; } } timer.expires_at(timer.expiry() + asio::chrono::milliseconds(5)); - timer.async_wait(std::bind(&Socket::sendIfAny, this, asio::placeholders::error)); + timer.async_wait(std::bind(&PPPSocket::sendIfAny, this, asio::placeholders::error)); } void onSent(asio::error_code ec, size_t len) @@ -127,14 +129,9 @@ private: void pppdump(uint8_t *buf, int len, bool egress) { -#ifdef DEBUG_PPP +#ifdef WIRESHARK_DUMP if (!len) return; - const auto& timems = []() -> time_t { - timeval tv; - gettimeofday(&tv, nullptr); - return tv.tv_sec * 1000 + tv.tv_usec / 1000; - }; if (dumpfp == nullptr) { dumpfp = fopen("ppp.dump", "a"); @@ -145,10 +142,10 @@ private: u32 reset_time = ntohl((u32)now); fputc(7, dumpfp); // Reset time fwrite(&reset_time, sizeof(reset_time), 1, dumpfp); - dump_last_time_ms = timems(); + dump_last_time_ms = getTimeMs(); } - u32 delta = (timems() - dump_last_time_ms) / 100; + u32 delta = getTimeMs() / 100 - dump_last_time_ms / 100; if (delta < 256) { fputc(6, dumpfp); // Time step (short) fwrite(&delta, 1, 1, dumpfp); @@ -159,7 +156,7 @@ private: fputc(5, dumpfp); // Time step (long) fwrite(&delta, sizeof(delta), 1, dumpfp); } - dump_last_time_ms = timems(); + dump_last_time_ms = getTimeMs(); fputc(egress ? 1 : 2, dumpfp); // Sent/received data @@ -170,23 +167,200 @@ private: #endif } - asio::ip::tcp::socket socket; + SocketT socket; asio::steady_timer timer; - std::array recvBuffer; - std::array sendBuffer; + std::array recvBuffer; + std::array sendBuffer; u32 sendBufSize = 0; bool sending = false; FILE *dumpfp = nullptr; - time_t dump_last_time_ms; + u64 dump_last_time_ms; }; +class EthSocket +{ +public: + EthSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint) + : socket(io_context) + { + socket.connect(endpoint); + os_notify("Connected to DCNet with ethernet", 5000); + receive(); + u8 prolog[] = { 'D', 'C', 'N', 'E', 'T', 1 }; + send(prolog, sizeof(prolog)); + Instance = this; + } + + ~EthSocket() { + if (dumpfp != nullptr) + fclose(dumpfp); + Instance = nullptr; + } + + void send(const u8 *frame, u32 size) + { + if (sendBufferIdx + size >= sendBuffer.size()) { + WARN_LOG(NETWORK, "Dropped out frame (buffer:%d + %d bytes). Increase send buffer size\n", sendBufferIdx, size); + return; + } + if (Instance != nullptr) + ethdump(frame, size); + *(u16 *)&sendBuffer[sendBufferIdx] = size; + sendBufferIdx += 2; + memcpy(&sendBuffer[sendBufferIdx], frame, size); + sendBufferIdx += size; + doSend(); + } + +private: + using iterator = asio::buffers_iterator; + + std::pair + static packetMatcher(iterator begin, iterator end) + { + if (end - begin < 3) + return std::make_pair(begin, false); + iterator i = begin; + uint16_t len = (uint8_t)*i++; + len |= uint8_t(*i++) << 8; + len += 2; + if (end - begin < len) + return std::make_pair(begin, false); + return std::make_pair(begin + len, true); + } + + void receive() { + asio::async_read_until(socket, asio::dynamic_vector_buffer(recvBuffer), packetMatcher, + std::bind(&EthSocket::onRecv, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); + } + void onRecv(asio::error_code ec, size_t len) + { + if (ec || len == 0) + { + if (ec) + ERROR_LOG(NETWORK, "onRecv error: %s", ec.message().c_str()); + socket.close(ec); + return; + } + /* + verify(len - 2 == *(u16 *)&recvBuffer[0]); + printf("In frame: dest %02x:%02x:%02x:%02x:%02x:%02x " + "src %02x:%02x:%02x:%02x:%02x:%02x, ethertype %04x, size %d bytes\n", + recvBuffer[2], recvBuffer[3], recvBuffer[4], recvBuffer[5], recvBuffer[6], recvBuffer[7], + recvBuffer[8], recvBuffer[9], recvBuffer[10], recvBuffer[11], recvBuffer[12], recvBuffer[13], + *(u16 *)&recvBuffer[14], (int)len - 2); + */ + ethdump(&recvBuffer[2], len - 2); + bba_recv_frame(&recvBuffer[2], len - 2); + if (len < recvBuffer.size()) + recvBuffer.erase(recvBuffer.begin(), recvBuffer.begin() + len); + else + recvBuffer.clear(); + receive(); + } + + void doSend() + { + if (sending) + return; + sending = true; + asio::async_write(socket, asio::buffer(sendBuffer, sendBufferIdx), + std::bind(&EthSocket::onSent, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); + } + + void onSent(asio::error_code ec, size_t len) + { + sending = false; + if (ec) { + ERROR_LOG(NETWORK, "onRecv error: %s", ec.message().c_str()); + socket.close(ec); + return; + } + sendBufferIdx -= len; + if (sendBufferIdx != 0) { + memmove(sendBuffer.data(), sendBuffer.data() + len, sendBufferIdx); + doSend(); + } + } + + void ethdump(const uint8_t *frame, int size) + { +#ifdef WIRESHARK_DUMP + if (dumpfp == nullptr) + { + dumpfp = fopen("bba.pcapng", "wb"); + if (dumpfp == nullptr) + { + const char *home = getenv("HOME"); + if (home != nullptr) + { + std::string path = home + std::string("/bba.pcapng"); + dumpfp = fopen(path.c_str(), "wb"); + } + if (dumpfp == nullptr) + return; + } + u32 blockType = 0x0A0D0D0A; // Section Header Block + fwrite(&blockType, sizeof(blockType), 1, dumpfp); + u32 blockLen = 28; + fwrite(&blockLen, sizeof(blockLen), 1, dumpfp); + u32 magic = 0x1A2B3C4D; + fwrite(&magic, sizeof(magic), 1, dumpfp); + u32 version = 1; // 1.0 + fwrite(&version, sizeof(version), 1, dumpfp); + u64 sectionLength = ~0; // unspecified + fwrite(§ionLength, sizeof(sectionLength), 1, dumpfp); + fwrite(&blockLen, sizeof(blockLen), 1, dumpfp); + + blockType = 1; // Interface Description Block + fwrite(&blockType, sizeof(blockType), 1, dumpfp); + blockLen = 20; + fwrite(&blockLen, sizeof(blockLen), 1, dumpfp); + const u32 linkType = 1; // Ethernet + fwrite(&linkType, sizeof(linkType), 1, dumpfp); + const u32 snapLen = 0; // no limit + fwrite(&snapLen, sizeof(snapLen), 1, dumpfp); + // TODO options? if name, ip/mac address + fwrite(&blockLen, sizeof(blockLen), 1, dumpfp); + } + const u32 blockType = 6; // Extended Packet Block + fwrite(&blockType, sizeof(blockType), 1, dumpfp); + u32 roundedSize = ((size + 3) & ~3) + 32; + fwrite(&roundedSize, sizeof(roundedSize), 1, dumpfp); + u32 ifId = 0; + fwrite(&ifId, sizeof(ifId), 1, dumpfp); + u64 now = getTimeMs() * 1000; + fwrite((u32 *)&now + 1, 4, 1, dumpfp); + fwrite(&now, 4, 1, dumpfp); + fwrite(&size, sizeof(size), 1, dumpfp); + fwrite(&size, sizeof(size), 1, dumpfp); + fwrite(frame, 1, size, dumpfp); + fwrite(frame, 1, roundedSize - size - 32, dumpfp); + fwrite(&roundedSize, sizeof(roundedSize), 1, dumpfp); +#endif + } + + asio::ip::tcp::socket socket; + std::vector recvBuffer; + std::array sendBuffer; + u32 sendBufferIdx = 0; + bool sending = false; + + FILE *dumpfp = nullptr; + +public: + static EthSocket *Instance; +}; +EthSocket *EthSocket::Instance; + class DCNetThread { public: void start() { - verify(!thread.joinable()); + if (thread.joinable()) + return; io_context = std::make_unique(); thread = std::thread(&DCNetThread::run, this); } @@ -198,6 +372,7 @@ public: io_context->stop(); thread.join(); io_context.reset(); + os_notify("DCNet disconnected", 3000); } private: @@ -205,6 +380,7 @@ private: std::thread thread; std::unique_ptr io_context; + friend DCNetService; }; static DCNetThread thread; @@ -221,37 +397,83 @@ void DCNetService::stop() { } void DCNetService::writeModem(u8 b) { - out_buffer.push(b); + fromModem.push(b); } int DCNetService::readModem() { - if (in_buffer.empty()) + if (toModem.empty()) return -1; else - return in_buffer.pop(); + return toModem.pop(); } int DCNetService::modemAvailable() { - return in_buffer.size(); + return toModem.size(); } -void DCNetService::receiveEthFrame(unsigned char const*, unsigned int) { - // TODO +void DCNetService::receiveEthFrame(u8 const *frame, unsigned int len) +{ + /* + printf("Out frame: dest %02x:%02x:%02x:%02x:%02x:%02x " + "src %02x:%02x:%02x:%02x:%02x:%02x, ethertype %04x, size %d bytes %s\n", + frame[0], frame[1], frame[2], frame[3], frame[4], frame[5], + frame[6], frame[7], frame[8], frame[9], frame[10], frame[11], + *(u16 *)&frame[12], len, EthSocket::Instance == nullptr ? "LOST" : ""); + */ + // Stop DCNet on DHCP Release + if (len >= 0x11d + && *(u16 *)&frame[0xc] == 0x0008 // type: IPv4 + && frame[0x17] == 0x11 // UDP + && ntohs(*(u16 *)&frame[0x22]) == 68 // src port: dhcpc + && ntohs(*(u16 *)&frame[0x24]) == 67) // dest port: dhcps + { + const u8 *options = &frame[0x11a]; + while (options - frame < len && *options != 0xff) { + if (*options == 53 // message type + && options[2] == 7) // release + { + stop(); + return; + } + options += options[1] + 2; + } + } + if (EthSocket::Instance != nullptr) + { + std::vector vbuf(frame, frame + len); + thread.io_context->post([vbuf]() { + EthSocket::Instance->send(vbuf.data(), vbuf.size()); + }); + } + else { + // restart the thread if previously stopped + start(); + } } - void DCNetThread::run() { try { + std::string port; + if (config::EmulateBBA) + port = "7655"; + else + port = "7654"; asio::ip::tcp::resolver resolver(*io_context); - auto it = resolver.resolve("dcnet.flyca.st", "7654"); + auto it = resolver.resolve("dcnet.flyca.st", port); if (it.empty()) throw std::runtime_error("Can't find dcnet host"); asio::ip::tcp::endpoint endpoint = *it.begin(); - Socket socket(*io_context, endpoint); - io_context->run(); + if (config::EmulateBBA) { + EthSocket socket(*io_context, endpoint); + io_context->run(); + } + else { + PPPSocket socket(*io_context, endpoint); + io_context->run(); + } } catch (const std::runtime_error& e) { ERROR_LOG(NETWORK, "DCNetThread::run error: %s", e.what()); } diff --git a/core/network/netservice.cpp b/core/network/netservice.cpp index 41998c5db..889688ba4 100644 --- a/core/network/netservice.cpp +++ b/core/network/netservice.cpp @@ -61,7 +61,7 @@ int modemAvailable() { } void receiveEthFrame(const u8 *frame, u32 size) { - verify(service != nullptr); + start(); service->receiveEthFrame(frame, size); } diff --git a/core/network/picoppp.cpp b/core/network/picoppp.cpp index 3ec8cad7b..b53c37168 100644 --- a/core/network/picoppp.cpp +++ b/core/network/picoppp.cpp @@ -171,6 +171,7 @@ static const GamePortList GamesPorts[] = { static bool pico_thread_running = false; extern "C" int dont_reject_opt_vj_hack; +static bool start_pico(); u32 makeDnsQueryPacket(void *buf, const char *host); pico_ip4 parseDnsResponsePacket(const void *buf, size_t len); @@ -272,6 +273,11 @@ public: return socket; } + void close() { + closeAll(); + directPlay.reset(); + } + private: TcpSocket(asio::io_context& io_context, std::shared_ptr directPlay) : io_context(io_context), socket(io_context), directPlay(directPlay) { @@ -565,6 +571,7 @@ public: void start() { TcpSocket::Ptr newSock = TcpSocket::create(io_context, directPlay); + sockets.push_back(newSock); acceptor.async_accept(newSock->getSocket(), std::bind(&TcpAcceptor::onAccept, shared_from_this(), newSock, asio::placeholders::error)); @@ -572,6 +579,10 @@ public: void stop() { acceptor.close(); + for (auto& socket : sockets) + socket->close(); + sockets.clear(); + directPlay.reset(); } private: @@ -601,6 +612,7 @@ private: asio::io_context& io_context; asio::ip::tcp::acceptor acceptor; std::shared_ptr directPlay; + std::vector sockets; friend super; }; @@ -641,6 +653,9 @@ public: if (pico_sock != nullptr) pico_socket_close(pico_sock); directPlay.reset(); + for (auto& socket : sockets) + socket->close(); + sockets.clear(); } private: @@ -667,6 +682,7 @@ private: TcpSocket::Ptr psock = TcpSocket::create(io_context, directPlay); psock->connect(sock_a); + sockets.push_back(psock); } } @@ -690,6 +706,7 @@ private: asio::io_context& io_context; std::shared_ptr directPlay; pico_socket *pico_sock; + std::vector sockets; }; // Handles inbound datagram to a given port @@ -942,10 +959,15 @@ public: ~DirectPlayImpl() { + stop(); if (upnpCmd.valid()) upnpCmd.get(); + } + + void stop() { if (acceptor) acceptor->stop(); + acceptor.reset(); } private: @@ -1141,9 +1163,13 @@ static void closeDumpFile() } static void pico_receive_eth_frame(const u8 *frame, u32 size) { - dumpFrame(frame, size); - if (pico_dev != nullptr) + if (pico_dev == nullptr) { + start_pico(); + } + else { + dumpFrame(frame, size); pico_stack_recv(pico_dev, (u8 *)frame, size); + } } static int send_eth_frame(pico_device *dev, void *data, int len) { @@ -1365,6 +1391,8 @@ void PicoThread::run() acceptors.clear(); tcpSink.stop(); udpSink.stop(); + directPlay->stop(); + directPlay.reset(); pico_stack_tick(); pico_stack_tick(); From 387e2427b40a28d95aad1165d5f5c62ec8ba24ff Mon Sep 17 00:00:00 2001 From: scribam Date: Sun, 9 Feb 2025 13:04:19 +0100 Subject: [PATCH 74/76] deps: update sdl to version 2.32.0 --- core/deps/SDL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/deps/SDL b/core/deps/SDL index fa24d868a..7a44b1ab0 160000 --- a/core/deps/SDL +++ b/core/deps/SDL @@ -1 +1 @@ -Subproject commit fa24d868ac2f8fd558e4e914c9863411245db8fd +Subproject commit 7a44b1ab002cee6efa56d3b4c0e146b7fbaed80b From 0414a905008b60e5abfbfa8969c323b00fd98cdf Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Sun, 16 Feb 2025 17:33:56 +0100 Subject: [PATCH 75/76] dcnet: don't use timer for modem. better error reporting. race condition Use io_context::post() instead of a timer to read from modem. Notify user if connection to DCNet fails. Fix race condition/crash with BBA. Use lambdas instead of std::bind. --- core/network/dcnet.cpp | 282 ++++++++++++++++++++++------------------- 1 file changed, 152 insertions(+), 130 deletions(-) diff --git a/core/network/dcnet.cpp b/core/network/dcnet.cpp index c3055788f..d2535dbe9 100644 --- a/core/network/dcnet.cpp +++ b/core/network/dcnet.cpp @@ -37,7 +37,6 @@ namespace net::modbba { static TsQueue toModem; -static TsQueue fromModem; class DCNetService : public Service { @@ -57,12 +56,14 @@ class PPPSocket { public: PPPSocket(asio::io_context& io_context, const typename SocketT::endpoint_type& endpoint) - : socket(io_context), timer(io_context) + : socket(io_context) { - socket.connect(endpoint); + asio::error_code ec; + socket.connect(endpoint, ec); + if (ec) + throw FlycastException(ec.message().c_str()); os_notify("Connected to DCNet with modem", 5000); receive(); - sendIfAny({}); } ~PPPSocket() { @@ -70,63 +71,69 @@ public: fclose(dumpfp); } + void send(u8 b) + { + if (sendBufSize == sendBuffer.size()) { + WARN_LOG(NETWORK, "PPP output buffer overflow"); + return; + } + sendBuffer[sendBufSize++] = b; + doSend(); + } + private: - void receive() { + void receive() + { socket.async_read_some(asio::buffer(recvBuffer), - std::bind(&PPPSocket::onRecv, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); - } - void onRecv(asio::error_code ec, size_t len) - { - if (ec) { - ERROR_LOG(NETWORK, "onRecv error: %s", ec.message().c_str()); - return; - } - pppdump(recvBuffer.data(), len, false); - for (size_t i = 0; i < len; i++) - toModem.push(recvBuffer[i]); - receive(); - } - - void sendIfAny(const std::error_code& ec) - { - if (ec) { - ERROR_LOG(NETWORK, "sendIfAny timer error: %s", ec.message().c_str()); - return; - } - if (!sending) - { - for (; !fromModem.empty() && sendBufSize < sendBuffer.size(); sendBufSize++) + [this](const std::error_code& ec, size_t len) { - sendBuffer[sendBufSize] = fromModem.pop(); - if (sendBufSize != 0 && sendBuffer[sendBufSize] == 0x7e) { - sendBufSize++; - break; + if (ec || len == 0) + { + if (ec) + ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str()); + close(); + return; } - } - if ((sendBufSize > 1 && sendBuffer[sendBufSize - 1] == 0x7e) - || sendBufSize == sendBuffer.size()) - { - pppdump(sendBuffer.data(), sendBufSize, true); - asio::async_write(socket, asio::buffer(sendBuffer, sendBufSize), - std::bind(&PPPSocket::onSent, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); - sending = true; - } - } - - timer.expires_at(timer.expiry() + asio::chrono::milliseconds(5)); - timer.async_wait(std::bind(&PPPSocket::sendIfAny, this, asio::placeholders::error)); + pppdump(recvBuffer.data(), len, false); + for (size_t i = 0; i < len; i++) + toModem.push(recvBuffer[i]); + receive(); + }); } - void onSent(asio::error_code ec, size_t len) + void doSend() { - sending = false; - sendBufSize = 0; - if (ec) { - ERROR_LOG(NETWORK, "onRecv error: %s", ec.message().c_str()); + if (sending) return; + if ((sendBufSize > 1 && sendBuffer[sendBufSize - 1] == 0x7e) + || sendBufSize == sendBuffer.size()) + { + pppdump(sendBuffer.data(), sendBufSize, true); + sending = true; + asio::async_write(socket, asio::buffer(sendBuffer, sendBufSize), + [this](const std::error_code& ec, size_t len) + { + if (ec) + { + ERROR_LOG(NETWORK, "Send error: %s", ec.message().c_str()); + close(); + return; + } + sending = false; + sendBufSize -= len; + if (sendBufSize > 0) { + memmove(&sendBuffer[0], &sendBuffer[len], sendBufSize); + doSend(); + } + }); } } + void close() { + std::error_code ignored; + socket.close(ignored); + } + void pppdump(uint8_t *buf, int len, bool egress) { #ifdef WIRESHARK_DUMP @@ -157,18 +164,14 @@ private: fwrite(&delta, sizeof(delta), 1, dumpfp); } dump_last_time_ms = getTimeMs(); - fputc(egress ? 1 : 2, dumpfp); // Sent/received data - uint16_t slen = htons(len); fwrite(&slen, 2, 1, dumpfp); - fwrite(buf, 1, len, dumpfp); #endif } SocketT socket; - asio::steady_timer timer; std::array recvBuffer; std::array sendBuffer; u32 sendBufSize = 0; @@ -178,24 +181,27 @@ private: u64 dump_last_time_ms; }; +using PPPTcpSocket = PPPSocket; + class EthSocket { public: EthSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint) : socket(io_context) { - socket.connect(endpoint); - os_notify("Connected to DCNet with ethernet", 5000); + asio::error_code ec; + socket.connect(endpoint, ec); + if (ec) + throw FlycastException(ec.message().c_str()); + os_notify("Connected to DCNet with Ethernet", 5000); receive(); u8 prolog[] = { 'D', 'C', 'N', 'E', 'T', 1 }; send(prolog, sizeof(prolog)); - Instance = this; } ~EthSocket() { if (dumpfp != nullptr) fclose(dumpfp); - Instance = nullptr; } void send(const u8 *frame, u32 size) @@ -204,7 +210,7 @@ public: WARN_LOG(NETWORK, "Dropped out frame (buffer:%d + %d bytes). Increase send buffer size\n", sendBufferIdx, size); return; } - if (Instance != nullptr) + if (size >= 32) // skip prolog ethdump(frame, size); *(u16 *)&sendBuffer[sendBufferIdx] = size; sendBufferIdx += 2; @@ -230,34 +236,35 @@ private: return std::make_pair(begin + len, true); } - void receive() { - asio::async_read_until(socket, asio::dynamic_vector_buffer(recvBuffer), packetMatcher, - std::bind(&EthSocket::onRecv, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); - } - void onRecv(asio::error_code ec, size_t len) + void receive() { - if (ec || len == 0) - { - if (ec) - ERROR_LOG(NETWORK, "onRecv error: %s", ec.message().c_str()); - socket.close(ec); - return; - } - /* - verify(len - 2 == *(u16 *)&recvBuffer[0]); - printf("In frame: dest %02x:%02x:%02x:%02x:%02x:%02x " - "src %02x:%02x:%02x:%02x:%02x:%02x, ethertype %04x, size %d bytes\n", - recvBuffer[2], recvBuffer[3], recvBuffer[4], recvBuffer[5], recvBuffer[6], recvBuffer[7], - recvBuffer[8], recvBuffer[9], recvBuffer[10], recvBuffer[11], recvBuffer[12], recvBuffer[13], - *(u16 *)&recvBuffer[14], (int)len - 2); - */ - ethdump(&recvBuffer[2], len - 2); - bba_recv_frame(&recvBuffer[2], len - 2); - if (len < recvBuffer.size()) - recvBuffer.erase(recvBuffer.begin(), recvBuffer.begin() + len); - else - recvBuffer.clear(); - receive(); + asio::async_read_until(socket, asio::dynamic_vector_buffer(recvBuffer), packetMatcher, + [this](const std::error_code& ec, size_t len) + { + if (ec || len == 0) + { + if (ec) + ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str()); + std::error_code ignored; + socket.close(ignored); + return; + } + /* + verify(len - 2 == *(u16 *)&recvBuffer[0]); + printf("In frame: dest %02x:%02x:%02x:%02x:%02x:%02x " + "src %02x:%02x:%02x:%02x:%02x:%02x, ethertype %04x, size %d bytes\n", + recvBuffer[2], recvBuffer[3], recvBuffer[4], recvBuffer[5], recvBuffer[6], recvBuffer[7], + recvBuffer[8], recvBuffer[9], recvBuffer[10], recvBuffer[11], recvBuffer[12], recvBuffer[13], + *(u16 *)&recvBuffer[14], (int)len - 2); + */ + ethdump(&recvBuffer[2], len - 2); + bba_recv_frame(&recvBuffer[2], len - 2); + if (len < recvBuffer.size()) + recvBuffer.erase(recvBuffer.begin(), recvBuffer.begin() + len); + else + recvBuffer.clear(); + receive(); + }); } void doSend() @@ -266,22 +273,22 @@ private: return; sending = true; asio::async_write(socket, asio::buffer(sendBuffer, sendBufferIdx), - std::bind(&EthSocket::onSent, this, asio::placeholders::error, asio::placeholders::bytes_transferred)); - } - - void onSent(asio::error_code ec, size_t len) - { - sending = false; - if (ec) { - ERROR_LOG(NETWORK, "onRecv error: %s", ec.message().c_str()); - socket.close(ec); - return; - } - sendBufferIdx -= len; - if (sendBufferIdx != 0) { - memmove(sendBuffer.data(), sendBuffer.data() + len, sendBufferIdx); - doSend(); - } + [this](const std::error_code& ec, size_t len) + { + sending = false; + if (ec) + { + ERROR_LOG(NETWORK, "Send error: %s", ec.message().c_str()); + std::error_code ignored; + socket.close(ignored); + return; + } + sendBufferIdx -= len; + if (sendBufferIdx != 0) { + memmove(sendBuffer.data(), sendBuffer.data() + len, sendBufferIdx); + doSend(); + } + }); } void ethdump(const uint8_t *frame, int size) @@ -348,11 +355,7 @@ private: bool sending = false; FILE *dumpfp = nullptr; - -public: - static EthSocket *Instance; }; -EthSocket *EthSocket::Instance; class DCNetThread { @@ -371,15 +374,42 @@ public: return; io_context->stop(); thread.join(); + pppSocket.reset(); + ethSocket.reset(); io_context.reset(); os_notify("DCNet disconnected", 3000); } + void sendModem(u8 v) + { + if (io_context == nullptr || pppSocket == nullptr) + return; + io_context->post([this, v]() { + pppSocket->send(v); + }); + } + void sendEthFrame(const u8 *frame, u32 len) + { + if (io_context != nullptr && ethSocket != nullptr) + { + std::vector vbuf(frame, frame + len); + io_context->post([this, vbuf]() { + ethSocket->send(vbuf.data(), vbuf.size()); + }); + } + else { + // restart the thread if previously stopped + start(); + } + } + private: void run(); std::thread thread; std::unique_ptr io_context; + std::unique_ptr pppSocket; + std::unique_ptr ethSocket; friend DCNetService; }; static DCNetThread thread; @@ -397,7 +427,7 @@ void DCNetService::stop() { } void DCNetService::writeModem(u8 b) { - fromModem.push(b); + thread.sendModem(b); } int DCNetService::readModem() @@ -429,7 +459,8 @@ void DCNetService::receiveEthFrame(u8 const *frame, unsigned int len) && ntohs(*(u16 *)&frame[0x24]) == 67) // dest port: dhcps { const u8 *options = &frame[0x11a]; - while (options - frame < len && *options != 0xff) { + while (options - frame < len && *options != 0xff) + { if (*options == 53 // message type && options[2] == 7) // release { @@ -439,17 +470,7 @@ void DCNetService::receiveEthFrame(u8 const *frame, unsigned int len) options += options[1] + 2; } } - if (EthSocket::Instance != nullptr) - { - std::vector vbuf(frame, frame + len); - thread.io_context->post([vbuf]() { - EthSocket::Instance->send(vbuf.data(), vbuf.size()); - }); - } - else { - // restart the thread if previously stopped - start(); - } + thread.sendEthFrame(frame, len); } void DCNetThread::run() @@ -461,19 +482,20 @@ void DCNetThread::run() else port = "7654"; asio::ip::tcp::resolver resolver(*io_context); - auto it = resolver.resolve("dcnet.flyca.st", port); - if (it.empty()) - throw std::runtime_error("Can't find dcnet host"); + asio::error_code ec; + auto it = resolver.resolve("dcnet.flyca.st", port, ec); + if (ec) + throw FlycastException(ec.message()); asio::ip::tcp::endpoint endpoint = *it.begin(); - if (config::EmulateBBA) { - EthSocket socket(*io_context, endpoint); - io_context->run(); - } - else { - PPPSocket socket(*io_context, endpoint); - io_context->run(); - } + if (config::EmulateBBA) + ethSocket = std::make_unique(*io_context, endpoint); + else + pppSocket = std::make_unique(*io_context, endpoint); + io_context->run(); + } catch (const FlycastException& e) { + ERROR_LOG(NETWORK, "DCNet connection error: %s", e.what()); + os_notify("Can't connect to DCNet", 8000, e.what()); } catch (const std::runtime_error& e) { ERROR_LOG(NETWORK, "DCNetThread::run error: %s", e.what()); } From 4e74619d9eab93a8d5d26a31738cfe30e772eaf0 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 16 Feb 2025 09:36:57 -0700 Subject: [PATCH 76/76] Harden serial comm interface (#1847) * Harden serial interface * Fixed build errors * Minor tweaks * Automatically attached A->A, B->B, etc * Interfaces go in decending order * Do a modulo in case I decide to move interface numbers * Do specifically mod 4 * Call SDL_JoystickGetDeviceInstanceID which seems to help with a Windows bug * Made MapleDevices smart pointers so they can freely be swapped out without leaking * Fixed issues some issues reported by kosekmi * Added missing bracket * Fixed another compile issue * Fixed a missing else * If device had no serial number, fall down to checking name * Use new DreamPort firmware dynamics * Added missing parens * Fixed screen blanking issue * Added const * Reset VMU Screen on game/emulator exit * Added gameTermination() hook to hardware gamepad * Send SW port on game termination * Added checks so port is only sent if data is valid * Fixed bug: wait for write to complete in sendCmd * Fixed bug leading to multiple pointers to VMU and Rumble pack --------- Co-authored-by: Mike Kosek --- core/hw/maple/maple_cfg.cpp | 15 +- core/hw/maple/maple_cfg.h | 3 +- core/hw/maple/maple_devs.cpp | 171 ++++++-- core/hw/maple/maple_devs.h | 4 +- core/hw/maple/maple_helper.cpp | 6 +- core/hw/maple/maple_if.cpp | 12 +- core/hw/maple/maple_if.h | 3 +- core/sdl/dreamconn.cpp | 696 +++++++++++++++++++++++---------- core/sdl/dreamconn.h | 14 +- core/sdl/sdl.cpp | 4 +- 10 files changed, 664 insertions(+), 264 deletions(-) diff --git a/core/hw/maple/maple_cfg.cpp b/core/hw/maple/maple_cfg.cpp index 72245b9ec..601454054 100644 --- a/core/hw/maple/maple_cfg.cpp +++ b/core/hw/maple/maple_cfg.cpp @@ -213,8 +213,8 @@ bool maple_atomiswave_coin_chute(int slot) static void mcfg_Create(MapleDeviceType type, u32 bus, u32 port, s32 player_num = -1) { - delete MapleDevices[bus][port]; - maple_device* dev = maple_Create(type); + MapleDevices[bus][port].reset(); + std::shared_ptr dev = maple_Create(type); dev->Setup(bus, port, player_num); } @@ -364,7 +364,7 @@ static void vmuDigest() for (int i = 0; i < MAPLE_PORTS; i++) for (int j = 0; j < 6; j++) { - const maple_device* device = MapleDevices[i][j]; + std::shared_ptr device = MapleDevices[i][j]; if (device != nullptr) { size_t size; @@ -414,8 +414,7 @@ void mcfg_DestroyDevices(bool full) if (MapleDevices[i][j] != nullptr && (full || MapleDevices[i][j]->get_device_type() != MDT_NaomiJamma)) { - delete MapleDevices[i][j]; - MapleDevices[i][j] = nullptr; + MapleDevices[i][j].reset(); } } } @@ -435,7 +434,7 @@ void mcfg_SerializeDevices(Serializer& ser) for (int j = 0; j < 6; j++) { u8 deviceType = MDT_None; - maple_device* device = MapleDevices[i][j]; + std::shared_ptr device = MapleDevices[i][j]; if (device != nullptr) deviceType = device->get_device_type(); ser << deviceType; @@ -490,9 +489,9 @@ void mcfg_DeserializeDevices(Deserializer& deser) memcpy(EEPROM, eeprom, sizeof(eeprom)); } -maple_naomi_jamma *getMieDevice() +std::shared_ptr getMieDevice() { if (MapleDevices[0][5] == nullptr || MapleDevices[0][5]->get_device_type() != MDT_NaomiJamma) return nullptr; - return (maple_naomi_jamma *)MapleDevices[0][5]; + return std::static_pointer_cast(MapleDevices[0][5]); } diff --git a/core/hw/maple/maple_cfg.h b/core/hw/maple/maple_cfg.h index 404d7e86f..cb46b3ae3 100644 --- a/core/hw/maple/maple_cfg.h +++ b/core/hw/maple/maple_cfg.h @@ -1,6 +1,7 @@ #pragma once #include "types.h" #include +#include enum MapleDeviceType { @@ -123,4 +124,4 @@ const u8 *getRfidCardData(int playerNum); void setRfidCardData(int playerNum, u8 *data); struct maple_naomi_jamma; -maple_naomi_jamma *getMieDevice(); +std::shared_ptr getMieDevice(); diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index 1602f746f..988cb6adb 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -44,7 +44,7 @@ void maple_device::Setup(u32 bus, u32 port, int playerNum) config = new MapleConfigMap(this); OnSetup(); - MapleDevices[bus][port] = this; + MapleDevices[bus][port] = shared_from_this(); } maple_device::~maple_device() { @@ -368,7 +368,7 @@ struct maple_sega_vmu: maple_base } fullSaveNeeded = true; } - + bool fullSave() { if (file == nullptr) @@ -402,7 +402,7 @@ struct maple_sega_vmu: maple_base { memset(flash_data, 0, sizeof(flash_data)); memset(lcd_data, 0, sizeof(lcd_data)); - + // Load existing vmu file if found std::string rpath = hostfs::getVmuPath(logical_port, false); // this might be a storage url @@ -2047,56 +2047,56 @@ struct RFIDReaderWriter : maple_base void insertRfidCard(int playerNum) { - maple_device *mapleDev = MapleDevices[1 + playerNum][5]; + std::shared_ptr mapleDev = MapleDevices[1 + playerNum][5]; if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter) - ((RFIDReaderWriter *)mapleDev)->insertCard(); + std::static_pointer_cast(mapleDev)->insertCard(); } void setRfidCardData(int playerNum, u8 *data) { - maple_device *mapleDev = MapleDevices[1 + playerNum][5]; + std::shared_ptr mapleDev = MapleDevices[1 + playerNum][5]; if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter) - ((RFIDReaderWriter *)mapleDev)->setCardData(data); + std::static_pointer_cast(mapleDev)->setCardData(data); } const u8 *getRfidCardData(int playerNum) { - maple_device *mapleDev = MapleDevices[1 + playerNum][5]; + std::shared_ptr mapleDev = MapleDevices[1 + playerNum][5]; if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter) - return ((RFIDReaderWriter *)mapleDev)->getCardData(); + return std::static_pointer_cast(mapleDev)->getCardData(); else return nullptr; } -maple_device* maple_Create(MapleDeviceType type) +std::shared_ptr maple_Create(MapleDeviceType type) { switch(type) { case MDT_SegaController: if (!settings.platform.isAtomiswave()) - return new maple_sega_controller(); + return std::make_shared(); else - return new maple_atomiswave_controller(); - case MDT_Microphone: return new maple_microphone(); - case MDT_SegaVMU: return new maple_sega_vmu(); - case MDT_PurupuruPack: return new maple_sega_purupuru(); - case MDT_Keyboard: return new maple_keyboard(); - case MDT_Mouse: return new maple_mouse(); + return std::make_shared(); + case MDT_Microphone: return std::make_shared(); + case MDT_SegaVMU: return std::make_shared(); + case MDT_PurupuruPack: return std::make_shared(); + case MDT_Keyboard: return std::make_shared(); + case MDT_Mouse: return std::make_shared(); case MDT_LightGun: if (!settings.platform.isAtomiswave()) - return new maple_lightgun(); + return std::make_shared(); else - return new atomiswave_lightgun(); - case MDT_NaomiJamma: return new maple_naomi_jamma(); - case MDT_TwinStick: return new maple_sega_twinstick(); - case MDT_AsciiStick: return new maple_ascii_stick(); - case MDT_MaracasController: return new maple_maracas_controller(); - case MDT_FishingController: return new maple_fishing_controller(); - case MDT_PopnMusicController: return new maple_popnmusic_controller(); - case MDT_RacingController: return new maple_racing_controller(); - case MDT_DenshaDeGoController: return new maple_densha_controller(); - case MDT_SegaControllerXL: return new FullController(); - case MDT_RFIDReaderWriter: return new RFIDReaderWriter(); + return std::make_shared(); + case MDT_NaomiJamma: return std::make_shared(); + case MDT_TwinStick: return std::make_shared(); + case MDT_AsciiStick: return std::make_shared(); + case MDT_MaracasController: return std::make_shared(); + case MDT_FishingController: return std::make_shared(); + case MDT_PopnMusicController: return std::make_shared(); + case MDT_RacingController: return std::make_shared(); + case MDT_DenshaDeGoController: return std::make_shared(); + case MDT_SegaControllerXL: return std::make_shared(); + case MDT_RFIDReaderWriter: return std::make_shared(); default: ERROR_LOG(MAPLE, "Invalid device type %d", type); @@ -2108,6 +2108,8 @@ maple_device* maple_Create(MapleDeviceType type) #if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) && defined(USE_SDL) && !defined(LIBRETRO) #include "sdl/dreamconn.h" +#include +#include struct DreamConnVmu : public maple_sega_vmu { @@ -2131,7 +2133,7 @@ struct DreamConnVmu : public maple_sega_vmu return maple_sega_vmu::dma(cmd); } - void copy(maple_sega_vmu *other) + void copyIn(std::shared_ptr other) { memcpy(flash_data, other->flash_data, sizeof(flash_data)); memcpy(lcd_data, other->lcd_data, sizeof(lcd_data)); @@ -2139,6 +2141,14 @@ struct DreamConnVmu : public maple_sega_vmu fullSaveNeeded = other->fullSaveNeeded; } + void copyOut(std::shared_ptr other) + { + memcpy(other->flash_data, flash_data, sizeof(other->flash_data)); + memcpy(other->lcd_data, lcd_data, sizeof(other->lcd_data)); + memcpy(other->lcd_data_decoded, lcd_data_decoded, sizeof(other->lcd_data_decoded)); + other->fullSaveNeeded = fullSaveNeeded; + } + void updateScreen() { MapleMsg msg; @@ -2170,32 +2180,111 @@ struct DreamConnPurupuru : public maple_sega_purupuru } }; +static std::list> dreamConnVmus; +static std::list> dreamConnPurupurus; + void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart) { const int bus = dreamconn->getBus(); + + bool vmuFound = false; + bool rumbleFound = false; + if (dreamconn->hasVmu()) { - maple_device *dev = MapleDevices[bus][0]; + std::shared_ptr vmu; + for (const std::shared_ptr& vmuIter : dreamConnVmus) + { + if (vmuIter->dreamconn.get() == dreamconn.get()) + { + vmuFound = true; + vmu = vmuIter; + break; + } + } + + std::shared_ptr dev = MapleDevices[bus][0]; if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_SegaVMU)) { - DreamConnVmu *vmu = new DreamConnVmu(dreamconn); - vmu->Setup(bus, 0); - if (!gameStart) { - // if loading a state, copy data from the regular vmu and send a screen update - vmu->copy(static_cast(dev)); - vmu->updateScreen(); + bool vmuCreated = false; + if (!vmu) + { + vmu = std::make_shared(dreamconn); + vmuCreated = true; } - delete dev; + + vmu->Setup(bus, 0); + + if ((!gameStart || !vmuCreated) && dev) { + // if loading a state or DreamConnVmu existed, copy data from the regular vmu and send a screen update + vmu->copyIn(std::static_pointer_cast(dev)); + if (!gameStart) { + vmu->updateScreen(); + } + } + + if (!vmuFound) dreamConnVmus.push_back(vmu); } } if (dreamconn->hasRumble()) { - maple_device *dev = MapleDevices[bus][1]; + std::shared_ptr rumble; + for (const std::shared_ptr& purupuru : dreamConnPurupurus) + { + if (purupuru->dreamconn.get() == dreamconn.get()) + { + rumbleFound = true; + rumble = purupuru; + break; + } + } + + std::shared_ptr dev = MapleDevices[bus][1]; if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_PurupuruPack)) { - delete dev; - DreamConnPurupuru *rumble = new DreamConnPurupuru(dreamconn); + if (!rumble) + { + rumble = std::make_shared(dreamconn); + } rumble->Setup(bus, 1); + + if (!rumbleFound) dreamConnPurupurus.push_back(rumble); + } + } +} + +void tearDownDreamConnDevices(std::shared_ptr dreamconn) +{ + const int bus = dreamconn->getBus(); + for (std::list>::const_iterator iter = dreamConnVmus.begin(); + iter != dreamConnVmus.end();) + { + if ((*iter)->dreamconn.get() == dreamconn.get()) + { + std::shared_ptr dev = maple_Create(MDT_SegaVMU); + dev->Setup(bus, 0); + (*iter)->copyOut(std::static_pointer_cast(dev)); + iter = dreamConnVmus.erase(iter); + break; + } + else + { + ++iter; + } + } + for (std::list>::const_iterator iter = dreamConnPurupurus.begin(); + iter != dreamConnPurupurus.end();) + { + if ((*iter)->dreamconn.get() == dreamconn.get()) + { + std::shared_ptr dev = maple_Create(MDT_PurupuruPack); + dev->Setup(bus, 1); + iter = dreamConnPurupurus.erase(iter); + break; + } + else + { + ++iter; } } } diff --git a/core/hw/maple/maple_devs.h b/core/hw/maple/maple_devs.h index 89f1e5836..5d69a2a3c 100755 --- a/core/hw/maple/maple_devs.h +++ b/core/hw/maple/maple_devs.h @@ -121,7 +121,7 @@ enum AWAVE_KEYS AWAVE_TRIGGER_KEY = 1 << 17, }; -struct maple_device +struct maple_device : public std::enable_shared_from_this { u8 maple_port; //raw maple port u8 bus_port; //0 .. 5 @@ -150,7 +150,7 @@ struct maple_device virtual const void *getData(size_t& size) const { size = 0; return nullptr; } }; -maple_device* maple_Create(MapleDeviceType type); +std::shared_ptr maple_Create(MapleDeviceType type); #define MAPLE_PORTS 4 diff --git a/core/hw/maple/maple_helper.cpp b/core/hw/maple/maple_helper.cpp index 6e683d972..639b5d66b 100644 --- a/core/hw/maple/maple_helper.cpp +++ b/core/hw/maple/maple_helper.cpp @@ -3,12 +3,12 @@ u32 maple_GetAttachedDevices(u32 bus) { - verify(MapleDevices[bus][5]!=0); + verify(MapleDevices[bus][5]!=nullptr); u32 rv=0; - + for (int i=0;i<5;i++) - rv|=(MapleDevices[bus][i]!=0?1:0)< + enum MaplePattern { MP_Start, @@ -17,7 +19,7 @@ enum MaplePattern MP_NOP = 7 }; -maple_device* MapleDevices[MAPLE_PORTS][6]; +std::shared_ptr MapleDevices[MAPLE_PORTS][6]; int maple_schid; @@ -202,13 +204,13 @@ static void maple_DoDma() } const u32 frame_header = swap_msb ? SWAP32(p_data[0]) : p_data[0]; - //Command code + //Command code u32 command = frame_header & 0xFF; - //Recipient address + //Recipient address u32 reci = (frame_header >> 8) & 0xFF;//0-5; - //Sender address + //Sender address //u32 send = (frame_header >> 16) & 0xFF; - //Number of additional words in frame + //Number of additional words in frame u32 inlen = (frame_header >> 24) & 0xFF; u32 port = getPort(reci); diff --git a/core/hw/maple/maple_if.h b/core/hw/maple/maple_if.h index a3feb59a9..04ee4510e 100644 --- a/core/hw/maple/maple_if.h +++ b/core/hw/maple/maple_if.h @@ -1,7 +1,8 @@ #pragma once #include "maple_devs.h" +#include -extern maple_device* MapleDevices[MAPLE_PORTS][6]; +extern std::shared_ptr MapleDevices[MAPLE_PORTS][6]; void maple_Init(); void maple_Reset(bool Manual); diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index e2d76bc8c..8b22c7904 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) #include @@ -42,40 +43,40 @@ #endif void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); +void tearDownDreamConnDevices(std::shared_ptr dreamconn); class DreamcastControllerConnection { -protected: - //! The maple bus index [0,3] - const int bus; +private: + MapleMsg connection_msg; public: DreamcastControllerConnection(const DreamcastControllerConnection&) = delete; - DreamcastControllerConnection() = delete; - explicit DreamcastControllerConnection(int bus) : bus(bus) - {} + DreamcastControllerConnection() = default; + ~DreamcastControllerConnection() = default; - std::optional connect(){ - if (!establishConnection()) { + std::optional connect(int bus){ + bool result = establishConnection(bus); + + if (!result) { return std::nullopt; } // Now get the controller configuration - MapleMsg msg; - msg.command = MDCF_GetCondition; - msg.destAP = (bus << 6) | 0x20; - msg.originAP = bus << 6; - msg.setData(MFID_0_Input); + connection_msg.command = MDCF_GetCondition; + connection_msg.destAP = (bus << 6) | 0x20; + connection_msg.originAP = bus << 6; + connection_msg.setData(MFID_0_Input); - asio::error_code ec = sendMsg(msg); + asio::error_code ec = sendMsg(connection_msg); if (ec) { WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); disconnect(); return std::nullopt; } - if (!receiveMsg(msg)) { + if (!receiveMsg(connection_msg)) { WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus); disconnect(); return std::nullopt; @@ -83,34 +84,24 @@ public: onConnectComplete(); - return msg; + return connection_msg; } virtual void disconnect() = 0; virtual asio::error_code sendMsg(const MapleMsg& msg) = 0; virtual bool receiveMsg(MapleMsg& msg) = 0; + virtual std::string getName() = 0; + virtual int getDefaultBus() { + // Value of -1 means to use enumeration order + return -1; + } + virtual void gameTermination() { + // Do nothing by default + } protected: - virtual bool establishConnection() = 0; + virtual bool establishConnection(int bus) = 0; virtual void onConnectComplete() = 0; - - std::string msgToString(const MapleMsg& msg, const std::string& delim = " ") - { - std::ostringstream s; - s.fill('0'); - - s << std::hex << std::uppercase - << std::setw(2) << (u32)msg.command << " " - << std::setw(2) << (u32)msg.destAP << " " - << std::setw(2) << (u32)msg.originAP << " " - << std::setw(2) << (u32)msg.size; - const u32 sz = msg.getDataSize(); - for (u32 i = 0; i < sz; i++) - s << " " << std::setw(2) << (u32)msg.data[i]; - s << "\r\n"; - - return s.str(); - } }; class DreamConnConnection : public DreamcastControllerConnection @@ -126,16 +117,14 @@ public: public: DreamConnConnection(const DreamConnConnection&) = delete; - DreamConnConnection() = delete; - explicit DreamConnConnection(int bus) : DreamcastControllerConnection(bus) - {} + DreamConnConnection() = default; ~DreamConnConnection() { disconnect(); } - bool establishConnection() override { + bool establishConnection(int bus) override { #if !defined(_WIN32) WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ / DreamConn S Controller supported on Windows only", bus); return false; @@ -155,29 +144,32 @@ public: iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow } - void disconnect() override - { + void disconnect() override { if (iostream) { iostream.close(); } } - asio::error_code sendMsg(const MapleMsg& msg) override - { - const std::string msgStr = msgToString(msg); - asio::error_code ec; + asio::error_code sendMsg(const MapleMsg& msg) override { + std::ostringstream s; + s.fill('0'); + s << std::hex << std::uppercase + << std::setw(2) << (u32)msg.command << " " + << std::setw(2) << (u32)msg.destAP << " " + << std::setw(2) << (u32)msg.originAP << " " + << std::setw(2) << (u32)msg.size; + const u32 sz = msg.getDataSize(); + for (u32 i = 0; i < sz; i++) + s << " " << std::setw(2) << (u32)msg.data[i]; + s << "\r\n"; - if (!iostream) { - return asio::error::not_connected; - } asio::ip::tcp::socket& sock = static_cast(iostream.socket()); - asio::write(sock, asio::buffer(msgStr), ec); - + asio::error_code ec; + asio::write(sock, asio::buffer(s.str()), ec); return ec; } - bool receiveMsg(MapleMsg& msg) override - { + bool receiveMsg(MapleMsg& msg) override { std::string response; if (!std::getline(iostream, response)) @@ -191,16 +183,19 @@ public: return false; } + + std::string getName() override { + return "DreamConn+ / DreamConn S Controller"; + } }; -//! See: https://github.com/OrangeFox86/DreamcastControllerUsbPico -class DreamcastControllerUsbPicoConnection : public DreamcastControllerConnection +class DreamPortSerialHandler { //! Asynchronous context for serial_handler asio::io_context io_context; //! Output buffer data for serial_handler std::string serial_out_data; - //! Handles communication to DreamcastControllerUsbPico + //! Handles communication to DreamPort asio::serial_port serial_handler{io_context}; //! Set to true while an async write is in progress with serial_handler bool serial_write_in_progress = false; @@ -218,29 +213,9 @@ class DreamcastControllerUsbPicoConnection : public DreamcastControllerConnectio std::condition_variable read_cv; //! Mutex for read_cv and serializes access to read_queue std::mutex read_cv_mutex; - //! Current timeout in milliseconds - std::chrono::milliseconds timeout_ms; public: - //! Dreamcast Controller USB VID:1209 PID:2f07 - static constexpr const char* VID_PID_GUID = "09120000072f0000"; - -public: - DreamcastControllerUsbPicoConnection(const DreamcastControllerUsbPicoConnection&) = delete; - DreamcastControllerUsbPicoConnection() = delete; - - explicit DreamcastControllerUsbPicoConnection(int bus) : DreamcastControllerConnection(bus) - {} - - ~DreamcastControllerUsbPicoConnection(){ - disconnect(); - } - - bool establishConnection() override { - asio::error_code ec; - - // Timeout is 1 second while establishing connection - timeout_ms = std::chrono::seconds(1); + DreamPortSerialHandler() { // the serial port isn't ready at this point, so we need to sleep briefly // we probably should have a better way to handle this @@ -252,38 +227,163 @@ public: std::string serial_device = ""; // use user-configured serial device if available, fallback to first available - if (cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default") != "default") { - serial_device = cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", "default"); - NOTICE_LOG(INPUT, "DreamcastController[%d] connecting to user-configured serial device: %s", bus, serial_device.c_str()); - } - else - { - serial_device = getFirstSerialDevice(); - NOTICE_LOG(INPUT, "DreamcastController[%d] connecting to autoselected serial device: %s", bus, serial_device.c_str()); + serial_device = cfgLoadStr("input", "DreamPortSerialDevice", ""); + if (serial_device.empty()) { + serial_device = cfgLoadStr("input", "DreamcastControllerUsbSerialDevice", ""); + if (!serial_device.empty()) { + WARN_LOG(INPUT, "DreamcastControllerUsbSerialDevice config is deprecated; use DreamPortSerialDevice instead"); + } } + if (!serial_device.empty()) + { + NOTICE_LOG(INPUT, "DreamPort connecting to user-configured serial device: %s", serial_device.c_str()); + } else { + serial_device = getFirstSerialDevice(); + NOTICE_LOG(INPUT, "DreamPort connecting to autoselected serial device: %s", serial_device.c_str()); + } + + asio::error_code ec; serial_handler.open(serial_device, ec); if (ec || !serial_handler.is_open()) { - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); + WARN_LOG(INPUT, "DreamPort serial connection failed: %s", ec.message().c_str()); disconnect(); - return false; + } else { + NOTICE_LOG(INPUT, "DreamPort serial connection successful!"); } // This must be done before the io_context is run because it will keep io_context from returning immediately startSerialRead(); io_context_thread = std::make_unique([this](){contextThreadEnty();}); + } + ~DreamPortSerialHandler() { + disconnect(); + io_context_thread->join(); + } + + bool is_open() const { + return serial_handler.is_open(); + } + + asio::error_code sendCmd(const std::string& cmd, std::chrono::milliseconds timeout_ms) { + asio::error_code ec; + + if (!serial_handler.is_open()) { + return asio::error::not_connected; + } + + // Wait for last write to complete + std::unique_lock lock(write_cv_mutex); + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + if (!write_cv.wait_until(lock, expiration, [this](){return (!serial_write_in_progress || !serial_handler.is_open());})) + { + return asio::error::timed_out; + } + + // Check again before continuing + if (!serial_handler.is_open()) { + return asio::error::not_connected; + } + + serial_out_data = cmd; + + // Clear out the read buffer before writing next command + read_queue.clear(); + serial_write_in_progress = true; + asio::async_write( + serial_handler, + asio::buffer(serial_out_data), + asio::transfer_exactly(serial_out_data.size()), + [this](const asio::error_code& error, size_t bytes_transferred) + { + std::unique_lock lock(write_cv_mutex); + if (error) { + try + { + serial_handler.cancel(); + } + catch(const asio::system_error&) + { + // Ignore cancel errors + } + } + serial_write_in_progress = false; + write_cv.notify_all(); + } + ); + + return ec; + } + + asio::error_code sendMsg(const MapleMsg& msg, int hardware_bus, std::chrono::milliseconds timeout_ms) { + // Build serial_out_data string + // Need to message the hardware bus instead of the software bus + u8 hwDestAP = (hardware_bus << 6) | (msg.destAP & 0x3F); + u8 hwOriginAP = (hardware_bus << 6) | (msg.originAP & 0x3F); + + std::ostringstream s; + s << "X "; // 'X' prefix triggers flycast command parser + s.fill('0'); + s << std::hex << std::uppercase + << std::setw(2) << (u32)msg.command + << std::setw(2) << (u32)hwDestAP // override dest + << std::setw(2) << (u32)hwOriginAP // override origin + << std::setw(2) << (u32)msg.size; + const u32 sz = msg.getDataSize(); + for (u32 i = 0; i < sz; i++) { + s << std::setw(2) << (u32)msg.data[i]; + } + s << "\n"; + + return sendCmd(s.str(), timeout_ms); + } + + bool receiveCmd(std::string& cmd, std::chrono::milliseconds timeout_ms) + { + // Wait for at least 2 lines to be received (first line is echo back) + std::unique_lock lock(read_cv_mutex); + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + if (!read_cv.wait_until(lock, expiration, [this](){return ((read_queue.size() >= 2) || !serial_handler.is_open());})) + { + // Timeout + return false; + } + + if (read_queue.size() < 2) { + // Connection was closed before data could be received + return false; + } + + // discard the first message as we are interested in the second only which returns the controller configuration + cmd = std::move(read_queue.back()); + read_queue.clear(); return true; } - void onConnectComplete() override { - // Timeout is extended to 5 seconds for all other communication after connection - timeout_ms = std::chrono::seconds(5); + bool receiveMsg(MapleMsg& msg, std::chrono::milliseconds timeout_ms) + { + std::string response; + if (!receiveCmd(response, timeout_ms)) { + return false; + } + + sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); + + if (serial_handler.is_open()) { + return true; + } + else { + return false; + } + + return false; } - void disconnect() override +private: + void disconnect() { io_context.stop(); @@ -313,89 +413,6 @@ public: // This context should never exit until disconnect due to read handler automatically rearming io_context.run(); } - - asio::error_code sendMsg(const MapleMsg& msg) override - { - asio::error_code ec; - - if (!serial_handler.is_open()) { - return asio::error::not_connected; - } - - // Wait for last write to complete - std::unique_lock lock(write_cv_mutex); - const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; - if (!write_cv.wait_until(lock, expiration, [this](){return (!serial_write_in_progress || !serial_handler.is_open());})) - { - return asio::error::timed_out; - } - - // Check again before continuing - if (!serial_handler.is_open()) { - return asio::error::not_connected; - } - - // Clear out the read buffer before writing next command - read_queue.clear(); - - serial_write_in_progress = true; - // Messages to Dreamcast Controller USB need to be prefixed to trigger the correct parser - serial_out_data = std::string("X ") + msgToString(msg); - asio::async_write(serial_handler, asio::buffer(serial_out_data), asio::transfer_exactly(serial_out_data.size()), [this](const asio::error_code& error, size_t bytes_transferred) - { - std::unique_lock lock(write_cv_mutex); - if (error) { - try - { - serial_handler.cancel(); - } - catch(const asio::system_error&) - { - // Ignore cancel errors - } - } - serial_write_in_progress = false; - write_cv.notify_all(); - }); - - return ec; - } - - bool receiveMsg(MapleMsg& msg) override - { - std::string response; - - // Wait for at least 2 lines to be received (first line is echo back) - std::unique_lock lock(read_cv_mutex); - const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; - if (!read_cv.wait_until(lock, expiration, [this](){return ((read_queue.size() >= 2) || !serial_handler.is_open());})) - { - // Timeout - return false; - } - - if (read_queue.size() < 2) { - // Connection was closed before data could be received - return false; - } - - // discard the first message as we are interested in the second only which returns the controller configuration - response = std::move(read_queue.back()); - read_queue.clear(); - - sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); - - if (serial_handler.is_open()) { - return true; - } - else { - return false; - } - - return false; - } - -private: static std::string getFirstSerialDevice() { // On Windows, we get the first serial device matching our VID/PID @@ -566,29 +583,288 @@ private: } }; -DreamConn::DreamConn(int bus, int dreamcastControllerType, const std::string& name) : - bus(bus), dreamcastControllerType(dreamcastControllerType), name(name) +//! See: https://github.com/OrangeFox86/DreamPort +class DreamPortConnection : public DreamcastControllerConnection { - change_bus(bus); + //! The one and only serial port + static std::unique_ptr serial; + //! Number of devices using the above serial + static std::atomic connected_dev_count; + //! Current timeout in milliseconds + std::chrono::milliseconds timeout_ms; + //! The bus ID dictated by flycast + int software_bus = -1; + //! The bus index of the hardware connection which will differ from the software bus + int hardware_bus = -1; + //! true iff only a single devices was found when enumerating devices + bool is_single_device = true; + //! True when initial enumeration failed + bool is_hardware_bus_implied = true; + //! True once connection is established + bool connection_established = false; + +public: + //! Dreamcast Controller USB VID:1209 PID:2f07 + static constexpr const std::uint16_t VID = 0x1209; + static constexpr const std::uint16_t PID = 0x2f07; + static constexpr const char* VID_PID_GUID = "09120000072f0000"; + +public: + DreamPortConnection(const DreamPortConnection&) = delete; + DreamPortConnection() = delete; + + DreamPortConnection(int joystick_idx, SDL_Joystick* sdl_joystick) : + DreamcastControllerConnection() + { +#if defined(_WIN32) + // Workaround: Getting the instance ID here fixes some sort of L/R trigger bug in Windows dinput for some reason + (void)SDL_JoystickGetDeviceInstanceID(joystick_idx); +#endif + determineHardwareBus(joystick_idx, sdl_joystick); + } + + ~DreamPortConnection(){ + disconnect(); + } + + int hardwareBus() const { + return hardware_bus; + } + + bool isHardwareBusImplied() const { + return is_hardware_bus_implied; + } + + bool isSingleDevice() const { + return is_single_device; + } + + bool establishConnection(int bus) override { + // Timeout is 1 second while establishing connection + timeout_ms = std::chrono::seconds(1); + + software_bus = bus; + + if (connection_established && serial) { + if (serial->is_open()) { + // This equipment is fixed to the hardware bus - the software bus isn't relevant + sendPort(); + return true; + } else { + disconnect(); + return false; + } + } + + ++connected_dev_count; + connection_established = true; + if (!serial) { + serial = std::make_unique(); + } + + if (serial && serial->is_open()) { + sendPort(); + return true; + } else { + disconnect(); + return false; + } + } + + void sendPort() { + if (connection_established && software_bus >= 0 && software_bus <= 3 && hardware_bus >=0 && hardware_bus <= 3) { + // This will update the displayed port letter on the screen + std::ostringstream s; + s << "XP "; // XP is flycast "set port" command + s << hardware_bus << " " << software_bus << "\n"; + serial->sendCmd(s.str(), timeout_ms); + // Don't really care about the response, just want to ensure it gets fully processed before continuing + std::string buffer; + serial->receiveCmd(buffer, timeout_ms); + } + } + + void onConnectComplete() override { + // Timeout is extended to 5 seconds for all other communication after connection + timeout_ms = std::chrono::seconds(5); + } + + void disconnect() override { + if (connection_established) { + connection_established = false; + if (--connected_dev_count == 0) { + // serial is no longer needed + serial.reset(); + } + } + } + + asio::error_code sendMsg(const MapleMsg& msg) override { + if (serial) { + return serial->sendMsg(msg, hardware_bus, timeout_ms); + } + + return asio::error::not_connected; + } + + bool receiveMsg(MapleMsg& msg) override { + if (serial) { + return serial->receiveMsg(msg, timeout_ms); + } + + return false; + } + + std::string getName() override { + std::string name = "DreamPort"; + if (!is_hardware_bus_implied && !is_single_device) { + const char portChar = ('A' + hardware_bus); + name += " " + std::string(1, portChar); + } + return name; + } + + int getDefaultBus() override { + if (!is_hardware_bus_implied && !is_single_device) { + return hardware_bus; + } else { + // Value of -1 means to use enumeration order + return -1; + } + } + + void gameTermination() override { + // Reset screen to selected port + sendPort(); + } + +private: + void determineHardwareBus(int joystick_idx, SDL_Joystick* sdl_joystick) { + // This function determines what bus index to use when communicating with the hardware. +#if defined(_WIN32) + // This only works in Windows because the joystick_path is not given in other OSes + const char* joystick_name = SDL_JoystickName(sdl_joystick); + const char* joystick_path = SDL_JoystickPath(sdl_joystick); + + struct SDL_hid_device_info* devs = SDL_hid_enumerate(VID, PID); + if (devs) { + if (!devs->next) { + // Only single device found, so this is simple (host-1p firmware used) + hardware_bus = 0; + is_hardware_bus_implied = false; + is_single_device = true; + } else { + struct SDL_hid_device_info* it = devs; + struct SDL_hid_device_info* my_dev = nullptr; + + if (joystick_path) + { + while (it) + { + // Note: hex characters will be differing case, so case-insensitive cmp is needed + if (it->path && 0 == SDL_strcasecmp(it->path, joystick_path)) { + my_dev = it; + break; + } + it = it->next; + } + } + + if (my_dev) { + it = devs; + int count = 0; + if (my_dev->serial_number) { + while (it) { + if (it->serial_number && + 0 == wcscmp(it->serial_number, my_dev->serial_number)) + { + ++count; + } + it = it->next; + } + + if (count == 1) { + // Single device of this serial found + is_single_device = true; + hardware_bus = 0; + is_hardware_bus_implied = false; + } else { + is_single_device = false; + if (my_dev->release_number < 0x0102) { + // Interfaces go in decending order + hardware_bus = (count - (my_dev->interface_number % 4) - 1); + is_hardware_bus_implied = false; + } else { + // Version 1.02 of interface will make interfaces in ascending order + hardware_bus = (my_dev->interface_number % 4); + is_hardware_bus_implied = false; + } + } + } + } + } + SDL_hid_free_enumeration(devs); + } +#endif + + if (hardware_bus < 0) { + // The number of buttons gives a clue as to what index the controller is + int nbuttons = SDL_JoystickNumButtons(sdl_joystick); + + if (nbuttons >= 32 || nbuttons <= 27) { + // Older version of firmware or single player + hardware_bus = 0; + is_hardware_bus_implied = true; + is_single_device = true; + } + else { + hardware_bus = 31 - nbuttons; + is_hardware_bus_implied = false; + is_single_device = false; + } + } + } +}; + +// Define the static instances here +std::unique_ptr DreamPortConnection::serial; +std::atomic DreamPortConnection::connected_dev_count = 0; + +DreamConn::DreamConn(int bus, int dreamcastControllerType, int joystick_idx, SDL_Joystick* sdl_joystick) : + bus(bus), dreamcastControllerType(dreamcastControllerType) +{ + switch (dreamcastControllerType) + { + case TYPE_DREAMCONN: + dcConnection = std::make_unique(); + break; + + case TYPE_DREAMPORT: + dcConnection = std::make_unique(joystick_idx, sdl_joystick); + break; + } } DreamConn::~DreamConn() { disconnect(); } -void DreamConn::change_bus(int bus) { - disconnect(); - dcConnection.reset(); - switch (dreamcastControllerType) - { - case TYPE_DREAMCONN: - dcConnection = std::make_unique(bus); - break; - - case TYPE_DREAMCASTCONTROLLERUSB: - dcConnection = std::make_unique(bus); - break; +int DreamConn::getDefaultBus() { + if (dcConnection) { + return dcConnection->getDefaultBus(); } + return -1; +} + +void DreamConn::changeBus(int newBus) { + bus = newBus; +} + +std::string DreamConn::getName() { + if (dcConnection) { + return dcConnection->getName(); + } + return "Unknown DreamConn"; } void DreamConn::connect() @@ -604,7 +880,7 @@ void DreamConn::connect() return; } - std::optional msg = dcConnection->connect(); + std::optional msg = dcConnection->connect(bus); if (!msg) { return; @@ -617,7 +893,7 @@ void DreamConn::connect() if (hasVmu() || hasRumble()) { - NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, name.c_str(), hasVmu(), hasRumble()); + NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", bus, getName().c_str(), hasVmu(), hasRumble()); maple_io_connected = true; } else @@ -662,6 +938,13 @@ bool DreamConn::send(const MapleMsg& msg) return true; } +void DreamConn::gameTermination() +{ + if (dcConnection) { + dcConnection->gameTermination(); + } +} + bool DreamConnGamepad::isDreamcastController(int deviceIndex) { char guid_str[33] {}; @@ -673,7 +956,7 @@ bool DreamConnGamepad::isDreamcastController(int deviceIndex) // DreamConn VID:4457 PID:4443 // Dreamcast Controller USB VID:1209 PID:2f07 if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0 || - memcmp(DreamcastControllerUsbPicoConnection::VID_PID_GUID, guid_str + 8, 16) == 0) + memcmp(DreamPortConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { NOTICE_LOG(INPUT, "Dreamcast controller found!"); return true; @@ -692,22 +975,34 @@ DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystic // Dreamcast Controller USB VID:1209 PID:2f07 if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { - _name = "DreamConn+ / DreamConn S Controller"; - dreamconn = std::make_shared(maple_port, TYPE_DREAMCONN, _name); + dreamconn = std::make_shared(maple_port, TYPE_DREAMCONN, joystick_idx, sdl_joystick); } - else if (memcmp(DreamcastControllerUsbPicoConnection::VID_PID_GUID, guid_str + 8, 16) == 0) + else if (memcmp(DreamPortConnection::VID_PID_GUID, guid_str + 8, 16) == 0) { - _name = "Dreamcast Controller USB"; - dreamconn = std::make_shared(maple_port, TYPE_DREAMCASTCONTROLLERUSB, _name); + dreamconn = std::make_shared(maple_port, TYPE_DREAMPORT, joystick_idx, sdl_joystick); + } + + if (dreamconn) { + _name = dreamconn->getName(); + int defaultBus = dreamconn->getDefaultBus(); + if (defaultBus >= 0 && defaultBus < 4) { + set_maple_port(defaultBus); + } } EventManager::listen(Event::Start, handleEvent, this); EventManager::listen(Event::LoadState, handleEvent, this); + EventManager::listen(Event::Terminate, handleEvent, this); } DreamConnGamepad::~DreamConnGamepad() { EventManager::unlisten(Event::Start, handleEvent, this); EventManager::unlisten(Event::LoadState, handleEvent, this); + EventManager::unlisten(Event::Terminate, handleEvent, this); + if (dreamconn) { + tearDownDreamConnDevices(dreamconn); + dreamconn.reset(); + } } void DreamConnGamepad::set_maple_port(int port) @@ -717,7 +1012,7 @@ void DreamConnGamepad::set_maple_port(int port) dreamconn->disconnect(); } else if (dreamconn->getBus() != port) { - dreamconn->change_bus(port); + dreamconn->changeBus(port); if (is_registered()) { dreamconn->connect(); } @@ -739,6 +1034,11 @@ void DreamConnGamepad::handleEvent(Event event, void *arg) DreamConnGamepad *gamepad = static_cast(arg); if (gamepad->dreamconn != nullptr) createDreamConnDevices(gamepad->dreamconn, event == Event::Start); + + if (gamepad->dreamconn != nullptr && event == Event::Terminate) + { + gamepad->dreamconn->gameTermination(); + } } bool DreamConnGamepad::gamepad_btn_input(u32 code, bool pressed) @@ -788,6 +1088,8 @@ void DreamConn::connect() { } void DreamConn::disconnect() { } +void DreamConn::gameTermination() { +} bool DreamConnGamepad::isDreamcastController(int deviceIndex) { return false; diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index 8a0f98513..8d3f9c74a 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -23,7 +23,7 @@ #if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) #define USE_DREAMCASTCONTROLLER 1 #define TYPE_DREAMCONN 1 -#define TYPE_DREAMCASTCONTROLLERUSB 2 +#define TYPE_DREAMPORT 2 #include #endif #include @@ -52,7 +52,6 @@ class DreamConn { int bus = -1; const int dreamcastControllerType; - const std::string name; #ifdef USE_DREAMCASTCONTROLLER std::unique_ptr dcConnection; #endif @@ -60,12 +59,15 @@ class DreamConn u8 expansionDevs = 0; public: - DreamConn(int bus, int dreamcastControllerType, const std::string& name); + DreamConn(int bus, int dreamcastControllerType, int joystick_idx, SDL_Joystick* sdl_joystick); ~DreamConn(); bool send(const MapleMsg& msg); + // When called, do teardown stuff like reset screen + void gameTermination(); + int getBus() const { return bus; } @@ -76,7 +78,11 @@ public: return expansionDevs & 2; } - void change_bus(int bus); + int getDefaultBus(); + + void changeBus(int newBus); + + std::string getName(); void connect(); void disconnect(); diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index 794c2f315..09acb494c 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -271,9 +271,9 @@ void input_sdl_init() // Linux mappings are OK by default // Can be removed once mapping is merged into SDL, see https://github.com/libsdl-org/SDL/pull/12039 #if (defined(__APPLE__) && defined(TARGET_OS_MAC)) - SDL_GameControllerAddMapping("0300000009120000072f000000010000,OrangeFox86 Dreamcast Controller USB,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:a2,righttrigger:a5,start:b11"); + SDL_GameControllerAddMapping("0300000009120000072f000000010000,OrangeFox86 DreamPort,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:a2,righttrigger:a5,start:b11"); #elif defined(_WIN32) - SDL_GameControllerAddMapping("0300000009120000072f000000000000,OrangeFox86 Dreamcast Controller USB,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:-a2,righttrigger:-a5,start:b11"); + SDL_GameControllerAddMapping("0300000009120000072f000000000000,OrangeFox86 DreamPort,a:b0,b:b1,x:b3,y:b4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,dpdown:h0.4,leftx:a0,lefty:a1,lefttrigger:-a2,righttrigger:-a5,start:b11"); #endif }