Integrate retcomm-rbengine as shared rollback host policy.

Wire lib/retcomm-rbengine beside recomp-net and replace MotK in-tree sched,
hash_confirm, input hist, snap ring, and rb_post with thin PSX shims so the
ecosystem can share invent/pacing policy outside each title facade.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alex Vanderveen
2026-08-11 23:52:31 -04:00
parent 37a5b7c7cd
commit 3acb8f6d59
17 changed files with 344 additions and 2665 deletions
+6
View File
@@ -1,3 +1,9 @@
[submodule "lib/recomp-net"]
path = lib/recomp-net
url = https://github.com/TechnicallyComputers/recomp-net.git
branch = main
[submodule "lib/retcomm-rbengine"]
path = lib/retcomm-rbengine
url = https://github.com/TechnicallyComputers/retcomm-rbengine.git
branch = main
+54 -36
View File
@@ -278,20 +278,61 @@ if(BUILD_TESTING)
add_test(NAME gpu_vk_upload_alignment_test
COMMAND gpu_vk_upload_alignment_test)
add_executable(netplay_hash_confirm_test
tests/test_netplay_hash_confirm.c
src/netplay_hash_confirm.c)
target_include_directories(netplay_hash_confirm_test PRIVATE include)
add_test(NAME netplay_hash_confirm_test COMMAND netplay_hash_confirm_test)
# Portable rollback helpers (retcomm-rbengine) — required for MotK netplay
# unit tests that use the netplay_* compatibility shims.
set(_RBE_ROOT "${PSXRECOMP_ROOT}/lib/retcomm-rbengine")
set(_RNET_ROOT "${PSXRECOMP_ROOT}/lib/recomp-net")
if(EXISTS "${_RBE_ROOT}/CMakeLists.txt" AND EXISTS "${_RNET_ROOT}/CMakeLists.txt")
if(NOT TARGET recomp_net)
set(RNET_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(RNET_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(RNET_ENABLE_ICE OFF CACHE BOOL "" FORCE)
add_subdirectory("${_RNET_ROOT}" "${CMAKE_BINARY_DIR}/recomp-net-tests")
endif()
if(NOT TARGET retcomm_rbengine)
set(RBE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(RECOMP_NET_ROOT "${_RNET_ROOT}" CACHE PATH "" FORCE)
add_subdirectory("${_RBE_ROOT}" "${CMAKE_BINARY_DIR}/retcomm-rbengine-tests")
endif()
set(PSXRECOMP_TEST_HAS_RBENGINE TRUE)
else()
set(PSXRECOMP_TEST_HAS_RBENGINE FALSE)
message(STATUS
"netplay_*_tests: lib/retcomm-rbengine or lib/recomp-net absent - skipped")
endif()
# Single TU: the test #includes src/netplay_snap_ring.c and stubs
# boot_state, so the .c must NOT also be listed here (that was PR #134's
# duplicate-symbol bug, in its documented command).
add_executable(netplay_snap_ring_test tests/test_netplay_snap_ring.c)
target_include_directories(netplay_snap_ring_test PRIVATE include)
target_compile_definitions(netplay_snap_ring_test PRIVATE
NETPLAY_SNAP_RING_TEST_STUB_BOOT_STATE)
add_test(NAME netplay_snap_ring_test COMMAND netplay_snap_ring_test)
if(PSXRECOMP_TEST_HAS_RBENGINE)
add_executable(netplay_hash_confirm_test
tests/test_netplay_hash_confirm.c)
target_include_directories(netplay_hash_confirm_test PRIVATE include)
target_link_libraries(netplay_hash_confirm_test PRIVATE retcomm_rbengine)
add_test(NAME netplay_hash_confirm_test COMMAND netplay_hash_confirm_test)
# save/load glue #included from the test; ring ops come from rbengine.
add_executable(netplay_snap_ring_test tests/test_netplay_snap_ring.c)
target_include_directories(netplay_snap_ring_test PRIVATE include)
target_compile_definitions(netplay_snap_ring_test PRIVATE
NETPLAY_SNAP_RING_TEST_STUB_BOOT_STATE
PSX_HAS_RECOMP_NET=1)
target_link_libraries(netplay_snap_ring_test PRIVATE retcomm_rbengine)
add_test(NAME netplay_snap_ring_test COMMAND netplay_snap_ring_test)
add_executable(rb_post_tip_filter_test tests/test_rb_post_tip_filter.c)
target_include_directories(rb_post_tip_filter_test PRIVATE include)
target_link_libraries(rb_post_tip_filter_test PRIVATE retcomm_rbengine)
add_test(NAME rb_post_tip_filter_test COMMAND rb_post_tip_filter_test)
add_executable(netplay_input_hist_test
tests/test_netplay_input_hist.c
src/netplay_input_hist.c)
target_include_directories(netplay_input_hist_test PRIVATE
include
${_RNET_ROOT}/include)
target_compile_definitions(netplay_input_hist_test PRIVATE
PSX_HAS_RECOMP_NET=1)
target_link_libraries(netplay_input_hist_test PRIVATE retcomm_rbengine)
add_test(NAME netplay_input_hist_test COMMAND netplay_input_hist_test)
endif()
add_executable(overlay_store_cycle_barrier_test
tests/test_overlay_store_cycle_barrier.c)
@@ -306,10 +347,6 @@ if(BUILD_TESTING)
add_test(NAME psx_cycle_event_boundaries_test
COMMAND psx_cycle_event_boundaries_test)
add_executable(rb_post_tip_filter_test tests/test_rb_post_tip_filter.c)
target_include_directories(rb_post_tip_filter_test PRIVATE include)
add_test(NAME rb_post_tip_filter_test COMMAND rb_post_tip_filter_test)
add_executable(spu_end_without_repeat_test
tests/test_spu_end_without_repeat.c)
target_include_directories(spu_end_without_repeat_test PRIVATE include)
@@ -361,25 +398,6 @@ if(BUILD_TESTING)
message(STATUS "overlay_posix_test: bash not found - skipped")
endif()
endif()
# recomp-net is an optional submodule; the netplay input-history contract
# cannot be compiled without it. Conditional, not skipped silently: the
# registration guard is told about it via the same EXISTS check.
if(EXISTS "${PSXRECOMP_ROOT}/lib/recomp-net/include/recomp_net/rollback.h")
add_executable(netplay_input_hist_test
tests/test_netplay_input_hist.c
src/netplay_input_hist.c
${PSXRECOMP_ROOT}/lib/recomp-net/src/input/rnet_input_contract.c)
target_include_directories(netplay_input_hist_test PRIVATE
include
${PSXRECOMP_ROOT}/lib/recomp-net/include)
target_compile_definitions(netplay_input_hist_test PRIVATE
PSX_HAS_RECOMP_NET=1)
add_test(NAME netplay_input_hist_test COMMAND netplay_input_hist_test)
else()
message(STATUS
"netplay_input_hist_test: lib/recomp-net submodule absent - skipped")
endif()
endif()
# ---- test-registration completeness guard (must stay LAST) ------------------
+15 -55
View File
@@ -2,69 +2,29 @@
#define PSX_NETPLAY_HASH_CONFIRM_H
/*
* Local digest ring + peer FRAME_COMMIT matching → resolved_through watermark.
*
* hash_confirm_through(T) is 1 iff every tick in (prev_resolved, T] has a
* local digest that matched a peer FRAME_COMMIT (contiguous from the prior
* watermark). Used as RNetInputContractHostGates.hash_confirm_promote /
* RNetRollbackVTable.hash_confirm_through.
* MotK compatibility shim → retcomm-rbengine hash_confirm.
* Prefer rbe_hc_* in new code.
*/
#include <stdint.h>
#include "retcomm_rbengine/hash_confirm.h"
#ifdef __cplusplus
extern "C" {
#endif
#define NETPLAY_HC_RING 128u
#define NETPLAY_HC_RING RBE_HC_RING
typedef RbeHashConfirm NetplayHashConfirm;
typedef struct NetplayHashConfirm {
uint32_t local_tick[NETPLAY_HC_RING];
uint32_t local_digest[NETPLAY_HC_RING];
uint8_t local_valid[NETPLAY_HC_RING];
uint32_t peer_tick[NETPLAY_HC_RING];
uint32_t peer_digest[NETPLAY_HC_RING];
uint8_t peer_valid[NETPLAY_HC_RING];
uint32_t resolved_through; /* inclusive; 0 = none yet (tick 0 may match) */
uint8_t resolved_valid; /* 0 until first match advances watermark */
} NetplayHashConfirm;
void netplay_hc_reset(NetplayHashConfirm* hc);
/* Clear the ring and set resolved_through = last_ok so the next compared tick
* is last_ok+1. Used at Replay entry to drop live invent FRAME_COMMITs that
* would false-trigger mid-resim diverge aborts. */
void netplay_hc_prime_after(NetplayHashConfirm* hc, uint32_t last_ok);
/* Record our digest after sim tick T completes. */
void netplay_hc_note_local(NetplayHashConfirm* hc, uint32_t tick, uint32_t digest);
/* Record peer RB_FRAME_COMMIT (through_tick, state_hash). Advances watermark
* when it matches our local digest for that tick. */
void netplay_hc_note_peer(NetplayHashConfirm* hc, uint32_t tick, uint32_t digest);
uint32_t netplay_hc_resolved_through(const NetplayHashConfirm* hc);
uint8_t netplay_hc_confirm_through(const NetplayHashConfirm* hc, uint32_t tick);
/* Peek local digest for tick; returns 0 if missing. */
uint8_t netplay_hc_local_digest(const NetplayHashConfirm* hc, uint32_t tick,
uint32_t* digest_out);
/* Peek peer FRAME_COMMIT digest for tick; returns 0 if missing. */
uint8_t netplay_hc_peer_digest(const NetplayHashConfirm* hc, uint32_t tick,
uint32_t* digest_out);
/* 1 if the next tick after resolved_through has both digests but they differ.
* Fills tick/local/peer when non-NULL. Used to log first live core fork. */
uint8_t netplay_hc_peek_mismatch(const NetplayHashConfirm* hc, uint32_t* tick_out,
uint32_t* local_out, uint32_t* peer_out);
/* Heal a stuck watermark when the next tick after resolved_through has aged
* out of the ring (slot reused) and is no longer a live mismatch. Scans the
* ring for the highest tick where local==peer with no newer mismatch present,
* then advances resolved_through there (ring contents kept).
* Returns 1 if the watermark moved. Call from FRAME_COMMIT ingress. */
uint8_t netplay_hc_heal_stale_gap(NetplayHashConfirm* hc);
#define netplay_hc_reset rbe_hc_reset
#define netplay_hc_prime_after rbe_hc_prime_after
#define netplay_hc_note_local rbe_hc_note_local
#define netplay_hc_note_peer rbe_hc_note_peer
#define netplay_hc_resolved_through rbe_hc_resolved_through
#define netplay_hc_confirm_through rbe_hc_confirm_through
#define netplay_hc_local_digest rbe_hc_local_digest
#define netplay_hc_peer_digest rbe_hc_peer_digest
#define netplay_hc_peek_mismatch rbe_hc_peek_mismatch
#define netplay_hc_heal_stale_gap rbe_hc_heal_stale_gap
#ifdef __cplusplus
}
+13 -36
View File
@@ -2,60 +2,37 @@
#define PSX_NETPLAY_INPUT_HIST_H
/*
* Per-slot tick → RNetRbFrame history for MotK rollback invent/contract.
*
* Local authority rows: is_predicted = 0.
* Invent (hold-last) remotes: is_predicted = 1.
* Late wire promote: replace the row in place (clears predicted).
*
* Only available when PSX_HAS_RECOMP_NET is defined (linked recomp-net).
* MotK input history: portable ring/invent in retcomm-rbengine; PSX pad
* conversion stays in netplay_input_hist.c.
*/
#if defined(PSX_HAS_RECOMP_NET)
#include <stdint.h>
#include "recomp_net/rollback.h"
#include "psx_netplay.h"
#include "retcomm_rbengine/input_hist.h"
#ifdef __cplusplus
extern "C" {
#endif
#define NETPLAY_INPUT_HIST_DEPTH 128u
#define NETPLAY_INPUT_HIST_MAX_SLOTS 8
#define NETPLAY_INPUT_HIST_DEPTH RBE_INPUT_HIST_DEPTH
#define NETPLAY_INPUT_HIST_MAX_SLOTS RBE_INPUT_HIST_MAX_SLOTS
typedef RbeInputHist NetplayInputHist;
typedef struct NetplayInputHist {
RNetRbFrame rows[NETPLAY_INPUT_HIST_MAX_SLOTS][NETPLAY_INPUT_HIST_DEPTH];
int slot_count;
uint32_t invent_count;
uint32_t promote_count;
uint32_t rewind_count;
} NetplayInputHist;
void netplay_ih_reset(NetplayInputHist *h, int slot_count);
#define netplay_ih_reset rbe_ih_reset
#define netplay_ih_frame_to_contract rbe_ih_frame_to_contract
#define netplay_ih_put rbe_ih_put
#define netplay_ih_get rbe_ih_get
#define netplay_ih_invent_hold_last rbe_ih_invent_hold_last
#define netplay_ih_invent_idle rbe_ih_invent_idle
#define netplay_ih_promote rbe_ih_promote
/* PsxNetPad ↔ RNetRbFrame (LX/LY only; RX/RY stay on the pad blob path). */
void netplay_ih_pad_to_frame(const PsxNetPad *pad, uint32_t tick, uint8_t predicted,
RNetRbFrame *out);
void netplay_ih_frame_to_pad(const RNetRbFrame *frame, PsxNetPad *pad);
void netplay_ih_frame_to_contract(const RNetRbFrame *frame, RNetInputContractFrame *out);
int netplay_ih_put(NetplayInputHist *h, int slot, const RNetRbFrame *frame);
int netplay_ih_get(const NetplayInputHist *h, int slot, uint32_t tick, RNetRbFrame *out);
/* Hold-last invent for missing remote at tick. Uses prior valid row for slot,
* else neutral (buttons 0xFFFF, sticks 0). Marks is_predicted=1 and stores. */
int netplay_ih_invent_hold_last(NetplayInputHist *h, int slot, uint32_t tick,
RNetRbFrame *out);
/* Neutral invent (buttons 0xFFFF). Seal gap-fill only — live MotK admit uses
* hold-last so a held D-pad does not re-episode every tick. */
int netplay_ih_invent_idle(NetplayInputHist *h, int slot, uint32_t tick,
RNetRbFrame *out);
/* Replace a published predicted/auth row with authoritative wire (promote). */
int netplay_ih_promote(NetplayInputHist *h, int slot, const RNetRbFrame *wire);
#ifdef __cplusplus
}
+3 -10
View File
@@ -2,23 +2,16 @@
#define PSX_NETPLAY_RB_POST_H
/*
* RB_POST already carries target_tick on the wire. After tip-extend the peer
* may still deliver a POST for the prior tip while we Verify the new tip —
* latching that digest caused false post diverge (local@T+1 vs peer@T).
*
* Accept only when peer tip equals the current episode tip.
* MotK compatibility shim → retcomm-rbengine rb_post tip filter.
*/
#include <stdint.h>
#include "retcomm_rbengine/rb_post.h"
#ifdef __cplusplus
extern "C" {
#endif
static inline int netplay_rb_peer_post_tip_ok(uint32_t peer_target, uint32_t episode_tip)
{
return peer_target == episode_tip;
}
#define netplay_rb_peer_post_tip_ok rbe_rb_peer_post_tip_ok
#ifdef __cplusplus
}
+21 -41
View File
@@ -2,65 +2,45 @@
#define PSX_NETPLAY_SNAP_RING_H
/*
* Tick-addressable in-memory .pst ring for rollback netplay.
*
* Depth defaults to 64 (enough for early soaks; raise toward
* RNET_RB_SEAL_MAX_SPAN=128 when episodes deepen). Each slot owns a
* boot_state_save_buffer_raw blob (uncompressed — FPS). save/load call the
* full-machine serializer; store/peek are for tests / pre-serialized blobs.
* MotK snap ring: opaque blob ring from retcomm-rbengine; save/load wrap
* boot_state serialize (PSX-specific).
*/
#include <stddef.h>
#include <stdint.h>
#include "cpu_state.h"
#include "retcomm_rbengine/snap_ring.h"
#ifdef __cplusplus
extern "C" {
#endif
/* §58: 64 → 80 — dense tip snaps (PSX_NET_SNAP_DENSE window) occupy up to
* ~24 extra slots alongside the interval snaps; keep the interval history
* span (~1024 ticks at iv=16) from shrinking under dense pressure. */
#define NETPLAY_SNAP_RING_DEFAULT_DEPTH 80u
#define NETPLAY_SNAP_RING_DEFAULT_DEPTH RBE_SNAP_RING_DEFAULT_DEPTH
typedef RbeSnapRing NetplaySnapRing;
typedef struct NetplaySnapRing NetplaySnapRing;
NetplaySnapRing* netplay_snap_ring_create(uint32_t depth);
void netplay_snap_ring_destroy(NetplaySnapRing* r);
void netplay_snap_ring_clear(NetplaySnapRing* r);
uint32_t netplay_snap_ring_depth(const NetplaySnapRing* r);
uint32_t netplay_snap_ring_count(const NetplaySnapRing* r);
int netplay_snap_ring_has(const NetplaySnapRing* r, uint32_t tick);
#define netplay_snap_ring_create rbe_snap_ring_create
#define netplay_snap_ring_destroy rbe_snap_ring_destroy
#define netplay_snap_ring_clear rbe_snap_ring_clear
#define netplay_snap_ring_depth rbe_snap_ring_depth
#define netplay_snap_ring_count rbe_snap_ring_count
#define netplay_snap_ring_has rbe_snap_ring_has
#define netplay_snap_ring_store rbe_snap_ring_store
#define netplay_snap_ring_peek rbe_snap_ring_peek
#define netplay_snap_ring_drop_after rbe_snap_ring_drop_after
#define netplay_snap_ring_drop_tick rbe_snap_ring_drop_tick
#define netplay_snap_ring_oldest_tick rbe_snap_ring_oldest_tick
#define netplay_snap_ring_newest_tick rbe_snap_ring_newest_tick
/* Serialize live machine into the ring at tick (overwrites same tick). */
int netplay_snap_ring_save(NetplaySnapRing* r, uint32_t tick,
const CPUState* cpu, uint32_t bios_checksum,
int netplay_snap_ring_save(NetplaySnapRing *r, uint32_t tick,
const CPUState *cpu, uint32_t bios_checksum,
uint32_t entry_pc);
/* Restore machine from the snap at tick. Returns 0 if missing/reject. */
int netplay_snap_ring_load(NetplaySnapRing* r, uint32_t tick, CPUState* cpu,
int netplay_snap_ring_load(NetplaySnapRing *r, uint32_t tick, CPUState *cpu,
uint32_t bios_checksum, uint32_t entry_pc);
/* Take ownership of data on success (caller must not free). For tests /
* pre-serialized blobs. Overwrites an existing entry for the same tick. */
int netplay_snap_ring_store(NetplaySnapRing* r, uint32_t tick,
uint8_t* data, size_t size);
/* Non-owning peek; NULL if missing. */
const uint8_t* netplay_snap_ring_peek(const NetplaySnapRing* r, uint32_t tick,
size_t* size_out);
/* Oldest/newest occupied tick, or 0 if empty (ambiguous with tick 0 — use
* count() first). */
/* Invalidate every snapshot with tick > tick (dead-timeline snaps after an
* abort realign). Returns the number of slots dropped. */
uint32_t netplay_snap_ring_drop_after(NetplaySnapRing* r, uint32_t tick);
/* Drop the single snap at tick (dense-window eviction). 1 if dropped. */
int netplay_snap_ring_drop_tick(NetplaySnapRing* r, uint32_t tick);
uint32_t netplay_snap_ring_oldest_tick(const NetplaySnapRing* r);
uint32_t netplay_snap_ring_newest_tick(const NetplaySnapRing* r);
#ifdef __cplusplus
}
#endif
+6 -5
View File
@@ -1,9 +1,10 @@
/* psx_netplay_sched.h — rollback admission scheduler (policy only).
/* psx_netplay_sched.h — MotK facade over retcomm-rbengine (rbe_sched_*).
*
* Split out of psx_netplay.c (§44) so the pacing/invent/delay policy can be
* reviewed, tuned and ported independently of the admission mechanics (ring
* peeks, input-history puts, tip-hold bookkeeping), which stay in
* psx_netplay.c.
* Policy lives in lib/retcomm-rbengine. This header keeps the historical
* np_sched_* / PsxNpSchedBridge surface; MotK dig0/FMV/RTT enter through
* gates in psx_netplay_sched.c.
*
* Env knobs: RBE_RB_* (preferred) with MotK PSX_RB_* fallback in rbengine.
*
* The scheduler's job (design contract, 2026-08-02):
* - keep both peers at the same simulation time (mispredict-driven pacing,
+40 -1
View File
@@ -259,7 +259,6 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/boot_state.c
${PSXRECOMP_ROOT}/runtime/src/netplay_snap_ring.c
${PSXRECOMP_ROOT}/runtime/src/netplay_state_digest.c
${PSXRECOMP_ROOT}/runtime/src/netplay_hash_confirm.c
${PSXRECOMP_ROOT}/runtime/src/netplay_input_hist.c
${PSXRECOMP_ROOT}/runtime/src/psx_netplay_rb.c
${PSXRECOMP_ROOT}/runtime/src/psx_netplay_sched.c
@@ -377,6 +376,43 @@ else()
endif()
endif()
# Portable rollback host policy (sched/hist/hash_confirm/snap). Sits next to
# recomp-net; MotK keeps thin PSX glue (pad↔frame, boot_state, FMV/dig0 gates).
set(RECOMP_RBENGINE_ROOT "" CACHE PATH "Path to retcomm-rbengine; empty = auto-discover")
if(PSXRECOMP_HAS_RECOMP_NET AND NOT RECOMP_RBENGINE_ROOT)
foreach(_cand
"${PSXRECOMP_ROOT}/lib/retcomm-rbengine"
"${CMAKE_SOURCE_DIR}/../retcomm-rbengine"
"${PSXRECOMP_ROOT}/../retcomm-rbengine")
get_filename_component(_abs "${_cand}" ABSOLUTE)
if(EXISTS "${_abs}/CMakeLists.txt")
set(RECOMP_RBENGINE_ROOT "${_abs}" CACHE PATH
"Path to retcomm-rbengine; empty = auto-discover" FORCE)
break()
endif()
endforeach()
endif()
if(PSXRECOMP_HAS_RECOMP_NET AND RECOMP_RBENGINE_ROOT
AND EXISTS "${RECOMP_RBENGINE_ROOT}/CMakeLists.txt")
if(NOT TARGET retcomm_rbengine)
set(RBE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# recomp_net already added above; rbengine skips nested add_subdirectory.
set(RECOMP_NET_ROOT "${RECOMP_NET_ROOT}" CACHE PATH "" FORCE)
add_subdirectory("${RECOMP_RBENGINE_ROOT}"
"${CMAKE_BINARY_DIR}/retcomm-rbengine")
endif()
set(PSXRECOMP_HAS_RBENGINE TRUE)
message(STATUS "psxrecomp: retcomm-rbengine enabled (${RECOMP_RBENGINE_ROOT})")
else()
set(PSXRECOMP_HAS_RBENGINE FALSE)
if(PSXRECOMP_HAS_RECOMP_NET)
message(FATAL_ERROR
"psxrecomp: PSX_NETPLAY needs retcomm-rbengine.\n"
" git submodule update --init lib/retcomm-rbengine\n"
" or -DRECOMP_RBENGINE_ROOT=/path/to/retcomm-rbengine")
endif()
endif()
# Lobby WebSocket client helpers are vendored under runtime/src/lobby_ws/
# (protocol talks to the proprietary recomp-net-server, not recomp-net).
set(PSXRECOMP_LOBBY_WS_DIR "${PSXRECOMP_ROOT}/runtime/src/lobby_ws")
@@ -1076,6 +1112,9 @@ function(psxrecomp_add_runtime_target target)
if(PSXRECOMP_HAS_RECOMP_NET)
target_compile_definitions(${target} PRIVATE PSX_HAS_RECOMP_NET=1)
target_link_libraries(${target} PRIVATE recomp_net)
if(PSXRECOMP_HAS_RBENGINE)
target_link_libraries(${target} PRIVATE retcomm_rbengine)
endif()
endif()
if(PSXRECOMP_HAS_LOBBY_CLIENT)
target_compile_definitions(${target} PRIVATE PSX_HAS_LOBBY_CLIENT=1)
-163
View File
@@ -1,163 +0,0 @@
#include "netplay_hash_confirm.h"
#include <string.h>
static uint32_t slot_of(uint32_t tick) {
return tick % NETPLAY_HC_RING;
}
void netplay_hc_reset(NetplayHashConfirm* hc) {
if (!hc) return;
memset(hc, 0, sizeof(*hc));
}
void netplay_hc_prime_after(NetplayHashConfirm* hc, uint32_t last_ok) {
if (!hc) return;
memset(hc, 0, sizeof(*hc));
hc->resolved_through = last_ok;
hc->resolved_valid = 1u;
}
static void try_advance(NetplayHashConfirm* hc);
void netplay_hc_note_local(NetplayHashConfirm* hc, uint32_t tick, uint32_t digest) {
uint32_t i;
if (!hc) return;
i = slot_of(tick);
hc->local_tick[i] = tick;
hc->local_digest[i] = digest;
hc->local_valid[i] = 1u;
try_advance(hc);
}
static int local_at(const NetplayHashConfirm* hc, uint32_t tick, uint32_t* dig) {
uint32_t i = slot_of(tick);
if (!hc->local_valid[i] || hc->local_tick[i] != tick) return 0;
if (dig) *dig = hc->local_digest[i];
return 1;
}
static int peer_at(const NetplayHashConfirm* hc, uint32_t tick, uint32_t* dig) {
uint32_t i = slot_of(tick);
if (!hc->peer_valid[i] || hc->peer_tick[i] != tick) return 0;
if (dig) *dig = hc->peer_digest[i];
return 1;
}
static void try_advance(NetplayHashConfirm* hc) {
/* Advance contiguous matches starting just after current watermark. */
for (;;) {
uint32_t next;
uint32_t ld = 0, pd = 0;
if (!hc->resolved_valid)
next = 0u;
else {
if (hc->resolved_through == 0xffffffffu) return;
next = hc->resolved_through + 1u;
}
if (!local_at(hc, next, &ld) || !peer_at(hc, next, &pd))
return;
if (ld != pd)
return;
hc->resolved_through = next;
hc->resolved_valid = 1u;
}
}
void netplay_hc_note_peer(NetplayHashConfirm* hc, uint32_t tick, uint32_t digest) {
uint32_t i;
if (!hc) return;
i = slot_of(tick);
hc->peer_tick[i] = tick;
hc->peer_digest[i] = digest;
hc->peer_valid[i] = 1u;
try_advance(hc);
}
uint32_t netplay_hc_resolved_through(const NetplayHashConfirm* hc) {
if (!hc || !hc->resolved_valid) return 0u;
return hc->resolved_through;
}
uint8_t netplay_hc_confirm_through(const NetplayHashConfirm* hc, uint32_t tick) {
if (!hc || !hc->resolved_valid) return 0u;
return (tick <= hc->resolved_through) ? 1u : 0u;
}
uint8_t netplay_hc_local_digest(const NetplayHashConfirm* hc, uint32_t tick,
uint32_t* digest_out) {
uint32_t d = 0;
if (!hc || !local_at(hc, tick, &d)) return 0u;
if (digest_out) *digest_out = d;
return 1u;
}
uint8_t netplay_hc_peer_digest(const NetplayHashConfirm* hc, uint32_t tick,
uint32_t* digest_out) {
uint32_t d = 0;
if (!hc || !peer_at(hc, tick, &d)) return 0u;
if (digest_out) *digest_out = d;
return 1u;
}
uint8_t netplay_hc_peek_mismatch(const NetplayHashConfirm* hc, uint32_t* tick_out,
uint32_t* local_out, uint32_t* peer_out) {
uint32_t next;
uint32_t ld = 0, pd = 0;
if (!hc) return 0u;
if (!hc->resolved_valid)
next = 0u;
else {
if (hc->resolved_through == 0xffffffffu) return 0u;
next = hc->resolved_through + 1u;
}
if (!local_at(hc, next, &ld) || !peer_at(hc, next, &pd))
return 0u;
if (ld == pd)
return 0u;
if (tick_out) *tick_out = next;
if (local_out) *local_out = ld;
if (peer_out) *peer_out = pd;
return 1u;
}
uint8_t netplay_hc_heal_stale_gap(NetplayHashConfirm* hc) {
uint32_t next;
uint32_t best = 0u;
uint8_t have_best = 0u;
uint32_t i;
uint32_t ld = 0, pd = 0;
if (!hc || !hc->resolved_valid)
return 0u;
if (hc->resolved_through == 0xffffffffu)
return 0u;
next = hc->resolved_through + 1u;
/* Still have both digests for next: let try_advance / peek_mismatch own it. */
if (local_at(hc, next, &ld) && peer_at(hc, next, &pd))
return 0u;
/* Incomplete (one side only) — wait; do not jump over a pending commit. */
if (local_at(hc, next, NULL) || peer_at(hc, next, NULL))
return 0u;
/* next aged out of the ring. Refuse heal if any ring slot above the
* watermark still records a local≠peer pair (live fork still visible). */
for (i = 0; i < NETPLAY_HC_RING; i++) {
uint32_t t;
if (!hc->local_valid[i] || !hc->peer_valid[i])
continue;
if (hc->local_tick[i] != hc->peer_tick[i])
continue;
t = hc->local_tick[i];
if (t <= hc->resolved_through)
continue;
if (hc->local_digest[i] != hc->peer_digest[i])
return 0u; /* known fork still in ring */
if (!have_best || t > best) {
best = t;
have_best = 1u;
}
}
if (!have_best || best <= hc->resolved_through)
return 0u;
hc->resolved_through = best;
return 1u;
}
+2 -110
View File
@@ -6,6 +6,8 @@
#include <string.h>
/* PSX pad ↔ RNetRbFrame only. Ring/invent/promote live in retcomm-rbengine. */
static int8_t u8_to_i8_stick(uint8_t v)
{
int d = (int)v - 0x80;
@@ -19,16 +21,6 @@ static uint8_t i8_to_u8_stick(int8_t v)
return (uint8_t)((int)v + 0x80);
}
void netplay_ih_reset(NetplayInputHist *h, int slot_count)
{
if (!h) return;
memset(h, 0, sizeof(*h));
if (slot_count < 1) slot_count = 1;
if (slot_count > NETPLAY_INPUT_HIST_MAX_SLOTS)
slot_count = NETPLAY_INPUT_HIST_MAX_SLOTS;
h->slot_count = slot_count;
}
void netplay_ih_pad_to_frame(const PsxNetPad *pad, uint32_t tick, uint8_t predicted,
RNetRbFrame *out)
{
@@ -67,104 +59,4 @@ void netplay_ih_frame_to_pad(const RNetRbFrame *frame, PsxNetPad *pad)
psx_netplay_normalize_pad(pad);
}
void netplay_ih_frame_to_contract(const RNetRbFrame *frame, RNetInputContractFrame *out)
{
if (!out) return;
memset(out, 0, sizeof(*out));
if (!frame) return;
out->tick = frame->tick;
out->buttons = frame->buttons;
out->stick_x = frame->stick_x;
out->stick_y = frame->stick_y;
out->is_predicted = frame->is_predicted ? 1u : 0u;
}
int netplay_ih_put(NetplayInputHist *h, int slot, const RNetRbFrame *frame)
{
RNetRbFrame *dst;
if (!h || !frame || !frame->is_valid) return 0;
if (slot < 0 || slot >= h->slot_count) return 0;
dst = &h->rows[slot][frame->tick % NETPLAY_INPUT_HIST_DEPTH];
*dst = *frame;
dst->is_valid = 1u;
return 1;
}
int netplay_ih_get(const NetplayInputHist *h, int slot, uint32_t tick, RNetRbFrame *out)
{
const RNetRbFrame *src;
if (!h || !out) return 0;
if (slot < 0 || slot >= h->slot_count) return 0;
src = &h->rows[slot][tick % NETPLAY_INPUT_HIST_DEPTH];
if (!src->is_valid || src->tick != tick) return 0;
*out = *src;
return 1;
}
int netplay_ih_invent_hold_last(NetplayInputHist *h, int slot, uint32_t tick,
RNetRbFrame *out)
{
RNetRbFrame invented;
RNetRbFrame prev;
uint32_t look;
if (!h || slot < 0 || slot >= h->slot_count) return 0;
memset(&invented, 0, sizeof(invented));
invented.tick = tick;
invented.buttons = 0xFFFFu;
invented.analog = 0u;
invented.is_predicted = 1u;
invented.is_valid = 1u;
for (look = 1; look < NETPLAY_INPUT_HIST_DEPTH; look++) {
if (tick < look) break;
if (netplay_ih_get(h, slot, tick - look, &prev)) {
invented.buttons = prev.buttons;
invented.stick_x = prev.stick_x;
invented.stick_y = prev.stick_y;
invented.analog = prev.analog ? 1u : 0u;
break;
}
}
if (!netplay_ih_put(h, slot, &invented)) return 0;
h->invent_count++;
if (out) *out = invented;
return 1;
}
int netplay_ih_invent_idle(NetplayInputHist *h, int slot, uint32_t tick,
RNetRbFrame *out)
{
RNetRbFrame invented;
if (!h || slot < 0 || slot >= h->slot_count) return 0;
memset(&invented, 0, sizeof(invented));
invented.tick = tick;
invented.buttons = 0xFFFFu;
invented.analog = 0u;
invented.is_predicted = 1u;
invented.is_valid = 1u;
/* stick_x/y = 0 → neutral 0x80 via frame_to_pad */
if (!netplay_ih_put(h, slot, &invented)) return 0;
h->invent_count++;
if (out) *out = invented;
return 1;
}
int netplay_ih_promote(NetplayInputHist *h, int slot, const RNetRbFrame *wire)
{
RNetRbFrame auth;
if (!h || !wire || !wire->is_valid) return 0;
auth = *wire;
auth.is_predicted = 0u;
auth.is_valid = 1u;
if (!netplay_ih_put(h, slot, &auth)) return 0;
h->promote_count++;
return 1;
}
#endif /* PSX_HAS_RECOMP_NET */
+17 -219
View File
@@ -1,9 +1,9 @@
/* clock_gettime/CLOCK_MONOTONIC are POSIX, not ISO C. Under a strict -std=cNN
* (no GNU extensions) glibc hides them unless _POSIX_C_SOURCE is set, so this
* TU fails to compile — loudly under -Werror, silently as an implicit
* declaration without it. The runtime build happens to use -std=gnu99, which
* is why this never surfaced there. Must precede the first libc header:
* glibc's <features.h> latches the feature-test macros on first inclusion. */
/* MotK snap save/load over retcomm-rbengine ring + boot_state serialize. */
#if !defined(PSX_HAS_RECOMP_NET)
/* Empty TU when netplay/rbengine is not linked. */
#else
#ifndef _POSIX_C_SOURCE
# define _POSIX_C_SOURCE 200809L
#endif
@@ -12,8 +12,6 @@
#include "boot_state.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
# include <windows.h>
#else
@@ -35,161 +33,11 @@ static double snap_mono_ms(void)
#endif
}
typedef struct NetplaySnapSlot {
int valid;
uint32_t tick;
uint8_t* data;
size_t size;
} NetplaySnapSlot;
struct NetplaySnapRing {
NetplaySnapSlot* slots;
uint32_t depth;
uint32_t count;
/* Next write index in the circular buffer (0 .. depth-1). */
uint32_t head;
};
static void slot_clear(NetplaySnapSlot* s) {
free(s->data);
s->data = NULL;
s->size = 0;
s->tick = 0;
s->valid = 0;
}
static int find_slot(const NetplaySnapRing* r, uint32_t tick) {
uint32_t i;
if (!r) return -1;
for (i = 0; i < r->depth; i++) {
if (r->slots[i].valid && r->slots[i].tick == tick)
return (int)i;
}
return -1;
}
NetplaySnapRing* netplay_snap_ring_create(uint32_t depth) {
NetplaySnapRing* r;
if (depth == 0)
depth = NETPLAY_SNAP_RING_DEFAULT_DEPTH;
if (depth > 512u)
depth = 512u;
r = (NetplaySnapRing*)calloc(1, sizeof(*r));
if (!r) return NULL;
r->slots = (NetplaySnapSlot*)calloc(depth, sizeof(NetplaySnapSlot));
if (!r->slots) {
free(r);
return NULL;
}
r->depth = depth;
return r;
}
void netplay_snap_ring_destroy(NetplaySnapRing* r) {
if (!r) return;
netplay_snap_ring_clear(r);
free(r->slots);
free(r);
}
void netplay_snap_ring_clear(NetplaySnapRing* r) {
uint32_t i;
if (!r) return;
for (i = 0; i < r->depth; i++)
slot_clear(&r->slots[i]);
r->count = 0;
r->head = 0;
}
uint32_t netplay_snap_ring_depth(const NetplaySnapRing* r) {
return r ? r->depth : 0u;
}
uint32_t netplay_snap_ring_count(const NetplaySnapRing* r) {
return r ? r->count : 0u;
}
int netplay_snap_ring_has(const NetplaySnapRing* r, uint32_t tick) {
return find_slot(r, tick) >= 0;
}
int netplay_snap_ring_store(NetplaySnapRing* r, uint32_t tick,
uint8_t* data, size_t size) {
int existing;
NetplaySnapSlot* s;
if (!r || !data || !size) return 0;
existing = find_slot(r, tick);
if (existing >= 0) {
s = &r->slots[existing];
free(s->data);
s->data = data;
s->size = size;
s->tick = tick;
s->valid = 1;
return 1;
}
if (r->count < r->depth) {
/* Prefer an unused slot; walk from head for locality. */
uint32_t i;
for (i = 0; i < r->depth; i++) {
uint32_t idx = (r->head + i) % r->depth;
if (!r->slots[idx].valid) {
s = &r->slots[idx];
s->data = data;
s->size = size;
s->tick = tick;
s->valid = 1;
r->count++;
r->head = (idx + 1u) % r->depth;
return 1;
}
}
}
/* Evict the oldest (lowest tick) occupied slot. */
{
uint32_t i;
int victim = -1;
uint32_t oldest = 0xffffffffu;
for (i = 0; i < r->depth; i++) {
if (!r->slots[i].valid) continue;
if (r->slots[i].tick <= oldest) {
oldest = r->slots[i].tick;
victim = (int)i;
}
}
if (victim < 0) {
free(data);
return 0;
}
s = &r->slots[victim];
free(s->data);
s->data = data;
s->size = size;
s->tick = tick;
s->valid = 1;
r->head = ((uint32_t)victim + 1u) % r->depth;
return 1;
}
}
const uint8_t* netplay_snap_ring_peek(const NetplaySnapRing* r, uint32_t tick,
size_t* size_out) {
int idx = find_slot(r, tick);
if (idx < 0) {
if (size_out) *size_out = 0;
return NULL;
}
if (size_out) *size_out = r->slots[idx].size;
return r->slots[idx].data;
}
int netplay_snap_ring_save(NetplaySnapRing* r, uint32_t tick,
const CPUState* cpu, uint32_t bios_checksum,
uint32_t entry_pc) {
uint8_t* data = NULL;
int netplay_snap_ring_save(NetplaySnapRing *r, uint32_t tick,
const CPUState *cpu, uint32_t bios_checksum,
uint32_t entry_pc)
{
uint8_t *data = NULL;
size_t len = 0;
double t0, dt;
static uint32_t s_tele_n;
@@ -198,8 +46,6 @@ int netplay_snap_ring_save(NetplaySnapRing* r, uint32_t tick,
static uint64_t s_tele_dirty_sum;
static uint32_t s_tele_incr_n;
if (!r || !cpu) return 0;
/* Raw (no zlib): compress2 on RAM+VRAM+SPU dominated live FPS. Load
* accepts uncompressed v4 sections. §96: dirty-scanline VRAM mirror. */
t0 = snap_mono_ms();
if (!boot_state_save_buffer_raw(cpu, bios_checksum, entry_pc, &data, &len))
return 0;
@@ -210,7 +56,6 @@ int netplay_snap_ring_save(NetplaySnapRing* r, uint32_t tick,
s_tele_dirty_sum += (uint64_t)boot_state_last_vram_dirty_rows();
if (boot_state_last_vram_incremental())
s_tele_incr_n++;
/* Heartbeat every 64 saves — visible during FMV dense / media windows. */
if ((s_tele_n & 63u) == 0u) {
fprintf(stderr,
"psxrecomp: rb snap tele n=%u last=%.2fms avg=%.2fms "
@@ -225,64 +70,17 @@ int netplay_snap_ring_save(NetplaySnapRing* r, uint32_t tick,
fflush(stderr);
}
if (!netplay_snap_ring_store(r, tick, data, len))
return 0; /* store frees data on hard failure */
return 0;
return 1;
}
int netplay_snap_ring_load(NetplaySnapRing* r, uint32_t tick, CPUState* cpu,
uint32_t bios_checksum, uint32_t entry_pc) {
int netplay_snap_ring_load(NetplaySnapRing *r, uint32_t tick, CPUState *cpu,
uint32_t bios_checksum, uint32_t entry_pc)
{
size_t size = 0;
const uint8_t* data = netplay_snap_ring_peek(r, tick, &size);
const uint8_t *data = netplay_snap_ring_peek(r, tick, &size);
if (!data || !size || !cpu) return 0;
return boot_state_load_buffer(data, size, bios_checksum, entry_pc, cpu);
}
int netplay_snap_ring_drop_tick(NetplaySnapRing* r, uint32_t tick) {
int idx = find_slot(r, tick);
if (idx < 0) return 0;
slot_clear(&r->slots[idx]);
if (r->count) r->count--;
return 1;
}
uint32_t netplay_snap_ring_drop_after(NetplaySnapRing* r, uint32_t tick) {
uint32_t i, n = 0;
if (!r) return 0;
for (i = 0; i < r->depth; i++) {
if (!r->slots[i].valid) continue;
if (r->slots[i].tick > tick) {
slot_clear(&r->slots[i]);
if (r->count) r->count--;
n++;
}
}
return n;
}
uint32_t netplay_snap_ring_oldest_tick(const NetplaySnapRing* r) {
uint32_t i, oldest = 0xffffffffu;
int any = 0;
if (!r) return 0;
for (i = 0; i < r->depth; i++) {
if (!r->slots[i].valid) continue;
if (!any || r->slots[i].tick < oldest) {
oldest = r->slots[i].tick;
any = 1;
}
}
return any ? oldest : 0u;
}
uint32_t netplay_snap_ring_newest_tick(const NetplaySnapRing* r) {
uint32_t i, newest = 0;
int any = 0;
if (!r) return 0;
for (i = 0; i < r->depth; i++) {
if (!r->slots[i].valid) continue;
if (!any || r->slots[i].tick > newest) {
newest = r->slots[i].tick;
any = 1;
}
}
return any ? newest : 0u;
}
#endif /* PSX_HAS_RECOMP_NET */
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -21,12 +21,16 @@
# define _POSIX_C_SOURCE 200809L
#endif
#ifndef PSX_HAS_RECOMP_NET
# define PSX_HAS_RECOMP_NET 1
#endif
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* Stub boot_state so the ring TU links without the full runtime. */
/* Stub boot_state so the MotK save/load glue links without the full runtime. */
#ifdef NETPLAY_SNAP_RING_TEST_STUB_BOOT_STATE
#include "cpu_state.h"
int boot_state_save_buffer(const CPUState* cpu, uint32_t bios_checksum,
@@ -55,6 +59,7 @@ uint32_t boot_state_last_vram_dirty_rows(void) { return 0; }
int boot_state_last_vram_incremental(void) { return 0; }
#endif
#include "netplay_snap_ring.h"
#include "../src/netplay_snap_ring.c"
static int failures;
+1
View File
@@ -36,3 +36,4 @@ pin() {
pin psxrecomp psxrecomp
pin recomp-ui recomp-ui
pin recomp-net psxrecomp/lib/recomp-net
pin retcomm-rbengine psxrecomp/lib/retcomm-rbengine
+1
View File
@@ -46,6 +46,7 @@ path_for() {
psxrecomp) echo psxrecomp ;;
recomp-ui) echo recomp-ui ;;
recomp-net) echo psxrecomp/lib/recomp-net ;;
retcomm-rbengine) echo psxrecomp/lib/retcomm-rbengine ;;
*) echo "" ;;
esac
}
@@ -630,6 +630,9 @@ fi
if [ "$ENABLE_NETPLAY" -eq 1 ] && [ ! -f psxrecomp/lib/recomp-net/CMakeLists.txt ]; then
echo "warning: netplay enabled but psxrecomp/lib/recomp-net missing after recursive update." >&2
fi
if [ "$ENABLE_NETPLAY" -eq 1 ] && [ ! -f psxrecomp/lib/retcomm-rbengine/CMakeLists.txt ]; then
echo "warning: netplay enabled but psxrecomp/lib/retcomm-rbengine missing after recursive update." >&2
fi
echo "== Packager stub =="
fill_template "$TEMPLATE_DIR/package_setup_release.sh.in" \