Skip to content

Add network output compatible with MAME.#303

Open
tbreckle wants to merge 1 commit intotrzy:masterfrom
tbreckle:feature/net-outputs
Open

Add network output compatible with MAME.#303
tbreckle wants to merge 1 commit intotrzy:masterfrom
tbreckle:feature/net-outputs

Conversation

@tbreckle
Copy link

@tbreckle tbreckle commented Feb 15, 2026

This pull request adds a new network-based output system to the SDL build, allowing output messages to be sent over TCP and UDP for integration with tools like MAMEHooker.

This is meant as a successor of the following PR: #253
Successfully tested with Daytona 2, LA Machineguns, Lost World, SW Trilogy.
So far tests only on Linux, waiting for the build system to produce a Windows build.
Tested on Windows with Hook Of The Reaper and QMameHooker / custom scripts / Wireshark on Linux.
Updating docs will follow before leaving draft state.
Docs updated.

PR summary (Copilot):

Network Output System Integration:

  • Added new files NetOutputs.cpp and NetOutputs.h implementing the CNetOutputs class, which sends output messages over TCP and UDP in a MAMEHooker-compatible format. This includes client registration, data transmission, and UDP broadcast for discovery. [1] [2]
  • Updated Main.cpp to support selecting the "net" output system via configuration, including instantiation, initialization, and configuration of TCP/UDP ports and line endings. [1] [2]

Configuration Enhancements:

  • Added new configuration options: OutputsWithLF (line ending), OutputsTCPPort, and OutputsUDPBroadcastPort to control network output behavior.

Build System and Project File Updates:

  • Included NetOutputs.cpp in the SDL source file list for building.
  • Added NetOutputs.h to the Visual Studio project and filter files, ensuring proper inclusion in the build environment. [1] [2] [3]

@tbreckle tbreckle marked this pull request as ready for review February 16, 2026 07:41
@dukeeeey
Copy link
Collaborator

Need more time to look at this but
using namespace std in a header is a terrible idea, it'll effect every cpp that pulls in this header.

bool m_running;
This should be an atomic bool so something like std::atomic. The reason is because the other thread might just being seeing a cached copy of the bool if it lives in a register. Ie if you set the bool to false it's not guaranteed to be seen by the other thread. Ask chatgpt, it'll help you with this lol

m_tcpServerThread.join();
std::this_thread::sleep_for(std::chrono::milliseconds(16));

Why do you have the sleep there? Shouldn't need it. Also what happens if the thread has already exited? I forget, will join() bork? Maybe need to check if it's joinable first?

unsigned int m_maxClients = 10;

Add const to this!

@trzy
Copy link
Owner

trzy commented Feb 16, 2026

I’d like to take a look later this week as well before this merges.

@tbreckle tbreckle force-pushed the feature/net-outputs branch from 9bcf7cc to 702906b Compare February 16, 2026 16:22
@tbreckle
Copy link
Author

Take your time, this is not urgent, I'm playing with local builds since some weeks.

@dukeeeey
Removed namespace.
Made m_running an atomic bool (no need for ChatGPT 😄), I like the explicit form using store/load, feel free to disagree. I'll change it to the implicit form then.
Removed sleep, this was a dev leftover from some tests.
join() is fine in case the thread exited already (at least in case of Linux 👀). Nevertheless I've added joinable() to make it more clear and to potentially prevent a (how ever possible) double join() which might fail then.
Moved default values to consts at the top of the file (plus some others as well).

@dukeeeey
Copy link
Collaborator

Last comments :) Looking good

const unsigned int NET_OUTPUTS_DEFAULT_TCP_PORT = 8000;
const unsigned int NET_OUTPUTS_DEFAULT_UDP_BROADCAST_PORT = 8001;
const std::string NET_OUTPUTS_DEFAULT_FRAME_ENDING = std::string("\r");
const std::string NET_OUTPUTS_DEFAULT_SEPARATOR_ID_AND_VALUE = std::string(" = ");

I'd put those inside a class? Class is also a namespace rather than global

Why are these lower case to start? :)

void setFrameEnding(const std::string& ending) { m_frameEnding = ending; }
void setTcpPort(unsigned int port) { m_tcpPort = port; }
void setUdpBroadcastPort(unsigned int port) { m_udpBroadcastPort = port; }

