summaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
author Vas Crabb <vas@vastheman.com>2026-05-15 03:53:21 +1000
committer Vas Crabb <vas@vastheman.com>2026-05-15 03:56:02 +1000
commite6eefd2f633bd223b1f73c6ff3568c0e2712347d (patch)
tree621063bf6c14b35fcd4454d0b798019610b2bb08 /src
parentfc622d98e8a4b61092cdd5cd0852e8217de9798d (diff)
-taito/gunbystr_link.cpp: Added Taito Gunbuster link simulation. [Vas Crabb, Alex Marshall]
Diffstat (limited to 'src')
-rw-r--r--src/mame/capcom/cps2comm.cpp1
-rw-r--r--src/mame/capcom/cps2comm.h11
-rw-r--r--src/mame/taito/gunbustr.cpp73
-rw-r--r--src/mame/taito/gunbustr_link.cpp663
-rw-r--r--src/mame/taito/gunbustr_link.h48
5 files changed, 749 insertions, 47 deletions
diff --git a/src/mame/capcom/cps2comm.cpp b/src/mame/capcom/cps2comm.cpp
index 2dac7035b1c..7882d39a73d 100644
--- a/src/mame/capcom/cps2comm.cpp
+++ b/src/mame/capcom/cps2comm.cpp
@@ -92,6 +92,7 @@
#include <locale>
#include <optional>
#include <sstream>
+#include <system_error>
#include <thread>
#include <utility>
diff --git a/src/mame/capcom/cps2comm.h b/src/mame/capcom/cps2comm.h
index f2adf378704..14076d8b6d7 100644
--- a/src/mame/capcom/cps2comm.h
+++ b/src/mame/capcom/cps2comm.h
@@ -22,6 +22,12 @@ public:
bool comm_enabled() const { return bool(m_context); }
protected:
+ virtual ioport_constructor device_input_ports() const override ATTR_COLD;
+ virtual void device_start() override ATTR_COLD;
+ virtual void device_stop() override ATTR_COLD;
+ virtual void device_reset() override ATTR_COLD;
+
+private:
enum
{
CTRL_MODE,
@@ -32,11 +38,6 @@ protected:
class context;
- virtual ioport_constructor device_input_ports() const override ATTR_COLD;
- virtual void device_start() override ATTR_COLD;
- virtual void device_stop() override ATTR_COLD;
- virtual void device_reset() override ATTR_COLD;
-
void start_comm() ATTR_COLD;
std::unique_ptr<context> m_context;
diff --git a/src/mame/taito/gunbustr.cpp b/src/mame/taito/gunbustr.cpp
index d07fdc2beae..e2d9303cd2a 100644
--- a/src/mame/taito/gunbustr.cpp
+++ b/src/mame/taito/gunbustr.cpp
@@ -46,6 +46,7 @@
#include "emu.h"
+#include "gunbustr_link.h"
#include "taito_en.h"
#include "taitoio.h"
#include "tc0480scp.h"
@@ -91,13 +92,10 @@ public:
m_hit_lamp(*this, "Hit_lamp")
{
- m_coin_lockout = true;
}
- void gunbustr(machine_config &config);
-
- void init_gunbustrj();
- void init_gunbustr();
+ void gunbustr(machine_config &config) ATTR_COLD;
+ void gunbustrj(machine_config &config) ATTR_COLD;
protected:
virtual void video_start() override ATTR_COLD;
@@ -106,8 +104,8 @@ private:
void prg_map(address_map &map) ATTR_COLD;
void motor_control_w(u32 data);
- u32 main_cycle_r();
void coin_word_w(u8 data);
+ void coin_word_lockout_w(u8 data);
u32 screen_update(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect);
INTERRUPT_GEN_MEMBER(gunbustr_interrupt);
void draw_sprites(screen_device &screen, bitmap_ind16 &bitmap, const rectangle &cliprect, const u32 *primasks, int x_offs, int y_offs);
@@ -136,7 +134,6 @@ private:
int primask = 0;
};
- bool m_coin_lockout;
std::unique_ptr<gb_tempsprite[]> m_spritelist{};
};
@@ -384,15 +381,17 @@ INTERRUPT_GEN_MEMBER(gunbustr_state::gunbustr_interrupt)
void gunbustr_state::coin_word_w(u8 data)
{
- if (m_coin_lockout)
- {
- machine().bookkeeping().coin_lockout_w(0, ~data & 0x01);
- machine().bookkeeping().coin_lockout_w(1, ~data & 0x02);
- }
-
// game does not write a separate counter for coin 2! maybe in linked mode?
- machine().bookkeeping().coin_counter_w(0, data & 0x04);
- machine().bookkeeping().coin_counter_w(1, data & 0x04);
+ machine().bookkeeping().coin_counter_w(0, BIT(data, 2));
+ machine().bookkeeping().coin_counter_w(1, BIT(data, 2));
+}
+
+void gunbustr_state::coin_word_lockout_w(u8 data)
+{
+ coin_word_w(data);
+
+ machine().bookkeeping().coin_lockout_w(0, BIT(~data, 0));
+ machine().bookkeeping().coin_lockout_w(1, BIT(~data, 1));
}
void gunbustr_state::motor_control_w(u32 data)
@@ -411,17 +410,18 @@ void gunbustr_state::motor_control_w(u32 data)
void gunbustr_state::prg_map(address_map &map)
{
+ map.unmap_value_high();
map(0x000000, 0x0fffff).rom();
map(0x200000, 0x21ffff).ram().share(m_ram); // main CPUA RAM
map(0x300000, 0x301fff).ram().share(m_spriteram);
map(0x380000, 0x380003).w(FUNC(gunbustr_state::motor_control_w)); // motor, lamps etc.
map(0x390000, 0x3907ff).rw("taito_en:dpram", FUNC(mb8421_device::left_r), FUNC(mb8421_device::left_w)); // Sound shared RAM
map(0x400000, 0x400007).rw("tc0510nio", FUNC(tc0510nio_device::read), FUNC(tc0510nio_device::write));
- map(0x500000, 0x500007).rw("adc", FUNC(adc0809_device::data_r), FUNC(adc0809_device::address_offset_start_w)).umask32(0xffffffff);
+ map(0x500000, 0x500007).rw("adc", FUNC(adc0809_device::data_r), FUNC(adc0809_device::address_offset_start_w));
map(0x800000, 0x80ffff).rw(m_tc0480scp, FUNC(tc0480scp_device::ram_r), FUNC(tc0480scp_device::ram_w));
map(0x830000, 0x83002f).rw(m_tc0480scp, FUNC(tc0480scp_device::ctrl_r), FUNC(tc0480scp_device::ctrl_w));
map(0x900000, 0x901fff).ram().w(m_palette, FUNC(palette_device::write32)).share("palette");
- map(0xc00000, 0xc03fff).ram(); // network RAM ?
+ map(0xc00000, 0xc007ff).mirror(0x003800).m("link", FUNC(gunbustr_link_device::map)); // shared RAM for link - is this actually mirrored or is 0xc00800-0xc03fff unmapped?
}
/***********************************************************
@@ -525,6 +525,7 @@ void gunbustr_state::gunbustr(machine_config &config)
tc0510nio.write_3_callback().set("eeprom", FUNC(eeprom_serial_93cxx_device::clk_write)).bit(5);
tc0510nio.write_3_callback().append("eeprom", FUNC(eeprom_serial_93cxx_device::di_write)).bit(6);
tc0510nio.write_3_callback().append("eeprom", FUNC(eeprom_serial_93cxx_device::cs_write)).bit(4);
+ tc0510nio.write_4_callback().set(FUNC(gunbustr_state::coin_word_lockout_w));
tc0510nio.write_4_callback().set(FUNC(gunbustr_state::coin_word_w));
tc0510nio.read_7_callback().set_ioport("SYSTEM");
@@ -541,6 +542,8 @@ void gunbustr_state::gunbustr(machine_config &config)
adc.in_callback<6>().set_constant(0xff);
adc.in_callback<7>().set_constant(0xff);
+ GUNBUSTR_LINK(config, "link", 0);
+
// video hardware
screen_device &screen(SCREEN(config, "screen", SCREEN_TYPE_RASTER));
screen.set_refresh_hz(60);
@@ -567,6 +570,14 @@ void gunbustr_state::gunbustr(machine_config &config)
taito_en.add_route(1, "speaker", 1.0, 1);
}
+void gunbustr_state::gunbustrj(machine_config &config)
+{
+ gunbustr(config);
+
+ // no coin lockout, perhaps this was a prototype version without proper coin handling?
+ subdevice<tc0510nio_device>("tc0510nio")->write_4_callback().set(FUNC(gunbustr_state::coin_word_w));
+}
+
/***************************************************************************/
ROM_START( gunbustr )
@@ -671,31 +682,9 @@ ROM_START( gunbustrj )
ROM_LOAD16_WORD( "eeprom-gunbustr.bin", 0x0000, 0x0080, CRC(ef3685a1) SHA1(899b4b6dd2fd78be3a2ce00a2ef1840de9f122c3) )
ROM_END
-u32 gunbustr_state::main_cycle_r()
-{
- if (m_maincpu->pc() == 0x55a && (m_ram[0x3acc / 4] & 0xff000000) == 0)
- m_maincpu->spin_until_interrupt();
-
- return m_ram[0x3acc/4];
-}
-
-void gunbustr_state::init_gunbustr()
-{
- // Speedup handler
- m_maincpu->space(AS_PROGRAM).install_read_handler(0x203acc, 0x203acf, read32smo_delegate(*this, FUNC(gunbustr_state::main_cycle_r)));
-}
-
-void gunbustr_state::init_gunbustrj()
-{
- init_gunbustr();
-
- // no coin lockout, perhaps this was a prototype version without proper coin handling?
- m_coin_lockout = false;
-}
-
} // anonymous namespace
-GAME( 1992, gunbustr, 0, gunbustr, gunbustr, gunbustr_state, init_gunbustr, ORIENTATION_FLIP_X, "Taito Corporation Japan", "Gunbuster (World)", MACHINE_NODEVICE_LAN | MACHINE_SUPPORTS_SAVE )
-GAME( 1992, gunbustru, gunbustr, gunbustr, gunbustr, gunbustr_state, init_gunbustr, ORIENTATION_FLIP_X, "Taito America Corporation", "Gunbuster (US)", MACHINE_NODEVICE_LAN | MACHINE_SUPPORTS_SAVE )
-GAME( 1992, gunbustrj, gunbustr, gunbustr, gunbustr, gunbustr_state, init_gunbustrj,ORIENTATION_FLIP_X, "Taito Corporation", "Gunbuster (Japan)", MACHINE_NODEVICE_LAN | MACHINE_SUPPORTS_SAVE )
+GAME( 1992, gunbustr, 0, gunbustr, gunbustr, gunbustr_state, empty_init, ORIENTATION_FLIP_X, "Taito Corporation Japan", "Gunbuster (World)", MACHINE_SUPPORTS_SAVE )
+GAME( 1992, gunbustru, gunbustr, gunbustr, gunbustr, gunbustr_state, empty_init, ORIENTATION_FLIP_X, "Taito America Corporation", "Gunbuster (US)", MACHINE_SUPPORTS_SAVE )
+GAME( 1992, gunbustrj, gunbustr, gunbustrj, gunbustr, gunbustr_state, empty_init, ORIENTATION_FLIP_X, "Taito Corporation", "Gunbuster (Japan)", MACHINE_SUPPORTS_SAVE )
diff --git a/src/mame/taito/gunbustr_link.cpp b/src/mame/taito/gunbustr_link.cpp
new file mode 100644
index 00000000000..63f6a28846b
--- /dev/null
+++ b/src/mame/taito/gunbustr_link.cpp
@@ -0,0 +1,663 @@
+// license:BSD-3-Clause
+// copyright-holders:Vas Crabb
+/***********************************************************************
+
+ High-level simulation of Taito Gunbuster communication.
+
+ (Thanks to Alex "trap15" Marshall for investigating the protocol and
+ writing a proof of concept implementation.)
+
+ To use this:
+ * Enable Link Simulation in MAME's Machine Configuration menu.
+ * Access the game's Tet Mode Menu, and select Configuration.
+ * Set Link Play to On, and set the ID Number to 0 for one instance
+ and 1 for the other instance (it helps to have separate NVRAM
+ directories for the two instances).
+ * Start one instance in listening mode by setting the comm_localhost
+ to the listening address and comm_remotehost to an empty string.
+ * Start the other instance in connecting mode by setting
+ comm_remotehost to the address to connect to (you should probably
+ set comm_localhost to an empty string, but if you set it, the
+ connecting socket will be bound to this address).
+ * Reset the two instances at approximately the same time, it may
+ help to reset the instance with ID 0 slightly before resetting the
+ instance with ID 1.
+
+ mame gunbustr -comm_localhost 0.0.0.0 -comm_localport 1234 -comm_remotehost ""
+ mame gunbustr -comm_localhost "" -comm_remotehost 127.0.0.1 -comm_remoteport 1234
+
+ Note that this implementation only works for the protocol used by
+ the Gunbuster game.
+
+ Communication uses a pair of MB8421 dual-port 2K*8 static RAMs, one
+ on each game board. The RAMs are connected to the most significant
+ byte of the data bus (D24-D31), corresponding to the least
+ significant byte (the CPU uses the big Endian convention). The
+ system accesses the "right" port of its own RAM and the "left" port
+ of the remote RAM.
+
+ The link between boards is a simple shared address/data bus,
+ carrying 11 address lines and 8 data lines, as well as CS, RW and
+ BUSY signals. A GAL16V8 is used to generate the control signals and
+ arbitrate the shared address/data bus.
+
+ The protocol used by the game treats the first 512 bytes of RAM as a
+ pair of 256-byte buffers. The first buffer is used by the system
+ with ID 0 to send data to the system with ID 1; the second buffer is
+ used by the system with ID 1 to send data to the system with ID 0.
+ The remainder of the RAM is cleared but never used.
+
+ The last four bytes of each buffer are used to coordinate
+ communication. The system will write 0xff0x to the last two bytes
+ of its buffer, where x is the node ID (0 or 1). This is used to
+ detect the presence of the remote system. "COMMUNICATION ERROR" is
+ displayed if the remote system is not detected. The byte at offset
+ 0x00fd is used for handshaking. 0x01 indicates that the system is
+ reading data received from the remote system; 0x02 indicates that
+ the system is writing data.
+
+ The two systems synchronise each frame. A frame counter is stored
+ in the long word at offset 0x00f8 in each buffer. If the value
+ doesn't increase for sixteen consecutive frames, "COMMUNICATION CUT"
+ is displayed.
+
+ ***********************************************************************/
+
+#include "emu.h"
+#include "gunbustr_link.h"
+
+#include "emuopts.h"
+
+#include "asio.h"
+
+#include "util/multibyte.h"
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include <locale>
+#include <mutex>
+#include <optional>
+#include <sstream>
+#include <system_error>
+#include <thread>
+#include <utility>
+
+//#define VERBOSE 1
+
+#include "logmacro.h"
+
+
+namespace {
+
+constexpr unsigned BUFFER_BYTES = 256;
+
+constexpr unsigned OFFSET_BUFFER = 0;
+constexpr unsigned OFFSET_BEGIN = 2;
+constexpr unsigned OFFSET_LENGTH = 4;
+constexpr unsigned OFFSET_DATA = 6;
+
+constexpr unsigned HEADER_LENGTH = 6;
+
+
+INPUT_PORTS_START(gunbustr_link)
+ PORT_START("CFG")
+ PORT_CONFNAME(0x01, 0x00, "Link Simulation")
+ PORT_CONFSETTING( 0x00, "Disable")
+ PORT_CONFSETTING( 0x01, "Enable")
+INPUT_PORTS_END
+
+} // anonymous namespace
+
+
+
+class gunbustr_link_device::context
+{
+public:
+ context(
+ gunbustr_link_device &device,
+ std::optional<asio::ip::tcp::endpoint> const &local,
+ std::optional<asio::ip::tcp::endpoint> const &remote)
+ : m_device(device)
+ , m_acceptor(m_ioctx)
+ , m_sock(m_ioctx)
+ , m_connect_timeout(m_ioctx)
+ , m_local(local)
+ , m_remote(remote)
+ , m_stopping(false)
+ , m_connected(false)
+ , m_sending(false)
+ , m_pending_begin{ BUFFER_BYTES, BUFFER_BYTES }
+ , m_pending_end{ 0, 0 }
+ , m_last_sent(0)
+ , m_received_bytes(0)
+ {
+ }
+
+ std::error_code start()
+ {
+ std::error_code err;
+
+ if (m_remote)
+ {
+ if (m_local)
+ {
+ m_sock.open(m_local->protocol(), err);
+ if (!err)
+ m_sock.bind(*m_local, err);
+ }
+ else
+ {
+ m_sock.open(m_remote->protocol(), err);
+ }
+ }
+ else
+ {
+ assert(m_local);
+ m_acceptor.open(m_local->protocol(), err);
+ if (!err)
+ m_acceptor.bind(*m_local, err);
+ if (!err)
+ m_acceptor.listen(1, err);
+ }
+ if (err)
+ return err;
+
+ m_thread = std::thread(
+ [this] ()
+ {
+ if (m_remote)
+ start_connect();
+ else
+ start_accept();
+ m_ioctx.run();
+ LOG("Network thread completed\n");
+ });
+
+ return err;
+ }
+
+ void stop()
+ {
+ asio::post(
+ m_ioctx,
+ [this] ()
+ {
+ m_stopping = true;
+ std::error_code err;
+ if (m_acceptor.is_open())
+ m_acceptor.close(err);
+ if (m_sock.is_open())
+ m_sock.close(err);
+ m_connect_timeout.cancel();
+ });
+ m_thread.join();
+ }
+
+ void update_buffer(unsigned buffer)
+ {
+ std::lock_guard<std::mutex> lock(m_mutex);
+ std::copy_n(
+ &m_buffer_copy[buffer][0],
+ BUFFER_BYTES,
+ &m_device.m_shared_ram[buffer * BUFFER_BYTES]);
+ m_device.m_pending[buffer].store(false, std::memory_order_release);
+ }
+
+ void send_buffer(unsigned buffer, unsigned begin, unsigned end)
+ {
+ assert(begin < end);
+ assert(begin < BUFFER_BYTES);
+ assert(end <= BUFFER_BYTES);
+ {
+ std::lock_guard<std::mutex> lock(m_mutex);
+ m_pending_begin[buffer] = std::min(m_pending_begin[buffer], begin);
+ m_pending_end[buffer] = std::max(m_pending_end[buffer], end);
+ std::copy_n(
+ &m_device.m_shared_ram[buffer * BUFFER_BYTES],
+ BUFFER_BYTES,
+ &m_buffer_copy[buffer][0]);
+ }
+ asio::post(m_ioctx, [this] () { send_pending(); });
+ }
+
+private:
+ template <typename Format, typename... Params>
+ void logerror(Format &&fmt, Params &&... args) const
+ {
+ util::stream_format(
+ std::cerr,
+ "[%s] %s",
+ m_device.tag(),
+ util::string_format(std::forward<Format>(fmt), std::forward<Params>(args)...));
+ }
+
+ void start_connect()
+ {
+ LOG("Initiating connection to %s\n", *m_remote);
+ m_connect_timeout.expires_after(std::chrono::seconds(10));
+ m_connect_timeout.async_wait(
+ [this] (std::error_code const &err)
+ {
+ if (!err && !m_stopping && !m_connected && m_sock.is_open())
+ {
+ LOG("Connection timed out\n");
+ schedule_reconnect();
+ }
+ });
+ m_sock.async_connect(
+ *m_remote,
+ [this] (std::error_code const &err)
+ {
+ m_connect_timeout.cancel();
+ if (err)
+ {
+ LOG("Connection error: %s\n", err.message());
+ if (!m_stopping)
+ schedule_reconnect();
+ }
+ else
+ {
+ LOG("Connection established\n");
+ m_connected = true;
+ start_receive();
+ }
+ });
+ }
+
+ void start_accept()
+ {
+ LOG("Accepting connection on %s\n", *m_local);
+ m_acceptor.async_accept(
+ [this] (std::error_code const &err, asio::ip::tcp::socket sock)
+ {
+ if (err)
+ {
+ LOG("Error accepting connection: %s\n", err.message());
+ if (!m_stopping)
+ start_accept();
+ }
+ else
+ {
+ LOG("Accepted connection from %s\n", sock.remote_endpoint());
+ m_sock = std::move(sock);
+ m_connected = true;
+ start_receive();
+ }
+ });
+ }
+
+ void schedule_reconnect()
+ {
+ LOG("Scheduling reconnection\n");
+ std::error_code err;
+ if (m_sock.is_open())
+ m_sock.close(err);
+ m_connect_timeout.cancel();
+ m_connect_timeout.expires_after(std::chrono::seconds(10));
+ m_connect_timeout.async_wait(
+ [this] (std::error_code const &err)
+ {
+ if (!err && !m_stopping)
+ {
+ std::error_code e;
+ if (m_local)
+ {
+ m_sock.open(m_local->protocol(), e);
+ if (!err)
+ m_sock.bind(*m_local, e);
+ }
+ else
+ {
+ m_sock.open(m_remote->protocol(), e);
+ }
+ if (e)
+ {
+ LOG("Error opening socket: %s\n", e.message());
+ schedule_reconnect();
+ }
+ else
+ {
+ start_connect();
+ }
+ }
+ });
+ }
+
+ void start_receive()
+ {
+ m_sock.async_read_some(
+ asio::buffer(&m_receive_buf[m_received_bytes], std::size(m_receive_buf) - m_received_bytes),
+ [this] (std::error_code const &err, std::size_t length)
+ {
+ if (err || !length)
+ {
+ if (err)
+ LOG("Error receiving: %s\n", err.message());
+ else
+ LOG("Connection lost\n");
+ receive_failed();
+ return;
+
+ }
+
+ m_received_bytes += length;
+ while (m_received_bytes >= HEADER_LENGTH)
+ {
+ unsigned const buffer = get_u16be(&m_receive_buf[OFFSET_BUFFER]);
+ unsigned const begin = get_u16be(&m_receive_buf[OFFSET_BEGIN]);
+ unsigned const bytes = get_u16be(&m_receive_buf[OFFSET_LENGTH]);
+ if ((buffer > 1) || (begin >= BUFFER_BYTES) || ((begin + bytes) > BUFFER_BYTES))
+ {
+ LOG("Received invalid header: buffer %u offset %u length %u\n", buffer, begin, bytes);
+ receive_failed();
+ return;
+ }
+
+ if (m_received_bytes < (bytes + HEADER_LENGTH))
+ {
+ start_receive();
+ return;
+ }
+
+ LOG("Received %u bytes of buffer %u from offset %u\n", bytes, buffer, begin);
+ {
+ std::lock_guard<std::mutex> lock(m_mutex);
+ std::copy_n(
+ &m_receive_buf[HEADER_LENGTH],
+ bytes,
+ &m_buffer_copy[buffer][begin]);
+ m_device.m_pending[buffer].store(true, std::memory_order_release);
+ }
+
+ m_received_bytes -= bytes + HEADER_LENGTH;
+ std::copy_n(
+ &m_receive_buf[0],
+ m_received_bytes,
+ &m_receive_buf[bytes + HEADER_LENGTH]);
+ }
+ start_receive();
+ });
+ }
+
+ void receive_failed()
+ {
+ m_connected = false;
+ m_received_bytes = 0;
+ if (!m_stopping)
+ {
+ if (m_remote)
+ {
+ schedule_reconnect();
+ }
+ else
+ {
+ std::error_code e;
+ if (m_sock.is_open())
+ m_sock.close(e);
+ start_accept();
+ }
+ }
+ }
+
+ void send_pending()
+ {
+ if (!m_connected || m_sending)
+ return;
+
+ unsigned bytes;
+ {
+ std::lock_guard<std::mutex> lock(m_mutex);
+ bool const pending[2] = {
+ m_pending_end[0] > m_pending_begin[0],
+ m_pending_end[1] > m_pending_begin[1] };
+ if (!pending[0] && !pending[1])
+ return;
+
+ unsigned const buffer = ((0 == m_last_sent) && pending[1]) ? 1 : pending[0] ? 0 : 1;
+ bytes = m_pending_end[buffer] - m_pending_begin[buffer];
+ put_u16be(&m_transmit_buf[OFFSET_BUFFER], buffer);
+ put_u16be(&m_transmit_buf[OFFSET_BEGIN], m_pending_begin[buffer]);
+ put_u16be(&m_transmit_buf[OFFSET_LENGTH], bytes);
+ std::copy_n(
+ &m_buffer_copy[buffer][m_pending_begin[buffer]],
+ bytes,
+ &m_transmit_buf[OFFSET_DATA]);
+ LOG("Sending %u bytes of buffer %u from offset %u\n", bytes, buffer, m_pending_begin[buffer]);
+
+ m_pending_begin[buffer] = BUFFER_BYTES;
+ m_pending_end[buffer] = 0;
+ }
+
+ m_sending = true;
+ asio::async_write(
+ m_sock,
+ asio::buffer(m_transmit_buf, bytes + HEADER_LENGTH),
+ [this] (std::error_code err, std::size_t length)
+ {
+ m_sending = false;
+ if (err)
+ {
+ LOG("Error sending: %s\n", err.message());
+ if (m_connected)
+ m_sock.close(); // will cause a receive error, triggering reconnection
+ }
+ else
+ {
+ send_pending();
+ }
+ });
+ }
+
+ gunbustr_link_device &m_device;
+ std::thread m_thread;
+ std::mutex m_mutex;
+ asio::io_context m_ioctx;
+ asio::ip::tcp::acceptor m_acceptor;
+ asio::ip::tcp::socket m_sock;
+ asio::steady_timer m_connect_timeout;
+ std::optional<asio::ip::tcp::endpoint> const m_local;
+ std::optional<asio::ip::tcp::endpoint> const m_remote;
+ bool m_stopping;
+ bool m_connected;
+ bool m_sending;
+
+ unsigned m_pending_begin[2];
+ unsigned m_pending_end[2];
+ unsigned m_last_sent;
+ unsigned m_received_bytes;
+ u8 m_buffer_copy[2][512];
+ u8 m_transmit_buf[512];
+ u8 m_receive_buf[512];
+};
+
+
+
+DEFINE_DEVICE_TYPE(GUNBUSTR_LINK, gunbustr_link_device, "gunbustr_link", "Taito Gunbuster link simulation")
+
+
+
+gunbustr_link_device::gunbustr_link_device(machine_config const &mconfig, char const *tag, device_t *owner, u32 clock)
+ : device_t(mconfig, GUNBUSTR_LINK, tag, owner, clock)
+ , m_config(*this, "CFG")
+ , m_shared_ram(*this, "shared_ram", 0x800, ENDIANNESS_BIG)
+{
+}
+
+
+gunbustr_link_device::~gunbustr_link_device()
+{
+}
+
+
+void gunbustr_link_device::map(address_map &map)
+{
+ map(0x000000, 0x0007ff).rw(FUNC(gunbustr_link_device::ram_r), FUNC(gunbustr_link_device::ram_w));
+
+ map(0x0000fd, 0x0000fd).w(FUNC(gunbustr_link_device::handshake_w<0>));
+ map(0x0000fe, 0x0000ff).rw(FUNC(gunbustr_link_device::sense_r<0>), FUNC(gunbustr_link_device::sense_w<0>));
+ map(0x0001fd, 0x0001fd).w(FUNC(gunbustr_link_device::handshake_w<1>));
+ map(0x0001fe, 0x0001ff).rw(FUNC(gunbustr_link_device::sense_r<1>), FUNC(gunbustr_link_device::sense_w<1>));
+}
+
+
+ioport_constructor gunbustr_link_device::device_input_ports() const
+{
+ return INPUT_PORTS_NAME(gunbustr_link);
+}
+
+
+void gunbustr_link_device::device_start()
+{
+ m_pending[0].store(false, std::memory_order_relaxed);
+ m_pending[1].store(false, std::memory_order_relaxed);
+}
+
+
+void gunbustr_link_device::device_stop()
+{
+ if (m_context)
+ {
+ m_context->stop();
+ m_context.reset();
+ }
+}
+
+
+void gunbustr_link_device::device_reset()
+{
+ if (!m_context)
+ start_link();
+}
+
+
+u8 gunbustr_link_device::ram_r(offs_t offset)
+{
+ return m_shared_ram[offset];
+}
+
+
+void gunbustr_link_device::ram_w(offs_t offset, u8 data)
+{
+ m_shared_ram[offset] = data;
+}
+
+
+
+template <unsigned Buffer>
+void gunbustr_link_device::handshake_w(u8 data)
+{
+ u8 &val = m_shared_ram[(Buffer * BUFFER_BYTES) + (BUFFER_BYTES - 3)];
+ if (BIT(data, 0) && !BIT(val, 0))
+ check_pending(Buffer ^ 1);
+ if (!BIT(data, 1) && BIT(val, 1) && m_context)
+ m_context->send_buffer(Buffer, 0, BUFFER_BYTES - 4);
+ val = data;
+}
+
+
+template <unsigned Buffer>
+u16 gunbustr_link_device::sense_r()
+{
+ check_pending(Buffer);
+ return get_u16be(&m_shared_ram[(Buffer * BUFFER_BYTES) + (BUFFER_BYTES - 2)]);
+}
+
+
+template <unsigned Buffer>
+void gunbustr_link_device::sense_w(offs_t offset, u16 data, u16 mem_mask)
+{
+ if (ACCESSING_BITS_0_7)
+ m_shared_ram[(Buffer * BUFFER_BYTES) + (BUFFER_BYTES - 1)] = data & 0x00ff;
+
+ if (ACCESSING_BITS_8_15)
+ {
+ m_shared_ram[(Buffer * BUFFER_BYTES) + (BUFFER_BYTES - 2)] = data >> 8;
+ if (m_context && ((data & 0xff00) == 0xff00))
+ m_context->send_buffer(Buffer, 0, BUFFER_BYTES);
+ }
+}
+
+
+void gunbustr_link_device::check_pending(unsigned buffer)
+{
+ if (m_pending[buffer].load(std::memory_order_acquire))
+ {
+ assert(m_context);
+ m_context->update_buffer(buffer);
+ }
+}
+
+
+void gunbustr_link_device::start_link()
+{
+ if (!BIT(m_config->read(), 0))
+ return;
+
+ auto const &opts = mconfig().options();
+ std::error_code err;
+ std::istringstream parsestr;
+ parsestr.imbue(std::locale::classic());
+
+ std::optional<asio::ip::tcp::endpoint> local;
+ if (*opts.comm_localhost())
+ {
+ asio::ip::address const bindaddr = asio::ip::make_address(opts.comm_localhost(), err);
+ if (err)
+ {
+ osd_printf_error("[%s] invalid local IP address %s, disabling link.\n", tag(), opts.comm_localhost());
+ return;
+ }
+
+ parsestr.str(opts.comm_localport());
+ parsestr.seekg(0, std::ios_base::beg);
+ asio::ip::port_type bindport;
+ parsestr >> bindport;
+ if (!parsestr || !bindport)
+ {
+ osd_printf_error("[%s] invalid local TCP port %s, disabling link.\n", tag(), opts.comm_localport());
+ return;
+ }
+
+ local.emplace(bindaddr, bindport);
+ }
+
+ std::optional<asio::ip::tcp::endpoint> remote;
+ if (*opts.comm_remotehost())
+ {
+ asio::ip::address const connaddr = asio::ip::make_address(opts.comm_remotehost(), err);
+ if (err)
+ {
+ osd_printf_error("[%s] invalid remote address %s, disabling link.\n", tag(), opts.comm_remotehost());
+ return;
+ }
+
+ parsestr.str(opts.comm_remoteport());
+ parsestr.seekg(0, std::ios_base::beg);
+ asio::ip::port_type connport;
+ parsestr >> connport;
+ if (!parsestr || !connport)
+ {
+ osd_printf_error("[%s] invalid remote TCP port %s, disabling link.\n", tag(), opts.comm_remoteport());
+ return;
+ }
+
+ remote.emplace(connaddr, connport);
+ }
+
+ if (!local && !remote)
+ {
+ osd_printf_error("[%s] no TCP addresses configured, disabling communication.\n", tag());
+ return;
+ }
+
+ auto ctx = std::make_unique<context>(*this, local, remote);
+ err = ctx->start();
+ if (err)
+ {
+ osd_printf_error("[%s] error opening/binding socket, disabling link (%s).\n", tag(), err.message());
+ return;
+ }
+
+ m_context = std::move(ctx);
+}
diff --git a/src/mame/taito/gunbustr_link.h b/src/mame/taito/gunbustr_link.h
new file mode 100644
index 00000000000..afe19c9390c
--- /dev/null
+++ b/src/mame/taito/gunbustr_link.h
@@ -0,0 +1,48 @@
+// license:BSD-3-Clause
+// copyright-holders:Vas Crabb
+#ifndef MAME_TAITO_GUNBUSTR_LINK_H
+#define MAME_TAITO_GUNBUSTR_LINK_H
+
+#pragma once
+
+#include <atomic>
+#include <memory>
+
+
+class gunbustr_link_device : public device_t
+{
+public:
+ gunbustr_link_device(machine_config const &mconfig, char const *tag, device_t *owner, u32 clock);
+ ~gunbustr_link_device();
+
+ void map(address_map &map) ATTR_COLD;
+
+protected:
+ virtual ioport_constructor device_input_ports() const override ATTR_COLD;
+ virtual void device_start() override ATTR_COLD;
+ virtual void device_stop() override ATTR_COLD;
+ virtual void device_reset() override ATTR_COLD;
+
+private:
+ class context;
+
+ u8 ram_r(offs_t offset);
+ void ram_w(offs_t offset, u8 data);
+ template <unsigned Buffer> void handshake_w(u8 data);
+ template <unsigned Buffer> u16 sense_r();
+ template <unsigned Buffer> void sense_w(offs_t offset, u16 data, u16 mem_mask = ~0);
+
+ void start_link() ATTR_COLD;
+ void check_pending(unsigned buffer);
+
+ std::unique_ptr<context> m_context;
+ required_ioport m_config;
+ memory_share_creator<u8> m_shared_ram;
+
+ std::atomic<bool> m_pending[2];
+};
+
+
+DECLARE_DEVICE_TYPE(GUNBUSTR_LINK, gunbustr_link_device)
+
+#endif // MAME_TAITO_GUNBUSTR_LINK_H