Skip to content

Prototype for DreamPotato connectivity#1989

Merged
flyinghead merged 34 commits intoflyinghead:devfrom
RikkiGibson:vmuconn
Oct 3, 2025
Merged

Prototype for DreamPotato connectivity#1989
flyinghead merged 34 commits intoflyinghead:devfrom
RikkiGibson:vmuconn

Conversation

@RikkiGibson
Copy link
Contributor

@RikkiGibson RikkiGibson commented Jun 28, 2025

Related to #1988
See https://github.com/RikkiGibson/DreamPotato?tab=readme-ov-file#flycast-integration-prototype for setup instructions

  • Allow DreamLinks to work without needing to be associated with an SDL controller.
    • DreamLink still requires SDL for now, but, we should be closer to permitting DreamLinks to work in libretro builds with this change. (though, this may not be a feature we can make available for libretro users for other reasons.)
  • Add per-port option "UseNetworkExpansionDevices". When this is checked, we attempt to connect DreamLink upon game startup, even if no physical DreamConn/DreamLink controller is being used.
    • Note that a physical controller's behavior (e.g. use DreamPicoPort and not DreamConn) will override the value of this setting, so, existing users of those physical devices should get the same behavior regardless of whether they use this new setting.
  • Add UI to indicate DreamLink connection status, and allow manually disconnecting and reconnecting.
  • Detect changes to expansion devices during gameplay (hot plugging). DreamConn now listens for a specific message from the TCP server, which prompts it to reconnect expansion devices.
    • I'm not certain how a real controller conveys to the console that an expansion device was just plugged/unplugged. If someone with better insight into the Maple protocol could assist, we could try to adjust the form of the message, to ensure it is fairly close to how real hardware works.
    • Update: per Prototype for DreamPotato connectivity #1989 (comment) it looks like a "hardware faithful" way of doing this would be to send a GetCondition+Input message each frame. This would have some benefits, but it feels like the current solution has the desired set of behaviors currently, so it doesn't feel necessary to change it.
    • There is a corresponding method in DreamPicoPort to support hot plugging that I did not implement. I would need assistance as I don't own the device in order to test it.
  • Tried my best to make things resilient to various combinations of:
    • Opening Flycast/DreamPotato in different order.
    • Connecting/disconnecting due to change in UseNetworkExpansionDevices checkbox value
    • Connecting/disconnecting using new button in commands UI.
    • Closing either Flycast/DreamPotato, relaunching, and reconnecting.
    • Performing any of the above with DreamPotato initially in "docked" or "undocked" state (whether the emulated VMU is virtually "plugged in to the controller".)
    • Loading state, then manually reconnecting DreamLinks.
    • Connecting/disconnecting due to changing the port for a DreamLink gamepad.
      • In cases where multiple DreamLink gamepads are using the same port, I made it so the "most recently connected one wins". Any other DreamLink gamepads on the same port are marked "inactive", and manual intervention is needed in order to make a different gamepad "active" on the same port instead. "Inactive" DreamLink gamepads continue to work for game input, but aren't able to contribute expansion devices.
      • I also made sure that when a gamepad is moved away from a port, that we use the "best available DreamLink" to occupy the "newly available" DreamLink port.
    • Performing manual "high load" memory card read/writes in release mode without a debugger attached..hopefully indicating we are free of synchronization bugs.
    • Performing the above actions with DreamLinks connected to multiple ports simultaneously.

@RikkiGibson
Copy link
Contributor Author

It looks like trying to use dreamlink stuff from maple_if breaks a bunch of builds, which may not include the dreamlink bits(?).

/home/runner/work/flycast/flycast/core/hw/maple/maple_if.cpp:12:10: fatal error: sdl\dreamlink.h: No such file or directory in BSD CI / build and so on.

Very likely there is a better way of accomplishing what I was trying to do. Essentially I just wanted to poll the socket regularly to check if the server has sent a reset message.

I wasn't sure if the backslashes in the #includes were also messing things up on Linux so went ahead and normalized those to forward slashes. I don't think that was the only problem as the UWP build was also broken.