@tbreckle tbreckle force-pushed the feature/net-outputs branch from 702906b to 8968f3d Compare February 18, 2026 20:21
@tbreckle
Copy link
Author

tbreckle commented Feb 18, 2026

const unsigned int NET_OUTPUTS_DEFAULT_TCP_PORT = 8000; const unsigned int NET_OUTPUTS_DEFAULT_UDP_BROADCAST_PORT = 8001; const std::string NET_OUTPUTS_DEFAULT_FRAME_ENDING = std::string("\r"); const std::string NET_OUTPUTS_DEFAULT_SEPARATOR_ID_AND_VALUE = std::string(" = ");

I'd put those inside a class? Class is also a namespace rather than global

This I didn't get fully, you meant something like this?

class CNetOutputs : public COutputs
{
private:
    static constexpr unsigned int DEFAULT_TCP_PORT = 8000;

Why are these lower case to start? :)

Company coding guide 😆 fixed it to uppercase.

@dukeeeey
Copy link
Collaborator

Code looks good

Copy link
Owner

@trzy trzy left a comment

Choose a reason for hiding this comment

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

Looks good but I have one request, if possible: I have never used an application that listens for these outputs. Would it be possible to write a Python script that listens for them and prints them out? We have a scripts directory. This would be great for testing.

Ideally, the Python script wouldn't depend on any external packages for now. I assume you would just vibe code that but I have Python code for UDP listeners and TCP servers and clients if you want me to point you to some references.

@tbreckle tbreckle force-pushed the feature/net-outputs branch from 8968f3d to dc7fb80 Compare February 24, 2026 20:13
@tbreckle
Copy link
Author

Looks good but I have one request, if possible: I have never used an application that listens for these outputs. Would it be possible to write a Python script that listens for them and prints them out? We have a scripts directory. This would be great for testing.

Ideally, the Python script wouldn't depend on any external packages for now. I assume you would just vibe code that but I have Python code for UDP listeners and TCP servers and clients if you want me to point you to some references.

I've had a test script from development, pimped it a bit and stored it in the Scripts folder. The script waits for UDP broadcast and continuously tries to connect to the TCP interface. Messages are logged with their content to the console.
Sample output:

$ python netoutput_tester.py 
  ####                                                      ###           ###
 ##  ##                                                      ##            ##
 ###     ##  ##  ## ###   ####   ## ###  ##  ##   ####       ##   ####     ##
  ###    ##  ##   ##  ## ##  ##   ### ## ####### ##  ##   #####  ##  ##    ##
    ###  ##  ##   ##  ## ######   ##  ## ####### ##  ##  ##  ##  ######    ##
 ##  ##  ##  ##   #####  ##       ##     ## # ## ##  ##  ##  ##  ##        ##
  ####    ### ##  ##      ####   ####    ##   ##  ####    ### ##  ####    ####
                 ####

Network Output Tester v1.0.0

2026-02-24 09:35:44,134 - __main__ - INFO - UDP receiver started on port 8001.
2026-02-24 09:35:44,134 - __main__ - INFO - UDP socket bound to port 8001.
2026-02-24 09:35:44,134 - __main__ - INFO - Attempting to connect to localhost:8000.
2026-02-24 09:35:44,134 - __main__ - INFO - TCP connector started, will connect to localhost:8000.
2026-02-24 09:35:44,134 - __main__ - INFO - Network output tester is running. Press Ctrl+C to stop.
2026-02-24 09:35:53,059 - __main__ - INFO - Connected to localhost:8000.
2026-02-24 09:35:53,064 - __main__ - INFO - MAME START command received from UDP with value: lostwsga
2026-02-24 09:35:53,064 - __main__ - INFO - TCP command received from UDP with value: 8000
2026-02-24 09:35:53,073 - __main__ - INFO - MAME START command received from TCP with value: lostwsga
2026-02-24 09:35:53,134 - __main__ - INFO - Dynamic key 'RawDrive' received from TCP with value: 0
2026-02-24 09:35:53,134 - __main__ - INFO - Dynamic key 'LampStart' received from TCP with value: 0
2026-02-24 09:35:53,134 - __main__ - INFO - Dynamic key 'LampView1' received from TCP with value: 0
2026-02-24 09:35:53,134 - __main__ - INFO - Dynamic key 'LampView2' received from TCP with value: 0
2026-02-24 09:35:53,134 - __main__ - INFO - Dynamic key 'LampView3' received from TCP with value: 0
2026-02-24 09:35:53,134 - __main__ - INFO - Dynamic key 'LampView4' received from TCP with value: 0
2026-02-24 09:35:53,134 - __main__ - INFO - Dynamic key 'LampLeader' received from TCP with value: 0
2026-02-24 09:35:53,134 - __main__ - INFO - Dynamic key 'RawLamps' received from TCP with value: 0
----8<----8<----8<----8<----
2026-02-24 09:36:00,585 - __main__ - INFO - Dynamic key 'LampStart' received from TCP with value: 1
2026-02-24 09:36:00,585 - __main__ - INFO - Dynamic key 'LampView1' received from TCP with value: 1
2026-02-24 09:36:00,585 - __main__ - INFO - Dynamic key 'RawLamps' received from TCP with value: 12
2026-02-24 09:36:01,117 - __main__ - INFO - Dynamic key 'LampStart' received from TCP with value: 0
2026-02-24 09:36:01,117 - __main__ - INFO - Dynamic key 'LampView1' received from TCP with value: 0
2026-02-24 09:36:01,117 - __main__ - INFO - Dynamic key 'RawLamps' received from TCP with value: 0
2026-02-24 09:36:01,651 - __main__ - INFO - Dynamic key 'LampStart' received from TCP with value: 1
2026-02-24 09:36:01,651 - __main__ - INFO - Dynamic key 'LampView1' received from TCP with value: 1
2026-02-24 09:36:01,651 - __main__ - INFO - Dynamic key 'RawLamps' received from TCP with value: 12
----8<----8<----8<----8<----
2026-02-24 09:36:19,763 - __main__ - INFO - MAME STOP command received from TCP with value: 1
2026-02-24 09:36:19,763 - __main__ - INFO - TCP connection closed by server.
^C2026-02-24 09:36:21,727 - __main__ - INFO - 
Shutting down...
2026-02-24 09:36:21,794 - __main__ - INFO - UDP receiver stopped.
2026-02-24 09:36:21,866 - __main__ - INFO - TCP connector stopped.
2026-02-24 09:36:21,866 - __main__ - INFO - Shutdown complete.

@tbreckle tbreckle force-pushed the feature/net-outputs branch from dc7fb80 to 4cbe3bd Compare March 2, 2026 08:22
@trzy
Copy link
Owner

trzy commented Mar 4, 2026

This doesn't compile for me:

In file included from Src/OSD/SDL/Main.cpp:81:
Src/OSD/SDL/NetOutputs.h:56:2: error: unknown type name 'TCPsocket'
   56 |         TCPsocket ClientSocket;
      |         ^
Src/OSD/SDL/NetOutputs.h:109:2: error: unknown type name 'TCPsocket'
  109 |         TCPsocket m_serverSocket;
      |         ^
Src/OSD/SDL/NetOutputs.h:110:5: error: unknown type name 'SDLNet_SocketSet'
  110 |     SDLNet_SocketSet m_tcpSocketSet;
      |     ^
Src/OSD/SDL/NetOutputs.h:147:22: error: unknown type name 'TCPsocket'
  147 |         bool RegisterClient(TCPsocket socket);
      |                             ^
Src/OSD/SDL/NetOutputs.h:162:24: error: unknown type name 'TCPsocket'
  162 |         bool UnregisterClient(TCPsocket socket);
      |                               ^
5 errors generated.

@trzy
Copy link
Owner

trzy commented Mar 4, 2026

Wait... this requires NET_BOARD=1 to pull in SDL_net. Do we just get rid of NET_BOARD altogether and always compile the net code? Can't see why not.

@trzy
Copy link
Owner

trzy commented Mar 4, 2026

Another change required:

-outputs is currently disabled in Main.cpp because it is only compiled on Windows. Now that we have net outputs, which work on macOS and Linux, this should be changed to always be available.

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.

3 participants