Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,27 @@ if(RETRO_RUNTIME_TOOLS)
runner/host_session.cpp
runner/state_keeper.cpp
runner/net_session.cpp
runner/net_module.cpp
)
# Netplay (runner/net_session.hpp): recomp-net's episode driver and UDP
# transport, compiled in from a checkout when one is named. Without it the
# runner answers `netplay 0` and refuses --net-*.
set(RETRO_RUNTIME_RECOMP_NET_DIR "" CACHE PATH
"recomp-net checkout: build retro-core-runner with netplay")
if(RETRO_RUNTIME_RECOMP_NET_DIR)
# With RETRO_RUNTIME_NETPLAY_MODULE the netcode is NOT linked: the runner
# opens a librecomp_net_module at run time (--net-module / RETRO_NETPLAY_MODULE,
# runner/net_module.hpp), so it updates without a new runner. The checkout
# then supplies headers only.
option(RETRO_RUNTIME_NETPLAY_MODULE "retro-core-runner: netplay through a loadable module" OFF)
if(RETRO_RUNTIME_NETPLAY_MODULE)
if(NOT RETRO_RUNTIME_RECOMP_NET_DIR OR NOT EXISTS "${RETRO_RUNTIME_RECOMP_NET_DIR}/include/recomp_net/module.h")
message(FATAL_ERROR "RETRO_RUNTIME_NETPLAY_MODULE needs RETRO_RUNTIME_RECOMP_NET_DIR with include/recomp_net/module.h")
endif()
target_include_directories(retro-core-runner PRIVATE ${RETRO_RUNTIME_RECOMP_NET_DIR}/include)
target_compile_definitions(retro-core-runner PRIVATE RETRO_RUNNER_NETPLAY=1 RETRO_RUNNER_NETPLAY_MODULE=1)
target_link_libraries(retro-core-runner PRIVATE ${CMAKE_DL_LIBS})
message(STATUS "retro-core-runner: netplay via loadable module (headers ${RETRO_RUNTIME_RECOMP_NET_DIR})")
elseif(RETRO_RUNTIME_RECOMP_NET_DIR)
set(RNET_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(RNET_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(RNET_ENABLE_ICE OFF CACHE BOOL "" FORCE)
Expand Down
3 changes: 3 additions & 0 deletions corelink/runner_probe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ bool probe_runner(const fs::path& runner, RunnerVersion& out, std::string* error
if (fields.count("netplay") && !num("netplay", v.netplay)) {
return fail("--version printed an unreadable netplay line");
}
if (fields.count("netplay_module_abi") && !num("netplay_module_abi", v.netplay_module_abi)) {
return fail("--version printed an unreadable netplay_module_abi line");
}
if (fields.count("transfer_pak_seats") && !num("transfer_pak_seats", v.transfer_pak_seats)) {
return fail("--version printed an unreadable transfer_pak_seats line");
}
Expand Down
4 changes: 4 additions & 0 deletions corelink/runner_probe.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ struct RunnerVersion {
// 1 when the runner was built with recomp-net (--net-*; CORE_RUNNER.md,
// "Netplay"); 0 for one without, or from before netplay.
std::uint32_t netplay = 0;
// The netplay module ABI (recomp-net module.h) the runner loads at run
// time, with `netplay` 1; 0 when netplay is linked in, absent, or the
// runner predates the line. A host installs the module with this ABI.
std::uint32_t netplay_module_abi = 0;
// 1 when the runner answers --describe (a core's declared options and
// inputs, no ROM; docs/CORE_RUNNER.md); 0 for a runner from before it.
std::uint32_t describe = 0;
Expand Down
11 changes: 11 additions & 0 deletions docs/CORE_RUNNER.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ checkout>`; `--version` then says `netplay 1`, otherwise `netplay 0` and every
[--net-content LINE ...]
```

- **As a module** (`-DRETRO_RUNTIME_NETPLAY_MODULE=ON` with
`RETRO_RUNTIME_RECOMP_NET_DIR`, headers only). recomp-net is not linked: the
runner opens `librecomp_net_module` from `--net-module <path>` or
`RETRO_NETPLAY_MODULE` (`runner/net_module.*`), refusing it at start, by name,
on a missing path, wrong ABI, no rbengine or any missing symbol. The module's
wire version joins the build fingerprint, and `runner_netplay: MODULE ...`
logs which module played. Measured: Pokemon Stadium, two processes on
loopback, 300 frames, identical `state_hash` at tick 280, 0 desyncs.
`--version` prints `netplay_module_abi N` (0 when netplay is linked in or
absent): the recomp-net module ABI this runner loads, so a host installs a
module with that ABI.
- **Transport.** Seat 0 hosts: with more than two seats it is recomp-net's
LAN hub (the host relays every guest's datagrams to the others), with two
a direct pair. Every other seat dials the host (`--net-peer`). With
Expand Down
90 changes: 90 additions & 0 deletions runner/net_module.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include "net_module.hpp"

#if defined(RETRO_RUNNER_NETPLAY_MODULE)

#include <dlfcn.h>

#include <mutex>

extern "C" {
#include "recomp_net/module.h"
#include "recomp_net/rb_driver.h"
#include "recomp_net/session.h"
}

#include "net_module_api.hpp"

namespace retro::runner {

NetModuleApi g_nm{};

namespace {
std::mutex g_mu;
NetModuleInfo g_info;
bool g_loaded = false;
} // namespace

const NetModuleInfo* net_module_info() { return g_loaded ? &g_info : nullptr; }
std::uint32_t net_module_abi_required() { return RNET_MODULE_ABI_VERSION; }

bool net_module_load(const std::string& path, std::string* error) {
std::lock_guard<std::mutex> lock(g_mu);
auto fail = [&](const std::string& why) {
if (error) *error = "netplay module " + path + ": " + why;
return false;
};
if (path.empty()) {
if (error) *error = "netplay unavailable: no netplay module (--net-module or RETRO_NETPLAY_MODULE)";
return false;
}
if (g_loaded) {
if (g_info.path == path) return true;
return fail("a different module (" + g_info.path + ") is already loaded");
}
void* h = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!h) {
const char* e = dlerror();
return fail(e ? e : "dlopen failed");
}
auto bail = [&](const std::string& why) {
dlclose(h);
return fail(why);
};
auto info_fn = reinterpret_cast<size_t (*)(RNetModuleInfo*, size_t)>(dlsym(h, "rnet_module_info"));
if (!info_fn) return bail("has no rnet_module_info (not a netplay module)");
RNetModuleInfo m{};
const size_t got = info_fn(&m, sizeof m);
if (const char* why = rnet_module_check(&m, got, RNET_MODULE_FEATURE_RBENGINE)) return bail(why);

NetModuleApi api{};
bool ok = true;
std::string missing;
#define RETRO_NM_SYM(n) \
api.n = reinterpret_cast<decltype(api.n)>(dlsym(h, #n)); \
if (!api.n) { ok = false; missing += " " #n; }
RETRO_NET_MODULE_SYMBOLS(RETRO_NM_SYM)
#undef RETRO_NM_SYM
if (!ok) return bail("missing symbols:" + missing);

g_nm = api;
g_info = NetModuleInfo{path, m.abi_version, m.abi_minor, m.wire_version, m.features,
m.version_major, m.version_minor, m.version_patch,
(got >= sizeof m && m.build_id) ? m.build_id : "unknown"};
g_loaded = true;
return true;
}

} // namespace retro::runner

#else

namespace retro::runner {
bool net_module_load(const std::string&, std::string* error) {
if (error) *error = "this runner was not built with the netplay module (RETRO_RUNTIME_NETPLAY_MODULE)";
return false;
}
const NetModuleInfo* net_module_info() { return nullptr; }
std::uint32_t net_module_abi_required() { return 0; }
} // namespace retro::runner

#endif
30 changes: 30 additions & 0 deletions runner/net_module.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// The netplay module (recomp-net include/recomp_net/module.h), opened at run
// time. In a module build (RETRO_RUNNER_NETPLAY_MODULE) net_session.cpp calls
// recomp-net through the pointers resolved here instead of linking it, so the
// netcode is versioned and shipped apart from the runner and the cores. The
// retcomm-launcher owns fetching and verifying the file; this only opens it.
#pragma once

#include <cstdint>
#include <string>

namespace retro::runner {

struct NetModuleInfo {
std::string path;
std::uint32_t abi_version = 0, abi_minor = 0, wire_version = 0, features = 0;
std::uint32_t major = 0, minor = 0, patch = 0;
std::string build_id;
};

// Opens `path` once per process and checks it: module ABI, rbengine, and every
// function the runner calls. Returns false with *error saying which check
// failed. No fallback: a runner with no usable module refuses --net-*.
bool net_module_load(const std::string& path, std::string* error);
const NetModuleInfo* net_module_info(); // null until loaded
// The module ABI this runner was built for (recomp-net RNET_MODULE_ABI_VERSION),
// or 0 when netplay is linked in or absent. Reported by --version so a host
// can pick a module whose ABI matches without opening it.
std::uint32_t net_module_abi_required();

} // namespace retro::runner
43 changes: 43 additions & 0 deletions runner/net_module_api.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// The recomp-net functions the runner calls, as pointers into the loaded
// module. Add a name here when net_session.cpp starts calling a new one: the
// loader refuses a module that lacks any entry, at load, by name.
#pragma once

extern "C" {
#include "recomp_net/rb_driver.h"
#include "recomp_net/session.h"
}

#define RETRO_NET_MODULE_SYMBOLS(X) \
X(rnet_rb_driver_confirmed_through) \
X(rnet_rb_driver_create) \
X(rnet_rb_driver_destroy) \
X(rnet_rb_driver_desync_count) \
X(rnet_rb_driver_episode_count) \
X(rnet_rb_driver_finish_frame) \
X(rnet_rb_driver_poll_admit) \
X(rnet_rb_driver_quiesce_state) \
X(rnet_rb_driver_refusal) \
X(rnet_rb_driver_request_quiesce) \
X(rnet_rb_driver_rtt_estimate_ms) \
X(rnet_rb_driver_set_identity) \
X(rnet_rb_driver_shutdown) \
X(rnet_rb_driver_start) \
X(rnet_session_create) \
X(rnet_session_destroy) \
X(rnet_session_is_running) \
X(rnet_session_peer_disconnected) \
X(rnet_session_pump) \
X(rnet_session_send_bye) \
X(rnet_session_start_lan) \
X(rnet_session_start_lan_hub) \
X(rnet_session_wait_recv)

namespace retro::runner {
struct NetModuleApi {
#define RETRO_NM_FIELD(n) decltype(&::n) n;
RETRO_NET_MODULE_SYMBOLS(RETRO_NM_FIELD)
#undef RETRO_NM_FIELD
};
extern NetModuleApi g_nm;
} // namespace retro::runner
28 changes: 28 additions & 0 deletions runner/net_module_redirect.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Included by net_session.cpp ONLY, after the recomp-net headers: turns each
// call it makes into a call through the loaded module (net_module_api.hpp).
#pragma once
#include "net_module_api.hpp"

#define rnet_rb_driver_confirmed_through (::retro::runner::g_nm.rnet_rb_driver_confirmed_through)
#define rnet_rb_driver_create (::retro::runner::g_nm.rnet_rb_driver_create)
#define rnet_rb_driver_destroy (::retro::runner::g_nm.rnet_rb_driver_destroy)
#define rnet_rb_driver_desync_count (::retro::runner::g_nm.rnet_rb_driver_desync_count)
#define rnet_rb_driver_episode_count (::retro::runner::g_nm.rnet_rb_driver_episode_count)
#define rnet_rb_driver_finish_frame (::retro::runner::g_nm.rnet_rb_driver_finish_frame)
#define rnet_rb_driver_poll_admit (::retro::runner::g_nm.rnet_rb_driver_poll_admit)
#define rnet_rb_driver_quiesce_state (::retro::runner::g_nm.rnet_rb_driver_quiesce_state)
#define rnet_rb_driver_refusal (::retro::runner::g_nm.rnet_rb_driver_refusal)
#define rnet_rb_driver_request_quiesce (::retro::runner::g_nm.rnet_rb_driver_request_quiesce)
#define rnet_rb_driver_rtt_estimate_ms (::retro::runner::g_nm.rnet_rb_driver_rtt_estimate_ms)
#define rnet_rb_driver_set_identity (::retro::runner::g_nm.rnet_rb_driver_set_identity)
#define rnet_rb_driver_shutdown (::retro::runner::g_nm.rnet_rb_driver_shutdown)
#define rnet_rb_driver_start (::retro::runner::g_nm.rnet_rb_driver_start)
#define rnet_session_create (::retro::runner::g_nm.rnet_session_create)
#define rnet_session_destroy (::retro::runner::g_nm.rnet_session_destroy)
#define rnet_session_is_running (::retro::runner::g_nm.rnet_session_is_running)
#define rnet_session_peer_disconnected (::retro::runner::g_nm.rnet_session_peer_disconnected)
#define rnet_session_pump (::retro::runner::g_nm.rnet_session_pump)
#define rnet_session_send_bye (::retro::runner::g_nm.rnet_session_send_bye)
#define rnet_session_start_lan (::retro::runner::g_nm.rnet_session_start_lan)
#define rnet_session_start_lan_hub (::retro::runner::g_nm.rnet_session_start_lan_hub)
#define rnet_session_wait_recv (::retro::runner::g_nm.rnet_session_wait_recv)
21 changes: 21 additions & 0 deletions runner/net_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ extern "C" {
#include "recomp_net/rb_driver.h"
#include "recomp_net/session.h"
}
#if defined(RETRO_RUNNER_NETPLAY_MODULE)
#include "net_module.hpp"
#include "net_module_redirect.hpp"
#endif
#endif

namespace retro::runner {
Expand Down Expand Up @@ -205,6 +209,18 @@ bool NetSession::start(const LoadedCore& core, const NetParams& p, std::string*
};
Impl& s = *impl_;
const rcore_core_api* a = core.api;
#if defined(RETRO_RUNNER_NETPLAY_MODULE)
// The netcode is a separate, versioned file: open and check it before any
// recomp-net call, and say which one a peer-visible log came from.
{
std::string why;
if (!net_module_load(p.module_path, &why)) return fail(why);
const NetModuleInfo* m = net_module_info();
std::fprintf(stderr, "runner_netplay: MODULE %s recomp-net %u.%u.%u abi %u.%u wire %u build %s\n",
m->path.c_str(), m->major, m->minor, m->patch, m->abi_version, m->abi_minor,
m->wire_version, m->build_id.c_str());
}
#endif
if (!(core.info->capabilities & RCORE_CAP_DETERMINISTIC))
return fail("the core does not declare DETERMINISTIC, so it cannot play netplay");
if (!(core.info->capabilities & RCORE_CAP_ROLLBACK) || !a->rb_snap_save || !a->rb_snap_load ||
Expand Down Expand Up @@ -243,6 +259,11 @@ bool NetSession::start(const LoadedCore& core, const NetParams& p, std::string*
// ---- identity: what every peer must hold identically ----
std::uint32_t build_fp = fnv1a(kRunnerNetVersion);
build_fp = fnv1a(core.sha256, build_fp);
#if defined(RETRO_RUNNER_NETPLAY_MODULE)
// A module whose wire protocol differs cannot play this match: it joins
// the identity every peer must hold identically (refused at the handshake).
build_fp = fnv1a("wire " + std::to_string(net_module_info()->wire_version), build_fp);
#endif
std::uint32_t content_fp = fnv1a("slots " + std::to_string(p.slots) + " occupied " +
std::to_string(p.occupied) + " epoch " +
std::to_string(p.epoch_s));
Expand Down
1 change: 1 addition & 0 deletions runner/net_session.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
namespace retro::runner {

struct NetParams {
std::string module_path; // the netplay module (module builds only)
int slot = 0; // this peer's seat
int slots = 2; // seats in the match
std::uint32_t occupied = 0; // bit i = seat i has a player; 0 = all
Expand Down
7 changes: 6 additions & 1 deletion runner/runner_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
#include "core_library.hpp"
#include "core_manifest.hpp"
#include "host_session.hpp"
#include "net_module.hpp"
#include "net_session.hpp"
#include "link_protocol.hpp"
#include "runner_link.hpp"
Expand Down Expand Up @@ -299,11 +300,12 @@ void print_version() {
"describe 1\n"
"transfer_pak_seats %zu\n"
"netplay %d\n"
"netplay_module_abi %u\n"
"accessory_data 1\n",
RETRO_RUNTIME_VERSION, RETRO_RUNTIME_VERSION, RETRO_RUNTIME_COMMIT,
retro::corelink::kProtocolMajor, retro::corelink::kProtocolMinor,
RCORE_ABI_MAJOR, RCORE_DRAFT_REVISION, gl, retro::runner::kTransferPakSeats,
NetSession::compiled_in() ? 1 : 0);
NetSession::compiled_in() ? 1 : 0, retro::runner::net_module_abi_required());
}

// One --describe field: \ TAB LF CR escaped, so a record is one line; a NULL
Expand Down Expand Up @@ -461,6 +463,7 @@ int main(int argc, char** argv) {
else if (a == "--list-options") list_options = true;
else if (a == "--describe") describe = true;
else if (a == "--replay-at") replay_at = num(val());
else if (a == "--net-module") { netplay = true; net.module_path = val(); }
else if (a == "--net-slot") { netplay = true; net.slot = static_cast<int>(num(val())); }
else if (a == "--net-slots") { netplay = true; net.slots = static_cast<int>(num(val())); }
else if (a == "--net-occupied") { netplay = true; net.occupied = static_cast<std::uint32_t>(num(val())); }
Expand Down Expand Up @@ -496,6 +499,8 @@ int main(int argc, char** argv) {
}
}
if (core_path.empty()) die("--core <library> is required");
if (net.module_path.empty())
if (const char* e = std::getenv("RETRO_NETPLAY_MODULE")) net.module_path = e;
if (netplay && (replay_at || load_state))
die("netplay starts from a cold boot: --replay-at and --load-state are refused");
if (netplay && !NetSession::compiled_in())
Expand Down
Loading