@flyinghead
Copy link
Owner

Forward slashes should be used everywhere since backslashes are only valid on windows.
Not all platforms use SDL (Android and the retroarch core don't use it for example) so you must use #ifdef USE_SDL. Also DreamLink/DreamConn is only supported on desktop platforms, not on consoles (xbox/UWP and Switch). See the last #if in maple/maple_devs.cpp.

There are other issues but I need to look at the code more closely.

@RikkiGibson
Copy link
Contributor Author

RikkiGibson commented Aug 1, 2025

Trying to make some progress with this change.

  • Minimizing changes to #defines/#ifdefs. I moved the definition of USE_DREAMCASTCONTROLLER out of dreamlink.h because I wanted to be able to use it as a prereq of including dreamlink.h in maple_if.cpp. Maybe USE_DREAMCASTCONTROLLER could also be used from the last section of maple_devs.cpp for consistency.
    • I did see some interest from @SkyeAmphi in making the DreamPotato connectivity available in libretro builds. It looks like she was working on that so will try to avoid any expansion of "which builds DreamLink is available in", in this PR.
  • Exposing functions for doing the dreamlink device refresh stuff, which maple_if.cpp can call conditionally.
  • Going to try soon to get rid of the controller hack. I was thinking the way to do this is to introduce a new "maple device type" called "Remote VMU" or some such, that user can select in the controller settings. So if you wanted the VMU in slot A1 to connect over TCP, you would just choose the "Remote VMU" device type for that slot, and flycast would try to connect. Then the dreamlink concept hopefully would be available for use independently of whether you have an actual "dreamlink controller" plugged in or not.

@SkyeAmphi
Copy link

  • Going to try soon to get rid of the controller hack. I was thinking the way to do this is to introduce a new "maple device type" called "Remote VMU" or some such, that user can select in the controller settings. So if you wanted the VMU in slot A1 to connect over TCP, you would just choose the "Remote VMU" device type for that slot, and flycast would try to connect. Then the dreamlink concept hopefully would be available for use independently of whether you have an actual "dreamlink controller" plugged in or not.

My work has been largely centered around the idea of using a network VMU, which is likely more-less what your "remote VMU" idea entails, it's still unfinished but if I can get it all working properly with dream potato it should serve that purpose. Naturally getting all of it working for libretro without dreamlink has required some workarounds and that's what I've primarily been chasing

@RikkiGibson
Copy link
Contributor Author

RikkiGibson commented Aug 2, 2025

Because dreamlinks today currently work by editing the MapleDevices array, and replacing ordinary devices with "DreamLink devices", it seemed like adding a new maple device type was not the easiest way to go here.

Instead what I have tried, is to add a checkbox per-port, to "enable network expansion devices". Each port which has this box enabled, will create a DreamLink upon game startup and try to connect via TCP.

Also, any dreamlinks which are "owned" by specially recognized DreamConn or DreamPicoPort controllers, will "override" a dreamlink created by the checkbox. And, such dreamlinks will never be destroyed based on the checkbox setting. Basically it was a goal that users of those devices, do not need to think about this setting, and the right thing will happen for them regardless of whether they checked the box or not.

I also tried to distinguish carefully "connecting/reconnecting" versus "refreshing". Refreshing is only done with dreamlinks which are already connected, and is consciously intended to be a "cheap" operation, so, it doesn't block on i.o., it only checks whether a particular message was already received in the socket buffer, telling us to refresh the expansion devices.

There are more bugs to iron out with some manual testing and TODOs and such to address. But any feedback you may have on whether the general approach here seems OK would be much appreciated.

dreamlink-setting.mp4

if (ggpo::active())
ggpo::nextFrame();
} catch (...) {
} catch (const std::exception& e) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this change because it simply made it a little easier to debug an issue from a few months back. Happy to revert if preferred.

maple_io_connected = false;

