code wiki / (root) / nx_rollback_session.nx

nx_rollback_session.nx source

↩ module page · 370 lines · 15616 B

1// nx_rollback_session.nx -- GGPO-class rollback netcode session handle. 2// 3// Composes the five foundation primitives: 4// - nx_sim_snapshot : ring of last-N state snapshots 5// - nx_state_delta_codec : 16-byte input packets via bit-packed varint 6// - nx_frame_hash : per-frame state hash for desync detection 7// - nx_zigzag_varint : signed-integer wire encoding 8// - (caller's sim primitives -- whatever advances the game state) 9// 10// ...into a single session handle that game code holds for one 11// multiplayer match. Modelled directly on GGPO's session callbacks 12// (Cannon 2006 / pond3r/ggpo) but adapted to NishiLang's callback- 13// less calling convention: storage-only, caller drives the sim loop. 14// 15// Caller pattern per tick: 16// 1. read local input -> save via session_save_local_input 17// 2. snapshot state -> save via session_save_state 18// 3. advance sim using local + last-known remote input 19// 4. compute frame_hash -> save via session_save_hash 20// 5. transmit (tick, local_input, frame_hash) to peer 21// 6. when (peer_tick, peer_input) arrives: 22// a. session_set_remote_input(peer_tick, peer_input) 23// b. if peer_tick < current_tick: 24// - session_restore_state(peer_tick) into sim state 25// - re-advance from peer_tick to current_tick with 26// freshly-corrected remote input each tick 27// 7. when (peer_tick, peer_hash) arrives: 28// a. our_hash = session_get_hash(peer_tick) 29// b. verdict = nx_frame_hash_compare(...) 30// c. if MISMATCH -> trigger desync recovery 31// 32// Session layout (i64 cells in a flat block): 33// h[0] = state_size 34// h[1] = max_rollback_frames (snapshot/input ring depth) 35// h[2] = current_tick (highest local frame saved) 36// h[3] = oldest_pending_remote_frame 37// (= highest local frame we have NOT yet received remote input for; 38// before we can rollback to frame F we need remote input at F) 39// h[4] = snapshot_ring_ptr (nx_snapshot_ring_alloc handle) 40// h[5] = local_input_ring_ptr (parallel i64 array, depth slots) 41// h[6] = remote_input_ring_ptr (parallel i64 array, depth slots) 42// h[7] = remote_input_present_ring_ptr (1/0 per slot) 43// h[8] = local_hash_ring_ptr (i64 hash per frame) 44// h[9] = local_hash_present_ring_ptr (1/0 per slot) 45// 46// Inputs are encoded as a single i64. Callers using >64 bits of input 47// state can pack into multiple sessions or use a wider encoding -- v1 48// stays i64 to match the GGPO 16-byte input convention (2x i64 = 16B). 49 50// nx_safety_envelope: 51// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 52// sil_target: SIL1 53// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 54// verdict: NOT_YET_EVALUATED 55 56import "nx_syscalls.nx" 57import "nx_tier.nx" 58import "nx_sim_snapshot.nx" 59import "nx_frame_hash.nx" 60 61const NX_RBS_HDR_STATE_SIZE: nx_int = 0 62const NX_RBS_HDR_MAX_ROLLBACK: nx_int = 1 63const NX_RBS_HDR_CURRENT_TICK: nx_int = 2 64const NX_RBS_HDR_OLDEST_PEND_REM: nx_int = 3 65const NX_RBS_HDR_SNAPSHOT_RING: nx_int = 4 66const NX_RBS_HDR_LOCAL_INPUT: nx_int = 5 67const NX_RBS_HDR_REMOTE_INPUT: nx_int = 6 68const NX_RBS_HDR_REMOTE_PRESENT: nx_int = 7 69const NX_RBS_HDR_LOCAL_HASH: nx_int = 8 70const NX_RBS_HDR_HASH_PRESENT: nx_int = 9 71const NX_RBS_HDR_SIZE: nx_int = 10 72 73// Result codes for the rollback-trigger query. 74const NX_RBS_NO_ROLLBACK: nx_int = 0 75const NX_RBS_NEED_ROLLBACK: nx_int = 1 76const NX_RBS_REMOTE_TOO_OLD: nx_int = 2 77 78// ===== Allocation ==================================================== 79 80func nx_rollback_session_new(state_size: nx_int, max_rollback: nx_int) -> *i64 { 81 if state_size <= 0 { return 0 as *i64 } 82 if max_rollback <= 0 { return 0 as *i64 } 83 let h: *i64 = (sys_mmap(NX_RBS_HDR_SIZE * 8)) as *i64 84 85 let snap: *i64 = nx_snapshot_ring_alloc(state_size, max_rollback) 86 if (snap as i64) == 0 { return 0 as *i64 } 87 88 let loc_in: *i64 = (sys_mmap(max_rollback * 8)) as *i64 89 let rem_in: *i64 = (sys_mmap(max_rollback * 8)) as *i64 90 let rem_pre: *i64 = (sys_mmap(max_rollback * 8)) as *i64 91 let lhash: *i64 = (sys_mmap(max_rollback * 8)) as *i64 92 let hpre: *i64 = (sys_mmap(max_rollback * 8)) as *i64 93 94 var i: nx_int = 0 95 while i < max_rollback { 96 loc_in[i] = 0 97 rem_in[i] = 0 98 rem_pre[i] = 0 99 lhash[i] = 0 100 hpre[i] = 0 101 i = i + 1 102 } 103 104 h[NX_RBS_HDR_STATE_SIZE] = state_size 105 h[NX_RBS_HDR_MAX_ROLLBACK] = max_rollback 106 h[NX_RBS_HDR_CURRENT_TICK] = -1 107 h[NX_RBS_HDR_OLDEST_PEND_REM] = 0 108 h[NX_RBS_HDR_SNAPSHOT_RING] = snap as i64 109 h[NX_RBS_HDR_LOCAL_INPUT] = loc_in as i64 110 h[NX_RBS_HDR_REMOTE_INPUT] = rem_in as i64 111 h[NX_RBS_HDR_REMOTE_PRESENT] = rem_pre as i64 112 h[NX_RBS_HDR_LOCAL_HASH] = lhash as i64 113 h[NX_RBS_HDR_HASH_PRESENT] = hpre as i64 114 return h 115} 116 117// ===== Slot indexing ================================================ 118 119func _rbs_slot(h: *i64, frame: nx_int) -> nx_int { 120 let depth: nx_int = h[NX_RBS_HDR_MAX_ROLLBACK] 121 var s: nx_int = frame % depth 122 if s < 0 { s = s + depth } 123 return s 124} 125 126// ===== Local-side save APIs ========================================= 127 128func nx_rollback_session_save_local_input(s: *i64, frame: nx_int, input: i64) -> nx_int { 129 if (s as i64) == 0 { return -1 } 130 if frame < 0 { return -1 } 131 let slot: nx_int = _rbs_slot(s, frame) 132 let ring: *i64 = (s[NX_RBS_HDR_LOCAL_INPUT]) as *i64 133 ring[slot] = input 134 let cur: nx_int = s[NX_RBS_HDR_CURRENT_TICK] 135 if frame > cur { s[NX_RBS_HDR_CURRENT_TICK] = frame } 136 return 0 137} 138 139func nx_rollback_session_save_state(s: *i64, frame: nx_int, src: *u8) -> nx_int { 140 if (s as i64) == 0 { return -1 } 141 let ring: *i64 = (s[NX_RBS_HDR_SNAPSHOT_RING]) as *i64 142 return nx_snapshot_ring_save(ring, frame, src) 143} 144 145func nx_rollback_session_save_hash(s: *i64, frame: nx_int, hash: i64) -> nx_int { 146 if (s as i64) == 0 { return -1 } 147 if frame < 0 { return -1 } 148 let slot: nx_int = _rbs_slot(s, frame) 149 let h_ring: *i64 = (s[NX_RBS_HDR_LOCAL_HASH]) as *i64 150 let pre_ring: *i64 = (s[NX_RBS_HDR_HASH_PRESENT]) as *i64 151 h_ring[slot] = hash 152 pre_ring[slot] = 1 153 return 0 154} 155 156// ===== Remote-input arrival ========================================= 157// 158// Returns one of: 159// NX_RBS_NO_ROLLBACK -- remote input was for current_tick or later; 160// no rollback needed; sim will pick it up 161// on the next tick. 162// NX_RBS_NEED_ROLLBACK -- remote input was for a past frame the sim 163// already advanced beyond. Caller must call 164// nx_rollback_session_restore_state(frame) 165// and re-advance from `frame` to current_tick. 166// NX_RBS_REMOTE_TOO_OLD-- remote input was older than oldest snapshot 167// still in the ring; rollback impossible 168// (caller should resync from scratch or 169// disconnect peer). 170 171func nx_rollback_session_set_remote_input(s: *i64, frame: nx_int, input: i64) -> nx_int { 172 if (s as i64) == 0 { return NX_RBS_REMOTE_TOO_OLD } 173 if frame < 0 { return NX_RBS_REMOTE_TOO_OLD } 174 let depth: nx_int = s[NX_RBS_HDR_MAX_ROLLBACK] 175 let cur_tick: nx_int = s[NX_RBS_HDR_CURRENT_TICK] 176 177 if frame > cur_tick { 178 // Future frame; just store -- sim hasn't advanced this far yet. 179 let slot: nx_int = _rbs_slot(s, frame) 180 let in_ring: *i64 = (s[NX_RBS_HDR_REMOTE_INPUT]) as *i64 181 let pre_ring: *i64 = (s[NX_RBS_HDR_REMOTE_PRESENT]) as *i64 182 in_ring[slot] = input 183 pre_ring[slot] = 1 184 return NX_RBS_NO_ROLLBACK 185 } 186 187 // frame <= cur_tick; check if it's still in the snapshot window. 188 let snap_ring: *i64 = (s[NX_RBS_HDR_SNAPSHOT_RING]) as *i64 189 let oldest: nx_int = nx_snapshot_ring_oldest(snap_ring) 190 if oldest < 0 { return NX_RBS_REMOTE_TOO_OLD } 191 if frame < oldest { return NX_RBS_REMOTE_TOO_OLD } 192 193 let slot: nx_int = _rbs_slot(s, frame) 194 let in_ring: *i64 = (s[NX_RBS_HDR_REMOTE_INPUT]) as *i64 195 let pre_ring: *i64 = (s[NX_RBS_HDR_REMOTE_PRESENT]) as *i64 196 197 let prev_present: i64 = pre_ring[slot] 198 let prev_input: i64 = in_ring[slot] 199 in_ring[slot] = input 200 pre_ring[slot] = 1 201 202 // If this is the FIRST time we've seen remote input for this frame, 203 // or if the input value differs from what we used during sim, 204 // rollback is needed. 205 if prev_present == 0 { return NX_RBS_NEED_ROLLBACK } 206 if prev_input != input { return NX_RBS_NEED_ROLLBACK } 207 return NX_RBS_NO_ROLLBACK 208} 209 210// ===== Restore from snapshot ======================================== 211 212func nx_rollback_session_restore_state(s: *i64, frame: nx_int, dst: *u8) -> nx_int { 213 if (s as i64) == 0 { return -1 } 214 let ring: *i64 = (s[NX_RBS_HDR_SNAPSHOT_RING]) as *i64 215 return nx_snapshot_ring_restore(ring, frame, dst) 216} 217 218// ===== Read helpers ================================================= 219 220func nx_rollback_session_get_local_input(s: *i64, frame: nx_int) -> i64 { 221 if (s as i64) == 0 { return 0 } 222 if frame < 0 { return 0 } 223 let slot: nx_int = _rbs_slot(s, frame) 224 let ring: *i64 = (s[NX_RBS_HDR_LOCAL_INPUT]) as *i64 225 return ring[slot] 226} 227 228func nx_rollback_session_get_remote_input(s: *i64, frame: nx_int) -> i64 { 229 if (s as i64) == 0 { return 0 } 230 if frame < 0 { return 0 } 231 let slot: nx_int = _rbs_slot(s, frame) 232 let ring: *i64 = (s[NX_RBS_HDR_REMOTE_INPUT]) as *i64 233 return ring[slot] 234} 235 236func nx_rollback_session_has_remote_input(s: *i64, frame: nx_int) -> nx_int { 237 if (s as i64) == 0 { return 0 } 238 if frame < 0 { return 0 } 239 let slot: nx_int = _rbs_slot(s, frame) 240 let pre_ring: *i64 = (s[NX_RBS_HDR_REMOTE_PRESENT]) as *i64 241 return pre_ring[slot] as nx_int 242} 243 244func nx_rollback_session_get_hash(s: *i64, frame: nx_int) -> i64 { 245 if (s as i64) == 0 { return 0 } 246 if frame < 0 { return 0 } 247 let slot: nx_int = _rbs_slot(s, frame) 248 let h_ring: *i64 = (s[NX_RBS_HDR_LOCAL_HASH]) as *i64 249 let pre_ring: *i64 = (s[NX_RBS_HDR_HASH_PRESENT]) as *i64 250 if pre_ring[slot] == 0 { return 0 } 251 return h_ring[slot] 252} 253 254func nx_rollback_session_current_tick(s: *i64) -> nx_int { 255 if (s as i64) == 0 { return -1 } 256 return s[NX_RBS_HDR_CURRENT_TICK] 257} 258 259// ===== Self-test ==================================================== 260 261// A tiny deterministic faux-simulation: state is a single i64 counter; 262// each tick adds (local_input + remote_input) to it. This lets the 263// smoke verify rollback re-simulation matches "ran with the correct 264// inputs from the start" exactly. 265 266func _faux_advance(state: i64, local_in: i64, remote_in: i64) -> i64 { 267 return state + local_in + remote_in 268} 269 270func main() -> i64 { 271 let STATE_BYTES: nx_int = 8 272 let DEPTH: nx_int = 8 273 274 let s: *i64 = nx_rollback_session_new(STATE_BYTES, DEPTH) 275 if (s as i64) == 0 { return __syscall(93, 1, 0, 0, 0, 0, 0) } 276 277 // T1: empty session reports current_tick = -1. 278 if nx_rollback_session_current_tick(s) != -1 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 279 280 // T2: drive 6 ticks with local=1, remote=2 each tick. Expected 281 // final state = 6 * (1 + 2) = 18. 282 let state_buf: *i64 = (sys_mmap(8)) as *i64 283 state_buf[0] = 0 284 var t: nx_int = 0 285 while t < 6 { 286 // Local pre-tick: 287 nx_rollback_session_save_local_input(s, t, 1) 288 nx_rollback_session_save_state(s, t, state_buf as *u8) 289 let remote: i64 = nx_rollback_session_get_remote_input(s, t) 290 // For T2 we already have remote input set externally; simulate 291 // having received it before tick. 292 state_buf[0] = _faux_advance(state_buf[0], 1, 2) 293 // Save hash of post-tick state. 294 let h: i64 = nx_frame_hash_region(state_buf as *u8, 8) 295 nx_rollback_session_save_hash(s, t, h) 296 // Treat remote as already-known for this run (skip the rollback 297 // path; we test it in T3). 298 nx_rollback_session_set_remote_input(s, t, 2) 299 t = t + 1 300 } 301 if state_buf[0] != 18 { return __syscall(93, 10, 0, 0, 0, 0, 0) } 302 if nx_rollback_session_current_tick(s) != 5 { return __syscall(93, 11, 0, 0, 0, 0, 0) } 303 304 // T3: ROLLBACK CORRECTION. Build a fresh session; advance 4 ticks 305 // assuming remote_input = 0 (we haven't received it yet); then 306 // remote arrives saying it was actually 3 for tick 2, triggering 307 // rollback. Verify post-rollback state matches "ran from start 308 // with correct values". 309 let s2: *i64 = nx_rollback_session_new(STATE_BYTES, DEPTH) 310 let sb2: *i64 = (sys_mmap(8)) as *i64 311 sb2[0] = 0 312 // Initial 4 ticks with assumed remote=0: 313 var t2: nx_int = 0 314 while t2 < 4 { 315 nx_rollback_session_save_local_input(s2, t2, 1) 316 nx_rollback_session_save_state(s2, t2, sb2 as *u8) 317 let assumed_remote: i64 = 0 318 sb2[0] = _faux_advance(sb2[0], 1, assumed_remote) 319 t2 = t2 + 1 320 } 321 // Expected: 4 ticks * (1+0) = 4. 322 if sb2[0] != 4 { return __syscall(93, 20, 0, 0, 0, 0, 0) } 323 324 // Remote correction for tick 2 arrives: it was actually 3. 325 let verdict: nx_int = nx_rollback_session_set_remote_input(s2, 2, 3) 326 if verdict != NX_RBS_NEED_ROLLBACK { return __syscall(93, 21, 0, 0, 0, 0, 0) } 327 328 // Caller-driven rollback: restore state at tick 2, re-advance. 329 nx_rollback_session_restore_state(s2, 2, sb2 as *u8) 330 // sb2[0] should now be the pre-tick-2 state which was 2 (after 331 // ticks 0 and 1 with assumed remote=0: 0 -> 1 -> 2). 332 if sb2[0] != 2 { return __syscall(93, 22, 0, 0, 0, 0, 0) } 333 334 // Re-advance ticks 2 and 3 with corrected remote=3 for tick 2 and 335 // remote=0 (unknown) for tick 3. 336 var rb: nx_int = 2 337 while rb < 4 { 338 let local_in: i64 = nx_rollback_session_get_local_input(s2, rb) 339 let rem_in: i64 = nx_rollback_session_get_remote_input(s2, rb) 340 let has: nx_int = nx_rollback_session_has_remote_input(s2, rb) 341 var use_rem: i64 = 0 342 if has == 1 { use_rem = rem_in } 343 sb2[0] = _faux_advance(sb2[0], local_in, use_rem) 344 rb = rb + 1 345 } 346 // Expected: state at tick 2 was 2; advance tick 2 (local=1, remote=3) -> 6; 347 // advance tick 3 (local=1, remote=0) -> 7. 348 if sb2[0] != 7 { return __syscall(93, 23, 0, 0, 0, 0, 0) } 349 350 // T4: remote input arriving for a future frame returns NO_ROLLBACK. 351 let v_future: nx_int = nx_rollback_session_set_remote_input(s2, 100, 5) 352 if v_future != NX_RBS_NO_ROLLBACK { return __syscall(93, 30, 0, 0, 0, 0, 0) } 353 if nx_rollback_session_get_remote_input(s2, 100) != 5 { return __syscall(93, 31, 0, 0, 0, 0, 0) } 354 355 // T5: idempotent remote input (same value twice for same frame) 356 // does NOT trigger rollback the second time. 357 nx_rollback_session_set_remote_input(s2, 2, 9) // first time, fresh value -> NEED_ROLLBACK 358 let v_again: nx_int = nx_rollback_session_set_remote_input(s2, 2, 9) 359 if v_again != NX_RBS_NO_ROLLBACK { return __syscall(93, 40, 0, 0, 0, 0, 0) } 360 361 // T6: hash save/retrieve. 362 nx_rollback_session_save_hash(s, 3, 0xCAFEBABEDEADBEEF) 363 if nx_rollback_session_get_hash(s, 3) != 0xCAFEBABEDEADBEEF { return __syscall(93, 50, 0, 0, 0, 0, 0) } 364 365 // T7: hash on never-saved frame returns 0 (not present). 366 let s3: *i64 = nx_rollback_session_new(STATE_BYTES, DEPTH) 367 if nx_rollback_session_get_hash(s3, 5) != 0 { return __syscall(93, 60, 0, 0, 0, 0, 0) } 368 369 return 0 370}