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 }} 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 df0c415f7..e1bb24806 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,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) @@ -220,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) @@ -255,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) @@ -846,6 +858,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 @@ -1181,7 +1196,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 @@ -1904,7 +1923,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/cfg/option.cpp b/core/cfg/option.cpp index 103b6dd92..d59f666f4 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); @@ -135,6 +139,7 @@ Option DiscordPresence("DiscordPresence", true); #if defined(__ANDROID__) && !defined(LIBRETRO) Option UseSafFilePicker("UseSafFilePicker", true); #endif +OptionString LogServer("LogServer", "", "log"); // Profiler Option ProfilerEnabled("Profiler.Enabled"); @@ -161,6 +166,8 @@ 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"); +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 c3ba52786..81703e728 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; @@ -521,6 +522,8 @@ extern Option GGPOChatTimeout; 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/cheats.cpp b/core/cheats.cpp index 999be2002..8dde4af9c 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) @@ -489,6 +489,51 @@ 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 + { + // 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 + // 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 + { + // 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); + // 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); + } + 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); + } + 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); + } + 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); + } + 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) setActive(true); diff --git a/core/deps/SDL b/core/deps/SDL index 9c821dc21..7a44b1ab0 160000 --- a/core/deps/SDL +++ b/core/deps/SDL @@ -1 +1 @@ -Subproject commit 9c821dc21ccbd69b2bda421fdb35cb4ae2da8f5e +Subproject commit 7a44b1ab002cee6efa56d3b4c0e146b7fbaed80b 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/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) { } 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 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; 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; } diff --git a/core/hw/bba/bba.cpp b/core/hw/bba/bba.cpp index be071d252..2b1b49d4a 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; } @@ -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); - start_pico(); + net::modbba::stop(); } 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/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/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/hw/gdrom/gdromv3.cpp b/core/hw/gdrom/gdromv3.cpp index 494f8adec..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); } @@ -1209,9 +1213,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; 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 33e76b772..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); @@ -2106,8 +2106,10 @@ 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" +#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; @@ -163,7 +165,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) { @@ -201,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); @@ -226,7 +229,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 +262,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 +275,7 @@ static void maple_DoDma() case MP_Reset: addr += 1 * 4; - xfer_count++; + xferIn++; break; case MP_NOP: @@ -285,9 +289,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/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/hw/maple/maple_jvs.cpp b/core/hw/maple/maple_jvs.cpp index 2cd83cc9d..8a60a39d8 100644 --- a/core/hw/maple/maple_jvs.cpp +++ b/core/hw/maple/maple_jvs.cpp @@ -627,6 +627,9 @@ protected: in = (in & 0xAA) >> 1 | (in & 0x55) << 1; out = process(in); + // 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; diff --git a/core/hw/modem/modem.cpp b/core/hw/modem/modem.cpp index 39384ed20..3f3337263 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" @@ -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) @@ -263,7 +261,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 +289,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 +326,7 @@ void ModemInit() void ModemReset() { - stop_pico(); + net::modbba::stop(); } void ModemTerm() @@ -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) { - stop_pico(); 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,10 +516,8 @@ static void ModemNormalWrite(u32 reg, u32 data) data_sent = true; #ifndef NDEBUG sent_bytes++; - if (sent_fp) - fputc(data, sent_fp); #endif - write_pico(data); + net::modbba::writeModem(data); modem_regs.reg1e.TDBE = 0; } break; @@ -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) 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/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/hw/sh4/dyna/shil_canonical.h b/core/hw/sh4/dyna/shil_canonical.h index 21e1a8230..a1be200cf 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 @@ -727,27 +719,33 @@ shil_opc(cvt_f2i_t) 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 { + res = (s32)f1; + // Fix result sign for Intel CPUs + if ((u32)res == 0x80000000 && f1 > 0) + res = 0x7fffffff; + } + return res; ) -#else +#elif HOST_CPU == CPU_ARM || HOST_CPU == CPU_ARM64 shil_canonical ( u32,f1,(f32 f1), - if (f1 > 2147483520.0f) // IEEE 754: 0x4effffff - return 0x7fffffff; - else - return (s32)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 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/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; +} 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/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/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/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..d2535dbe9 --- /dev/null +++ b/core/network/dcnet.cpp @@ -0,0 +1,504 @@ +/* + 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 +#include "netservice.h" +#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 WIRESHARK_DUMP 1 +#endif + +namespace net::modbba +{ + +static TsQueue toModem; + +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; +}; + +template +class PPPSocket +{ +public: + PPPSocket(asio::io_context& io_context, const typename SocketT::endpoint_type& endpoint) + : socket(io_context) + { + 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(); + } + + ~PPPSocket() { + if (dumpfp != nullptr) + 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() + { + socket.async_read_some(asio::buffer(recvBuffer), + [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()); + close(); + return; + } + pppdump(recvBuffer.data(), len, false); + for (size_t i = 0; i < len; i++) + toModem.push(recvBuffer[i]); + receive(); + }); + } + + void doSend() + { + 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 + if (!len) + return; + 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 = getTimeMs(); + } + + u32 delta = getTimeMs() / 100 - 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 = 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; + std::array recvBuffer; + std::array sendBuffer; + u32 sendBufSize = 0; + bool sending = false; + + FILE *dumpfp = nullptr; + 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) + { + 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)); + } + + ~EthSocket() { + if (dumpfp != nullptr) + fclose(dumpfp); + } + + 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 (size >= 32) // skip prolog + 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, + [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() + { + if (sending) + return; + sending = true; + asio::async_write(socket, asio::buffer(sendBuffer, sendBufferIdx), + [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) + { +#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; +}; + +class DCNetThread +{ +public: + void start() + { + if (thread.joinable()) + return; + io_context = std::make_unique(); + thread = std::thread(&DCNetThread::run, this); + } + + void stop() + { + if (!thread.joinable()) + 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; + +bool DCNetService::start() +{ + emu.setNetworkState(true); + thread.start(); + return true; +} + +void DCNetService::stop() { + thread.stop(); + emu.setNetworkState(false); +} + +void DCNetService::writeModem(u8 b) { + thread.sendModem(b); +} + +int DCNetService::readModem() +{ + if (toModem.empty()) + return -1; + else + return toModem.pop(); +} + +int DCNetService::modemAvailable() { + return toModem.size(); +} + +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; + } + } + thread.sendEthFrame(frame, len); +} + +void DCNetThread::run() +{ + try { + std::string port; + if (config::EmulateBBA) + port = "7655"; + else + port = "7654"; + asio::ip::tcp::resolver resolver(*io_context); + 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 = 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()); + } +} + +} 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/dns.cpp b/core/network/dns.cpp index ad3ae58b5..4d45c4a23 100644 --- a/core/network/dns.cpp +++ b/core/network/dns.cpp @@ -34,10 +34,8 @@ extern "C" { #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(); @@ -59,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; @@ -75,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) @@ -105,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/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/netservice.cpp b/core/network/netservice.cpp new file mode 100644 index 000000000..889688ba4 --- /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) { + start(); + 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/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); } } diff --git a/core/network/picoppp.cpp b/core/network/picoppp.cpp index efc748b08..b53c37168 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 @@ -43,138 +41,44 @@ extern "C" { #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 "hw/bba/bba.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 }, @@ -185,8 +89,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 @@ -228,26 +137,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" }, @@ -267,29 +168,19 @@ 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); +static bool start_pico(); +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; } @@ -298,268 +189,695 @@ 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(); +static void write_pico(u8 b) { out_buffer.push(b); - out_buffer_lock.unlock(); } -int read_pico() +static 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; +static 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; + } + + 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) { + } + + 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); + sockets.push_back(newSock); + + acceptor.async_accept(newSock->getSocket(), + std::bind(&TcpAcceptor::onAccept, shared_from_this(), newSock, asio::placeholders::error)); + } + + void stop() { + acceptor.close(); + for (auto& socket : sockets) + socket->close(); + sockets.clear(); + directPlay.reset(); + } + +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; + std::vector sockets; + 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(); + for (auto& socket : sockets) + socket->close(); + sockets.clear(); + } + +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); + sockets.push_back(psock); } - 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; + std::vector sockets; +}; + +// 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)) { @@ -578,275 +896,189 @@ 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; + stop(); + if (upnpCmd.valid()) + upnpCmd.get(); + } - 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; + void stop() { + if (acceptor) + acceptor->stop(); + acceptor.reset(); + } - 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) +private: + DirectPlayImpl(asio::io_context& io_context, UdpSink& udpSink, std::shared_ptr upnp) + : io_context(io_context), udpSink(udpSink), upnp(upnp) { + } + + 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() @@ -924,41 +1156,72 @@ static void dumpFrame(const u8 *frame, u32 size) } static void closeDumpFile() { - if (pcapngDump != nullptr) - { + if (pcapngDump != nullptr) { fclose(pcapngDump); 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) + 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) -{ +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 *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]; @@ -978,40 +1241,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 { @@ -1019,10 +1274,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); @@ -1035,17 +1293,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); @@ -1057,7 +1317,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); @@ -1067,7 +1327,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); @@ -1082,7 +1342,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; @@ -1096,89 +1356,51 @@ 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(); + directPlay->stop(); + directPlay.reset(); - 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 @@ -1190,31 +1412,34 @@ static void *pico_thread_func(void *) pico_dev = nullptr; } pico_stack_deinit(); - - if (ports != nullptr) - upnp.get().Term(); - - return NULL; + if (upnp) + { + std::thread pnpTerm([upnp = this->upnp]() { + upnp->Term(); + }); + pnpTerm.detach(); + upnp.reset(); + } } -static cThread pico_thread(pico_thread_func, nullptr, "PicoTCP"); +static PicoThread pico_thread; -bool start_pico() +static bool start_pico() { emu.setNetworkState(true); if (pico_thread_running) return false; pico_thread_running = true; - pico_thread.Start(); + pico_thread.start(); return true; } -void stop_pico() +static void stop_pico() { emu.setNetworkState(false); pico_thread_running = false; - pico_thread.WaitToEnd(); + pico_thread.stop(); } // picotcp mutex implementation @@ -1237,3 +1462,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/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/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)); 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/CustomTexture.cpp b/core/rend/CustomTexture.cpp index 1ff8444b2..2df7fbf06 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,33 @@ #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(); + 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 +72,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 +90,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 +104,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 +127,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 +141,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 +268,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/rend/TexCache.cpp b/core/rend/TexCache.cpp index fe73022ed..46743b57a 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; @@ -544,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; } diff --git a/core/rend/dx11/dx11context.cpp b/core/rend/dx11/dx11context.cpp index dcdf39d1c..470b187bb 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/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 diff --git a/core/rend/vulkan/vulkan_context.cpp b/core/rend/vulkan/vulkan_context.cpp index 557d6bb11..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); @@ -1340,9 +1346,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 +1371,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 +1436,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(); 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; diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index c379042fb..8b22c7904 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 @@ -26,135 +26,1019 @@ #include #include #include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) +#include +#endif + +#if defined(_WIN32) +#include +#include +#endif void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); +void tearDownDreamConnDevices(std::shared_ptr dreamconn); -static bool sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream) +class DreamcastControllerConnection { - 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"; +private: + MapleMsg connection_msg; - asio::ip::tcp::socket& sock = static_cast(stream.socket()); - asio::error_code ec; - asio::write(sock, asio::buffer(s.str()), ec); - return !ec; +public: + DreamcastControllerConnection(const DreamcastControllerConnection&) = delete; + + DreamcastControllerConnection() = default; + ~DreamcastControllerConnection() = default; + + std::optional connect(int bus){ + bool result = establishConnection(bus); + + if (!result) { + return std::nullopt; + } + + // Now get the controller configuration + 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(connection_msg); + if (ec) + { + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); + disconnect(); + return std::nullopt; + } + if (!receiveMsg(connection_msg)) { + WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus); + disconnect(); + return std::nullopt; + } + + onConnectComplete(); + + 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(int bus) = 0; + virtual void onConnectComplete() = 0; +}; + +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() = default; + + ~DreamConnConnection() { + disconnect(); + } + + 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; +#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 { + 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"; + + asio::ip::tcp::socket& sock = static_cast(iostream.socket()); + asio::error_code ec; + asio::write(sock, asio::buffer(s.str()), 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 !iostream.fail(); + + return false; + } + + std::string getName() override { + return "DreamConn+ / DreamConn S Controller"; + } +}; + +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 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; + //! 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; + +public: + 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 + 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 + 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, "DreamPort serial connection failed: %s", ec.message().c_str()); + disconnect(); + } 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; + } + + 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; + } + +private: + void disconnect() + { + 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(); + } + 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 ""; + } + + 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 + +#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__) + 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 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; + } + } + } +}; + +//! See: https://github.com/OrangeFox86/DreamPort +class DreamPortConnection : public DreamcastControllerConnection +{ + //! 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; + } } -static bool receiveMsg(MapleMsg& msg, std::istream& stream) -{ - 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(); +DreamConn::~DreamConn() { + disconnect(); +} + +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() { - 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()); + if (maple_io_connected) { disconnect(); + } + + maple_io_connected = false; + expansionDevs = 0; + + if (!dcConnection) { 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)) + + std::optional msg = dcConnection->connect(bus); + if (!msg) { - WARN_LOG(INPUT, "DreamConn[%d] communication failed", bus); - disconnect(); return; } - if (!receiveMsg(msg, iostream)) { - WARN_LOG(INPUT, "DreamConn[%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 - expansionDevs = msg.originAP & 0x1f; - NOTICE_LOG(INPUT, "Connected to DreamConn[%d]: VMU:%d, Rumble Pack:%d", bus, hasVmu(), hasRumble()); + + 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, getName().c_str(), 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 (!dcConnection) { + return; } + + dcConnection->disconnect(); + + maple_io_connected = false; + + NOTICE_LOG(INPUT, "Disconnected from DreamcastController[%d]", bus); } bool DreamConn::send(const MapleMsg& msg) { - if (!iostream) + if (!dcConnection) { return false; - if (!sendMsg(msg, iostream)) { - WARN_LOG(INPUT, "DreamConn[%d] send failed: %s", bus, iostream.error().message().c_str()); + } + + asio::error_code ec; + + if (maple_io_connected) + ec = dcConnection->sendMsg(msg); + else + return false; + 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) +void DreamConn::gameTermination() +{ + if (dcConnection) { + dcConnection->gameTermination(); + } +} + +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 - return memcmp("5744000043440000", guid_str + 8, 16) == 0; + // Dreamcast Controller USB VID:1209 PID:2f07 + if (memcmp(DreamConnConnection::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; + } + 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(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0) + { + dreamconn = std::make_shared(maple_port, TYPE_DREAMCONN, joystick_idx, sdl_joystick); + } + else if (memcmp(DreamPortConnection::VID_PID_GUID, guid_str + 8, 16) == 0) + { + 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) { - if (port < 0 || port >= 4) { - dreamconn.reset(); - } - else if (dreamconn == nullptr || dreamconn->getBus() != port) { - dreamconn.reset(); - dreamconn = std::make_shared(port); + if (dreamconn) { + if (port < 0 || port >= 4) { + dreamconn->disconnect(); + } + else if (dreamconn->getBus() != port) { + dreamconn->changeBus(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); 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) @@ -198,14 +1082,16 @@ void DreamConnGamepad::checkKeyCombo() { gui_open_settings(); } -#else +#else // USE_DREAMCASTCONTROLLER void DreamConn::connect() { } void DreamConn::disconnect() { } +void DreamConn::gameTermination() { +} -bool DreamConnGamepad::isDreamConn(int deviceIndex) { +bool DreamConnGamepad::isDreamcastController(int deviceIndex) { return false; } DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) @@ -216,6 +1102,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 bf2e32680..8d3f9c74a 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -20,10 +20,13 @@ #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_DREAMPORT 2 #include #endif +#include struct MapleMsg { @@ -47,23 +50,24 @@ static_assert(sizeof(MapleMsg) == 1028); class DreamConn { - const int bus; -#ifdef USE_DREAMCONN - asio::ip::tcp::iostream iostream; + int bus = -1; + const int dreamcastControllerType; +#ifdef USE_DREAMCASTCONTROLLER + std::unique_ptr dcConnection; #endif + bool maple_io_connected = false; u8 expansionDevs = 0; - static constexpr u16 BASE_PORT = 37393; public: - DreamConn(int bus) : bus(bus) { - connect(); - } - ~DreamConn() { - disconnect(); - } + 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; } @@ -74,7 +78,12 @@ public: return expansionDevs & 2; } -private: + int getDefaultBus(); + + void changeBus(int newBus); + + std::string getName(); + void connect(); void disconnect(); }; @@ -86,9 +95,10 @@ 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 isDreamConn(int deviceIndex); + static bool isDreamcastController(int deviceIndex); private: static void handleEvent(Event event, void *arg); diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index a93c19e3c..09acb494c 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); @@ -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 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 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 } void input_sdl_quit() @@ -538,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; @@ -593,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) @@ -620,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; @@ -636,7 +645,7 @@ bool sdl_recreate_window(u32 flags) SDL_UnloadObject(shcoreDLL); } #endif - + #ifdef __SWITCH__ AppletOperationMode om = appletGetOperationMode(); if (om == AppletOperationMode_Handheld) 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 2b541e108..f33a8caa0 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 diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index b8c028243..f9f162509 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) { @@ -2789,6 +2795,11 @@ 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."); + 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(); @@ -2839,14 +2850,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/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/core/windows/dynlink.h b/core/windows/dynlink.h new file mode 100755 index 000000000..0149e4ee9 --- /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 (WINAPI *)(Args...); + }; +} + +class WinLibLoader +{ +public: + WinLibLoader(const char* name = nullptr) : name(name) { + } + ~WinLibLoader() { + if (hinst != NULL) + FreeLibrary(hinst); + } + + 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 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..ceed7f005 100644 --- a/core/windows/fault_handler.cpp +++ b/core/windows/fault_handler.cpp @@ -24,6 +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) { @@ -145,7 +153,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 3e9805e44..61b550cf9 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 (WINAPI *SetThreadDescription)(HANDLE, PCWSTR) = kernelBaseLib.getFunc("SetThreadDescription", SetThreadDescription); if (SetThreadDescription != nullptr) SetThreadDescription(GetCurrentThread(), wname.get()); } -#endif +} + +const char *getThreadName() +{ + static HRESULT (WINAPI *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 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 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..7f9799a36 100644 --- a/shell/libretro/option.cpp +++ b/shell/libretro/option.cpp @@ -122,6 +122,8 @@ Option GGPOAnalogAxes("", 0); 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 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 */ 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()); +} diff --git a/tools/dreampi/Makefile b/tools/dreampi/Makefile new file mode 100644 index 000000000..bcf2a5e03 --- /dev/null +++ b/tools/dreampi/Makefile @@ -0,0 +1,18 @@ +# +# 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/dreampi/dcnet.rpi + 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 000000000..0eb751899 Binary files /dev/null and b/tools/dreampi/dcnet.rpi differ diff --git a/tools/dreampi/dreampi.py b/tools/dreampi/dreampi.py new file mode 100755 index 000000000..41d424e73 --- /dev/null +++ b/tools/dreampi/dreampi.py @@ -0,0 +1,1113 @@ +#!/usr/bin/env python +#dreampi.py_version=202402202004 +# from __future__ import absolute_import +# from __future__ import print_function +import atexit +# from typing import List, Optional, Tuple +import serial +import socket +import os +import logging +import logging.handlers +import sys +import time +import subprocess +import sh +import signal +import re +import config_server +import iptc +import select +import requests + +from dcnow import DreamcastNowService +from port_forwarding import PortForwarding + +from datetime import datetime, timedelta +def updater(): + + if os.path.isfile("/boot/noautoupdates.txt") == True: + logger.info("Dreampi script auto updates are disabled") + return + netlink_script_url = "https://raw.githubusercontent.com/eaudunord/Netlink/latest/tunnel/netlink.py" + xband_script_url = "https://raw.githubusercontent.com/eaudunord/Netlink/latest/tunnel/xband.py" + checkScripts = [netlink_script_url,xband_script_url] + restartFlag = False + for script in checkScripts: + url = script + try: + r=requests.get(url, stream = True) + r.raise_for_status() + for line in r.iter_lines(): + if b'_version' in line: + upstream_version = str(line.decode().split('version=')[1]).strip() + break + local_script = "/home/pi/dreampi/"+script.split("/")[-1] + if os.path.isfile(local_script) == False: + local_version = None + else: + with open(local_script,'rb') as f: + for line in f: + if b'_version' in line: + local_version = str(line.decode().split('version=')[1]).strip() + break + if upstream_version == local_version: + logger.info('%s Up To Date' % local_script) + else: + r = requests.get(url) + r.raise_for_status() + with open(local_script,'wb') as f: + f.write(r.content) + logger.info('%s Updated' % local_script) + if local_script == "dreampi.py": + os.system("sudo chmod +x dreampi.py") + restartFlag = True + + except requests.exceptions.HTTPError: + logger.info("Couldn't check updates for: %s" % local_script) + continue + + except requests.exceptions.SSLError: + logger.info("SSL error while checking for updates. System time may need to be synced") + return + + if restartFlag: + logger.info('Updated. Rebooting') + os.system("sudo reboot") + +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 """ + + IP_ADDRESS_LIST = [ + "1.1.1.1", # Cloudflare + "1.0.0.1", + "8.8.8.8", # Google DNS + "8.8.4.4", + "208.67.222.222", # Open DNS + "208.67.220.220", + ] + + port = 53 + timeout = 3 + + for host in IP_ADDRESS_LIST: + try: + socket.setdefaulttimeout(timeout) + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port)) + return True + except socket.error: + pass + else: + logger.exception("No internet connection") + return False + + +def restart_dnsmasq(): + subprocess.call("sudo service dnsmasq restart".split()) + + +def update_dns_file(): + """ + Download a DNS settings file for the DreamPi configuration (avoids forwarding requests to the main DNS server + and provides a backup if that ever goes down) + """ + # check for a remote configuration + try: + response = requests.get(DNS_FILE) + response.raise_for_status() + except requests.exceptions.HTTPError: + logging.info( + "Did not find remote DNS config; will use upstream" + ) + return + except requests.exceptions.Timeout: + logging.info( + "Request timed out; will use upstream" + ) + return + except requests.exceptions.SSLError: + logging.info( + "SSL error; will use upstream" + ) + return + + # Stop the server + subprocess.check_call("sudo service dnsmasq stop".split()) + + # Update the configuration + try: + with open("/etc/dnsmasq.d/dreampi.conf", "w") as f: + f.write(response.read()) + except IOError: + logging.exception("Found remote DNS config but failed to apply it locally") + + # Start the server again + subprocess.check_call("sudo service dnsmasq start".split()) + + +# Update dreampi.py if file exists in /boot +def dreampi_py_local_update(): + if os.path.isfile("/boot/dpiupdate.py") == False: + logger.info("No update file is found in /boot") + return + + os.system("sudo mv /boot/dpiupdate.py /home/pi/dreampi/dreampi.py") + os.system("sudo chown pi:pi /home/pi/dreampi/dreampi.py") + os.system("sudo chmod +x /home/pi/dreampi/dreampi.py") + logger.info('Updated the dreampi.py from /boot/dpiupdate.py ... Rebooting') + os.system("sudo reboot") + +# Increase the TTL in the IP HDR from 30 to 64 +def add_increased_ttl(): + table = iptc.Table(iptc.Table.MANGLE) + chain = iptc.Chain(table, "PREROUTING") + + rule = iptc.Rule() + rule.in_interface = "ppp0" + rule.create_target("TTL").ttl_set = str(64) + + chain.insert_rule(rule) + + logger.info("DC TTL increased from 30 to 64") + return rule + +def remove_increased_ttl(ttl_rule): + if ttl_rule: + table = iptc.Table(iptc.Table.MANGLE) + chain = iptc.Chain(table, "PREROUTING") + chain.delete_rule(ttl_rule) + logger.info("DC TTL removed") + +# Add additional DNAT rules +def start_dnat_rules(): + rules = [] + + def fetch_replacement_ips(): + url = "https://shumania.ddns.net/dnat.txt" + try: + r = requests.get(url, verify=False) + r.raise_for_status() + return r.text.strip() + except requests.exceptions.HTTPError: + logging.info( + "HTTP error; will skip adding DNAT rules" + ) + return None + except requests.exceptions.Timeout: + logging.info( + "Request timed out; will skip adding DNAT rules" + ) + return None + except requests.exceptions.SSLError: + logging.info( + "SSL error; will skip adding DNAT rules" + ) + return None + + data = fetch_replacement_ips() + + if data is None: + logger.info("No DNAT rules added") + return None + + for ips in data.splitlines(): + ip = ips.split() + + if ip[0] is None: + logger.info("Missing SRC in DNAT rule - SKIP") + return None + + if ip[1] is None: + logger.info("Missing DST in DNAT rule - SKIP") + return None + + table = iptc.Table(iptc.Table.NAT) + chain = iptc.Chain(table, "PREROUTING") + + rule = iptc.Rule() + rule.protocol = "tcp" + rule.dst = ip[0] + rule.create_target("DNAT") + rule.target.to_destination = ip[1] + + chain.append_rule(rule) + logger.info("DNAT rule appended %s -> %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 + 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.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 + 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/dreampi/dcnet.rpi"]) + 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