#if !defined(_WIN32)
if (isForPhysicalController()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't test the non-windows path where this is false yet. Have to get cross-platform builds of DreamPotato working first to be able to test it meaningfully.

Copy link
Contributor Author

@RikkiGibson RikkiGibson Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did test this more recently with a local Mac build of DreamPotato and it works.

#include <array>

#if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP)
#define USE_DREAMCASTCONTROLLER 1
Copy link
Contributor Author

@RikkiGibson RikkiGibson Aug 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to build.h because I wanted to use this as a general condition for whether to include DreamLink functions, import DreamLink related headers etc or not. The latter particularly required this, since the header I want to conditionally import, can't be the one that defines the condition.

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 ((dreamlink->getFunctionCode(i + 1) & MFID_1_Storage))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes bug where we would create a DreamLink device despite the DreamLink telling us it doesn't have the device, because the user settings caused an ordinary Maple device to get created for this slot beforehand.

Without this change, there was a tendency for the DreamLink to report nothing in slot 1, but for flycast to put a DreamLinkVmu in that slot anyway. Then when the game started to try and communicate with that VMU, we would get bad behavior. IIRC, the flycast main thread would block waiting for a reply in the expected form, making the game unresponsive, which would continue until the emulated VMU was "plugged in".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This certainly makes sense. That OR condition is a bit too heavy-handed, possibly a remnant of something I needed during testing.

@RikkiGibson RikkiGibson marked this pull request as ready for review August 23, 2025 19:28
@RikkiGibson
Copy link
Contributor Author

It looks like CI is blocked on an apple homebrew issue.

@RikkiGibson
Copy link
Contributor Author

@flyinghead @Tails86 could you please take a look when you are available?

@RikkiGibson
Copy link
Contributor Author

As far as how the Dreamcast host determines when a device is attached or detached, the controller (aka "main peripheral") responds with bits in its source address corresponding with what sub-peripherals are currently attached.

Thanks! This looks super helpful. I don't think I understand yet how the real Dreamcast detects expansion device changes on the fly. Is the Dreamcast sending messages regularly, even when it isn't actually performing any operations on the expansion devices, and noticing changes to those sender address bits?

@Tails86
Copy link
Contributor

Tails86 commented Aug 23, 2025

Thanks! This looks super helpful. I don't think I understand yet how the real Dreamcast detects expansion device changes on the fly. Is the Dreamcast sending messages regularly, even when it isn't actually performing any operations on the expansion devices, and noticing changes to those sender address bits?

Yes, the host (Dreamcast) always initiates each communication, so it is polling the controller on each frame for button data. Every packet has this header information in it, so the button data back from the controller also tells the Dreamcast what sub-peripherals are present on that controller.

@RikkiGibson
Copy link
Contributor Author

Had some more time to work on this today. I tried implementing the behavior to choose a new "active DreamLink" for a "vacated port", when a controller's port is changed, based on #1989 (comment). I added a bit of UI to indicate which gamepads are "active" or not.

e.g. in this scenario, both DreamLinkGamepads are on the same port and only one is "active":
image

Then if you change the port on the active controller, both become active:
image

There are still bugs to work out, but, I hope within another week or so I can have them ironed out.

@Tails86
Copy link
Contributor

Tails86 commented Sep 25, 2025

I see what you have done here, and it works for DreamPicoPort! Thanks for making those corrections. Let me know when you'd like me to give it another look-over.
image

In absence of this change I noticed strange return values from 'recv()'. For example, 'len == -1' and 'errno == 0'. In some cases 'len == -1' would come back, and on the next attempt I would get 'len == 0'. I found it comes down to a need to synchronize when using the socket. 'refreshifNeeded()' has a similar issue which I had solved in the past by ensuring the lock is held. Not sure why it didn't occur to me as necessary here also, until now.
`refreshIfNeeded` was only tearing down DreamLinkDevices for the single DreamLink identified as needing refresh. But `maple_ReconnectDevices()` applies to all the devices. If we call `maple_ReconnectDevices()` without first calling `tearDownDreamLinkDevices()` for all active DreamLinks, we get into an inconsistent state, where a subsequent call to `createDreamLinkDevices()` sees an existing device in the `dreamLinkVmus` list, and therefore doesn't update `MapleDevices`.

The solution I am going with, is to ensure that if *any* DreamLink needs devices refreshed, then `tearDownDreamLinkDevices()` is called for all the active DreamLinks, before `maple_ReconnectDevices()` is called.

As a "side benefit" of this, we can delete `dreamLinkNeedsRefresh`. In the path where we used to check that, we will always create devices for all the active DreamLinks now. There's no way to reduce work by trying to skip refreshing the ones whose configuration didn't change.

Also, `allDreamLinks` is renamed to `activeDreamLinks`, to indicate it stores only the active DreamLink for each port, and `DreamLink::refreshIfNeeded()` is now `DreamLink::needsRefresh()`, to reflect that while we do check for a changed device configuration, we don't actually change any of the MapleDevices.
@RikkiGibson
Copy link
Contributor Author

Thank you for trying it out @Tails86. I have manually tested a bunch of behaviors and tried to iron out remaining bugs as much as possible. Could you please take another look when you have some time?

@Tails86
Copy link
Contributor

Tails86 commented Sep 29, 2025

Sweet, I will take a look in the next few days today.

On a side note, I was taking a look at what kind of features LibRetro has to implement bespoke interfaces like the maple bus, and I found this:

Environment callback
While libretro has callbacks for video, audio and input, there’s a callback type dubbed the environment callback. This callback (retro_environment_t) is a generic way for the libretro implementation to access features of the API that are considered too obscure to deserve its own symbols. It can be extended without breaking ABI. The callback has a return type of bool which tells if the frontend recognized the request given to it.

https://docs.libretro.com/development/cores/developing-cores/#environment-callback

May possibly be a path towards getting these features in libretro?

Copy link
Contributor

@Tails86 Tails86 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like these changes, and they do seem to work. I like the addition of connection status within settings.

Only a couple of minor comments.

Comment on lines +169 to +182
// The active DreamLink, if any, for each port.
// Note that multiple gamepad DreamLinks may exist for a given port, in which case only one is active.
extern std::array<std::shared_ptr<DreamLink>, 4> activeDreamLinks;

// Creates and destroys DreamLinks according to config settings.
// Attempts to connect/reconnect DreamLinks which are not connected.
// Returns true if any new connection was established.
// Note that this doesn't createDreamLinkDevices for DreamLinks created by this function. Caller is responsible for that.
bool reconnectDreamLinks();

void refreshDreamLinksIfNeeded();
void createAllDreamLinkDevices();
void createDreamLinkDevices(std::shared_ptr<DreamLink> dreamlink, bool gameStart, bool stateLoaded);
void tearDownDreamLinkDevices(std::shared_ptr<DreamLink> dreamlink);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be moving into the right direction in connecting DreamLink to maple_devs. I assume these are declared here so that "dream link" functionality may be accessed from including dreamlink.h. They are then defined in maple_devs.cpp in order to access the internal data there.

What would you think about at least moving activeDreamLinks to a public, static member of DreamLink? The rest of these organizational issues could be solved down the line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the particular placement of the declarations is being done for the reasons you describe.

Moving to static members works for me. I think it's good for these to be contained "in something" rather than being in global scope. I am hoping to send a follow-up PR sometime down the line to clean that up, fill in some functionality gaps, and so on.

RikkiGibson and others added 2 commits September 29, 2025 18:23
@RikkiGibson
Copy link
Contributor Author

@flyinghead could you please take a look when you are available?

@flyinghead
Copy link
Owner

I had a quick look and it looks good to me so far. I can't really test it though so I'll have to trust you on this.
One thing I don't like is the location of the "Use Network Expansion Devices" checkbox. I would put it on the same line as the combo boxes.

I'll try to spend more time on this PR later today.

@RikkiGibson
Copy link
Contributor Author

I made a change to put the checkbox on the same line:
image

@flyinghead flyinghead merged commit 909ea49 into flyinghead:dev Oct 3, 2025
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants