Merge remote-tracking branch 'origin/dev'
This commit is contained in:
commit
0bf9c64b1b
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|||
$<$<BOOL:${MSVC}>:NOMINMAX>
|
||||
$<$<BOOL:${TEST_AUTOMATION}>:TEST_AUTOMATION>
|
||||
$<$<BOOL:${WINDOWS_STORE}>:NOCRYPT>
|
||||
$<$<BOOL:${WINDOWS_STORE}>:_WIN32_WINNT=0x0A00>
|
||||
$<$<OR:$<BOOL:${MINGW}>,$<BOOL:${MSVC}>>:_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)
|
||||
|
|
|
@ -37,8 +37,9 @@
|
|||
#include <atomic>
|
||||
#include <utility>
|
||||
#include <xxhash.h>
|
||||
#include <thread>
|
||||
#include <functional>
|
||||
#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<std::string, bool> getCachedImage(const char *url);
|
||||
void diskChange();
|
||||
void asyncTask(std::function<void()> f);
|
||||
void startThread();
|
||||
void stopThread();
|
||||
void backgroundThread();
|
||||
void asyncTask(std::function<void()>&& 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<u64, std::string> cacheMap;
|
||||
std::mutex cacheMutex;
|
||||
std::vector<std::function<void()>> 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<void()> f)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> _(taskMutex);
|
||||
tasks.emplace_back(f);
|
||||
}
|
||||
resetEvent.Set();
|
||||
void Achievements::asyncTask(std::function<void()>&& 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<std::function<void()>> localTasks;
|
||||
{
|
||||
std::lock_guard<std::mutex> _(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;
|
||||
}
|
||||
|
|
|
@ -105,7 +105,11 @@ Option<int> TextureFiltering("rend.TextureFiltering", 0); // Default
|
|||
Option<bool> ThreadedRendering("rend.ThreadedRendering", true);
|
||||
Option<bool> DupeFrames("rend.DupeFrames", false);
|
||||
Option<int> PerPixelLayers("rend.PerPixelLayers", 32);
|
||||
#ifdef TARGET_UWP
|
||||
Option<bool> NativeDepthInterpolation("rend.NativeDepthInterpolation", true);
|
||||
#else
|
||||
Option<bool> NativeDepthInterpolation("rend.NativeDepthInterpolation", false);
|
||||
#endif
|
||||
Option<bool> EmulateFramebuffer("rend.EmulateFramebuffer", false);
|
||||
Option<bool> FixUpscaleBleedingEdge("rend.FixUpscaleBleedingEdge", true);
|
||||
Option<bool> CustomGpuDriver("rend.CustomGpuDriver", false);
|
||||
|
@ -135,6 +139,7 @@ Option<bool, false> DiscordPresence("DiscordPresence", true);
|
|||
#if defined(__ANDROID__) && !defined(LIBRETRO)
|
||||
Option<bool, false> UseSafFilePicker("UseSafFilePicker", true);
|
||||
#endif
|
||||
OptionString LogServer("LogServer", "", "log");
|
||||
|
||||
// Profiler
|
||||
Option<bool> ProfilerEnabled("Profiler.Enabled");
|
||||
|
@ -161,6 +166,8 @@ Option<int> GGPOChatTimeout("GGPOChatTimeout", 10, "network");
|
|||
Option<bool> NetworkOutput("NetworkOutput", false, "network");
|
||||
Option<int> MultiboardSlaves("MultiboardSlaves", 1, "network");
|
||||
Option<bool> BattleCableEnable("BattleCable", false, "network");
|
||||
Option<bool> UseDCNet("DCNet", false, "network");
|
||||
OptionString ISPUsername("ISPUsername", "flycast1", "network");
|
||||
|
||||
#ifdef USE_OMX
|
||||
Option<int> OmxAudioLatency("audio_latency", 100, "omx");
|
||||
|
|
|
@ -495,6 +495,7 @@ extern Option<bool, false> DiscordPresence;
|
|||
#if defined(__ANDROID__) && !defined(LIBRETRO)
|
||||
extern Option<bool, false> UseSafFilePicker;
|
||||
#endif
|
||||
extern OptionString LogServer;
|
||||
|
||||
// Profiling
|
||||
extern Option<bool> ProfilerEnabled;
|
||||
|
@ -521,6 +522,8 @@ extern Option<int> GGPOChatTimeout;
|
|||
extern Option<bool> NetworkOutput;
|
||||
extern Option<int> MultiboardSlaves;
|
||||
extern Option<bool> BattleCableEnable;
|
||||
extern Option<bool> UseDCNet;
|
||||
extern OptionString ISPUsername;
|
||||
|
||||
#ifdef USE_OMX
|
||||
extern Option<int> OmxAudioLatency;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 9c821dc21ccbd69b2bda421fdb35cb4ae2da8f5e
|
||||
Subproject commit 7a44b1ab002cee6efa56d3b4c0e146b7fbaed80b
|
|
@ -1 +1 @@
|
|||
Subproject commit 03ae834edbace31a96157b89bf50e5ee464e5ef9
|
||||
Subproject commit d3402006e84efb6114ff93e4f2b8508412ed80d5
|
|
@ -13,7 +13,7 @@
|
|||
template<class T, int N> class RingBuffer
|
||||
{
|
||||
public:
|
||||
RingBuffer<T, N>() :
|
||||
RingBuffer() :
|
||||
_head(0),
|
||||
_tail(0),
|
||||
_size(0) {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
template<class T, int N> class StaticBuffer
|
||||
{
|
||||
public:
|
||||
StaticBuffer<T, N>() :
|
||||
StaticBuffer() :
|
||||
_size(0) {
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit fab7b33b896a42dcc865ba5ecdbacd9f409137f8
|
||||
Subproject commit 5d21e35633a1f87ed08af115b07d3386096f792b
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DCFlashChip*>(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<DCFlashChip*>(sys_nvmem));
|
||||
add_isp_to_nvmem(static_cast<DCFlashChip*>(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];
|
||||
|
|
|
@ -140,6 +140,10 @@ void DmaBuffer::deserialize(Deserializer& deser)
|
|||
{
|
||||
deser >> index;
|
||||
deser >> size;
|
||||
if (index >= sizeof(cache))
|
||||
clear();
|
||||
else
|
||||
size = std::min<u32>(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<u32>(10240, len - SB_GDLEND), 25'000'000);
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
|
|
|
@ -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<maple_device> 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<const maple_device> 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<maple_device> 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<maple_naomi_jamma> 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<maple_naomi_jamma>(MapleDevices[0][5]);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
#include "types.h"
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
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<maple_naomi_jamma> getMieDevice();
|
||||
|
|
|
@ -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<maple_device> mapleDev = MapleDevices[1 + playerNum][5];
|
||||
if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter)
|
||||
((RFIDReaderWriter *)mapleDev)->insertCard();
|
||||
std::static_pointer_cast<RFIDReaderWriter>(mapleDev)->insertCard();
|
||||
}
|
||||
|
||||
void setRfidCardData(int playerNum, u8 *data)
|
||||
{
|
||||
maple_device *mapleDev = MapleDevices[1 + playerNum][5];
|
||||
std::shared_ptr<maple_device> mapleDev = MapleDevices[1 + playerNum][5];
|
||||
if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter)
|
||||
((RFIDReaderWriter *)mapleDev)->setCardData(data);
|
||||
std::static_pointer_cast<RFIDReaderWriter>(mapleDev)->setCardData(data);
|
||||
}
|
||||
|
||||
const u8 *getRfidCardData(int playerNum)
|
||||
{
|
||||
maple_device *mapleDev = MapleDevices[1 + playerNum][5];
|
||||
std::shared_ptr<maple_device> mapleDev = MapleDevices[1 + playerNum][5];
|
||||
if (mapleDev != nullptr && mapleDev->get_device_type() == MDT_RFIDReaderWriter)
|
||||
return ((RFIDReaderWriter *)mapleDev)->getCardData();
|
||||
return std::static_pointer_cast<RFIDReaderWriter>(mapleDev)->getCardData();
|
||||
else
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
maple_device* maple_Create(MapleDeviceType type)
|
||||
std::shared_ptr<maple_device> maple_Create(MapleDeviceType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case MDT_SegaController:
|
||||
if (!settings.platform.isAtomiswave())
|
||||
return new maple_sega_controller();
|
||||
return std::make_shared<maple_sega_controller>();
|
||||
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<maple_atomiswave_controller>();
|
||||
case MDT_Microphone: return std::make_shared<maple_microphone>();
|
||||
case MDT_SegaVMU: return std::make_shared<maple_sega_vmu>();
|
||||
case MDT_PurupuruPack: return std::make_shared<maple_sega_purupuru>();
|
||||
case MDT_Keyboard: return std::make_shared<maple_keyboard>();
|
||||
case MDT_Mouse: return std::make_shared<maple_mouse>();
|
||||
case MDT_LightGun:
|
||||
if (!settings.platform.isAtomiswave())
|
||||
return new maple_lightgun();
|
||||
return std::make_shared<maple_lightgun>();
|
||||
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<atomiswave_lightgun>();
|
||||
case MDT_NaomiJamma: return std::make_shared<maple_naomi_jamma>();
|
||||
case MDT_TwinStick: return std::make_shared<maple_sega_twinstick>();
|
||||
case MDT_AsciiStick: return std::make_shared<maple_ascii_stick>();
|
||||
case MDT_MaracasController: return std::make_shared<maple_maracas_controller>();
|
||||
case MDT_FishingController: return std::make_shared<maple_fishing_controller>();
|
||||
case MDT_PopnMusicController: return std::make_shared<maple_popnmusic_controller>();
|
||||
case MDT_RacingController: return std::make_shared<maple_racing_controller>();
|
||||
case MDT_DenshaDeGoController: return std::make_shared<maple_densha_controller>();
|
||||
case MDT_SegaControllerXL: return std::make_shared<FullController>();
|
||||
case MDT_RFIDReaderWriter: return std::make_shared<RFIDReaderWriter>();
|
||||
|
||||
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 <list>
|
||||
#include <memory>
|
||||
|
||||
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<maple_sega_vmu> 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<maple_sega_vmu> 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<std::shared_ptr<DreamConnVmu>> dreamConnVmus;
|
||||
static std::list<std::shared_ptr<DreamConnPurupuru>> dreamConnPurupurus;
|
||||
|
||||
void createDreamConnDevices(std::shared_ptr<DreamConn> 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<DreamConnVmu> vmu;
|
||||
for (const std::shared_ptr<DreamConnVmu>& vmuIter : dreamConnVmus)
|
||||
{
|
||||
if (vmuIter->dreamconn.get() == dreamconn.get())
|
||||
{
|
||||
vmuFound = true;
|
||||
vmu = vmuIter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<maple_device> 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<maple_sega_vmu*>(dev));
|
||||
vmu->updateScreen();
|
||||
bool vmuCreated = false;
|
||||
if (!vmu)
|
||||
{
|
||||
vmu = std::make_shared<DreamConnVmu>(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<maple_sega_vmu>(dev));
|
||||
if (!gameStart) {
|
||||
vmu->updateScreen();
|
||||
}
|
||||
}
|
||||
|
||||
if (!vmuFound) dreamConnVmus.push_back(vmu);
|
||||
}
|
||||
}
|
||||
if (dreamconn->hasRumble())
|
||||
{
|
||||
maple_device *dev = MapleDevices[bus][1];
|
||||
std::shared_ptr<DreamConnPurupuru> rumble;
|
||||
for (const std::shared_ptr<DreamConnPurupuru>& purupuru : dreamConnPurupurus)
|
||||
{
|
||||
if (purupuru->dreamconn.get() == dreamconn.get())
|
||||
{
|
||||
rumbleFound = true;
|
||||
rumble = purupuru;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<maple_device> 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<DreamConnPurupuru>(dreamconn);
|
||||
}
|
||||
rumble->Setup(bus, 1);
|
||||
|
||||
if (!rumbleFound) dreamConnPurupurus.push_back(rumble);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void tearDownDreamConnDevices(std::shared_ptr<DreamConn> dreamconn)
|
||||
{
|
||||
const int bus = dreamconn->getBus();
|
||||
for (std::list<std::shared_ptr<DreamConnVmu>>::const_iterator iter = dreamConnVmus.begin();
|
||||
iter != dreamConnVmus.end();)
|
||||
{
|
||||
if ((*iter)->dreamconn.get() == dreamconn.get())
|
||||
{
|
||||
std::shared_ptr<maple_device> dev = maple_Create(MDT_SegaVMU);
|
||||
dev->Setup(bus, 0);
|
||||
(*iter)->copyOut(std::static_pointer_cast<maple_sega_vmu>(dev));
|
||||
iter = dreamConnVmus.erase(iter);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
for (std::list<std::shared_ptr<DreamConnPurupuru>>::const_iterator iter = dreamConnPurupurus.begin();
|
||||
iter != dreamConnPurupurus.end();)
|
||||
{
|
||||
if ((*iter)->dreamconn.get() == dreamconn.get())
|
||||
{
|
||||
std::shared_ptr<maple_device> dev = maple_Create(MDT_PurupuruPack);
|
||||
dev->Setup(bus, 1);
|
||||
iter = dreamConnPurupurus.erase(iter);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ enum AWAVE_KEYS
|
|||
AWAVE_TRIGGER_KEY = 1 << 17,
|
||||
};
|
||||
|
||||
struct maple_device
|
||||
struct maple_device : public std::enable_shared_from_this<maple_device>
|
||||
{
|
||||
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_device> maple_Create(MapleDeviceType type);
|
||||
|
||||
#define MAPLE_PORTS 4
|
||||
|
||||
|
|
|
@ -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)<<i;
|
||||
rv|=(MapleDevices[bus][i]!=nullptr?1:0)<<i;
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
#include "network/ggpo.h"
|
||||
#include "hw/naomi/card_reader.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
enum MaplePattern
|
||||
{
|
||||
MP_Start,
|
||||
|
@ -17,7 +19,7 @@ enum MaplePattern
|
|||
MP_NOP = 7
|
||||
};
|
||||
|
||||
maple_device* MapleDevices[MAPLE_PORTS][6];
|
||||
std::shared_ptr<maple_device> 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<u32>(cycles, SH4_MAIN_CLOCK);
|
||||
sh4_sched_request(maple_schid, cycles);
|
||||
}
|
||||
}
|
||||
|
||||
static int maple_schd(int tag, int cycles, int jitter, void *arg)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
#pragma once
|
||||
#include "maple_devs.h"
|
||||
#include <memory>
|
||||
|
||||
extern maple_device* MapleDevices[MAPLE_PORTS][6];
|
||||
extern std::shared_ptr<maple_device> MapleDevices[MAPLE_PORTS][6];
|
||||
|
||||
void maple_Init();
|
||||
void maple_Reset(bool Manual);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -101,6 +101,7 @@ void FinishRender(TA_context* ctx)
|
|||
}
|
||||
|
||||
static std::mutex mtx_pool;
|
||||
using Lock = std::lock_guard<std::mutex>;
|
||||
|
||||
static std::vector<TA_context*> ctx_pool;
|
||||
static std::vector<TA_context*> ctx_list;
|
||||
|
@ -108,17 +109,15 @@ static std::vector<TA_context*> 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;
|
||||
|
|
|
@ -141,19 +141,11 @@ shil_compile( \
|
|||
template<int Stride = 1>
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<GamepadDevice>& gamepad)
|
|||
Lock _(_gamepads_mutex);
|
||||
_gamepads.push_back(gamepad);
|
||||
MapleConfigMap::UpdateVibration = updateVibration;
|
||||
|
||||
gamepad->_is_registered = true;
|
||||
gamepad->registered();
|
||||
}
|
||||
|
||||
void GamepadDevice::Unregister(const std::shared_ptr<GamepadDevice>& gamepad)
|
||||
|
|
|
@ -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<GamepadDevice>& gamepad);
|
||||
static void Unregister(const std::shared_ptr<GamepadDevice>& 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<DreamcastKey, int> lastAxisValue[4];
|
||||
bool perGameMapping = false;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<LogTypes::LOG_LEVELS>(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<LogListener>(listener);
|
||||
}
|
||||
|
||||
void LogManager::EnableListener(LogListener::LISTENER id, bool enable)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <array>
|
||||
#include <cstdarg>
|
||||
#include <memory>
|
||||
|
||||
#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<LogContainer, LogTypes::NUMBER_OF_LOGS> m_log{};
|
||||
std::array<LogListener*, LogListener::NUMBER_OF_LISTENERS> m_listeners{};
|
||||
std::array<std::unique_ptr<LogListener>, LogListener::NUMBER_OF_LISTENERS> m_listeners{};
|
||||
BitSet32 m_listener_ids;
|
||||
size_t m_path_cutoff_point = 0;
|
||||
std::string logServer;
|
||||
};
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include <asio.hpp>
|
||||
#include <stdio.h>
|
||||
#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 };
|
||||
};
|
|
@ -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 <vector>
|
||||
#include <deque>
|
||||
|
@ -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")
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "types.h"
|
||||
#include <asio.hpp>
|
||||
#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 <thread>
|
||||
#include <memory>
|
||||
#include <array>
|
||||
#ifndef __ANDROID__
|
||||
//#define WIRESHARK_DUMP 1
|
||||
#endif
|
||||
|
||||
namespace net::modbba
|
||||
{
|
||||
|
||||
static TsQueue<u8> 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<typename SocketT>
|
||||
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<u8, 1542> recvBuffer;
|
||||
std::array<u8, 1542> sendBuffer;
|
||||
u32 sendBufSize = 0;
|
||||
bool sending = false;
|
||||
|
||||
FILE *dumpfp = nullptr;
|
||||
u64 dump_last_time_ms;
|
||||
};
|
||||
|
||||
using PPPTcpSocket = PPPSocket<asio::ip::tcp::socket>;
|
||||
|
||||
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<asio::const_buffers_1>;
|
||||
|
||||
std::pair<iterator, bool>
|
||||
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<u8> recvBuffer;
|
||||
std::array<u8, 1600> sendBuffer;
|
||||
u32 sendBufferIdx = 0;
|
||||
bool sending = false;
|
||||
|
||||
FILE *dumpfp = nullptr;
|
||||
};
|
||||
|
||||
class DCNetThread
|
||||
{
|
||||
public:
|
||||
void start()
|
||||
{
|
||||
if (thread.joinable())
|
||||
return;
|
||||
io_context = std::make_unique<asio::io_context>();
|
||||
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<u8> 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<asio::io_context> io_context;
|
||||
std::unique_ptr<PPPTcpSocket> pppSocket;
|
||||
std::unique_ptr<EthSocket> 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<EthSocket>(*io_context, endpoint);
|
||||
else
|
||||
pppSocket = std::make_unique<PPPTcpSocket>(*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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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__)
|
||||
|
|
|
@ -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<std::mutex> _(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<std::mutex> _(mutex);
|
||||
mappedPorts.emplace_back(portStr, tcp);
|
||||
}
|
||||
DEBUG_LOG(NETWORK, "MiniUPnP: forwarding %s port %d", tcp ? "TCP" : "UDP", port);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -24,29 +24,28 @@
|
|||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
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<std::pair<std::string, bool>> 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,13 +19,20 @@
|
|||
along with reicast. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@ void SaveSettings()
|
|||
void SaveAndroidSettings();
|
||||
SaveAndroidSettings();
|
||||
#endif
|
||||
LogManager::GetInstance()->UpdateConfig();
|
||||
}
|
||||
|
||||
void flycast_term()
|
||||
|
|
|
@ -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 ?
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <sstream>
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
|
@ -32,58 +34,33 @@
|
|||
#include <stb_image_write.h>
|
||||
|
||||
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<std::mutex> 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<std::mutex> 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<std::mutex> 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);
|
||||
|
|
|
@ -17,41 +17,30 @@
|
|||
along with reicast. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "texconv.h"
|
||||
#include "stdclass.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
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<BaseTextureCacheData *> work_queue;
|
||||
std::mutex work_queue_mutex;
|
||||
std::map<u32, std::string> texture_map;
|
||||
};
|
||||
|
||||
|
|
|
@ -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<u32> 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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include <d3d11.h>
|
||||
#include <dxgi1_2.h>
|
||||
#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;
|
||||
|
|
|
@ -457,19 +457,18 @@ void D3DShaders::init(const ComPtr<IDirect3DDevice9>& 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;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#pragma once
|
||||
#include <unordered_map>
|
||||
#include "dxcontext.h"
|
||||
#include "windows/dynlink.h"
|
||||
#include <d3dx9shader.h>
|
||||
|
||||
class D3DShaders
|
||||
|
@ -41,7 +42,7 @@ private:
|
|||
std::unordered_map<u32, ComPtr<IDirect3DPixelShader9>> shaders;
|
||||
ComPtr<IDirect3DVertexShader9> vertexShaders[4];
|
||||
ComPtr<IDirect3DPixelShader9> modVolShaders[2];
|
||||
HMODULE d3dx9Library = NULL;
|
||||
WinLibLoader d3dx9Library;
|
||||
decltype(D3DXCompileShader) *pD3DXCompileShader = nullptr;
|
||||
decltype(D3DXGetVertexShaderProfile) *pD3DXGetVertexShaderProfile = nullptr;
|
||||
decltype(D3DXGetPixelShaderProfile) *pD3DXGetPixelShaderProfile = nullptr;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include <windows.h>
|
||||
#include <d3d9.h>
|
||||
#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<IDirect3D9> pD3D;
|
||||
ComPtr<IDirect3DDevice9> pDevice;
|
||||
D3DPRESENT_PARAMETERS d3dpp{};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -42,6 +42,7 @@ VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
|
|||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <vulkan/vulkan_format_traits.hpp>
|
||||
|
||||
void ReInitOSD();
|
||||
|
||||
|
@ -632,23 +633,28 @@ void VulkanContext::CreateSwapChain()
|
|||
for (auto& img : imageViews)
|
||||
img.reset();
|
||||
|
||||
// get the supported VkFormats
|
||||
std::vector<vk::SurfaceFormatKHR> 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<vk::SurfaceFormatKHR> 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<u8>& 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<u8>& 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<u8>& 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();
|
||||
|
|
|
@ -240,7 +240,7 @@ private:
|
|||
vk::UniqueSwapchainKHR swapChain;
|
||||
std::vector<vk::UniqueImageView> imageViews;
|
||||
u32 currentImage = 0;
|
||||
vk::Format colorFormat = vk::Format::eUndefined;
|
||||
vk::Format presentFormat = vk::Format::eUndefined;
|
||||
|
||||
vk::Queue graphicsQueue;
|
||||
vk::Queue presentQueue;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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 <asio.hpp>
|
||||
#endif
|
||||
#include <memory>
|
||||
|
||||
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<class DreamcastControllerConnection> 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);
|
||||
|
|
|
@ -83,7 +83,7 @@ static void sdl_open_joystick(int index)
|
|||
std::shared_ptr<SDLGamepad> gamepad = std::make_shared<SwitchGamepad>(index < MAPLE_PORTS ? index : -1, index, pJoystick);
|
||||
#else
|
||||
std::shared_ptr<SDLGamepad> gamepad;
|
||||
if (DreamConnGamepad::isDreamConn(index))
|
||||
if (DreamConnGamepad::isDreamcastController(index))
|
||||
gamepad = std::make_shared<DreamConnGamepad>(index < MAPLE_PORTS ? index : -1, index, pJoystick);
|
||||
else
|
||||
gamepad = std::make_shared<SDLGamepad>(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)
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <algorithm>
|
||||
|
|
|
@ -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 <cstdint>
|
||||
#include <cstddef>
|
||||
|
|
|
@ -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<int>(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/<game id>");
|
||||
|
||||
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
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include "stdclass.h"
|
||||
#include "oslib/oslib.h"
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <functional>
|
||||
#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<std::mutex>;
|
||||
const char *name;
|
||||
int period;
|
||||
cResetEvent event;
|
||||
std::thread thread;
|
||||
std::atomic<bool> running = false;
|
||||
std::mutex mutex;
|
||||
};
|
||||
|
||||
class PeriodicThread : public VPeriodicThread
|
||||
{
|
||||
public:
|
||||
template<class F, class... Args>
|
||||
PeriodicThread(const char *name, F&& f, Args&&... args)
|
||||
: VPeriodicThread(name, 0)
|
||||
{
|
||||
work = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
private:
|
||||
void doWork() override {
|
||||
work();
|
||||
}
|
||||
|
||||
std::function<void()> work;
|
||||
};
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
template<typename T>
|
||||
class TsQueue
|
||||
{
|
||||
public:
|
||||
void push(const T& t)
|
||||
{
|
||||
std::lock_guard<std::mutex> _(mutex);
|
||||
queue.emplace(t);
|
||||
condVar.notify_one();
|
||||
}
|
||||
void push(T&& t)
|
||||
{
|
||||
std::lock_guard<std::mutex> _(mutex);
|
||||
queue.push(std::move(t));
|
||||
condVar.notify_one();
|
||||
}
|
||||
|
||||
T pop()
|
||||
{
|
||||
std::unique_lock<std::mutex> 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<std::mutex> _(mutex);
|
||||
return queue.size();
|
||||
}
|
||||
bool empty() const {
|
||||
std::lock_guard<std::mutex> _(mutex);
|
||||
return queue.empty();
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
std::queue<T> empty;
|
||||
std::lock_guard<std::mutex> _(mutex);
|
||||
std::swap(queue, empty);
|
||||
}
|
||||
// TODO bool tryPop(T& t, std::chrono::duration timeout) ?
|
||||
|
||||
private:
|
||||
std::queue<T> queue;
|
||||
mutable std::mutex mutex;
|
||||
std::condition_variable condVar;
|
||||
};
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include "tsqueue.h"
|
||||
#include "oslib/oslib.h"
|
||||
#include <variant>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
|
||||
class WorkerThread
|
||||
{
|
||||
public:
|
||||
using Function = std::function<void()>;
|
||||
|
||||
WorkerThread(const char *name) : name(name) {
|
||||
}
|
||||
~WorkerThread() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
std::lock_guard<std::mutex> _(mutex);
|
||||
if (thread != nullptr && thread->joinable())
|
||||
{
|
||||
queue.push(Exit());
|
||||
thread->join();
|
||||
thread.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void run(Function&& task) {
|
||||
start();
|
||||
queue.push(std::move(task));
|
||||
}
|
||||
|
||||
template<class F, class... Args>
|
||||
auto runFuture(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>
|
||||
{
|
||||
using return_type = typename std::result_of<F(Args...)>::type;
|
||||
auto task = std::make_shared<std::packaged_task<return_type()>>(
|
||||
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
|
||||
|
||||
run([task]() {
|
||||
(*task)();
|
||||
});
|
||||
return task->get_future();
|
||||
}
|
||||
|
||||
private:
|
||||
void start()
|
||||
{
|
||||
std::lock_guard<std::mutex> _(mutex);
|
||||
if (thread != nullptr && thread->joinable())
|
||||
return;
|
||||
queue.clear();
|
||||
thread = std::make_unique<std::thread>([this]()
|
||||
{
|
||||
ThreadName _(name);
|
||||
while (true)
|
||||
{
|
||||
Task t = queue.pop();
|
||||
if (std::get_if<Exit>(&t) != nullptr)
|
||||
break;
|
||||
Function& func = std::get<Function>(t);
|
||||
func();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const char * const name;
|
||||
using Exit = std::monostate;
|
||||
using Task = std::variant<Exit, Function>;
|
||||
TsQueue<Task> queue;
|
||||
std::unique_ptr<std::thread> thread;
|
||||
std::mutex mutex;
|
||||
};
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
|
||||
namespace detail
|
||||
{
|
||||
template <typename Ret, typename... Args>
|
||||
struct ProxyTraits {
|
||||
using funcType = Ret (WINAPI *)(Args...);
|
||||
};
|
||||
}
|
||||
|
||||
class WinLibLoader
|
||||
{
|
||||
public:
|
||||
WinLibLoader(const char* name = nullptr) : name(name) {
|
||||
}
|
||||
~WinLibLoader() {
|
||||
if (hinst != NULL)
|
||||
FreeLibrary(hinst);
|
||||
}
|
||||
|
||||
template <typename Ret, typename... Args>
|
||||
auto getFunc(const char* functionName, Ret(WINAPI * const funcPtr)(Args...))
|
||||
{
|
||||
using funcType = typename detail::ProxyTraits<Ret, Args...>::funcType;
|
||||
if (!loaded()) {
|
||||
if (!load(name))
|
||||
return static_cast<funcType>(nullptr);
|
||||
}
|
||||
return reinterpret_cast<funcType>(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;
|
||||
};
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "oslib/virtmem.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include "dynlink.h"
|
||||
|
||||
namespace virtmem
|
||||
{
|
||||
|
@ -54,12 +55,22 @@ static std::vector<void *> 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<typename Mapper>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -122,6 +122,8 @@ Option<int> GGPOAnalogAxes("", 0);
|
|||
Option<bool> NetworkOutput(CORE_OPTION_NAME "_network_output", false);
|
||||
Option<int> MultiboardSlaves("", 0);
|
||||
Option<bool> BattleCableEnable("", false);
|
||||
Option<bool> UseDCNet(CORE_OPTION_NAME "_dcnet", false);
|
||||
OptionString ISPUsername("", "flycast1");
|
||||
|
||||
// Maple
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
#ifndef SYS_UIO_H_
|
||||
#define SYS_UIO_H_
|
||||
#include <sys/types.h>
|
||||
#include <sys/_iovec.h>
|
||||
|
||||
#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 <unistd.h>) 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 <unistd.h>) 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_ */
|
|
@ -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
|
||||
<https://www.gnu.org/licenses/>. */
|
||||
|
||||
#ifndef _SYS_UN_H
|
||||
#define _SYS_UN_H 1
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#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 */
|
|
@ -0,0 +1,26 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include "util/periodic_thread.h"
|
||||
#include <atomic>
|
||||
|
||||
class PeriodicThreadTest : public ::testing::Test
|
||||
{
|
||||
};
|
||||
|
||||
TEST_F(PeriodicThreadTest, Basic)
|
||||
{
|
||||
std::atomic<int> 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);
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include "util/tsqueue.h"
|
||||
#include <atomic>
|
||||
#include <future>
|
||||
|
||||
class TsQueueTest : public ::testing::Test
|
||||
{
|
||||
};
|
||||
|
||||
TEST_F(TsQueueTest, Basic)
|
||||
{
|
||||
TsQueue<int> 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<bool> queue;
|
||||
std::atomic<bool> gotResult = false;
|
||||
std::future<bool> 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<T1> q1;
|
||||
q1.push({ 3.14f });
|
||||
T1 r1 = q1.pop();
|
||||
ASSERT_EQ(3.14f, r1.f);
|
||||
|
||||
class T2
|
||||
{
|
||||
public:
|
||||
std::string s;
|
||||
};
|
||||
TsQueue<T2> 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<T3> q3;
|
||||
q3.push(T3("pi"));
|
||||
T3 r3 = q3.pop();
|
||||
ASSERT_EQ("pi", r3.s);
|
||||
}
|
||||
|
||||
TEST_F(TsQueueTest, Clear)
|
||||
{
|
||||
TsQueue<const char*> q;
|
||||
q.push("a");
|
||||
q.push("b");
|
||||
q.clear();
|
||||
q.push("c");
|
||||
ASSERT_EQ(0, strcmp("c", q.pop()));
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
#include "gtest/gtest.h"
|
||||
#include "util/worker_thread.h"
|
||||
#include <atomic>
|
||||
#include <future>
|
||||
|
||||
class WorkerThreadTest : public ::testing::Test
|
||||
{
|
||||
};
|
||||
|
||||
TEST_F(WorkerThreadTest, Basic)
|
||||
{
|
||||
WorkerThread worker{"Test"};
|
||||
std::atomic<bool> 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<int> counter = 0;
|
||||
const auto& task = [&]() {
|
||||
++counter;
|
||||
};
|
||||
const auto& consumer = [&]() {
|
||||
for (int i = 0; i < 100; i++)
|
||||
worker.run(task);
|
||||
};
|
||||
std::future<void> 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<int> counter = 0;
|
||||
const auto& task = [&]() {
|
||||
++counter;
|
||||
};
|
||||
const auto& consumer = [&]() {
|
||||
for (int i = 0; i < 100; i++)
|
||||
worker.run(task);
|
||||
};
|
||||
std::future<void> future = std::async(std::launch::async, consumer);
|
||||
std::future<void> 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<u32> f = worker.runFuture(task, 42);
|
||||
ASSERT_EQ(42, f.get());
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/select.h>
|
||||
#include <fcntl.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <termios.h>
|
||||
#include <netdb.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#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 <tty>] [-h <server name] [-p <server port>]\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;
|
||||
}
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue