Skip to content

Add network output compatible with MAME.#303

Open
tbreckle wants to merge 2 commits intotrzy:masterfrom
tbreckle:feature/net-outputs
Open

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

Conversation

@tbreckle
Copy link
Copy Markdown

@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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

Code looks good

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

@tbreckle
Copy link
Copy Markdown
Author

tbreckle commented Mar 26, 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.

Should I remove NET_BOARD completely with this PR or change this PR so SDL_net is included and start another PR for removing NET_BOARD? Removing it this this PR might make it huge, but fine for me. Just want to avoid that this PR is rejected due to it's size 😆

Edit: Hm, while browsing the code I've noticed also occurrences of NET_BOARD in regards to SDL_net in Makefile and .inc files. So maybe it's best to really remove it with this PR. Or another PR upfront to this one. Opinions @trzy @dukeeeey ?

@trzy
Copy link
Copy Markdown
Owner

trzy commented Mar 26, 2026 via email

cpu->AddRegion(0x90000000, 0x9000000B, false, false, "Real3D VROM Texture Port");
cpu->AddRegion(0x94000000, 0x940FFFFF, false, false, "Real3D Texture FIFO");
cpu->AddRegion(0x98000000, 0x980FFFFF, false, false, "Real3D Polygon RAM");
#ifndef NET_BOARD
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@trzy
How to properly handle this case? Is Step 1.x meaning 1.0 or really 1.0 and 1.5? If so there's an overlap of the net board shared RAM of the line below.
I tried to figure it out on my own but am stuck.
The model3.cpp states it slightly different but also with an overlap:

 * 98000000-980FFFFF  Real3D Polygon RAM
 * C0000000-C000FFFF  Netboard Shared RAM (Step 1.5+)
 * C0010000-C00101FF  Netboard Registers (Step 1.5+)
 * C0020000-C002FFFF  Netboard Program RAM (Step 1.5+)
 * C0000000-C00000FF  SCSI (Step 1.x)
 * C1000000-C10000FF  SCSI (Step 1.x) (Lost World expects it here)
 * C2000000-C20000FF  Real3D DMA (Step 2.x)

Do we require per game / hw stepping / hw feature conditions here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Does something like below make sense (needs some more tweaking for lostwsga but shows the idea)?

if (!model3->GetGame().netboard_present)
{
	// Netboard not present, so SCSI is at 0xC0000000.
	cpu->AddRegion(0xC0000000, 0xC00000FF, false, false, "SCSI (Step 1.x)");
}
else
{
	// Netboard present, so SCSI is at 0xF9000000 and 0xC0000000+ is used for netboard RAM.
	cpu->AddRegion(0xC0000000, 0xC000FFFF, false, false, "Netboard Shared RAM (Step 1.5+)");
	cpu->AddRegion(0xC0020000, 0xC002FFFF, false, false, "Netboard Program RAM (Step 1.5+)");
}

if (model3->GetGame().name == "lostwsga" || model3->GetGame().name == "lostwsgo")
{
	cpu->AddRegion(0xC1000000, 0xC10000FF, false, false, "SCSI (Step 1.x) (Lost World expects it here)");
}

@trzy
Copy link
Copy Markdown
Owner

trzy commented Mar 27, 2026 via email

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