Enable physical VMU memory access (#1881)

This commit is contained in:
James 2025-03-22 08:59:42 -06:00 committed by GitHub
parent ea6d5f8732
commit 2631706fe2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 2108 additions and 1189 deletions

View File

@ -227,7 +227,7 @@ else()
add_executable(${PROJECT_NAME} core/emulator.cpp)
endif()
set_target_properties(${PROJECT_NAME} PROPERTIES
set_target_properties(${PROJECT_NAME} PROPERTIES
CMAKE_CXX_STANDARD 20
CMAKE_CXX_STANDARD_REQUIRED ON)
@ -481,6 +481,10 @@ if(NOT LIBRETRO)
core/sdl/sdl_gamepad.h
core/sdl/sdl_keyboard.h
core/sdl/sdl_keyboard_mac.h
core/sdl/dreamlink.cpp
core/sdl/dreamlink.h
core/sdl/dreampicoport.cpp
core/sdl/dreampicoport.h
core/sdl/dreamconn.cpp
core/sdl/dreamconn.h)
@ -523,7 +527,7 @@ if(NOT WITH_SYSTEM_ZLIB)
# help libzip find the package
set(ZLIB_FOUND TRUE)
set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/${ZLIB_RELATIVE_PATH}" "${CMAKE_CURRENT_BINARY_DIR}/${ZLIB_RELATIVE_PATH}")
cmake_policy(SET CMP0026 OLD)
get_target_property(ZLIB_LIBRARY_RELEASE zlibstatic LOCATION)
get_target_property(ZLIB_LIBRARY_DEBUG zlibstatic LOCATION_Debug)

View File

@ -203,6 +203,7 @@ Option<bool> PerGameVmu("PerGameVmu", false, "config");
#ifdef _WIN32
Option<bool, false> UseRawInput("RawInput", false, "input");
#endif
Option<bool> UsePhysicalVmuMemory("UsePhysicalVmuMemory", false);
#ifdef USE_LUA
Option<std::string, false> LuaFileName("LuaFileName", "flycast.lua");

View File

@ -543,6 +543,7 @@ extern Option<bool, false> UseRawInput;
#else
constexpr bool UseRawInput = false;
#endif
extern Option<bool> UsePhysicalVmuMemory;
#ifdef USE_LUA
extern Option<std::string, false> LuaFileName;

View File

@ -11,6 +11,8 @@
#include <zlib.h>
#include <cerrno>
#include <ctime>
#include <thread>
#include <chrono>
const char* maple_sega_controller_name = "Dreamcast Controller";
const char* maple_sega_vmu_name = "Visual Memory";
@ -369,7 +371,7 @@ struct maple_sega_vmu: maple_base
fullSaveNeeded = true;
}
bool fullSave()
virtual bool fullSave()
{
if (file == nullptr)
return false;
@ -2107,29 +2109,215 @@ std::shared_ptr<maple_device> maple_Create(MapleDeviceType type)
}
#if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) && defined(USE_SDL) && !defined(LIBRETRO)
#include "sdl/dreamconn.h"
#include "sdl/dreamlink.h"
#include <list>
#include <memory>
struct DreamConnVmu : public maple_sega_vmu
struct DreamLinkVmu : public maple_sega_vmu
{
std::shared_ptr<DreamConn> dreamconn;
bool running = true;
DreamConnVmu(std::shared_ptr<DreamConn> dreamconn) : dreamconn(dreamconn) {
std::shared_ptr<DreamLink> dreamlink;
bool useRealVmuMemory; //!< Set this to true to use physical VMU memory, false for virtual memory
std::chrono::time_point<std::chrono::system_clock> lastWriteTime;
bool mirroredBlocks[256]; //!< Set to true for block that has been loaded/written
s16 lastWriteBlock = -1;
std::list<u8> blocksToWrite;
std::mutex writeMutex;
std::condition_variable writeCv;
std::thread writeThread;
static u64 lastNotifyTime;
static u64 lastErrorNotifyTime;
DreamLinkVmu(std::shared_ptr<DreamLink> dreamlink) :
dreamlink(dreamlink),
writeThread([this](){writeEntrypoint();})
{
// Initialize useRealVmuMemory with our config setting
useRealVmuMemory = config::UsePhysicalVmuMemory;
}
virtual ~DreamLinkVmu() {
running = false;
// Entering lock context
{
std::unique_lock<std::mutex> lock(writeMutex);
writeCv.notify_all();
}
writeThread.join();
}
void OnSetup() override
{
// Update useRealVmuMemory in case config changed
useRealVmuMemory = config::UsePhysicalVmuMemory;
// All data must be re-read
memset(mirroredBlocks, 0, sizeof(mirroredBlocks));
if (useRealVmuMemory)
{
// Ensure file is not being used
if (file != nullptr)
{
std::fclose(file);
file = nullptr;
}
memset(flash_data, 0, sizeof(flash_data));
memset(lcd_data, 0, sizeof(lcd_data));
}
else
{
maple_sega_vmu::OnSetup();
}
}
bool fullSave() override
{
if (useRealVmuMemory)
{
// Skip virtual save when using physical VMU
//DEBUG_LOG(MAPLE, "Not saving because this is a real vmu");
NOTICE_LOG(MAPLE, "Saving to physical VMU");
return true;
}
else
{
return maple_sega_vmu::fullSave();
}
}
u32 dma(u32 cmd) override
{
// Physical VMU logic
if (dma_count_in >= 4)
{
const u32 functionId = *(u32 *)dma_buffer_in;
if ((cmd == MDCF_BlockWrite && functionId == MFID_2_LCD) // LCD screen
|| (cmd == MDCF_SetCondition && functionId == MFID_3_Clock)) // Buzzer
const u32 functionId = *(u32*)dma_buffer_in;
const MapleMsg* msg = reinterpret_cast<const MapleMsg*>(dma_buffer_in - 4);
if (functionId == MFID_1_Storage)
{
const MapleMsg *msg = reinterpret_cast<const MapleMsg*>(dma_buffer_in - 4);
dreamconn->send(*msg);
if (useRealVmuMemory)
{
const u64 currentTime = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
// Only show notification once every 6 seconds to avoid spam
if (cmd == MDCF_BlockRead &&
(currentTime - lastNotifyTime) > 4000 &&
(currentTime - lastErrorNotifyTime) > 4000)
{
// This is a read operation (loading)
os_notify("ATTENTION: Loading from a physical VMU", 6000,
"Game data is being loaded from your physical VMU");
lastNotifyTime = currentTime;
}
switch (cmd)
{
case MDCF_BlockWrite:
{
// Throw away function
r32();
// Save the write to RAM
u32 bph=r32();
u32 Block = lastWriteBlock = (SWAP32(bph))&0xffff;
u32 Phase = ((SWAP32(bph))>>16)&0xff;
u32 write_adr=Block*512+Phase*(512/4);
u32 write_len=r_count();
DEBUG_LOG(MAPLE, "VMU %s block write: Block %d Phase %d addr %x len %d", logical_port, Block, Phase, write_adr, write_len);
if (write_adr + write_len > sizeof(flash_data))
{
INFO_LOG(MAPLE, "Failed to write VMU %s: overflow", logical_port);
skip(write_len);
return MDRE_FileError; //invalid params
}
rptr(&flash_data[write_adr],write_len);
// All done - wait until GetLastError to queue the write
return MDRS_DeviceReply;
}
case MDCF_GetLastError:
{
mirroredBlocks[lastWriteBlock] = true;
// Entering lock context
{
std::unique_lock<std::mutex> lock(writeMutex);
if (std::find(blocksToWrite.begin(), blocksToWrite.end(), lastWriteBlock) == blocksToWrite.end())
{
blocksToWrite.push_back(lastWriteBlock);
writeCv.notify_all();
}
}
lastWriteTime = std::chrono::system_clock::now();
return MDRS_DeviceReply;
}
case MDCF_BlockRead:
{
u8 requestBlock = msg->data[7];
if (!mirroredBlocks[requestBlock]) {
// Try up to 4 times to read
bool readSuccess = false;
for (u32 i = 0; i < 4; ++i) {
if (i > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(30));
}
MapleMsg rcvMsg;
if (dreamlink->send(*msg, rcvMsg) && rcvMsg.size == 130) {
// Something read!
u8 block = rcvMsg.data[7];
memcpy(&flash_data[block * 4 * 128], &rcvMsg.data[8], 4 * 128);
mirroredBlocks[block] = true;
readSuccess = true;
break;
}
}
if (!readSuccess) {
ERROR_LOG(MAPLE, "Failed to read VMU %s: I/O error", logical_port);
return MDRE_FileError; // I/O error
}
}
break;
}
default:
// do nothing
break;
}
}
}
else if (functionId == MFID_2_LCD)
{
if (cmd == MDCF_BlockWrite)
{
dreamlink->send(*msg);
}
}
else if (functionId == MFID_3_Clock)
{
if (cmd == MDCF_SetCondition)
{
dreamlink->send(*msg);
}
}
}
// If made it here, call base's dma to handle return value
return maple_sega_vmu::dma(cmd);
}
@ -2143,10 +2331,14 @@ struct DreamConnVmu : public maple_sega_vmu
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;
// Never copy data to virtual VMU if physical VMU is enabled
if (!config::UsePhysicalVmuMemory && !useRealVmuMemory)
{
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()
@ -2156,130 +2348,301 @@ struct DreamConnVmu : public maple_sega_vmu
msg.destAP = maple_port;
msg.originAP = bus_id << 6;
msg.size = 2 + sizeof(lcd_data) / 4;
*(u32 *)&msg.data[0] = MFID_2_LCD;
*(u32 *)&msg.data[4] = 0; // PT, phase, block#
*(u32*)&msg.data[0] = MFID_2_LCD;
*(u32*)&msg.data[4] = 0; // PT, phase, block#
memcpy(&msg.data[8], lcd_data, sizeof(lcd_data));
dreamconn->send(msg);
dreamlink->send(msg);
}
private:
//! Thread entrypoint for write operations
void writeEntrypoint()
{
while (true)
{
u8 block = 0;
// Entering lock context
{
std::unique_lock<std::mutex> lock(writeMutex);
writeCv.wait(lock, [this](){ return (!running || !blocksToWrite.empty()); });
if (!running)
{
break;
}
block = blocksToWrite.front();
blocksToWrite.pop_front();
}
const u64 currentTime = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
// Only show notification once every 6 seconds to avoid spam
if ((currentTime - lastNotifyTime) > 4000 && (currentTime - lastErrorNotifyTime) > 4000)
{
// This is a write operation (saving)
os_notify("ATTENTION: You are saving to a physical VMU", 6000,
"Do not disconnect the VMU or close the game");
lastNotifyTime = currentTime;
}
bool writeSuccess = true;
const u8* blockData = &flash_data[block * 4 * 128];
std::chrono::milliseconds delay(10);
const std::chrono::milliseconds delayInc(5);
// Try up to 4 times to write
for (u32 i = 0; i < 4; ++i) {
if (i > 0) {
// Slow down writes to avoid overloading the VMU
delay += delayInc;
}
writeSuccess = true;
// 4 write phases per block
for (u32 phase = 0; phase < 4; ++phase) {
MapleMsg writeMsg;
writeMsg.command = MDCF_BlockWrite;
writeMsg.destAP = (bus_id << 6) | (1 << bus_port);;
writeMsg.originAP = (bus_id << 6);
writeMsg.size = 34;
writeMsg.setWord(MFID_1_Storage, 0);
const u32 locationWord = (block << 24) | (phase << 8);
writeMsg.setWord(locationWord, 1);
memcpy(&writeMsg.data[8], &blockData[phase * 128], 128);
// Delay before writing
std::this_thread::sleep_for(delay);
MapleMsg rcvMsg;
if (!dreamlink->send(writeMsg, rcvMsg) || rcvMsg.command != MDRS_DeviceReply) {
// Not acknowledged
writeSuccess = false;
break;
}
}
if (writeSuccess) {
// Delay before committing
std::this_thread::sleep_for(delay);
// Send the GetLastError command to commit the data
MapleMsg writeMsg;
writeMsg.command = MDCF_GetLastError;
writeMsg.destAP = (bus_id << 6) | (1 << bus_port);;
writeMsg.originAP = (bus_id << 6);
writeMsg.size = 2;
writeMsg.setWord(MFID_1_Storage, 0);
const u32 phase = 4;
const u32 locationWord = (block << 24) | (phase << 8);
writeMsg.setWord(locationWord, 1);
MapleMsg rcvMsg;
if (dreamlink->send(writeMsg, rcvMsg) && rcvMsg.command == MDRS_DeviceReply) {
// Acknowledged
break;
}
}
// else: continue to retry
}
if (!writeSuccess) {
ERROR_LOG(MAPLE, "Failed to save VMU %s: I/O error", logical_port);
const u64 currentTime = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
if ((currentTime - lastErrorNotifyTime) > 4000)
{
os_notify("ATTENTION: Write to VMU failed", 6000);
lastErrorNotifyTime = currentTime;
}
}
}
}
};
struct DreamConnPurupuru : public maple_sega_purupuru
{
std::shared_ptr<DreamConn> dreamconn;
u64 DreamLinkVmu::lastNotifyTime = 0;
u64 DreamLinkVmu::lastErrorNotifyTime = 0;
DreamConnPurupuru(std::shared_ptr<DreamConn> dreamconn) : dreamconn(dreamconn) {
struct DreamLinkPurupuru : public maple_sega_purupuru
{
std::shared_ptr<DreamLink> dreamlink;
//! Number of consecutive stop conditions sent
u32 stopSendCount = 0;
DreamLinkPurupuru(std::shared_ptr<DreamLink> dreamlink) : dreamlink(dreamlink) {
}
u32 dma(u32 cmd) override
{
if (cmd == MDCF_BlockWrite || cmd == MDCF_SetCondition) {
const MapleMsg *msg = reinterpret_cast<const MapleMsg*>(dma_buffer_in - 4);
dreamconn->send(*msg);
const u32 functionId = *(u32*)dma_buffer_in;
const u32 condition = *(u32*)(dma_buffer_in + 4);
if (functionId == MFID_8_Vibration && condition == 0x00000010) {
++stopSendCount;
} else {
stopSendCount = 0;
}
// Only send 2 consecutive stop commands; ignore the rest to avoid unnecessary communications
if (stopSendCount <= 2) {
dreamlink->send(*msg);
}
}
return maple_sega_purupuru::dma(cmd);
}
};
static std::list<std::shared_ptr<DreamConnVmu>> dreamConnVmus;
static std::list<std::shared_ptr<DreamConnPurupuru>> dreamConnPurupurus;
static std::list<std::shared_ptr<DreamLinkVmu>> dreamLinkVmus[2];
static std::list<std::shared_ptr<DreamLinkPurupuru>> dreamLinkPurupurus;
void createDreamConnDevices(std::shared_ptr<DreamConn> dreamconn, bool gameStart)
static void disablePhysicalVmuMemoryOption()
{
const int bus = dreamconn->getBus();
bool vmuFound = false;
bool rumbleFound = false;
if (dreamconn->hasVmu())
// Make setting read only
config::UsePhysicalVmuMemory.override(config::UsePhysicalVmuMemory);
}
static void enablePhysicalVmuMemoryOption()
{
// Remove read-only setting and preserve current value
bool val = config::UsePhysicalVmuMemory;
config::UsePhysicalVmuMemory.reset();
config::UsePhysicalVmuMemory.set(val);
}
void createDreamLinkDevices(std::shared_ptr<DreamLink> dreamlink, bool gameStart, bool gameEnd)
{
const int bus = dreamlink->getBus();
for (int i = 0; i < 2; ++i)
{
std::shared_ptr<DreamConnVmu> vmu;
for (const std::shared_ptr<DreamConnVmu>& vmuIter : dreamConnVmus)
std::shared_ptr<maple_device> dev = MapleDevices[bus][i];
if ((dreamlink->getFunctionCode(i + 1) & MFID_1_Storage) || (dev != nullptr && dev->get_device_type() == MDT_SegaVMU))
{
if (vmuIter->dreamconn.get() == dreamconn.get())
bool vmuFound = false;
std::shared_ptr<DreamLinkVmu> vmu;
for (const std::shared_ptr<DreamLinkVmu>& vmuIter : dreamLinkVmus[i])
{
vmuFound = true;
vmu = vmuIter;
break;
}
}
std::shared_ptr<maple_device> dev = MapleDevices[bus][0];
if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_SegaVMU))
{
bool vmuCreated = false;
if (!vmu)
{
vmu = std::make_shared<DreamConnVmu>(dreamconn);
vmuCreated = true;
}
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 (vmuIter->dreamlink.get() == dreamlink.get())
{
vmuFound = true;
vmu = vmuIter;
break;
}
}
if (!vmuFound) dreamConnVmus.push_back(vmu);
}
}
if (dreamconn->hasRumble())
{
std::shared_ptr<DreamConnPurupuru> rumble;
for (const std::shared_ptr<DreamConnPurupuru>& purupuru : dreamConnPurupurus)
{
if (purupuru->dreamconn.get() == dreamconn.get())
if (gameStart || !vmuFound)
{
rumbleFound = true;
rumble = purupuru;
break;
}
}
if (!vmu)
{
vmu = std::make_shared<DreamLinkVmu>(dreamlink);
}
std::shared_ptr<maple_device> dev = MapleDevices[bus][1];
if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_PurupuruPack))
{
if (!rumble)
{
rumble = std::make_shared<DreamConnPurupuru>(dreamconn);
vmu->Setup(bus, i);
if (!vmuFound && dev && dev->get_device_type() == MDT_SegaVMU && !vmu->useRealVmuMemory) {
// Only copy data from virtual VMU if Physical VMU Only is disabled
vmu->copyIn(std::static_pointer_cast<maple_sega_vmu>(dev));
if (!gameStart) {
vmu->updateScreen();
}
}
if (!vmuFound) {
dreamLinkVmus[i].push_back(vmu);
}
}
if (gameStart)
{
disablePhysicalVmuMemoryOption();
}
else if (gameEnd)
{
enablePhysicalVmuMemoryOption();
}
}
else if (i == 1 && ((dreamlink->getFunctionCode(i + 1) & MFID_8_Vibration) || (dev != nullptr && dev->get_device_type() == MDT_PurupuruPack)))
{
bool rumbleFound = false;
std::shared_ptr<DreamLinkPurupuru> rumble;
for (const std::shared_ptr<DreamLinkPurupuru>& purupuru : dreamLinkPurupurus)
{
if (purupuru->dreamlink.get() == dreamlink.get())
{
rumbleFound = true;
rumble = purupuru;
break;
}
}
if (gameStart || !rumbleFound)
{
if (!rumble)
{
rumble = std::make_shared<DreamLinkPurupuru>(dreamlink);
}
rumble->Setup(bus, i);
if (!rumbleFound) dreamLinkPurupurus.push_back(rumble);
}
rumble->Setup(bus, 1);
if (!rumbleFound) dreamConnPurupurus.push_back(rumble);
}
}
}
void tearDownDreamConnDevices(std::shared_ptr<DreamConn> dreamconn)
void tearDownDreamLinkDevices(std::shared_ptr<DreamLink> dreamlink)
{
const int bus = dreamconn->getBus();
for (std::list<std::shared_ptr<DreamConnVmu>>::const_iterator iter = dreamConnVmus.begin();
iter != dreamConnVmus.end();)
const int bus = dreamlink->getBus();
for (int i = 0; i < 2; ++i)
{
if ((*iter)->dreamconn.get() == dreamconn.get())
for (std::list<std::shared_ptr<DreamLinkVmu>>::const_iterator iter = dreamLinkVmus[i].begin();
iter != dreamLinkVmus[i].end();)
{
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;
if ((*iter)->dreamlink.get() == dreamlink.get())
{
DEBUG_LOG(MAPLE, "VMU teardown - Physical VMU: %s", (*iter)->useRealVmuMemory ? "true" : "false");
std::shared_ptr<maple_device> dev = maple_Create(MDT_SegaVMU);
dev->Setup(bus, 0);
if (!(*iter)->useRealVmuMemory)
{
(*iter)->copyOut(std::static_pointer_cast<maple_sega_vmu>(dev));
}
DEBUG_LOG(MAPLE, "VMU teardown - Copy completed");
iter = dreamLinkVmus[i].erase(iter);
break;
}
else
{
++iter;
}
}
}
for (std::list<std::shared_ptr<DreamConnPurupuru>>::const_iterator iter = dreamConnPurupurus.begin();
iter != dreamConnPurupurus.end();)
std::size_t dreamLinkVmuCount = 0;
for (int i = 0; i < 2; ++i)
{
if ((*iter)->dreamconn.get() == dreamconn.get())
dreamLinkVmuCount += dreamLinkVmus[i].size();
}
if (dreamLinkVmuCount == 0)
{
enablePhysicalVmuMemoryOption();
}
for (std::list<std::shared_ptr<DreamLinkPurupuru>>::const_iterator iter = dreamLinkPurupurus.begin();
iter != dreamLinkPurupurus.end();)
{
if ((*iter)->dreamlink.get() == dreamlink.get())
{
std::shared_ptr<maple_device> dev = maple_Create(MDT_PurupuruPack);
dev->Setup(bus, 1);
iter = dreamConnPurupurus.erase(iter);
iter = dreamLinkPurupurus.erase(iter);
break;
}
else

File diff suppressed because it is too large Load Diff

View File

@ -17,95 +17,68 @@
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "types.h"
#include "emulator.h"
#include "sdl_gamepad.h"
#if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP)
#define USE_DREAMCASTCONTROLLER 1
#define TYPE_DREAMCONN 1
#define TYPE_DREAMPICOPORT 2
#include <asio.hpp>
#endif
#include <memory>
#include "dreamlink.h"
struct MapleMsg
{
u8 command;
u8 destAP;
u8 originAP;
u8 size;
u8 data[1024];
u32 getDataSize() const {
return size * 4;
}
template<typename T>
void setData(const T& p) {
memcpy(data, &p, sizeof(T));
this->size = (sizeof(T) + 3) / 4;
}
};
static_assert(sizeof(MapleMsg) == 1028);
class DreamConn
{
int bus = -1;
const int dreamcastControllerType;
#ifdef USE_DREAMCASTCONTROLLER
std::unique_ptr<class DreamcastControllerConnection> dcConnection;
#endif
#include <asio.hpp>
#include <mutex>
class DreamConn : public DreamLink
{
//! Base port of communication to DreamConn
static constexpr u16 BASE_PORT = 37393;
int bus = -1;
bool maple_io_connected = false;
u8 expansionDevs = 0;
asio::ip::tcp::iostream iostream;
std::mutex send_mutex;
public:
DreamConn(int bus, int dreamcastControllerType, int joystick_idx, SDL_Joystick* sdl_joystick);
//! DreamConn VID:4457 PID:4443
static constexpr const char* VID_PID_GUID = "5744000043440000";
public:
DreamConn(int bus);
~DreamConn();
bool send(const MapleMsg& msg);
bool send(const MapleMsg& msg) override;
// When called, do teardown stuff like reset screen
void gameTermination();
bool send(const MapleMsg& txMsg, MapleMsg& rxMsg) override;
int getBus() const {
int getBus() const override {
return bus;
}
bool hasVmu() {
u32 getFunctionCode(int forPort) const override {
if (forPort == 1 && hasVmu()) {
return 0x0E000000;
}
else if (forPort == 2 && hasRumble()) {
return 0x00010000;
}
return 0;
}
bool hasVmu() const {
return expansionDevs & 1;
}
bool hasRumble() {
bool hasRumble() const {
return expansionDevs & 2;
}
int getDefaultBus();
void changeBus(int newBus) override;
void changeBus(int newBus);
std::string getName() const override {
return "DreamConn+ / DreamConn S Controller";
}
std::string getName();
void connect() override;
void connect();
void disconnect();
void disconnect() override;
};
class DreamConnGamepad : public SDLGamepad
{
public:
DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick);
~DreamConnGamepad();
void set_maple_port(int port) override;
void registered() override;
bool gamepad_btn_input(u32 code, bool pressed) override;
bool gamepad_axis_input(u32 code, int value) override;
static bool isDreamcastController(int deviceIndex);
private:
static void handleEvent(Event event, void *arg);
void checkKeyCombo();
std::shared_ptr<DreamConn> dreamconn;
bool ltrigPressed = false;
bool rtrigPressed = false;
bool startPressed = false;
};
#endif // USE_DREAMCASTCONTROLLER

222
core/sdl/dreamlink.cpp Normal file
View File

@ -0,0 +1,222 @@
/*
Copyright 2024 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 "dreamlink.h"
#ifdef USE_DREAMCASTCONTROLLER
#include "dreamconn.h"
#include "dreampicoport.h"
#include "hw/maple/maple_devs.h"
#include "ui/gui.h"
#include <cfg/option.h>
#include <SDL.h>
#include <asio.hpp>
#include <iomanip>
#include <sstream>
#include <optional>
#include <thread>
#include <list>
#include <mutex>
#include <condition_variable>
#include <atomic>
#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))
#include <dirent.h>
#endif
#if defined(_WIN32)
#include <windows.h>
#include <setupapi.h>
#endif
void createDreamLinkDevices(std::shared_ptr<DreamLink> dreamlink, bool gameStart, bool gameEnd);
void tearDownDreamLinkDevices(std::shared_ptr<DreamLink> dreamlink);
bool DreamLinkGamepad::isDreamcastController(int deviceIndex)
{
char guid_str[33] {};
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(deviceIndex), guid_str, sizeof(guid_str));
NOTICE_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str,
guid_str[10], guid_str[11], guid_str[8], guid_str[9],
guid_str[18], guid_str[19], guid_str[16], guid_str[17]);
// DreamConn VID:4457 PID:4443
// Dreamcast Controller USB VID:1209 PID:2f07
const char* pid_vid_guid_str = guid_str + 8;
if (memcmp(DreamConn::VID_PID_GUID, pid_vid_guid_str, 16) == 0 ||
memcmp(DreamPicoPort::VID_PID_GUID, pid_vid_guid_str, 16) == 0)
{
NOTICE_LOG(INPUT, "Dreamcast controller found!");
return true;
}
return false;
}
DreamLinkGamepad::DreamLinkGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick)
: SDLGamepad(maple_port, joystick_idx, sdl_joystick)
{
char guid_str[33] {};
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(joystick_idx), guid_str, sizeof(guid_str));
// DreamConn VID:4457 PID:4443
// Dreamcast Controller USB VID:1209 PID:2f07
if (memcmp(DreamConn::VID_PID_GUID, guid_str + 8, 16) == 0)
{
dreamlink = std::make_shared<DreamConn>(maple_port);
}
else if (memcmp(DreamPicoPort::VID_PID_GUID, guid_str + 8, 16) == 0)
{
dreamlink = std::make_shared<DreamPicoPort>(maple_port, joystick_idx, sdl_joystick);
}
if (dreamlink) {
_name = dreamlink->getName();
int defaultBus = dreamlink->getDefaultBus();
if (defaultBus >= 0 && defaultBus < 4) {
set_maple_port(defaultBus);
}
}
EventManager::listen(Event::Start, handleEvent, this);
EventManager::listen(Event::LoadState, handleEvent, this);
EventManager::listen(Event::Terminate, handleEvent, this);
}
DreamLinkGamepad::~DreamLinkGamepad() {
EventManager::unlisten(Event::Start, handleEvent, this);
EventManager::unlisten(Event::LoadState, handleEvent, this);
EventManager::unlisten(Event::Terminate, handleEvent, this);
if (dreamlink) {
tearDownDreamLinkDevices(dreamlink);
dreamlink.reset();
// Make sure settings are open in case disconnection happened mid-game
if (!gui_is_open()) {
gui_open_settings();
}
}
}
void DreamLinkGamepad::set_maple_port(int port)
{
if (dreamlink) {
if (port < 0 || port >= 4) {
dreamlink->disconnect();
}
else if (dreamlink->getBus() != port) {
dreamlink->changeBus(port);
if (is_registered()) {
dreamlink->connect();
}
}
}
SDLGamepad::set_maple_port(port);
}
void DreamLinkGamepad::registered()
{
if (dreamlink)
{
dreamlink->connect();
// Create DreamLink Maple Devices here just in case game is already running
createDreamLinkDevices(dreamlink, false, false);
}
}
void DreamLinkGamepad::handleEvent(Event event, void *arg)
{
DreamLinkGamepad *gamepad = static_cast<DreamLinkGamepad*>(arg);
if (gamepad->dreamlink != nullptr && event != Event::Terminate) {
createDreamLinkDevices(gamepad->dreamlink, event == Event::Start, event == Event::Terminate);
}
if (gamepad->dreamlink != nullptr && event == Event::Terminate)
{
gamepad->dreamlink->gameTermination();
}
}
bool DreamLinkGamepad::gamepad_btn_input(u32 code, bool pressed)
{
if (!is_detecting_input() && input_mapper)
{
DreamcastKey key = input_mapper->get_button_id(0, code);
if (key == DC_BTN_START) {
startPressed = pressed;
checkKeyCombo();
}
}
else {
startPressed = false;
}
return SDLGamepad::gamepad_btn_input(code, pressed);
}
bool DreamLinkGamepad::gamepad_axis_input(u32 code, int value)
{
if (!is_detecting_input())
{
if (code == leftTrigger) {
ltrigPressed = value > 0;
checkKeyCombo();
}
else if (code == rightTrigger) {
rtrigPressed = value > 0;
checkKeyCombo();
}
}
else {
ltrigPressed = false;
rtrigPressed = false;
}
return SDLGamepad::gamepad_axis_input(code, value);
}
void DreamLinkGamepad::checkKeyCombo() {
if (ltrigPressed && rtrigPressed && startPressed)
gui_open_settings();
}
#else // USE_DREAMCASTCONTROLLER
bool DreamLinkGamepad::isDreamcastController(int deviceIndex) {
return false;
}
DreamLinkGamepad::DreamLinkGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick)
: SDLGamepad(maple_port, joystick_idx, sdl_joystick) {
}
DreamLinkGamepad::~DreamLinkGamepad() {
}
void DreamLinkGamepad::set_maple_port(int port) {
SDLGamepad::set_maple_port(port);
}
void DreamLinkGamepad::registered() {
}
bool DreamLinkGamepad::gamepad_btn_input(u32 code, bool pressed) {
return SDLGamepad::gamepad_btn_input(code, pressed);
}
bool DreamLinkGamepad::gamepad_axis_input(u32 code, int value) {
return SDLGamepad::gamepad_axis_input(code, value);
}
#endif

130
core/sdl/dreamlink.h Normal file
View File

@ -0,0 +1,130 @@
/*
Copyright 2024 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
// This file contains abstraction layer for access to different kinds of physical controllers
#include "types.h"
#include "emulator.h"
#include "sdl_gamepad.h"
#include <functional>
#include <memory>
#if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP)
#define USE_DREAMCASTCONTROLLER 1
#endif
#include <memory>
struct MapleMsg
{
u8 command = 0;
u8 destAP = 0;
u8 originAP = 0;
u8 size = 0;
u8 data[1024];
u32 getDataSize() const {
return size * 4;
}
template<typename T>
void setData(const T& p) {
memcpy(data, &p, sizeof(T));
this->size = (sizeof(T) + 3) / 4;
}
void setWord(const u32& p, int index) {
if (index < 0 || index >= 256) {
return;
}
memcpy(&data[index * 4], &p, sizeof(u32));
if (this->size <= index) {
this->size = index + 1;
}
}
};
static_assert(sizeof(MapleMsg) == 1028);
// Abstract base class for physical controller implementations
class DreamLink
{
public:
DreamLink() = default;
virtual ~DreamLink() = default;
//! Sends a message to the controller, ignoring the response
//! @note The implementation shall be thread safe
virtual bool send(const MapleMsg& msg) = 0;
//! Sends a message to the controller and waits for a response
//! @note The implementation shall be thread safe
virtual bool send(const MapleMsg& txMsg, MapleMsg& rxMsg) = 0;
//! When called, do teardown stuff like reset screen
virtual inline void gameTermination() {}
//! @param[in] forPort The port number to get the function code of (1 or 2)
//! @return the device type for the given port
virtual u32 getFunctionCode(int forPort) const = 0;
//! @return the default bus number to select for this controller or -1 to not select a default
virtual int getDefaultBus() const {
return -1;
}
//! @return the selected bus number of the controller
virtual int getBus() const = 0;
//! Changes the selected maple port is changed by the user
virtual void changeBus(int newBus) = 0;
//! @return the display name of the controller
virtual std::string getName() const = 0;
//! Attempt connection to the hardware controller
virtual void connect() = 0;
//! Disconnect from the hardware controller
virtual void disconnect() = 0;
};
class DreamLinkGamepad : public SDLGamepad
{
public:
DreamLinkGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick);
~DreamLinkGamepad();
void set_maple_port(int port) override;
void registered() override;
bool gamepad_btn_input(u32 code, bool pressed) override;
bool gamepad_axis_input(u32 code, int value) override;
static bool isDreamcastController(int deviceIndex);
private:
static void handleEvent(Event event, void *arg);
void checkKeyCombo();
std::shared_ptr<DreamLink> dreamlink;
bool ltrigPressed = false;
bool rtrigPressed = false;
bool startPressed = false;
};

1035
core/sdl/dreampicoport.cpp Normal file

File diff suppressed because it is too large Load Diff

110
core/sdl/dreampicoport.h Normal file
View File

@ -0,0 +1,110 @@
/*
Copyright 2024 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 "dreamlink.h"
#ifdef USE_DREAMCASTCONTROLLER
#include <asio.hpp>
#include <atomic>
#include <chrono>
#include <vector>
#include <array>
// Forward declaration of underlying serial connection
class DreamPicoPortSerialHandler;
//! See: https://github.com/OrangeFox86/DreamPicoPort
class DreamPicoPort : public DreamLink
{
u8 expansionDevs = 0;
//! The one and only serial port
static std::unique_ptr<DreamPicoPortSerialHandler> serial;
//! Number of devices using the above serial
static std::atomic<std::uint32_t> connected_dev_count;
//! Current timeout in milliseconds
std::chrono::milliseconds timeout_ms;
//! The bus ID dictated by flycast
int software_bus = -1;
//! The bus index of the hardware connection which will differ from the software bus
int hardware_bus = -1;
//! true iff only a single devices was found when enumerating devices
bool is_single_device = true;
//! True when initial enumeration failed
bool is_hardware_bus_implied = true;
//! True once connection is established
bool connection_established = false;
//! The queried interface version
double interface_version = 0.0;
//! The queried peripherals; for each function, index 0 is function code and index 1 is the function definition
std::vector<std::vector<std::array<uint32_t, 2>>> peripherals;
public:
//! Dreamcast Controller USB VID:1209 PID:2f07
static constexpr const std::uint16_t VID = 0x1209;
static constexpr const std::uint16_t PID = 0x2f07;
static constexpr const char* VID_PID_GUID = "09120000072f0000";
public:
DreamPicoPort(int bus, int joystick_idx, SDL_Joystick* sdl_joystick);
virtual ~DreamPicoPort();
bool send(const MapleMsg& msg) override;
bool send(const MapleMsg& txMsg, MapleMsg& rxMsg) override;
void gameTermination() override;
int getBus() const override;
u32 getFunctionCode(int forPort) const override;
int getDefaultBus() const override;
void changeBus(int newBus);
std::string getName() const override;
void connect() override;
void disconnect() override;
void sendPort();
int hardwareBus() const;
bool isHardwareBusImplied() const;
bool isSingleDevice() const;
private:
asio::error_code sendCmd(const std::string& cmd);
asio::error_code sendMsg(const MapleMsg& msg);
asio::error_code receiveCmd(std::string& cmd);
asio::error_code receiveMsg(MapleMsg& msg);
void determineHardwareBus(int joystick_idx, SDL_Joystick* sdl_joystick);
bool queryInterfaceVersion();
bool queryPeripherals();
};
#endif // USE_DREAMCASTCONTROLLER

View File

@ -30,7 +30,7 @@
#include "nswitch.h"
#include "switch_gamepad.h"
#endif
#include "dreamconn.h"
#include "dreamlink.h"
#include <unordered_map>
static SDL_Window* window = NULL;
@ -83,8 +83,8 @@ 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::isDreamcastController(index))
gamepad = std::make_shared<DreamConnGamepad>(index < MAPLE_PORTS ? index : -1, index, pJoystick);
if (DreamLinkGamepad::isDreamcastController(index))
gamepad = std::make_shared<DreamLinkGamepad>(index < MAPLE_PORTS ? index : -1, index, pJoystick);
else
gamepad = std::make_shared<SDLGamepad>(index < MAPLE_PORTS ? index : -1, index, pJoystick);
#endif

View File

@ -54,6 +54,7 @@
#include "hw/pvr/Renderer_if.h"
#if defined(USE_SDL)
#include "sdl/sdl.h"
#include "sdl/dreamlink.h"
#endif
#include "vgamepad.h"
@ -434,7 +435,7 @@ static void gui_newFrame()
io.AddKeyEvent(ImGuiKey_GamepadDpadRight, ((kcode[0] & DC_DPAD_RIGHT) == 0));
io.AddKeyEvent(ImGuiKey_GamepadDpadUp, ((kcode[0] & DC_DPAD_UP) == 0));
io.AddKeyEvent(ImGuiKey_GamepadDpadDown, ((kcode[0] & DC_DPAD_DOWN) == 0));
float analog;
analog = joyx[0] < 0 ? -(float)joyx[0] / 32768.f : 0.f;
io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickLeft, analog > 0.1f, analog);
@ -820,8 +821,8 @@ const char *maple_device_types[] =
// "Dreameye",
};
const char *maple_expansion_device_types[] =
{
const char *maple_expansion_device_types[] =
{
"None",
"Sega VMU",
"Vibration Pack",
@ -2032,6 +2033,10 @@ static void gui_settings_controls(bool& maple_devices_changed)
#if defined(_WIN32) && !defined(TARGET_UWP)
OptionCheckbox("Use Raw Input", config::UseRawInput, "Supports multiple pointing devices (mice, light guns) and keyboards");
#endif
#ifdef USE_DREAMCASTCONTROLLER
OptionCheckbox("Use Physical VMU Memory", config::UsePhysicalVmuMemory,
"Enables direct read/write access to physical VMU memory via DreamPicoPort/DreamConn.");
#endif
ImGui::Spacing();
header("Dreamcast Devices");
@ -2848,7 +2853,6 @@ static void gui_settings_advanced()
}
OptionCheckbox("Dump Textures", config::DumpTextures,
"Dump all textures into data/texdump/<game id>");
bool logToFile = cfgLoadBool("log", "LogToFile", false);
if (ImGui::Checkbox("Log to File", &logToFile))
cfgSaveBool("log", "LogToFile", logToFile);
@ -3268,7 +3272,7 @@ static void gui_display_content()
const int itemsPerLine = std::max<int>(totalWidth / (uiScaled(150) + ImGui::GetStyle().ItemSpacing.x), 1);
const float responsiveBoxSize = totalWidth / itemsPerLine - ImGui::GetStyle().FramePadding.x * 2;
const ImVec2 responsiveBoxVec2 = ImVec2(responsiveBoxSize, responsiveBoxSize);
if (config::BoxartDisplayMode)
ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f, 0.5f));
else
@ -3737,7 +3741,7 @@ void gui_display_profiler()
ImGui::Unindent();
}
}
for (const fc_profiler::ProfileThread* profileThread : fc_profiler::ProfileThread::s_allThreads)
{
fc_profiler::drawGraph(*profileThread);