From ade3f42620288c5dd81937e331a9d9c69097f353 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 15:14:40 -0300 Subject: [PATCH 01/14] WIP --- samples/SpaceWar.Shared/GameSession.cs | 2 +- samples/SpaceWar/Game1.cs | 2 +- src/Backdash/Options/NetcodeOptions.cs | 2 +- src/Backdash/Session/Backends/LocalSession.cs | 2 +- src/Backdash/Session/Backends/RemoteSession.cs | 2 +- src/Backdash/Session/Backends/ReplaySession.cs | 2 +- src/Backdash/Session/Backends/SpectatorSession.cs | 2 +- src/Backdash/Session/Backends/SyncTestSession.cs | 4 ++-- src/Backdash/Session/INetcodeSession.cs | 12 ++++++------ src/Backdash/Session/NetcodeSessionExtensions.cs | 2 +- src/Backdash/Synchronizing/Input/Synchronizer.cs | 2 +- .../Synchronizing/State/DefaultStateStore.cs | 14 +++++++------- src/Backdash/Synchronizing/State/IStateStore.cs | 12 ++++++------ .../State/{SavedFrame.cs => SavedState.cs} | 4 ++-- src/Backdash/Synchronizing/State/StateSnapshot.cs | 8 ++++++-- 15 files changed, 38 insertions(+), 34 deletions(-) rename src/Backdash/Synchronizing/State/{SavedFrame.cs => SavedState.cs} (83%) diff --git a/samples/SpaceWar.Shared/GameSession.cs b/samples/SpaceWar.Shared/GameSession.cs index 4cde5f49..52844f37 100644 --- a/samples/SpaceWar.Shared/GameSession.cs +++ b/samples/SpaceWar.Shared/GameSession.cs @@ -117,7 +117,7 @@ public void TimeSync(FrameSpan framesAhead) void UpdateStats() { nonGameState.RollbackFrames = session.RollbackFrames; - var saved = session.GetCurrentSavedFrame(); + var saved = session.GetSavedState(); nonGameState.StateChecksum = saved.Checksum; nonGameState.StateSize = saved.Size; } diff --git a/samples/SpaceWar/Game1.cs b/samples/SpaceWar/Game1.cs index ef341f5f..61341dcf 100644 --- a/samples/SpaceWar/Game1.cs +++ b/samples/SpaceWar/Game1.cs @@ -170,7 +170,7 @@ void HandleNonGameKeys() if (keyboard.IsKeyPressed(Keys.S)) { - snapshot = session.CurrentStateSnapshot(); + snapshot = session.GetStateSnapshot(); session.WriteLog($"Snapshot saved {snapshot?.Frame}"); } else if (keyboard.IsKeyPressed(Keys.L) && snapshot is not null) diff --git a/src/Backdash/Options/NetcodeOptions.cs b/src/Backdash/Options/NetcodeOptions.cs index 0b734f8e..9d07eb09 100644 --- a/src/Backdash/Options/NetcodeOptions.cs +++ b/src/Backdash/Options/NetcodeOptions.cs @@ -83,7 +83,7 @@ internal EndiannessSerializer.INumberSerializer GetEndiannessNumberStateSerializ public int InputDelayFrames { get; set; } = 2; /// - /// Value to override the total number of in state store. + /// Value to override the total number of in state store. /// /// Defaults to + /// diff --git a/src/Backdash/Session/Backends/LocalSession.cs b/src/Backdash/Session/Backends/LocalSession.cs index a4277fe6..272df3ec 100644 --- a/src/Backdash/Session/Backends/LocalSession.cs +++ b/src/Backdash/Session/Backends/LocalSession.cs @@ -75,7 +75,7 @@ SessionServices services public FrameSpan FramesBehind => FrameSpan.Zero; public FrameSpan RollbackFrames => FrameSpan.Zero; public bool IsInRollback => false; - public SavedFrame GetCurrentSavedFrame() => stateStore.Last(); + public SavedState GetSavedState() => stateStore.Last(); public IReadOnlySet GetPlayers() => addedPlayers; diff --git a/src/Backdash/Session/Backends/RemoteSession.cs b/src/Backdash/Session/Backends/RemoteSession.cs index 1b97a1be..4006d7d9 100644 --- a/src/Backdash/Session/Backends/RemoteSession.cs +++ b/src/Backdash/Session/Backends/RemoteSession.cs @@ -199,7 +199,7 @@ void Close() public FrameSpan FramesBehind => synchronizer.FramesBehind; public FrameSpan RollbackFrames => synchronizer.RollbackFrames; public bool IsInRollback => synchronizer.InRollback; - public SavedFrame GetCurrentSavedFrame() => synchronizer.GetLastSavedFrame(); + public SavedState GetSavedState() => synchronizer.GetLastSavedFrame(); public int NumberOfPlayers => addedPlayers.Count; public int NumberOfSpectators => addedSpectators.Count; public int LocalPort => udp.BindPort; diff --git a/src/Backdash/Session/Backends/ReplaySession.cs b/src/Backdash/Session/Backends/ReplaySession.cs index 2e23f560..b45f0749 100644 --- a/src/Backdash/Session/Backends/ReplaySession.cs +++ b/src/Backdash/Session/Backends/ReplaySession.cs @@ -101,7 +101,7 @@ public void Close() public FrameSpan RollbackFrames => FrameSpan.Zero; public FrameSpan FramesBehind => FrameSpan.Zero; public bool IsInRollback => false; - public SavedFrame GetCurrentSavedFrame() => stateStore.Last(); + public SavedState GetSavedState() => stateStore.Last(); public int NumberOfSpectators => 0; public int LocalPort => 0; diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index 43d810cb..fc565362 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -158,7 +158,7 @@ public void Close() public FrameSpan RollbackFrames => FrameSpan.Zero; public FrameSpan FramesBehind => FrameSpan.Zero; public bool IsInRollback => false; - public SavedFrame GetCurrentSavedFrame() => stateStore.Last(); + public SavedState GetSavedState() => stateStore.Last(); public INetcodeRandom Random => random; public INetcodeSessionHandler GetHandler() => callbacks; public int NumberOfPlayers { get; private set; } diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index c3de6921..3d4470e3 100644 --- a/src/Backdash/Session/Backends/SyncTestSession.cs +++ b/src/Backdash/Session/Backends/SyncTestSession.cs @@ -130,7 +130,7 @@ public async ValueTask DisposeAsync() public ReadOnlySpan CurrentInputs => inputBuffer; public bool IsInRollback => synchronizer.InRollback; - public SavedFrame GetCurrentSavedFrame() => synchronizer.GetLastSavedFrame(); + public SavedState GetSavedState() => synchronizer.GetLastSavedFrame(); public IReadOnlySet GetPlayers() => addedPlayers.Count is 0 ? localPlayerFallback : addedPlayers.Keys.ToHashSet(); @@ -353,7 +353,7 @@ public void AdvanceFrame() inRollback = false; } - void HandleDesync(Frame frame, SavedFrameBytes current, SavedFrame previous) + void HandleDesync(Frame frame, SavedFrameBytes current, SavedState previous) { const LogLevel level = LogLevel.Error; var message = diff --git a/src/Backdash/Session/INetcodeSession.cs b/src/Backdash/Session/INetcodeSession.cs index 7cf78d19..ead26fa9 100644 --- a/src/Backdash/Session/INetcodeSession.cs +++ b/src/Backdash/Session/INetcodeSession.cs @@ -75,24 +75,24 @@ public interface INetcodeSessionInfo Endianness InputSerializationEndianness { get; } /// - /// Returns the last saved state. + /// Returns the current saved state. /// - SavedFrame GetCurrentSavedFrame(); + SavedState GetSavedState(); /// /// Returns the checksum of the current saved state. /// - uint CurrentChecksum => GetCurrentSavedFrame().Checksum; + uint CurrentChecksum => GetSavedState().Checksum; /// /// Returns the size of the current saved state. /// - ByteSize CurrentStateSize => GetCurrentSavedFrame().Size; + ByteSize CurrentStateSize => GetSavedState().Size; /// - /// Returns the last saved state snapshot. + /// Returns the current saved state snapshot. /// - StateSnapshot CurrentStateSnapshot() => GetCurrentSavedFrame().ToSnapshot(); + StateSnapshot GetStateSnapshot() => GetSavedState().ToSnapshot(); } /// diff --git a/src/Backdash/Session/NetcodeSessionExtensions.cs b/src/Backdash/Session/NetcodeSessionExtensions.cs index d9184925..ab87825c 100644 --- a/src/Backdash/Session/NetcodeSessionExtensions.cs +++ b/src/Backdash/Session/NetcodeSessionExtensions.cs @@ -14,7 +14,7 @@ public static class NetcodeSessionExtensions public static string GetStateString(this INetcodeSession @this, IStateStringParser? parser = null) where T : unmanaged { - var state = @this.GetCurrentSavedFrame(); + var state = @this.GetSavedState(); var currentBytes = state.GameState.WrittenSpan; return @this.GetStateString(state.Frame, currentBytes, parser); } diff --git a/src/Backdash/Synchronizing/Input/Synchronizer.cs b/src/Backdash/Synchronizing/Input/Synchronizer.cs index 2610b8da..a84570a1 100644 --- a/src/Backdash/Synchronizing/Input/Synchronizer.cs +++ b/src/Backdash/Synchronizing/Input/Synchronizer.cs @@ -236,7 +236,7 @@ public void LoadFrame(Frame frame) throw new NetcodeException($"Save state not found for frame {frame.Number}"); } - public SavedFrame GetLastSavedFrame() => stateStore.Last(); + public SavedState GetLastSavedFrame() => stateStore.Last(); public void SaveCurrentFrame() { diff --git a/src/Backdash/Synchronizing/State/DefaultStateStore.cs b/src/Backdash/Synchronizing/State/DefaultStateStore.cs index df938346..e61d31a7 100644 --- a/src/Backdash/Synchronizing/State/DefaultStateStore.cs +++ b/src/Backdash/Synchronizing/State/DefaultStateStore.cs @@ -12,18 +12,18 @@ namespace Backdash.Synchronizing.State; public sealed class DefaultStateStore(int hintSize) : IStateStore { int head; - SavedFrame[] savedStates = []; + SavedState[] savedStates = []; /// public void Initialize(int saveCount) { - savedStates = new SavedFrame[saveCount]; + savedStates = new SavedState[saveCount]; for (var i = 0; i < saveCount; i++) savedStates[i] = new(Frame.Null, new(hintSize), 0); } /// - public ref SavedFrame Next() + public ref SavedState Next() { ref var result = ref savedStates[head]; result.GameState.ResetWrittenCount(); @@ -31,7 +31,7 @@ public ref SavedFrame Next() } /// - public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedFrame savedFrame) + public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState savedState) { var i = 0; var span = savedStates.AsSpan(); @@ -44,7 +44,7 @@ public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedFrame savedFram { head = i; Advance(); - savedFrame = current; + savedState = current; return true; } @@ -52,12 +52,12 @@ public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedFrame savedFram current = ref Unsafe.Add(ref current, 1)!; } - savedFrame = null; + savedState = null; return false; } /// - public SavedFrame Last() + public SavedState Last() { var i = head - 1; var index = i < 0 ? savedStates.Length - 1 : i; diff --git a/src/Backdash/Synchronizing/State/IStateStore.cs b/src/Backdash/Synchronizing/State/IStateStore.cs index c47d237b..450e3055 100644 --- a/src/Backdash/Synchronizing/State/IStateStore.cs +++ b/src/Backdash/Synchronizing/State/IStateStore.cs @@ -14,20 +14,20 @@ public interface IStateStore void Initialize(int saveCount); /// - /// Try loads a for . + /// Try loads a for . /// /// true if the frame was found, false otherwise - bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedFrame savedFrame); + bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState savedState); /// - /// Returns last . + /// Returns last . /// - SavedFrame Last(); + SavedState Last(); /// - /// Returns next writable . + /// Returns next writable . /// - ref SavedFrame Next(); + ref SavedState Next(); /// /// Advance the store pointer diff --git a/src/Backdash/Synchronizing/State/SavedFrame.cs b/src/Backdash/Synchronizing/State/SavedState.cs similarity index 83% rename from src/Backdash/Synchronizing/State/SavedFrame.cs rename to src/Backdash/Synchronizing/State/SavedState.cs index bae5676f..d9975a0b 100644 --- a/src/Backdash/Synchronizing/State/SavedFrame.cs +++ b/src/Backdash/Synchronizing/State/SavedState.cs @@ -9,7 +9,7 @@ namespace Backdash.Synchronizing.State; /// Saved frame number /// Game state on /// Checksum of state -public sealed record SavedFrame(Frame Frame, ArrayBufferWriter GameState, uint Checksum) +public sealed record SavedState(Frame Frame, ArrayBufferWriter GameState, uint Checksum) { /// Saved frame number public Frame Frame = Frame; @@ -24,5 +24,5 @@ public sealed record SavedFrame(Frame Frame, ArrayBufferWriter GameState, public ByteSize Size => ByteSize.FromBytes(GameState.WrittenCount); /// Returns a snapshot of the current saved state - public StateSnapshot ToSnapshot() => new(Frame, GameState.WrittenSpan.ToArray()); + public StateSnapshot ToSnapshot() => new(Frame, Checksum, GameState.WrittenSpan.ToArray()); } diff --git a/src/Backdash/Synchronizing/State/StateSnapshot.cs b/src/Backdash/Synchronizing/State/StateSnapshot.cs index ebd71183..13cee275 100644 --- a/src/Backdash/Synchronizing/State/StateSnapshot.cs +++ b/src/Backdash/Synchronizing/State/StateSnapshot.cs @@ -7,13 +7,17 @@ namespace Backdash.Synchronizing.State; /// /// Saved frame number /// Game state on +/// Checksum of state [Serializable] -public sealed class StateSnapshot(Frame frame, byte[] state) +public sealed class StateSnapshot(Frame frame, uint checksum, byte[] state) { /// /// Creates an empty state snapshot. /// - public StateSnapshot() : this(Frame.Null, []) { } + public StateSnapshot() : this(Frame.Null, 0, []) { } + + /// Saved frame number + public uint Checksum = checksum; /// Saved frame number public readonly Frame Frame = frame; From b35962444513d6c1980ab586b315284feb27b598 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 15:27:46 -0300 Subject: [PATCH 02/14] WIP --- src/Backdash/Session/Backends/LocalSession.cs | 3 ++- .../Session/Backends/RemoteSession.cs | 3 ++- .../Session/Backends/ReplaySession.cs | 3 ++- .../Session/Backends/SpectatorSession.cs | 3 ++- .../Session/Backends/SyncTestSession.cs | 7 +++--- src/Backdash/Session/INetcodeSession.cs | 20 +++++++++++---- .../Synchronizing/Input/Synchronizer.cs | 25 ++++++++----------- .../Synchronizing/State/DefaultStateStore.cs | 2 +- .../Synchronizing/State/IStateStore.cs | 7 +++++- .../Unit/Sync/State/DefaultStateStoreTests.cs | 2 +- 10 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/Backdash/Session/Backends/LocalSession.cs b/src/Backdash/Session/Backends/LocalSession.cs index 272df3ec..9afb1097 100644 --- a/src/Backdash/Session/Backends/LocalSession.cs +++ b/src/Backdash/Session/Backends/LocalSession.cs @@ -76,6 +76,7 @@ SessionServices services public FrameSpan RollbackFrames => FrameSpan.Zero; public bool IsInRollback => false; public SavedState GetSavedState() => stateStore.Last(); + public SavedState? GetSavedState(Frame frame) => stateStore.Get(frame); public IReadOnlySet GetPlayers() => addedPlayers; @@ -215,7 +216,7 @@ public bool LoadFrame(Frame frame) return true; } - if (!stateStore.TryLoad(frame, out var savedFrame)) + if (!stateStore.TryGet(frame, out var savedFrame)) return false; var offset = 0; diff --git a/src/Backdash/Session/Backends/RemoteSession.cs b/src/Backdash/Session/Backends/RemoteSession.cs index 4006d7d9..310f28c7 100644 --- a/src/Backdash/Session/Backends/RemoteSession.cs +++ b/src/Backdash/Session/Backends/RemoteSession.cs @@ -199,7 +199,8 @@ void Close() public FrameSpan FramesBehind => synchronizer.FramesBehind; public FrameSpan RollbackFrames => synchronizer.RollbackFrames; public bool IsInRollback => synchronizer.InRollback; - public SavedState GetSavedState() => synchronizer.GetLastSavedFrame(); + public SavedState GetSavedState() => synchronizer.Store.Last(); + public SavedState? GetSavedState(Frame frame) => synchronizer.Store.Get(frame); public int NumberOfPlayers => addedPlayers.Count; public int NumberOfSpectators => addedSpectators.Count; public int LocalPort => udp.BindPort; diff --git a/src/Backdash/Session/Backends/ReplaySession.cs b/src/Backdash/Session/Backends/ReplaySession.cs index b45f0749..0a47015d 100644 --- a/src/Backdash/Session/Backends/ReplaySession.cs +++ b/src/Backdash/Session/Backends/ReplaySession.cs @@ -102,6 +102,7 @@ public void Close() public FrameSpan FramesBehind => FrameSpan.Zero; public bool IsInRollback => false; public SavedState GetSavedState() => stateStore.Last(); + public SavedState? GetSavedState(Frame frame) => stateStore.Get(frame); public int NumberOfSpectators => 0; public int LocalPort => 0; @@ -229,7 +230,7 @@ public bool LoadFrame(Frame frame) return true; } - if (!stateStore.TryLoad(frame, out var savedFrame)) + if (!stateStore.TryGet(frame, out var savedFrame)) { ReplayController.IsBackward = false; ReplayController.Pause(); diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index fc565362..33c184e3 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -159,6 +159,7 @@ public void Close() public FrameSpan FramesBehind => FrameSpan.Zero; public bool IsInRollback => false; public SavedState GetSavedState() => stateStore.Last(); + public SavedState? GetSavedState(Frame frame) => stateStore.Get(frame); public INetcodeRandom Random => random; public INetcodeSessionHandler GetHandler() => callbacks; public int NumberOfPlayers { get; private set; } @@ -337,7 +338,7 @@ public bool LoadFrame(Frame frame) return true; } - if (!stateStore.TryLoad(frame, out var savedFrame)) + if (!stateStore.TryGet(frame, out var savedFrame)) return false; logger.Write(LogLevel.Trace, diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index 3d4470e3..b82463aa 100644 --- a/src/Backdash/Session/Backends/SyncTestSession.cs +++ b/src/Backdash/Session/Backends/SyncTestSession.cs @@ -130,7 +130,8 @@ public async ValueTask DisposeAsync() public ReadOnlySpan CurrentInputs => inputBuffer; public bool IsInRollback => synchronizer.InRollback; - public SavedState GetSavedState() => synchronizer.GetLastSavedFrame(); + public SavedState GetSavedState() => synchronizer.Store.Last(); + public SavedState? GetSavedState(Frame frame) => synchronizer.Store.Get(frame); public IReadOnlySet GetPlayers() => addedPlayers.Count is 0 ? localPlayerFallback : addedPlayers.Keys.ToHashSet(); @@ -298,7 +299,7 @@ public void AdvanceFrame() // Hold onto the current frame in our queue of saved states. // We'll need the checksum later to verify that our replay of the same frame got the same results. - var lastSaved = synchronizer.GetLastSavedFrame(); + var lastSaved = synchronizer.Store.Last(); var stateBytes = ArrayPool.Shared.Rent(lastSaved.GameState.WrittenCount); lastSaved.GameState.WrittenSpan.CopyTo(stateBytes); @@ -336,7 +337,7 @@ public void AdvanceFrame() throw new NetcodeException(message); } - var last = synchronizer.GetLastSavedFrame(); + var last = synchronizer.Store.Last(); if (current.Checksum != last.Checksum) HandleDesync(frame, current, last); else diff --git a/src/Backdash/Session/INetcodeSession.cs b/src/Backdash/Session/INetcodeSession.cs index ead26fa9..28f48e4b 100644 --- a/src/Backdash/Session/INetcodeSession.cs +++ b/src/Backdash/Session/INetcodeSession.cs @@ -74,11 +74,6 @@ public interface INetcodeSessionInfo /// Endianness InputSerializationEndianness { get; } - /// - /// Returns the current saved state. - /// - SavedState GetSavedState(); - /// /// Returns the checksum of the current saved state. /// @@ -89,6 +84,21 @@ public interface INetcodeSessionInfo /// ByteSize CurrentStateSize => GetSavedState().Size; + /// + /// Returns the current saved state for if exists, null otherwise. + /// + SavedState? GetSavedState(Frame frame); + + /// + /// Returns the current saved state. + /// + SavedState GetSavedState(); + + /// + /// Returns the current state snapshot for if exists, null otherwise. + /// + StateSnapshot? GetStateSnapshot(Frame frame) => GetSavedState(frame)?.ToSnapshot(); + /// /// Returns the current saved state snapshot. /// diff --git a/src/Backdash/Synchronizing/Input/Synchronizer.cs b/src/Backdash/Synchronizing/Input/Synchronizer.cs index a84570a1..922af32c 100644 --- a/src/Backdash/Synchronizing/Input/Synchronizer.cs +++ b/src/Backdash/Synchronizing/Input/Synchronizer.cs @@ -16,7 +16,6 @@ sealed class Synchronizer where TInput : unmanaged readonly NetcodeOptions options; readonly Logger logger; readonly IReadOnlyCollection players; - readonly IStateStore stateStore; readonly IChecksumProvider checksumProvider; readonly ChecksumStore checksumStore; readonly ConnectionsState localConnections; @@ -28,8 +27,6 @@ sealed class Synchronizer where TInput : unmanaged bool reachedPredictionBarrier; int NumberOfPlayers => players.Count; - readonly EndiannessSerializer.INumberSerializer endianness; - public Synchronizer( NetcodeOptions options, Logger logger, @@ -44,25 +41,27 @@ public Synchronizer( this.options = options; this.logger = logger; this.players = players; - this.stateStore = stateStore; + this.Store = stateStore; this.checksumProvider = checksumProvider; this.localConnections = localConnections; this.inputComparer = inputComparer ?? EqualityComparer.Default; this.checksumStore = checksumStore; inputQueues = new(2); - endianness = options.GetEndiannessNumberStateSerializer(); + NumberSerializer = options.GetEndiannessNumberStateSerializer(); var saveBufferSize = options.TotalSavedFramesAllowed; stateStore.Initialize(saveBufferSize); } public bool InRollback { get; private set; } + public IStateStore Store { get; } + public EndiannessSerializer.INumberSerializer NumberSerializer { get; } float rollbackFrameCounter; public Frame CurrentFrame => currentFrame; - public EndiannessSerializer.INumberSerializer NumberSerializer => endianness; - public Endianness SerializationEndianness => endianness.Endianness; + + public Endianness SerializationEndianness => NumberSerializer.Endianness; public FrameSpan FramesBehind => new(currentFrame.Number - lastConfirmedFrame.Number); public FrameSpan RollbackFrames => new((int)Math.Round(rollbackFrameCounter)); @@ -213,14 +212,14 @@ public bool TryLoadFrame(Frame frame) return true; } - if (!stateStore.TryLoad(frame, out var savedFrame)) + if (!Store.TryGet(frame, out var savedFrame)) return false; logger.Write(LogLevel.Information, $"* Loading frame info {savedFrame.Frame} (checksum: {savedFrame.Checksum:x8})"); var offset = 0; - BinaryBufferReader reader = new(savedFrame.GameState.WrittenSpan, ref offset, endianness); + BinaryBufferReader reader = new(savedFrame.GameState.WrittenSpan, ref offset, NumberSerializer); Callbacks.LoadState(frame, ref reader); @@ -236,19 +235,17 @@ public void LoadFrame(Frame frame) throw new NetcodeException($"Save state not found for frame {frame.Number}"); } - public SavedState GetLastSavedFrame() => stateStore.Last(); - public void SaveCurrentFrame() { - ref var nextState = ref stateStore.Next(); + ref var nextState = ref Store.Next(); - BinaryBufferWriter writer = new(nextState.GameState, endianness); + BinaryBufferWriter writer = new(nextState.GameState, NumberSerializer); Callbacks.SaveState(currentFrame, ref writer); nextState.Frame = currentFrame; nextState.Checksum = checksumProvider.Compute(nextState.GameState.WrittenSpan); checksumStore.Add(nextState.Frame, nextState.Checksum); - stateStore.Advance(); + Store.Advance(); logger.Write(LogLevel.Trace, $"sync: saved frame {nextState.Frame} (checksum: {nextState.Checksum:x8})"); } diff --git a/src/Backdash/Synchronizing/State/DefaultStateStore.cs b/src/Backdash/Synchronizing/State/DefaultStateStore.cs index e61d31a7..0a646087 100644 --- a/src/Backdash/Synchronizing/State/DefaultStateStore.cs +++ b/src/Backdash/Synchronizing/State/DefaultStateStore.cs @@ -31,7 +31,7 @@ public ref SavedState Next() } /// - public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState savedState) + public bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState savedState) { var i = 0; var span = savedStates.AsSpan(); diff --git a/src/Backdash/Synchronizing/State/IStateStore.cs b/src/Backdash/Synchronizing/State/IStateStore.cs index 450e3055..77f987fa 100644 --- a/src/Backdash/Synchronizing/State/IStateStore.cs +++ b/src/Backdash/Synchronizing/State/IStateStore.cs @@ -17,7 +17,7 @@ public interface IStateStore /// Try loads a for . /// /// true if the frame was found, false otherwise - bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState savedState); + bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState savedState); /// /// Returns last . @@ -33,4 +33,9 @@ public interface IStateStore /// Advance the store pointer /// void Advance(); + + /// + /// Return a for if exists, null otherwise. + /// + SavedState? Get(Frame frame) => TryGet(frame, out var savedState) ? savedState : null; } diff --git a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs index c78c0f02..2c5fb8eb 100644 --- a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs @@ -20,7 +20,7 @@ public void ShouldInitializeCorrectly() var gameState = GameState.CreateRandom(); GameStateSerializer.Shared.Serialize(new(currentState.GameState), in gameState); - if (!store.TryLoad(Frame.One, out var loaded)) + if (!store.TryGet(Frame.One, out var loaded)) Assert.Fail("Failed to load state."); GameState newGameState = new(); From 4322d23461d767f209bcff9ff79efb1de98927c6 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 15:44:39 -0300 Subject: [PATCH 03/14] WIP --- src/Backdash/Session/Backends/LocalSession.cs | 2 +- .../Session/Backends/ReplaySession.cs | 2 +- .../Session/Backends/SpectatorSession.cs | 2 +- .../Synchronizing/Input/Synchronizer.cs | 2 +- .../Synchronizing/State/DefaultStateStore.cs | 34 ++++++++++++++----- .../Synchronizing/State/IStateStore.cs | 10 ++++-- .../Unit/Sync/State/DefaultStateStoreTests.cs | 2 +- 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/Backdash/Session/Backends/LocalSession.cs b/src/Backdash/Session/Backends/LocalSession.cs index 9afb1097..ac00ed72 100644 --- a/src/Backdash/Session/Backends/LocalSession.cs +++ b/src/Backdash/Session/Backends/LocalSession.cs @@ -216,7 +216,7 @@ public bool LoadFrame(Frame frame) return true; } - if (!stateStore.TryGet(frame, out var savedFrame)) + if (!stateStore.TryLoad(frame, out var savedFrame)) return false; var offset = 0; diff --git a/src/Backdash/Session/Backends/ReplaySession.cs b/src/Backdash/Session/Backends/ReplaySession.cs index 0a47015d..bb6d84ba 100644 --- a/src/Backdash/Session/Backends/ReplaySession.cs +++ b/src/Backdash/Session/Backends/ReplaySession.cs @@ -230,7 +230,7 @@ public bool LoadFrame(Frame frame) return true; } - if (!stateStore.TryGet(frame, out var savedFrame)) + if (!stateStore.TryLoad(frame, out var savedFrame)) { ReplayController.IsBackward = false; ReplayController.Pause(); diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index 33c184e3..837e4473 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -338,7 +338,7 @@ public bool LoadFrame(Frame frame) return true; } - if (!stateStore.TryGet(frame, out var savedFrame)) + if (!stateStore.TryLoad(frame, out var savedFrame)) return false; logger.Write(LogLevel.Trace, diff --git a/src/Backdash/Synchronizing/Input/Synchronizer.cs b/src/Backdash/Synchronizing/Input/Synchronizer.cs index 922af32c..15a589ac 100644 --- a/src/Backdash/Synchronizing/Input/Synchronizer.cs +++ b/src/Backdash/Synchronizing/Input/Synchronizer.cs @@ -212,7 +212,7 @@ public bool TryLoadFrame(Frame frame) return true; } - if (!Store.TryGet(frame, out var savedFrame)) + if (!Store.TryLoad(frame, out var savedFrame)) return false; logger.Write(LogLevel.Information, diff --git a/src/Backdash/Synchronizing/State/DefaultStateStore.cs b/src/Backdash/Synchronizing/State/DefaultStateStore.cs index 0a646087..39904c5f 100644 --- a/src/Backdash/Synchronizing/State/DefaultStateStore.cs +++ b/src/Backdash/Synchronizing/State/DefaultStateStore.cs @@ -31,28 +31,46 @@ public ref SavedState Next() } /// - public bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState savedState) + public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState result) { var i = 0; - var span = savedStates.AsSpan(); - ref var current = ref MemoryMarshal.GetReference(span); - ref var limit = ref Unsafe.Add(ref current, span.Length); - + ref var current = ref MemoryMarshal.GetReference(savedStates.AsSpan()); + ref var limit = ref Unsafe.Add(ref current, savedStates.Length); while (Unsafe.IsAddressLessThan(ref current, ref limit)) { if (current.Frame.Number == frame.Number) { head = i; Advance(); - savedState = current; + result = current; return true; } i++; - current = ref Unsafe.Add(ref current, 1)!; + current = ref Unsafe.Subtract(ref current, 1)!; + } + + result = null; + return false; + } + + /// + public bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState result) + { + ref var current = ref MemoryMarshal.GetReference(savedStates.AsSpan()); + ref var limit = ref Unsafe.Add(ref current, savedStates.Length); + while (Unsafe.IsAddressLessThan(ref current, ref limit)) + { + if (current.Frame.Number == frame.Number) + { + result = current; + return true; + } + + current = ref Unsafe.Subtract(ref current, 1)!; } - savedState = null; + result = null; return false; } diff --git a/src/Backdash/Synchronizing/State/IStateStore.cs b/src/Backdash/Synchronizing/State/IStateStore.cs index 77f987fa..fa0ea7c4 100644 --- a/src/Backdash/Synchronizing/State/IStateStore.cs +++ b/src/Backdash/Synchronizing/State/IStateStore.cs @@ -14,10 +14,16 @@ public interface IStateStore void Initialize(int saveCount); /// - /// Try loads a for . + /// Try to load the for . /// /// true if the frame was found, false otherwise - bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState savedState); + bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState result); + + /// + /// Try to read the for . + /// + /// true if the frame was found, false otherwise + bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState result); /// /// Returns last . diff --git a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs index 2c5fb8eb..c78c0f02 100644 --- a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs @@ -20,7 +20,7 @@ public void ShouldInitializeCorrectly() var gameState = GameState.CreateRandom(); GameStateSerializer.Shared.Serialize(new(currentState.GameState), in gameState); - if (!store.TryGet(Frame.One, out var loaded)) + if (!store.TryLoad(Frame.One, out var loaded)) Assert.Fail("Failed to load state."); GameState newGameState = new(); From 9be458dac7e94023a1424e1c16e52d7621665c9f Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 16:02:01 -0300 Subject: [PATCH 04/14] WIP --- src/Backdash/Session/Backends/SyncTestSession.cs | 8 +++++++- src/Backdash/Synchronizing/Input/Synchronizer.cs | 12 ++++++++---- src/Backdash/Synchronizing/State/StateSnapshot.cs | 4 ++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index b82463aa..2c614cd9 100644 --- a/src/Backdash/Session/Backends/SyncTestSession.cs +++ b/src/Backdash/Session/Backends/SyncTestSession.cs @@ -278,7 +278,13 @@ public bool LoadFrame(Frame frame) return synchronizer.TryLoadFrame(frame); } - public void LoadSnapshot(StateSnapshot snapshot) { } + public void LoadSnapshot(StateSnapshot snapshot) + { + if (snapshot.State is []) return; + var frame = snapshot.Frame.Number <= 0 ? CurrentFrame : snapshot.Frame; + synchronizer.ApplyState(frame, snapshot.State); + savedFrames.Clear(); + } public void AdvanceFrame() { diff --git a/src/Backdash/Synchronizing/Input/Synchronizer.cs b/src/Backdash/Synchronizing/Input/Synchronizer.cs index 15a589ac..83674a84 100644 --- a/src/Backdash/Synchronizing/Input/Synchronizer.cs +++ b/src/Backdash/Synchronizing/Input/Synchronizer.cs @@ -218,15 +218,19 @@ public bool TryLoadFrame(Frame frame) logger.Write(LogLevel.Information, $"* Loading frame info {savedFrame.Frame} (checksum: {savedFrame.Checksum:x8})"); - var offset = 0; - BinaryBufferReader reader = new(savedFrame.GameState.WrittenSpan, ref offset, NumberSerializer); + ApplyState(savedFrame.Frame, savedFrame.GameState.WrittenSpan); + return true; + } + public void ApplyState(Frame frame, ReadOnlySpan state) + { + var offset = 0; + BinaryBufferReader reader = new(state, ref offset, NumberSerializer); Callbacks.LoadState(frame, ref reader); // Reset frame count and the head of the state ring-buffer to point in // advance of the current frame (as if we had just finished executing it). - currentFrame = savedFrame.Frame; - return true; + currentFrame = frame; } public void LoadFrame(Frame frame) diff --git a/src/Backdash/Synchronizing/State/StateSnapshot.cs b/src/Backdash/Synchronizing/State/StateSnapshot.cs index 13cee275..086bc67c 100644 --- a/src/Backdash/Synchronizing/State/StateSnapshot.cs +++ b/src/Backdash/Synchronizing/State/StateSnapshot.cs @@ -17,10 +17,10 @@ public sealed class StateSnapshot(Frame frame, uint checksum, byte[] state) public StateSnapshot() : this(Frame.Null, 0, []) { } /// Saved frame number - public uint Checksum = checksum; + public Frame Frame = frame; /// Saved frame number - public readonly Frame Frame = frame; + public readonly uint Checksum = checksum; /// Saved game state public readonly byte[] State = state; From 5d8061c5576c75a037970935bb748c3a5a07f2d1 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 16:33:29 -0300 Subject: [PATCH 05/14] improve pool data type --- src/Backdash/Data/ObjectPool.cs | 88 +++++++++++++++---- .../Serialization/BinaryBufferReader.cs | 14 +-- .../BinaryBufferReadWriteValueTests.cs | 3 +- 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/src/Backdash/Data/ObjectPool.cs b/src/Backdash/Data/ObjectPool.cs index 97ff1e92..4e8907db 100644 --- a/src/Backdash/Data/ObjectPool.cs +++ b/src/Backdash/Data/ObjectPool.cs @@ -22,33 +22,37 @@ public interface IObjectPool /// /// Default object pool for types with empty constructor /// -public sealed class DefaultObjectPool : IObjectPool, IEnumerable where T : class, new() +public sealed class ObjectPool : IObjectPool, IEnumerable, IDisposable where T : class { - /// - /// Default object pool singleton. - /// - public static readonly IObjectPool Instance = new DefaultObjectPool(); - /// /// Maximum number of objects allowed in the pool /// - public readonly int MaxCapacity; // -1 to account for fastItem + public int Capacity { get; } // -1 to account for fastItem int numItems; + T? fastItem; readonly Stack items; readonly HashSet set; + readonly Func createFunc; + readonly Action? returnFunc; readonly IEqualityComparer comparer; - T? fastItem; /// - /// Instantiate new + /// Instantiate new /// - public DefaultObjectPool(int capacity = 100, IEqualityComparer? comparer = null) + public ObjectPool( + Func createFunc, + Action? returnFunc = null, + int? capacity = null, + IEqualityComparer? comparer = null + ) { - MaxCapacity = capacity - 1; + this.createFunc = createFunc; + this.returnFunc = returnFunc; this.comparer = comparer ?? ReferenceEqualityComparer.Instance; - items = new(MaxCapacity); - set = new(MaxCapacity, this.comparer); + Capacity = (capacity ?? 100) - 1; + items = new(Capacity); + set = new(Capacity, this.comparer); } bool Contains(T value) => comparer.Equals(fastItem, value) || set.Contains(value); @@ -65,7 +69,7 @@ public T Rent() } if (!items.TryPop(out item)) - return new(); + return createFunc(); numItems--; set.Remove(item); @@ -78,13 +82,15 @@ public bool Return(T value) ArgumentNullException.ThrowIfNull(value); if (Contains(value)) return true; + returnFunc?.Invoke(value); + if (fastItem is null) { fastItem = value; return true; } - if (numItems >= MaxCapacity) + if (numItems >= Capacity) return false; if (!set.Add(value)) return true; @@ -104,11 +110,34 @@ public void Clear() set.Clear(); } + /// + /// Preload pool items. + /// + public void WarmUp(int count) + { + List temp = []; + for (var i = 0; i < count; i++) temp.Add(Rent()); + foreach (var player in temp) Return(player); + } + /// /// Number of instances in the object pool /// public int Count => numItems + (fastItem is null ? 0 : 1); + /// + /// Dispose all disposable objects in the pool + /// + public void Dispose() + { + (fastItem as IDisposable)?.Dispose(); + while (items.TryPop(out var item)) + if (item is IDisposable disposable) + disposable.Dispose(); + + Clear(); + } + /// public Stack.Enumerator GetEnumerator() => items.GetEnumerator(); @@ -116,3 +145,32 @@ public void Clear() IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + +/// +/// Factory for +/// +public static class ObjectPool +{ + /// + /// Create new instance of object pool for with constructor. + /// + public static ObjectPool Create(int? capacity = null, Action? returnWith = null) + where T : class, new() => + new(static () => new(), returnWith, capacity); + + /// + /// Create new instance of object pool for with constructor. + /// + public static ObjectPool Create(Action returnWith) where T : class, new() => + Create(null, returnWith); + + /// + /// Object pool singleton factory + /// + public static ObjectPool Singleton() where T : class, new() => SingletonWrapper.Instance; + + static class SingletonWrapper where T : class, new() + { + public static readonly ObjectPool Instance = Create(); + } +} diff --git a/src/Backdash/Serialization/BinaryBufferReader.cs b/src/Backdash/Serialization/BinaryBufferReader.cs index 7d764f88..af28a594 100644 --- a/src/Backdash/Serialization/BinaryBufferReader.cs +++ b/src/Backdash/Serialization/BinaryBufferReader.cs @@ -432,7 +432,7 @@ public void ReadNullable(ref T? value, IObjectPool pool, bool forceReturn /// Reads a nullable from the buffer. /// A nullable reference type that implements . public void ReadNullable(ref T? value, bool forceReturn = true) where T : class, IBinarySerializable, new() => - ReadNullable(ref value, DefaultObjectPool.Instance, forceReturn); + ReadNullable(ref value, ObjectPool.Singleton(), forceReturn); /// Reads a from the buffer. /// A value type that implements . @@ -504,7 +504,7 @@ public void Read(ref T? value, bool nullable, bool forceReturn = true) /// Reads a span of into buffer. /// A list of a reference type that implements . - public void Read(in Span values, in IObjectPool pool) where T : IBinarySerializable + public void Read(in Span values, IObjectPool pool) where T : IBinarySerializable { if (values.IsEmpty) return; ref var current = ref MemoryMarshal.GetReference(values); @@ -527,20 +527,20 @@ public void Read(in Span values, in IObjectPool pool) where T : IBinary /// A reference type that implements . [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Read(in T[] values, in IObjectPool pool) where T : class, IBinarySerializable => - Read(values.AsSpan(), in pool); + Read(values.AsSpan(), pool); /// Reads an array of into buffer. /// A reference that implements . public void Read(in List values, IObjectPool pool) where T : class, IBinarySerializable => - Read(GetListSpan(in values, pool), in pool); + Read(GetListSpan(in values, pool), pool); /// /// Reads a span of from the buffer. /// - /// + /// /// A reference that implements . public void Read(Span values) where T : class, IBinarySerializable, new() => - Read(values, in DefaultObjectPool.Instance); + Read(values, ObjectPool.Singleton()); /// /// Reads an array of from the buffer. @@ -557,7 +557,7 @@ public void Read(in List values, IObjectPool pool) where T : class, IBi /// /// A reference that implements . public void Read(List values) where T : class, IBinarySerializable, new() => - Read(GetListSpan(in values, in DefaultObjectPool.Instance)); + Read(GetListSpan(in values, ObjectPool.Singleton())); /// /// Reads a StringBuilder into from the buffer. diff --git a/tests/Backdash.Tests/Specs/Unit/Serialization/BinaryBufferReadWriteValueTests.cs b/tests/Backdash.Tests/Specs/Unit/Serialization/BinaryBufferReadWriteValueTests.cs index 404a3c22..d43ac8bd 100644 --- a/tests/Backdash.Tests/Specs/Unit/Serialization/BinaryBufferReadWriteValueTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Serialization/BinaryBufferReadWriteValueTests.cs @@ -404,8 +404,7 @@ public bool SerializableObject(SimpleRefData value, SimpleRefData result, Endian return value == result; } - static DefaultObjectPool DataPool => - (DefaultObjectPool)DefaultObjectPool.Instance; + static ObjectPool DataPool => ObjectPool.Singleton(); [PropertyTest] public bool SerializableNullableObjectToObject( From bb0cc3d10371f0d4b9781fc2b347545ac27715ad Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 17:11:24 -0300 Subject: [PATCH 06/14] add Checksum data type --- src/Backdash/Core/Checksum.cs | 141 ++++++++++++++++++ src/Backdash/Core/Frame/Frame.cs | 13 +- .../Core/Json/InternalJsonConverters.cs | 4 +- .../Specs/Unit/Data/JsonSerializationTests.cs | 7 + tests/Backdash.Tests/TestUtils/Gen.cs | 1 + 5 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 src/Backdash/Core/Checksum.cs diff --git a/src/Backdash/Core/Checksum.cs b/src/Backdash/Core/Checksum.cs new file mode 100644 index 00000000..ae751dd3 --- /dev/null +++ b/src/Backdash/Core/Checksum.cs @@ -0,0 +1,141 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Serialization; +using Backdash.Core; + +namespace Backdash; + +/// +/// Value representation of a Checksum +/// +[Serializable] +[DebuggerDisplay("{ToString()}")] +[JsonConverter(typeof(JsonConverter))] +public readonly record struct Checksum : + IComparable, + IComparable, + IEquatable, + IUtf8SpanFormattable, + ISpanFormattable, + IComparisonOperators, + IComparisonOperators +{ + /// Return frame value 0 + public static readonly Checksum Empty = new(0); + + /// Returns the value for the current . + public readonly uint Value; + + /// + /// Initialize new for frame . + /// + /// + public Checksum(uint value) => Value = value; + + /// + public int CompareTo(Checksum other) => Value.CompareTo(other.Value); + + /// + public int CompareTo(uint other) => Value.CompareTo(other); + + /// + public bool Equals(uint other) => Value == other; + + const string DefaultFormat = "x8"; + + /// + public string ToString( + [StringSyntax(StringSyntaxAttribute.NumericFormat)] + string? format, + IFormatProvider? formatProvider + ) => + Value.ToString(format ?? DefaultFormat, formatProvider); + + /// + public override string ToString() => ToString(null, null); + + /// + public string ToString( + [StringSyntax(StringSyntaxAttribute.NumericFormat)] + string format) => ToString(format, null); + + /// + public bool TryFormat( + Span utf8Destination, out int bytesWritten, + ReadOnlySpan format, + IFormatProvider? provider + ) + { + bytesWritten = 0; + if (format.IsEmpty) format = DefaultFormat; + Utf8StringBuilder writer = new(in utf8Destination, ref bytesWritten); + return writer.Write(Value, format); + } + + /// + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, + IFormatProvider? provider) + { + if (format.IsEmpty) format = DefaultFormat; + return Value.TryFormat(destination, out charsWritten, format, provider); + } + + /// + public static bool operator >(Checksum left, Checksum right) => left.Value > right.Value; + + /// + public static bool operator >=(Checksum left, Checksum right) => left.Value >= right.Value; + + /// + public static bool operator <(Checksum left, Checksum right) => left.Value < right.Value; + + /// + public static bool operator <=(Checksum left, Checksum right) => left.Value <= right.Value; + + /// + public static bool operator ==(Checksum left, uint right) => left.Value == right; + + /// + public static bool operator !=(Checksum left, uint right) => left.Value != right; + + /// + public static bool operator >(Checksum left, uint right) => left.Value > right; + + /// + public static bool operator >=(Checksum left, uint right) => left.Value >= right; + + /// + public static bool operator <(Checksum left, uint right) => left.Value < right; + + /// + public static bool operator <=(Checksum left, uint right) => left.Value <= right; + + /// + public static implicit operator uint(Checksum frame) => frame.Value; + + /// + public static explicit operator Checksum(uint frame) => new(frame); + + internal sealed class JsonConverter : JsonConverter + { + public override Checksum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return string.IsNullOrWhiteSpace(value) ? Empty : new(uint.Parse(value, NumberStyles.HexNumber, null)); + } + + public override void Write(Utf8JsonWriter writer, Checksum value, JsonSerializerOptions options) + { + Span buffer = stackalloc char[8]; + if (value.TryFormat(buffer, out var charCount, DefaultFormat, null)) + { + writer.WriteStringValue(buffer[..charCount]); + } + else + writer.WriteStringValue(value.ToString(DefaultFormat, null)); + } + } +} diff --git a/src/Backdash/Core/Frame/Frame.cs b/src/Backdash/Core/Frame/Frame.cs index a2d7ff5e..072c8573 100644 --- a/src/Backdash/Core/Frame/Frame.cs +++ b/src/Backdash/Core/Frame/Frame.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Backdash.Core; @@ -87,12 +88,22 @@ namespace Backdash; const string DefaultFormat = "(Frame 0);(Frame -#)"; /// - public string ToString(string? format, IFormatProvider? formatProvider) => + public string ToString( + [StringSyntax(StringSyntaxAttribute.NumericFormat)] + string? format, + IFormatProvider? formatProvider + ) => Number.ToString(format ?? DefaultFormat, formatProvider); /// public override string ToString() => ToString(null, null); + /// + public string ToString( + [StringSyntax(StringSyntaxAttribute.NumericFormat)] + string format + ) => ToString(format, null); + /// public bool TryFormat( Span utf8Destination, out int bytesWritten, diff --git a/src/Backdash/Core/Json/InternalJsonConverters.cs b/src/Backdash/Core/Json/InternalJsonConverters.cs index 3b8e12bd..f617e66e 100644 --- a/src/Backdash/Core/Json/InternalJsonConverters.cs +++ b/src/Backdash/Core/Json/InternalJsonConverters.cs @@ -11,7 +11,7 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial Unsafe.BitCast(reader.GetInt32()); public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => - writer.WriteNumberValue(Unsafe.As(ref Unsafe.AsRef(ref value))); + writer.WriteNumberValue(Unsafe.As(ref value)); } [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field)] @@ -25,7 +25,7 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial Unsafe.BitCast(reader.GetInt64()); public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => - writer.WriteNumberValue(Unsafe.As(ref Unsafe.AsRef(ref value))); + writer.WriteNumberValue(Unsafe.As(ref value)); } [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Field)] diff --git a/tests/Backdash.Tests/Specs/Unit/Data/JsonSerializationTests.cs b/tests/Backdash.Tests/Specs/Unit/Data/JsonSerializationTests.cs index d2140e8e..d4fa06fa 100644 --- a/tests/Backdash.Tests/Specs/Unit/Data/JsonSerializationTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Data/JsonSerializationTests.cs @@ -35,6 +35,13 @@ public class ByteSizeJsonSerializationTests() : [Fact] public void ShouldSerialize() => SerializeTest(); } +public class ChecksumJsonSerializationTests() : + BaseJsonConverterTests(Gen.Checksum, x => $"\"{x.Value:x8}\"") +{ + [Fact] public void ShouldDeserialize() => DeserializeTest(); + [Fact] public void ShouldSerialize() => SerializeTest(); +} + public class CircularBufferJsonSerializationTests() : BaseJsonConverterTests>( () => CircularBuffer.CreateFrom([10, 99, 22, 11, 77]), diff --git a/tests/Backdash.Tests/TestUtils/Gen.cs b/tests/Backdash.Tests/TestUtils/Gen.cs index 215cddaa..8ef2458f 100644 --- a/tests/Backdash.Tests/TestUtils/Gen.cs +++ b/tests/Backdash.Tests/TestUtils/Gen.cs @@ -12,6 +12,7 @@ static class Gen public static Vector3 Vector3() => new(Random.Float(), Random.Float(), Random.Float()); public static PeerAddress Peer() => Faker.Internet.IpEndPoint(); public static Frame Frame() => new(Faker.Random.Int(0)); + public static Checksum Checksum() => new(Faker.Random.UInt()); public static FrameSpan FrameSpan() => new(Faker.Random.Int(0)); public static FrameRange FrameRange() => new(Faker.Random.Int(0), Faker.Random.Int(0)); public static ByteSize ByteSize() => new(Faker.Random.Long(0)); From b9fdaf6649efc4262482cff95411788a9c226109 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 17:44:34 -0300 Subject: [PATCH 07/14] WIP --- .../Cases/ChecksumBenchmark.cs | 25 +++++++-------- samples/ConsoleGame/GameState.cs | 2 +- samples/ConsoleGame/Util.cs | 6 ---- .../Core/Builders/SpanStringBuilder.cs | 32 +++++++++++++++++++ .../Core/Builders/Utf8StringBuilder.cs | 4 +-- src/Backdash/Core/Checksum.cs | 3 ++ src/Backdash/Core/Frame/Frame.cs | 5 +++ .../Network/Messages/ConsistencyCheckReply.cs | 6 ++-- .../Messages/ConsistencyCheckRequest.cs | 2 +- src/Backdash/Network/Messages/InputAck.cs | 2 +- src/Backdash/Network/Messages/InputMessage.cs | 2 +- src/Backdash/Network/Messages/QualityReply.cs | 2 +- .../Network/Messages/QualityReport.cs | 2 +- src/Backdash/Network/Messages/SyncReply.cs | 2 +- src/Backdash/Network/Messages/SyncRequest.cs | 2 +- src/Backdash/Network/PeerConnection.cs | 2 +- .../Network/Protocol/Comm/ProtocolInbox.cs | 28 ++++++++-------- .../Network/Protocol/ProtocolState.cs | 2 +- src/Backdash/Player/PeerEvent.cs | 6 +++- .../Serialization/BinaryBufferReader.cs | 12 +++++++ .../Serialization/BinarySpanWriter.cs | 3 ++ .../Session/Backends/SyncTestSession.cs | 4 +-- src/Backdash/Session/INetcodeSession.cs | 6 ++-- .../Synchronizing/State/ChecksumProvider.cs | 12 +++---- .../Synchronizing/State/ChecksumStore.cs | 8 ++--- .../Synchronizing/State/DefaultStateStore.cs | 2 +- .../State/IStateDesyncHandler.cs | 4 +-- .../Synchronizing/State/IStateStore.cs | 2 +- .../Synchronizing/State/SavedState.cs | 4 +-- .../Synchronizing/State/StateSnapshot.cs | 8 ++--- .../Unit/Sync/State/DefaultStateStoreTests.cs | 2 +- .../TestUtils/TestGenerators.cs | 7 +++- 32 files changed, 133 insertions(+), 76 deletions(-) delete mode 100644 samples/ConsoleGame/Util.cs diff --git a/benchmarks/Backdash.Benchmarks/Cases/ChecksumBenchmark.cs b/benchmarks/Backdash.Benchmarks/Cases/ChecksumBenchmark.cs index e1dc4a32..ca555643 100644 --- a/benchmarks/Backdash.Benchmarks/Cases/ChecksumBenchmark.cs +++ b/benchmarks/Backdash.Benchmarks/Cases/ChecksumBenchmark.cs @@ -44,9 +44,9 @@ public void Setup() public sealed class Fletcher32SpanChecksumProvider : IChecksumProvider { /// - public uint Compute(ReadOnlySpan data) + public Checksum Compute(ReadOnlySpan data) { - if (data.IsEmpty) return 0; + if (data.IsEmpty) return Checksum.Empty; var buffer = MemoryMarshal.Cast(data); uint sum1 = 0xFFFF, sum2 = 0xFFFF; var dataIndex = 0; @@ -75,7 +75,7 @@ public uint Compute(ReadOnlySpan data) sum1 = (sum1 & 0xFFFF) + (sum1 >> 16); sum2 = (sum2 & 0xFFFF) + (sum2 >> 16); - return (sum2 << 16) | sum1; + return new((sum2 << 16) | sum1); } } @@ -84,9 +84,9 @@ public sealed class Fletcher32UnsafeChecksumProvider : IChecksumProvider const int BlockSize = 360; /// - public unsafe uint Compute(ReadOnlySpan data) + public unsafe Checksum Compute(ReadOnlySpan data) { - if (data.IsEmpty) return 0; + if (data.IsEmpty) return Checksum.Empty; uint sum1 = 0xFFFF, sum2 = 0xFFFF; var dataIndex = 0; @@ -121,16 +121,16 @@ public unsafe uint Compute(ReadOnlySpan data) sum1 = (sum1 & 0xFFFF) + (sum1 >> 16); sum2 = (sum2 & 0xFFFF) + (sum2 >> 16); - return (sum2 << 16) | sum1; + return new((sum2 << 16) | sum1); } } public sealed class Crc32ChecksumProvider : IChecksumProvider { /// - public uint Compute(ReadOnlySpan data) + public Checksum Compute(ReadOnlySpan data) { - if (data.Length == 0) return 0; + if (data.Length == 0) return Checksum.Empty; uint sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0; @@ -146,17 +146,16 @@ public uint Compute(ReadOnlySpan data) } var sum = sum3 + (sum2 << 8) + (sum1 << 16) + (sum0 << 24); - - return sum; + return (Checksum)sum; } } public sealed class Crc32BigEndianChecksumProvider : IChecksumProvider { /// - public unsafe uint Compute(ReadOnlySpan data) + public unsafe Checksum Compute(ReadOnlySpan data) { - if (data.Length == 0) return 0; + if (data.Length == 0) return Checksum.Empty; fixed (byte* ptr = data) { @@ -203,7 +202,7 @@ public unsafe uint Compute(ReadOnlySpan data) break; } - return sum; + return (Checksum)sum; } } } diff --git a/samples/ConsoleGame/GameState.cs b/samples/ConsoleGame/GameState.cs index 51be8822..e6578f75 100644 --- a/samples/ConsoleGame/GameState.cs +++ b/samples/ConsoleGame/GameState.cs @@ -31,7 +31,7 @@ public class NonGameState public bool IsRunning; public float SyncProgress; public string LastError = ""; - public uint Checksum; + public Checksum Checksum; public PlayerStatus RemotePlayerStatus; public DateTime LostConnectionTime; public TimeSpan DisconnectTimeout; diff --git a/samples/ConsoleGame/Util.cs b/samples/ConsoleGame/Util.cs deleted file mode 100644 index f2abc561..00000000 --- a/samples/ConsoleGame/Util.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Backdash.Serialization; - -namespace ConsoleGame; - -[BinarySerializer] -public partial class GameStateSerializer; diff --git a/src/Backdash/Core/Builders/SpanStringBuilder.cs b/src/Backdash/Core/Builders/SpanStringBuilder.cs index 31c9560a..b34fcf53 100644 --- a/src/Backdash/Core/Builders/SpanStringBuilder.cs +++ b/src/Backdash/Core/Builders/SpanStringBuilder.cs @@ -103,4 +103,36 @@ public void AppendFormatted(T value, string? format) where T : ISpanFormattab succeeded = succeeded && builder.Write(value, format); } } + + public readonly ref struct ObjectBuilder + { + readonly SpanStringBuilder writer; + readonly int firstOffset; + readonly ref int offset; + + public ObjectBuilder( + in Span destination, + ref int writtenCount, + IFormatProvider? formatProvider = null + ) + { + offset = ref writtenCount; + writer = new(in destination, ref offset, formatProvider); + writer.Write('{'); + firstOffset = offset; + } + + public bool Write( + in T value, + ReadOnlySpan format = default, + [CallerArgumentExpression(nameof(value))] + string name = "" + ) where T : ISpanFormattable + { + if (firstOffset != offset && !writer.Write(", ")) return false; + return writer.Write(name) && writer.Write(": ") && writer.Write(value, format); + } + + public void Dispose() => writer.Write("}"); + } } diff --git a/src/Backdash/Core/Builders/Utf8StringBuilder.cs b/src/Backdash/Core/Builders/Utf8StringBuilder.cs index 80279880..fa2ea83a 100644 --- a/src/Backdash/Core/Builders/Utf8StringBuilder.cs +++ b/src/Backdash/Core/Builders/Utf8StringBuilder.cs @@ -71,13 +71,13 @@ public bool WriteEnum(in T value, ReadOnlySpan format = default) where } } -readonly ref struct Utf8ObjectStringWriter +readonly ref struct Utf8ObjectStringBuilder { readonly Utf8StringBuilder writer; readonly int firstOffset; readonly ref int offset; - public Utf8ObjectStringWriter(in Span bufferArg, ref int offset) + public Utf8ObjectStringBuilder(in Span bufferArg, ref int offset) { writer = new(in bufferArg, ref offset); this.offset = ref offset; diff --git a/src/Backdash/Core/Checksum.cs b/src/Backdash/Core/Checksum.cs index ae751dd3..c9b21357 100644 --- a/src/Backdash/Core/Checksum.cs +++ b/src/Backdash/Core/Checksum.cs @@ -44,6 +44,9 @@ namespace Backdash; /// public bool Equals(uint other) => Value == other; + /// Return true if current value is 0 + public bool IsEmpty => Value is 0u; + const string DefaultFormat = "x8"; /// diff --git a/src/Backdash/Core/Frame/Frame.cs b/src/Backdash/Core/Frame/Frame.cs index 072c8573..b3829c52 100644 --- a/src/Backdash/Core/Frame/Frame.cs +++ b/src/Backdash/Core/Frame/Frame.cs @@ -76,6 +76,11 @@ namespace Backdash; /// public bool IsNull => Number is NullValue; + /// + /// Returns if the current frame is 0 + /// + public bool IsZero => Number is 0; + /// public int CompareTo(Frame other) => Number.CompareTo(other.Number); diff --git a/src/Backdash/Network/Messages/ConsistencyCheckReply.cs b/src/Backdash/Network/Messages/ConsistencyCheckReply.cs index d7619f2e..9660919f 100644 --- a/src/Backdash/Network/Messages/ConsistencyCheckReply.cs +++ b/src/Backdash/Network/Messages/ConsistencyCheckReply.cs @@ -7,7 +7,7 @@ namespace Backdash.Network.Messages; record struct ConsistencyCheckReply : IUtf8SpanFormattable { public Frame Frame; - public uint Checksum; + public Checksum Checksum; public readonly void Serialize(in BinarySpanWriter writer) { @@ -18,7 +18,7 @@ public readonly void Serialize(in BinarySpanWriter writer) public void Deserialize(in BinaryBufferReader reader) { Frame = reader.ReadFrame(); - Checksum = reader.ReadUInt32(); + Checksum = reader.ReadChecksum(); } public readonly bool TryFormat( @@ -28,7 +28,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in Frame) && writer.Write(in Checksum); } } diff --git a/src/Backdash/Network/Messages/ConsistencyCheckRequest.cs b/src/Backdash/Network/Messages/ConsistencyCheckRequest.cs index b3d56ffd..9dea2455 100644 --- a/src/Backdash/Network/Messages/ConsistencyCheckRequest.cs +++ b/src/Backdash/Network/Messages/ConsistencyCheckRequest.cs @@ -21,7 +21,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in Frame); } } diff --git a/src/Backdash/Network/Messages/InputAck.cs b/src/Backdash/Network/Messages/InputAck.cs index efd3da17..bca45abc 100644 --- a/src/Backdash/Network/Messages/InputAck.cs +++ b/src/Backdash/Network/Messages/InputAck.cs @@ -19,7 +19,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in AckFrame); } } diff --git a/src/Backdash/Network/Messages/InputMessage.cs b/src/Backdash/Network/Messages/InputMessage.cs index 9e868758..acf40086 100644 --- a/src/Backdash/Network/Messages/InputMessage.cs +++ b/src/Backdash/Network/Messages/InputMessage.cs @@ -78,7 +78,7 @@ public readonly bool TryFormat( ReadOnlySpan format, IFormatProvider? provider) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in StartFrame) && writer.Write(in AckFrame) && writer.Write(in NumBits); } diff --git a/src/Backdash/Network/Messages/QualityReply.cs b/src/Backdash/Network/Messages/QualityReply.cs index 59392819..74b89912 100644 --- a/src/Backdash/Network/Messages/QualityReply.cs +++ b/src/Backdash/Network/Messages/QualityReply.cs @@ -18,7 +18,7 @@ public readonly bool TryFormat(Span utf8Destination, out int bytesWritten, IFormatProvider? provider) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in Pong); } } diff --git a/src/Backdash/Network/Messages/QualityReport.cs b/src/Backdash/Network/Messages/QualityReport.cs index edc5a183..7e6f700b 100644 --- a/src/Backdash/Network/Messages/QualityReport.cs +++ b/src/Backdash/Network/Messages/QualityReport.cs @@ -26,7 +26,7 @@ public readonly bool TryFormat(Span utf8Destination, out int bytesWritten, IFormatProvider? provider) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in FrameAdvantage) && writer.Write(in Ping); } } diff --git a/src/Backdash/Network/Messages/SyncReply.cs b/src/Backdash/Network/Messages/SyncReply.cs index db2a17e9..7531c863 100644 --- a/src/Backdash/Network/Messages/SyncReply.cs +++ b/src/Backdash/Network/Messages/SyncReply.cs @@ -29,7 +29,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in RandomReply) && writer.Write(in Pong); } } diff --git a/src/Backdash/Network/Messages/SyncRequest.cs b/src/Backdash/Network/Messages/SyncRequest.cs index 1c5fa986..f135cf51 100644 --- a/src/Backdash/Network/Messages/SyncRequest.cs +++ b/src/Backdash/Network/Messages/SyncRequest.cs @@ -29,7 +29,7 @@ public readonly bool TryFormat( ) { bytesWritten = 0; - using Utf8ObjectStringWriter writer = new(in utf8Destination, ref bytesWritten); + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); return writer.Write(in RandomRequest) && writer.Write(in Ping); } } diff --git a/src/Backdash/Network/PeerConnection.cs b/src/Backdash/Network/PeerConnection.cs index 52b24b9a..3816a476 100644 --- a/src/Backdash/Network/PeerConnection.cs +++ b/src/Backdash/Network/PeerConnection.cs @@ -378,7 +378,7 @@ void OnConsistencyCheck(object? sender, ElapsedEventArgs e) state.Consistency.AskedFrame = new(checkFrame); state.Consistency.AskedChecksum = checksumStore.Get(state.Consistency.AskedFrame); - if (state.Consistency.AskedFrame.IsNull || state.Consistency.AskedChecksum is 0) + if (state.Consistency.AskedFrame.IsNull || state.Consistency.AskedChecksum.IsEmpty) return; if (state.Consistency.LastCheck is 0) diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index 85d30e2e..8ef6c616 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -262,12 +262,12 @@ bool OnSyncReply(ref readonly ProtocolMessage msg, ref ProtocolMessage replyMsg) else { networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.Synchronizing) - { - Synchronizing = new( + { + Synchronizing = new( TotalSteps: options.NumberOfSyncRoundTrips, CurrentStep: options.NumberOfSyncRoundTrips - state.Sync.RemainingRoundTrips ), - } + } ); sync.CreateRequestMessage(ref replyMsg); } @@ -295,36 +295,36 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) var checksum = message.ConsistencyCheckReply.Checksum; var localChecksum = state.Consistency.AskedChecksum; - logger.Write(LogLevel.Debug, $"Reply consistency-check for {checkFrame} #{checksum:x8}"); + logger.Write(LogLevel.Debug, $"Reply consistency-check for {checkFrame} #{checksum}"); - if (state.Consistency.AskedFrame != checkFrame || localChecksum is 0 || checksum is 0) + if (state.Consistency.AskedFrame != checkFrame || localChecksum.IsEmpty || checksum.IsEmpty) { - logger.Write(LogLevel.Warning, $"Unable to find reply local checksum #{checksum:x8} for {checkFrame}"); + logger.Write(LogLevel.Warning, $"Unable to find reply local checksum #{checksum} for {checkFrame}"); return false; } if (localChecksum != checksum) { logger.Write(LogLevel.Error, - $"Invalid remote checksum on frame {checkFrame}, {localChecksum:x8} != {checksum:x8}"); + $"Invalid remote checksum on frame {checkFrame}, {localChecksum:x8} != {checksum}"); networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) - { - ChecksumMismatch = new( + { + ChecksumMismatch = new( MismatchFrame: checkFrame, LocalChecksum: localChecksum, RemoteChecksum: checksum ), - } + } ); return false; } - logger.Write(LogLevel.Debug, $"Finish consistency-check request check for {checkFrame} #{checksum:x8}"); + logger.Write(LogLevel.Debug, $"Finish consistency-check request check for {checkFrame} #{checksum}"); state.Consistency.LastCheck = Stopwatch.GetTimestamp(); state.Consistency.AskedFrame = Frame.Null; - state.Consistency.AskedChecksum = 0; + state.Consistency.AskedChecksum = Checksum.Empty; return true; } @@ -334,9 +334,9 @@ bool OnConsistencyCheckRequest(ref readonly ProtocolMessage message, ref Protoco var checkFrame = message.ConsistencyCheckRequest.Frame; var checksum = checksumStore.Get(checkFrame); - logger.Write(LogLevel.Debug, $"Received consistency request check for: {checkFrame} (reply {checksum:x8})"); + logger.Write(LogLevel.Debug, $"Received consistency request check for: {checkFrame} (reply {checksum})"); - if (checksum is 0) + if (checksum.IsEmpty) { logger.Write(LogLevel.Warning, $"Unable to find requested local checksum for {checkFrame}"); return false; diff --git a/src/Backdash/Network/Protocol/ProtocolState.cs b/src/Backdash/Network/Protocol/ProtocolState.cs index 6f115676..8a543875 100644 --- a/src/Backdash/Network/Protocol/ProtocolState.cs +++ b/src/Backdash/Network/Protocol/ProtocolState.cs @@ -37,7 +37,7 @@ public sealed class ConsistencyState { public long LastCheck; public Frame AskedFrame; - public uint AskedChecksum; + public Checksum AskedChecksum; } public sealed class AdvantageState diff --git a/src/Backdash/Player/PeerEvent.cs b/src/Backdash/Player/PeerEvent.cs index 5625491f..991e29a3 100644 --- a/src/Backdash/Player/PeerEvent.cs +++ b/src/Backdash/Player/PeerEvent.cs @@ -172,4 +172,8 @@ public bool TryFormat( /// /// Data for event. /// -public readonly record struct ChecksumMismatchEventInfo(Frame MismatchFrame, uint LocalChecksum, uint RemoteChecksum); +public readonly record struct ChecksumMismatchEventInfo( + Frame MismatchFrame, + Checksum LocalChecksum, + Checksum RemoteChecksum +); diff --git a/src/Backdash/Serialization/BinaryBufferReader.cs b/src/Backdash/Serialization/BinaryBufferReader.cs index af28a594..da83c121 100644 --- a/src/Backdash/Serialization/BinaryBufferReader.cs +++ b/src/Backdash/Serialization/BinaryBufferReader.cs @@ -274,6 +274,12 @@ public DateTimeOffset ReadDateTimeOffset() /// public Frame? ReadNullableFrame() => ReadBoolean() ? ReadFrame() : null; + /// Reads single from the buffer. + public Checksum ReadChecksum() => ReadAsUInt32(); + + /// Reads single from the buffer. + public Checksum? ReadNullableChecksum() => ReadBoolean() ? ReadChecksum() : null; + /// Reads an unmanaged struct from the buffer. public void ReadStruct(ref T value) where T : unmanaged { @@ -702,6 +708,12 @@ public void Read(in StringBuilder values) /// public void Read(ref Frame? value) => value = ReadNullableFrame(); + /// + public void Read(ref Checksum value) => value = ReadChecksum(); + + /// + public void Read(ref Checksum? value) => value = ReadNullableChecksum(); + /// Reads a span of from buffer into . public void Read(in Span values) { diff --git a/src/Backdash/Serialization/BinarySpanWriter.cs b/src/Backdash/Serialization/BinarySpanWriter.cs index 82fa4f07..aeb53ac5 100644 --- a/src/Backdash/Serialization/BinarySpanWriter.cs +++ b/src/Backdash/Serialization/BinarySpanWriter.cs @@ -184,6 +184,9 @@ public void Write(in DateTimeOffset value) /// Writes single into buffer. public void Write(in Frame value) => WriteAsInt32(in value); + /// Writes single into buffer. + public void Write(in Checksum value) => WriteAsUInt32(in value); + /// Writes a span of into buffer. public void Write(in ReadOnlySpan value) { diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index 2c614cd9..9cc07156 100644 --- a/src/Backdash/Session/Backends/SyncTestSession.cs +++ b/src/Backdash/Session/Backends/SyncTestSession.cs @@ -18,7 +18,7 @@ sealed class SyncTestSession : INetcodeSession { readonly record struct SavedFrameBytes( Frame Frame, - uint Checksum, + Checksum Checksum, byte[] State, int StateSize, GameInput> Inputs @@ -396,7 +396,7 @@ void HandleDesync(Frame frame, SavedFrameBytes current, SavedState previous) void LogSaveState(LogLevel level, string description, string body, - uint checksum, Frame frame, + Checksum checksum, Frame frame, object? extra = null ) { diff --git a/src/Backdash/Session/INetcodeSession.cs b/src/Backdash/Session/INetcodeSession.cs index 28f48e4b..f857c9a7 100644 --- a/src/Backdash/Session/INetcodeSession.cs +++ b/src/Backdash/Session/INetcodeSession.cs @@ -77,7 +77,7 @@ public interface INetcodeSessionInfo /// /// Returns the checksum of the current saved state. /// - uint CurrentChecksum => GetSavedState().Checksum; + Checksum CurrentChecksum => GetSavedState().Checksum; /// /// Returns the size of the current saved state. @@ -85,7 +85,7 @@ public interface INetcodeSessionInfo ByteSize CurrentStateSize => GetSavedState().Size; /// - /// Returns the current saved state for if exists, null otherwise. + /// Returns the current saved state for if exists, otherwise. /// SavedState? GetSavedState(Frame frame); @@ -95,7 +95,7 @@ public interface INetcodeSessionInfo SavedState GetSavedState(); /// - /// Returns the current state snapshot for if exists, null otherwise. + /// Returns the current state snapshot for if exists, otherwise. /// StateSnapshot? GetStateSnapshot(Frame frame) => GetSavedState(frame)?.ToSnapshot(); diff --git a/src/Backdash/Synchronizing/State/ChecksumProvider.cs b/src/Backdash/Synchronizing/State/ChecksumProvider.cs index d4cc2102..b89bdb61 100644 --- a/src/Backdash/Synchronizing/State/ChecksumProvider.cs +++ b/src/Backdash/Synchronizing/State/ChecksumProvider.cs @@ -10,7 +10,7 @@ public interface IChecksumProvider /// /// /// checksum value - uint Compute(ReadOnlySpan data); + Checksum Compute(ReadOnlySpan data); } /// @@ -18,7 +18,7 @@ public interface IChecksumProvider sealed class DelegateChecksumProvider(ChecksumDelegate compute) : IChecksumProvider { - public uint Compute(ReadOnlySpan data) => compute(data); + public Checksum Compute(ReadOnlySpan data) => (Checksum)compute(data); } /// @@ -27,7 +27,7 @@ sealed class DelegateChecksumProvider(ChecksumDelegate compute) : IChecksumProvi public class EmptyChecksumProvider : IChecksumProvider { /// - public uint Compute(ReadOnlySpan data) => 0; + public Checksum Compute(ReadOnlySpan data) => Checksum.Empty; } /// @@ -39,9 +39,9 @@ public sealed class Fletcher32ChecksumProvider : IChecksumProvider const int BlockSize = 360; /// - public unsafe uint Compute(ReadOnlySpan data) + public unsafe Checksum Compute(ReadOnlySpan data) { - if (data.IsEmpty) return 0; + if (data.IsEmpty) return Checksum.Empty; uint sum1 = 0xFFFF, sum2 = 0xFFFF; var dataIndex = 0; @@ -77,6 +77,6 @@ public unsafe uint Compute(ReadOnlySpan data) sum1 = (sum1 & 0xFFFF) + (sum1 >> 16); sum2 = (sum2 & 0xFFFF) + (sum2 >> 16); - return (sum2 << 16) | sum1; + return new((sum2 << 16) | sum1); } } diff --git a/src/Backdash/Synchronizing/State/ChecksumStore.cs b/src/Backdash/Synchronizing/State/ChecksumStore.cs index cf5ed902..b75d997d 100644 --- a/src/Backdash/Synchronizing/State/ChecksumStore.cs +++ b/src/Backdash/Synchronizing/State/ChecksumStore.cs @@ -10,22 +10,22 @@ public ChecksumStore(int size) data = new Entry[size]; } - public void Add(Frame frame, uint checksum) + public void Add(Frame frame, Checksum checksum) { ref var entry = ref data[frame.Number % data.Length]; entry.Frame = frame; entry.Checksum = checksum; } - public uint Get(Frame frame) + public Checksum Get(Frame frame) { var entry = data[frame.Number % data.Length]; - return entry.Frame == frame ? entry.Checksum : 0; + return entry.Frame == frame ? entry.Checksum : Checksum.Empty; } struct Entry { public Frame Frame; - public uint Checksum; + public Checksum Checksum; } } diff --git a/src/Backdash/Synchronizing/State/DefaultStateStore.cs b/src/Backdash/Synchronizing/State/DefaultStateStore.cs index 39904c5f..d7a0a5cd 100644 --- a/src/Backdash/Synchronizing/State/DefaultStateStore.cs +++ b/src/Backdash/Synchronizing/State/DefaultStateStore.cs @@ -19,7 +19,7 @@ public void Initialize(int saveCount) { savedStates = new SavedState[saveCount]; for (var i = 0; i < saveCount; i++) - savedStates[i] = new(Frame.Null, new(hintSize), 0); + savedStates[i] = new(Frame.Null, new(hintSize), Checksum.Empty); } /// diff --git a/src/Backdash/Synchronizing/State/IStateDesyncHandler.cs b/src/Backdash/Synchronizing/State/IStateDesyncHandler.cs index e68dc77f..65ae6240 100644 --- a/src/Backdash/Synchronizing/State/IStateDesyncHandler.cs +++ b/src/Backdash/Synchronizing/State/IStateDesyncHandler.cs @@ -19,7 +19,7 @@ public interface IStateDesyncHandler public readonly ref struct DesyncState( string value, ref readonly BinaryBufferReader reader, - uint checksum, + Checksum checksum, object? state ) { @@ -34,7 +34,7 @@ public readonly ref struct DesyncState( public readonly BinaryBufferReader Reader = reader; /// State checksum value - public readonly uint Checksum = checksum; + public readonly Checksum Checksum = checksum; /// public override string ToString() => Value; diff --git a/src/Backdash/Synchronizing/State/IStateStore.cs b/src/Backdash/Synchronizing/State/IStateStore.cs index fa0ea7c4..fd4c3086 100644 --- a/src/Backdash/Synchronizing/State/IStateStore.cs +++ b/src/Backdash/Synchronizing/State/IStateStore.cs @@ -41,7 +41,7 @@ public interface IStateStore void Advance(); /// - /// Return a for if exists, null otherwise. + /// Return a for if exists, otherwise. /// SavedState? Get(Frame frame) => TryGet(frame, out var savedState) ? savedState : null; } diff --git a/src/Backdash/Synchronizing/State/SavedState.cs b/src/Backdash/Synchronizing/State/SavedState.cs index d9975a0b..066b61cc 100644 --- a/src/Backdash/Synchronizing/State/SavedState.cs +++ b/src/Backdash/Synchronizing/State/SavedState.cs @@ -9,13 +9,13 @@ namespace Backdash.Synchronizing.State; /// Saved frame number /// Game state on /// Checksum of state -public sealed record SavedState(Frame Frame, ArrayBufferWriter GameState, uint Checksum) +public sealed record SavedState(Frame Frame, ArrayBufferWriter GameState, Checksum Checksum) { /// Saved frame number public Frame Frame = Frame; /// Saved checksum - public uint Checksum = Checksum; + public Checksum Checksum = Checksum; /// Saved game state public readonly ArrayBufferWriter GameState = GameState; diff --git a/src/Backdash/Synchronizing/State/StateSnapshot.cs b/src/Backdash/Synchronizing/State/StateSnapshot.cs index 086bc67c..9a6f2313 100644 --- a/src/Backdash/Synchronizing/State/StateSnapshot.cs +++ b/src/Backdash/Synchronizing/State/StateSnapshot.cs @@ -9,18 +9,18 @@ namespace Backdash.Synchronizing.State; /// Game state on /// Checksum of state [Serializable] -public sealed class StateSnapshot(Frame frame, uint checksum, byte[] state) +public sealed class StateSnapshot(Frame frame, Checksum checksum, byte[] state) { /// /// Creates an empty state snapshot. /// - public StateSnapshot() : this(Frame.Null, 0, []) { } + public StateSnapshot() : this(Frame.Null, Checksum.Empty, []) { } /// Saved frame number public Frame Frame = frame; - /// Saved frame number - public readonly uint Checksum = checksum; + /// Saved state checksum + public readonly Checksum Checksum = checksum; /// Saved game state public readonly byte[] State = state; diff --git a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs index c78c0f02..b351a131 100644 --- a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs @@ -15,7 +15,7 @@ public void ShouldInitializeCorrectly() ref var currentState = ref store.Next(); currentState.Frame = Frame.One; - currentState.Checksum = 0; + currentState.Checksum = Checksum.Empty; var gameState = GameState.CreateRandom(); GameStateSerializer.Shared.Serialize(new(currentState.GameState), in gameState); diff --git a/tests/Backdash.Tests/TestUtils/TestGenerators.cs b/tests/Backdash.Tests/TestUtils/TestGenerators.cs index 00ef77df..3df0c75f 100644 --- a/tests/Backdash.Tests/TestUtils/TestGenerators.cs +++ b/tests/Backdash.Tests/TestUtils/TestGenerators.cs @@ -60,6 +60,11 @@ public static Arbitrary FrameGenerator() => .Select(x => new Frame(x.Item)) .ToArbitrary(); + public static Arbitrary ChecksumGenerator() => + ArbMap.Default.GeneratorFor() + .Select(x => new Checksum(x)) + .ToArbitrary(); + public static Arbitrary TimeOnlyGenerator() => ArbMap.Default.GeneratorFor() .Where(x => x >= TimeOnly.MinValue.Ticks && x <= TimeOnly.MaxValue.Ticks) @@ -242,7 +247,7 @@ from frame in Generate() public static Arbitrary ConsistencyCheckReplyGenerator() => Arb.From( from frame in Generate() - from checksum in Generate() + from checksum in Generate() select new ConsistencyCheckReply { Frame = frame, From fd4e885514f52974c0208c0e47b3a5ec8ce098f4 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 17:53:56 -0300 Subject: [PATCH 08/14] WIP --- samples/ConsoleGame/View.cs | 2 +- samples/SpaceWar.Shared/Logic/Renderer.cs | 2 +- src/Backdash/Network/PeerConnection.cs | 2 +- src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs | 2 +- src/Backdash/Session/Backends/LocalSession.cs | 4 ++-- src/Backdash/Session/Backends/ReplaySession.cs | 6 +++--- src/Backdash/Session/Backends/SpectatorSession.cs | 6 +++--- src/Backdash/Session/Backends/SyncTestSession.cs | 8 ++++---- src/Backdash/Synchronizing/Input/Synchronizer.cs | 6 +++--- src/Backdash/Synchronizing/State/DefaultStateStore.cs | 8 ++++---- src/Backdash/Synchronizing/State/IStateStore.cs | 2 +- .../Specs/Unit/Sync/State/DefaultStateStoreTests.cs | 2 +- 12 files changed, 25 insertions(+), 25 deletions(-) diff --git a/samples/ConsoleGame/View.cs b/samples/ConsoleGame/View.cs index 56aff3bc..2e274c2b 100644 --- a/samples/ConsoleGame/View.cs +++ b/samples/ConsoleGame/View.cs @@ -169,7 +169,7 @@ static void DrawStats(GameState currentState, NonGameState nonGameState) $""" Ping: {peer.Ping.TotalMilliseconds:f4} ms Rollback: {info.RollbackFrames} - Checksum: {nonGameState.Checksum:x8} + Checksum: {nonGameState.Checksum} Rng Seed: {currentState.RandomSeed:x8} """ ); diff --git a/samples/SpaceWar.Shared/Logic/Renderer.cs b/samples/SpaceWar.Shared/Logic/Renderer.cs index ab23502b..66a6ce05 100644 --- a/samples/SpaceWar.Shared/Logic/Renderer.cs +++ b/samples/SpaceWar.Shared/Logic/Renderer.cs @@ -279,7 +279,7 @@ void DrawStateStats(GameState gs, NonGameState ngs) const int statsPadding = 2; stateInfoString.Clear(); - stateInfoString.Append($"State: {ngs.StateChecksum:x8} {ngs.StateSize}"); + stateInfoString.Append($"State: {ngs.StateChecksum} {ngs.StateSize}"); var size = gameAssets.MainFont.MeasureString(stateInfoString) * statsScale; Vector2 pos = new((gs.Bounds.Width - size.X) / 2, gs.Bounds.Top - Config.WindowPadding); diff --git a/src/Backdash/Network/PeerConnection.cs b/src/Backdash/Network/PeerConnection.cs index 3816a476..c63d1edd 100644 --- a/src/Backdash/Network/PeerConnection.cs +++ b/src/Backdash/Network/PeerConnection.cs @@ -394,7 +394,7 @@ void OnConsistencyCheck(object? sender, ElapsedEventArgs e) } logger.Write(LogLevel.Debug, - $"Begin consistency-check request for frame {state.Consistency.AskedFrame.Number} #{state.Consistency.AskedChecksum:x8}"); + $"Begin consistency-check request for frame {state.Consistency.AskedFrame.Number} #{state.Consistency.AskedChecksum}"); outbox .SendMessage(new(MessageType.ConsistencyCheckRequest) diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index 8ef6c616..19d399b1 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -306,7 +306,7 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) if (localChecksum != checksum) { logger.Write(LogLevel.Error, - $"Invalid remote checksum on frame {checkFrame}, {localChecksum:x8} != {checksum}"); + $"Invalid remote checksum on frame {checkFrame}, {localChecksum} != {checksum}"); networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) { diff --git a/src/Backdash/Session/Backends/LocalSession.cs b/src/Backdash/Session/Backends/LocalSession.cs index ac00ed72..562ee69f 100644 --- a/src/Backdash/Session/Backends/LocalSession.cs +++ b/src/Backdash/Session/Backends/LocalSession.cs @@ -256,7 +256,7 @@ void DiscardInputsAfter(Frame frame) void SaveCurrentFrame() { var currentFrame = CurrentFrame; - ref var nextState = ref stateStore.Next(); + ref var nextState = ref stateStore.Current(); BinaryBufferWriter writer = new(nextState.GameState, endianness); callbacks.SaveState(currentFrame, ref writer); @@ -264,7 +264,7 @@ void SaveCurrentFrame() nextState.Checksum = checksumProvider.Compute(nextState.GameState.WrittenSpan); stateStore.Advance(); - logger.Write(LogLevel.Trace, $"replay: saved frame {nextState.Frame} (checksum: {nextState.Checksum:x8})"); + logger.Write(LogLevel.Trace, $"replay: saved frame {nextState.Frame} (checksum: {nextState.Checksum})"); } public void SetFrameDelay(NetcodePlayer player, int delayInFrames) diff --git a/src/Backdash/Session/Backends/ReplaySession.cs b/src/Backdash/Session/Backends/ReplaySession.cs index bb6d84ba..de9776b0 100644 --- a/src/Backdash/Session/Backends/ReplaySession.cs +++ b/src/Backdash/Session/Backends/ReplaySession.cs @@ -209,7 +209,7 @@ public ResultCode SynchronizeInputs() void SaveCurrentFrame() { var currentFrame = CurrentFrame; - ref var nextState = ref stateStore.Next(); + ref var nextState = ref stateStore.Current(); BinaryBufferWriter writer = new(nextState.GameState, endianness); callbacks.SaveState(currentFrame, ref writer); @@ -217,7 +217,7 @@ void SaveCurrentFrame() nextState.Checksum = checksumProvider.Compute(nextState.GameState.WrittenSpan); stateStore.Advance(); - logger.Write(LogLevel.Trace, $"replay: saved frame {nextState.Frame} (checksum: {nextState.Checksum:x8})"); + logger.Write(LogLevel.Trace, $"replay: saved frame {nextState.Frame} (checksum: {nextState.Checksum})"); } public bool LoadFrame(Frame frame) @@ -238,7 +238,7 @@ public bool LoadFrame(Frame frame) } logger.Write(LogLevel.Trace, - $"Loading replay frame {savedFrame.Frame} (checksum: {savedFrame.Checksum:x8})"); + $"Loading replay frame {savedFrame.Frame} (checksum: {savedFrame.Checksum})"); var offset = 0; BinaryBufferReader reader = new(savedFrame.GameState.WrittenSpan, ref offset, endianness); callbacks.LoadState(frame, ref reader); diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index 837e4473..7675262f 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -317,7 +317,7 @@ public ResultCode SynchronizeInputs() void SaveCurrentFrame() { - ref var nextState = ref stateStore.Next(); + ref var nextState = ref stateStore.Current(); BinaryBufferWriter writer = new(nextState.GameState, endianness); callbacks.SaveState(CurrentFrame, ref writer); @@ -325,7 +325,7 @@ void SaveCurrentFrame() nextState.Checksum = checksumProvider.Compute(nextState.GameState.WrittenSpan); stateStore.Advance(); - logger.Write(LogLevel.Trace, $"spectator: saved frame {nextState.Frame} (checksum: {nextState.Checksum:x8})."); + logger.Write(LogLevel.Trace, $"spectator: saved frame {nextState.Frame} (checksum: {nextState.Checksum})."); } public bool LoadFrame(Frame frame) @@ -342,7 +342,7 @@ public bool LoadFrame(Frame frame) return false; logger.Write(LogLevel.Trace, - $"Loading replay frame {savedFrame.Frame} (checksum: {savedFrame.Checksum:x8})"); + $"Loading replay frame {savedFrame.Frame} (checksum: {savedFrame.Checksum})"); var offset = 0; BinaryBufferReader reader = new(savedFrame.GameState.WrittenSpan, ref offset, endianness); diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index 9cc07156..d5d71ed2 100644 --- a/src/Backdash/Session/Backends/SyncTestSession.cs +++ b/src/Backdash/Session/Backends/SyncTestSession.cs @@ -348,7 +348,7 @@ public void AdvanceFrame() HandleDesync(frame, current, last); else logger.Write(LogLevel.Trace, - $"Checksum #{last.Checksum:x8} for frame {current.Frame.Number} matches"); + $"Checksum #{last.Checksum} for frame {current.Frame.Number} matches"); } finally { @@ -364,7 +364,7 @@ void HandleDesync(Frame frame, SavedFrameBytes current, SavedState previous) { const LogLevel level = LogLevel.Error; var message = - $"Checksum for frame {frame} does NOT match: (#{previous.Checksum:x8} != #{current.Checksum:x8})\n"; + $"Checksum for frame {frame} does NOT match: (#{previous.Checksum} != #{current.Checksum})\n"; logger.Write(LogLevel.Error, message); @@ -402,9 +402,9 @@ void LogSaveState(LogLevel level, { if (!logStateOnDesync) return; logger.Write(level, $"=> SAVED [{description}] (Frame {frame}{(extra is not null ? $" / {extra}" : "")})"); - logger.Write(level, $"== START STATE #{checksum:x8} =="); + logger.Write(level, $"== START STATE #{checksum} =="); LogText(level, body); - logger.Write(level, $"== END STATE #{checksum:x8} ==\n"); + logger.Write(level, $"== END STATE #{checksum} ==\n"); } void LogText(LogLevel level, string text) diff --git a/src/Backdash/Synchronizing/Input/Synchronizer.cs b/src/Backdash/Synchronizing/Input/Synchronizer.cs index 83674a84..682e8c3b 100644 --- a/src/Backdash/Synchronizing/Input/Synchronizer.cs +++ b/src/Backdash/Synchronizing/Input/Synchronizer.cs @@ -216,7 +216,7 @@ public bool TryLoadFrame(Frame frame) return false; logger.Write(LogLevel.Information, - $"* Loading frame info {savedFrame.Frame} (checksum: {savedFrame.Checksum:x8})"); + $"* Loading frame info {savedFrame.Frame} (checksum: {savedFrame.Checksum})"); ApplyState(savedFrame.Frame, savedFrame.GameState.WrittenSpan); return true; @@ -241,7 +241,7 @@ public void LoadFrame(Frame frame) public void SaveCurrentFrame() { - ref var nextState = ref Store.Next(); + ref var nextState = ref Store.Current(); BinaryBufferWriter writer = new(nextState.GameState, NumberSerializer); Callbacks.SaveState(currentFrame, ref writer); @@ -250,7 +250,7 @@ public void SaveCurrentFrame() checksumStore.Add(nextState.Frame, nextState.Checksum); Store.Advance(); - logger.Write(LogLevel.Trace, $"sync: saved frame {nextState.Frame} (checksum: {nextState.Checksum:x8})"); + logger.Write(LogLevel.Trace, $"sync: saved frame {nextState.Frame} (checksum: {nextState.Checksum})"); } bool CheckSimulationConsistency(out Frame seekTo) diff --git a/src/Backdash/Synchronizing/State/DefaultStateStore.cs b/src/Backdash/Synchronizing/State/DefaultStateStore.cs index d7a0a5cd..bba6bba5 100644 --- a/src/Backdash/Synchronizing/State/DefaultStateStore.cs +++ b/src/Backdash/Synchronizing/State/DefaultStateStore.cs @@ -23,7 +23,10 @@ public void Initialize(int saveCount) } /// - public ref SavedState Next() + public void Advance() => head = (head + 1) % savedStates.Length; + + /// + public ref SavedState Current() { ref var result = ref savedStates[head]; result.GameState.ResetWrittenCount(); @@ -81,7 +84,4 @@ public SavedState Last() var index = i < 0 ? savedStates.Length - 1 : i; return savedStates[index]; } - - /// - public void Advance() => head = (head + 1) % savedStates.Length; } diff --git a/src/Backdash/Synchronizing/State/IStateStore.cs b/src/Backdash/Synchronizing/State/IStateStore.cs index fd4c3086..bcf0dfe3 100644 --- a/src/Backdash/Synchronizing/State/IStateStore.cs +++ b/src/Backdash/Synchronizing/State/IStateStore.cs @@ -33,7 +33,7 @@ public interface IStateStore /// /// Returns next writable . /// - ref SavedState Next(); + ref SavedState Current(); /// /// Advance the store pointer diff --git a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs index b351a131..1c0142f8 100644 --- a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs @@ -13,7 +13,7 @@ public void ShouldInitializeCorrectly() DefaultStateStore store = new(40); store.Initialize(1); - ref var currentState = ref store.Next(); + ref var currentState = ref store.Current(); currentState.Frame = Frame.One; currentState.Checksum = Checksum.Empty; From d5fe873d535c6736c5c6454c8825b4503b041e01 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 18:12:07 -0300 Subject: [PATCH 09/14] WIP --- src/Backdash/Session/Backends/LocalSession.cs | 3 +- .../Session/Backends/ReplaySession.cs | 2 +- .../Session/Backends/SpectatorSession.cs | 2 +- .../Session/Backends/SyncTestSession.cs | 9 +++- .../Synchronizing/Input/Synchronizer.cs | 2 +- .../Synchronizing/State/DefaultStateStore.cs | 50 ++++++++++++++++--- .../Synchronizing/State/IStateStore.cs | 7 ++- .../Unit/Sync/State/DefaultStateStoreTests.cs | 2 +- 8 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/Backdash/Session/Backends/LocalSession.cs b/src/Backdash/Session/Backends/LocalSession.cs index 562ee69f..287888c3 100644 --- a/src/Backdash/Session/Backends/LocalSession.cs +++ b/src/Backdash/Session/Backends/LocalSession.cs @@ -234,6 +234,7 @@ public void LoadSnapshot(StateSnapshot snapshot) { CurrentFrame = snapshot.Frame; DiscardInputsAfter(CurrentFrame); + stateStore.Seek(CurrentFrame); } var offset = 0; @@ -256,7 +257,7 @@ void DiscardInputsAfter(Frame frame) void SaveCurrentFrame() { var currentFrame = CurrentFrame; - ref var nextState = ref stateStore.Current(); + ref var nextState = ref stateStore.Next(); BinaryBufferWriter writer = new(nextState.GameState, endianness); callbacks.SaveState(currentFrame, ref writer); diff --git a/src/Backdash/Session/Backends/ReplaySession.cs b/src/Backdash/Session/Backends/ReplaySession.cs index de9776b0..ee4069ed 100644 --- a/src/Backdash/Session/Backends/ReplaySession.cs +++ b/src/Backdash/Session/Backends/ReplaySession.cs @@ -209,7 +209,7 @@ public ResultCode SynchronizeInputs() void SaveCurrentFrame() { var currentFrame = CurrentFrame; - ref var nextState = ref stateStore.Current(); + ref var nextState = ref stateStore.Next(); BinaryBufferWriter writer = new(nextState.GameState, endianness); callbacks.SaveState(currentFrame, ref writer); diff --git a/src/Backdash/Session/Backends/SpectatorSession.cs b/src/Backdash/Session/Backends/SpectatorSession.cs index 7675262f..1b47a6e5 100644 --- a/src/Backdash/Session/Backends/SpectatorSession.cs +++ b/src/Backdash/Session/Backends/SpectatorSession.cs @@ -317,7 +317,7 @@ public ResultCode SynchronizeInputs() void SaveCurrentFrame() { - ref var nextState = ref stateStore.Current(); + ref var nextState = ref stateStore.Next(); BinaryBufferWriter writer = new(nextState.GameState, endianness); callbacks.SaveState(CurrentFrame, ref writer); diff --git a/src/Backdash/Session/Backends/SyncTestSession.cs b/src/Backdash/Session/Backends/SyncTestSession.cs index d5d71ed2..47f75155 100644 --- a/src/Backdash/Session/Backends/SyncTestSession.cs +++ b/src/Backdash/Session/Backends/SyncTestSession.cs @@ -281,7 +281,14 @@ public bool LoadFrame(Frame frame) public void LoadSnapshot(StateSnapshot snapshot) { if (snapshot.State is []) return; - var frame = snapshot.Frame.Number <= 0 ? CurrentFrame : snapshot.Frame; + var frame = CurrentFrame; + + if (snapshot.Frame.Number > 0) + { + frame = snapshot.Frame; + synchronizer.Store.Seek(frame); + } + synchronizer.ApplyState(frame, snapshot.State); savedFrames.Clear(); } diff --git a/src/Backdash/Synchronizing/Input/Synchronizer.cs b/src/Backdash/Synchronizing/Input/Synchronizer.cs index 682e8c3b..2d05902a 100644 --- a/src/Backdash/Synchronizing/Input/Synchronizer.cs +++ b/src/Backdash/Synchronizing/Input/Synchronizer.cs @@ -241,7 +241,7 @@ public void LoadFrame(Frame frame) public void SaveCurrentFrame() { - ref var nextState = ref Store.Current(); + ref var nextState = ref Store.Next(); BinaryBufferWriter writer = new(nextState.GameState, NumberSerializer); Callbacks.SaveState(currentFrame, ref writer); diff --git a/src/Backdash/Synchronizing/State/DefaultStateStore.cs b/src/Backdash/Synchronizing/State/DefaultStateStore.cs index bba6bba5..e01e6bf1 100644 --- a/src/Backdash/Synchronizing/State/DefaultStateStore.cs +++ b/src/Backdash/Synchronizing/State/DefaultStateStore.cs @@ -26,16 +26,38 @@ public void Initialize(int saveCount) public void Advance() => head = (head + 1) % savedStates.Length; /// - public ref SavedState Current() + public ref SavedState Next() { ref var result = ref savedStates[head]; result.GameState.ResetWrittenCount(); return ref result!; } + /// + public SavedState Last() + { + var i = head - 1; + var index = i < 0 ? savedStates.Length - 1 : i; + return savedStates[index]; + } + + bool IsInRange(Frame frame) + { + if (frame.IsNull) return false; + var last = Last().Frame; + if (last.Number <= 0) return true; + return frame.Number <= last.Number; + } + /// public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState result) { + if (!IsInRange(frame)) + { + result = null; + return false; + } + var i = 0; ref var current = ref MemoryMarshal.GetReference(savedStates.AsSpan()); ref var limit = ref Unsafe.Add(ref current, savedStates.Length); @@ -50,7 +72,7 @@ public bool TryLoad(Frame frame, [MaybeNullWhen(false)] out SavedState result) } i++; - current = ref Unsafe.Subtract(ref current, 1)!; + current = ref Unsafe.Add(ref current, 1)!; } result = null; @@ -70,7 +92,7 @@ public bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState result) return true; } - current = ref Unsafe.Subtract(ref current, 1)!; + current = ref Unsafe.Add(ref current, 1)!; } result = null; @@ -78,10 +100,24 @@ public bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState result) } /// - public SavedState Last() + public bool Seek(Frame frame) { - var i = head - 1; - var index = i < 0 ? savedStates.Length - 1 : i; - return savedStates[index]; + if (!IsInRange(frame)) return false; + var i = 0; + ref var current = ref MemoryMarshal.GetReference(savedStates.AsSpan()); + ref var limit = ref Unsafe.Add(ref current, savedStates.Length); + while (Unsafe.IsAddressLessThan(ref current, ref limit)) + { + if (current.Frame == frame) + { + head = i; + return true; + } + + i++; + current = ref Unsafe.Add(ref current, 1)!; + } + + return false; } } diff --git a/src/Backdash/Synchronizing/State/IStateStore.cs b/src/Backdash/Synchronizing/State/IStateStore.cs index bcf0dfe3..3e52f9a2 100644 --- a/src/Backdash/Synchronizing/State/IStateStore.cs +++ b/src/Backdash/Synchronizing/State/IStateStore.cs @@ -25,6 +25,11 @@ public interface IStateStore /// true if the frame was found, false otherwise bool TryGet(Frame frame, [MaybeNullWhen(false)] out SavedState result); + /// + /// Try set the state pointer to the first entry. + /// + bool Seek(Frame frame); + /// /// Returns last . /// @@ -33,7 +38,7 @@ public interface IStateStore /// /// Returns next writable . /// - ref SavedState Current(); + ref SavedState Next(); /// /// Advance the store pointer diff --git a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs index 1c0142f8..b351a131 100644 --- a/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Sync/State/DefaultStateStoreTests.cs @@ -13,7 +13,7 @@ public void ShouldInitializeCorrectly() DefaultStateStore store = new(40); store.Initialize(1); - ref var currentState = ref store.Current(); + ref var currentState = ref store.Next(); currentState.Frame = Frame.One; currentState.Checksum = Checksum.Empty; From 58484871eff5c4c5f32bf61f29a968f31e4bce5d Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 18:47:21 -0300 Subject: [PATCH 10/14] WIP --- samples/SpaceWar.Shared/GameSession.cs | 5 ++- samples/SpaceWar/Game1.cs | 12 +++++++ .../Session/NetcodeSessionExtensions.cs | 32 ++++++++++++++++--- .../Synchronizing/State/StateSnapshot.cs | 24 ++++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/samples/SpaceWar.Shared/GameSession.cs b/samples/SpaceWar.Shared/GameSession.cs index 52844f37..3d4aecfb 100644 --- a/samples/SpaceWar.Shared/GameSession.cs +++ b/samples/SpaceWar.Shared/GameSession.cs @@ -164,7 +164,10 @@ public void OnPeerEvent(NetcodePlayer player, in PeerEventInfo evt) // used by SyncTest, the return value is used on the state desync handler call object? INetcodeSessionHandler.CreateState(Frame frame, ref readonly BinaryBufferReader reader) { - GameState state = new(); + GameState state = new() + { + Ships = new Ship[session.NumberOfPlayers], + }; state.LoadState(in reader); return state; } diff --git a/samples/SpaceWar/Game1.cs b/samples/SpaceWar/Game1.cs index 61341dcf..0b11655e 100644 --- a/samples/SpaceWar/Game1.cs +++ b/samples/SpaceWar/Game1.cs @@ -165,6 +165,18 @@ void HandleNonGameKeys() if (keyboard.IsKeyPressed(Keys.Escape)) Exit(); + if (keyboard.IsKeyPressed(Keys.Z)) + { + foreach (var s in session.EnumerateStateStrings().Take(3)) + { + Console.WriteLine($"--> State(Frame: {s.Frame.Number}, Checksum: {s.Checksum}"); + Console.WriteLine(s.State); + Console.WriteLine($"--> End State(Frame: {s.Frame.Number})"); + } + + return; + } + if (session.IsOnline()) return; diff --git a/src/Backdash/Session/NetcodeSessionExtensions.cs b/src/Backdash/Session/NetcodeSessionExtensions.cs index ab87825c..c34d8cba 100644 --- a/src/Backdash/Session/NetcodeSessionExtensions.cs +++ b/src/Backdash/Session/NetcodeSessionExtensions.cs @@ -11,25 +11,27 @@ public static class NetcodeSessionExtensions /// /// Returns the current state string representation /// - public static string GetStateString(this INetcodeSession @this, IStateStringParser? parser = null) + public static StatePreview GetStateString(this INetcodeSession @this, IStateStringParser? parser = null) where T : unmanaged { var state = @this.GetSavedState(); var currentBytes = state.GameState.WrittenSpan; - return @this.GetStateString(state.Frame, currentBytes, parser); + var text = @this.GetStateString(state.Frame, currentBytes, parser); + return new(state.Frame, state.Checksum, text); } /// /// Returns string representation for given /// - public static string GetStateString( + public static StatePreview GetStateString( this INetcodeSession @this, StateSnapshot state, IStateStringParser? parser = null ) where T : unmanaged { var stateBytes = state.State.AsSpan(0, (int)state.Size); - return @this.GetStateString(state.Frame, stateBytes, parser); + var text = @this.GetStateString(state.Frame, stateBytes, parser); + return new(state.Frame, state.Checksum, text); } /// @@ -48,4 +50,26 @@ static string GetStateString( var stateObject = @this.GetHandler().CreateState(frame, ref reader); return parser.GetStateString(frame, in reader, stateObject); } + + /// + /// Enumerate all valid state saved snapshots in descending order + /// + public static IEnumerable EnumerateSnapshots(this INetcodeSession @this, Frame? frame = null) + where T : unmanaged + { + StateSnapshot? next = frame.HasValue ? @this.GetStateSnapshot(frame.Value) : @this.GetStateSnapshot(); + while (next is not null) + { + yield return next; + next = @this.GetStateSnapshot(next.Frame.Previous()); + } + } + + /// + /// Enumerate string representation for all saved states in descending order + /// + public static IEnumerable EnumerateStateStrings( + this INetcodeSession @this, Frame? frame = null, IStateStringParser? parser = null) + where T : unmanaged => + @this.EnumerateSnapshots(frame).Select(s => @this.GetStateString(s, parser)); } diff --git a/src/Backdash/Synchronizing/State/StateSnapshot.cs b/src/Backdash/Synchronizing/State/StateSnapshot.cs index 9a6f2313..b6518152 100644 --- a/src/Backdash/Synchronizing/State/StateSnapshot.cs +++ b/src/Backdash/Synchronizing/State/StateSnapshot.cs @@ -28,3 +28,27 @@ public StateSnapshot() : this(Frame.Null, Checksum.Empty, []) { } /// Saved state size public ByteSize Size => ByteSize.FromBytes(State.Length); } + +/// +/// A specific frame saved state text representation. +/// +/// Saved frame number +/// Game state string for +/// Checksum of state +[Serializable] +public sealed class StatePreview(Frame frame, Checksum checksum, string state) +{ + /// + /// Creates an empty state snapshot. + /// + public StatePreview() : this(Frame.Null, Checksum.Empty, string.Empty) { } + + /// Saved frame number + public readonly Frame Frame = frame; + + /// State checksum + public readonly Checksum Checksum = checksum; + + /// Game state text + public readonly string State = state; +} From 591dbb6ea8f62ff5d8903033dea9147850f07df4 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 20:26:55 -0300 Subject: [PATCH 11/14] consistency check fail message --- .../Network/Messages/ConsistencyCheckFail.cs | 37 ++++++++++++ src/Backdash/Network/Messages/MessageType.cs | 1 + .../Network/Messages/ProtocolMessage.cs | 12 ++++ .../Network/Protocol/Comm/ProtocolInbox.cs | 57 +++++++++++++------ .../Unit/Network/MessageSerializationTests.cs | 7 +++ .../TestUtils/TestGenerators.cs | 21 ++++++- 6 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 src/Backdash/Network/Messages/ConsistencyCheckFail.cs diff --git a/src/Backdash/Network/Messages/ConsistencyCheckFail.cs b/src/Backdash/Network/Messages/ConsistencyCheckFail.cs new file mode 100644 index 00000000..ec366b3a --- /dev/null +++ b/src/Backdash/Network/Messages/ConsistencyCheckFail.cs @@ -0,0 +1,37 @@ +using Backdash.Core; +using Backdash.Serialization; + +namespace Backdash.Network.Messages; + +[Serializable] +record struct ConsistencyCheckFail : IUtf8SpanFormattable +{ + public Frame Frame; + public Checksum LocalChecksum; + public Checksum RemoteChecksum; + + public readonly void Serialize(in BinarySpanWriter writer) + { + writer.Write(in Frame); + writer.Write(in LocalChecksum); + writer.Write(in RemoteChecksum); + } + + public void Deserialize(in BinaryBufferReader reader) + { + Frame = reader.ReadFrame(); + LocalChecksum = reader.ReadChecksum(); + RemoteChecksum = reader.ReadChecksum(); + } + + public readonly bool TryFormat( + Span utf8Destination, + out int bytesWritten, ReadOnlySpan format, + IFormatProvider? provider + ) + { + bytesWritten = 0; + using Utf8ObjectStringBuilder writer = new(in utf8Destination, ref bytesWritten); + return writer.Write(in Frame) && writer.Write(in LocalChecksum) && writer.Write(in RemoteChecksum); + } +} diff --git a/src/Backdash/Network/Messages/MessageType.cs b/src/Backdash/Network/Messages/MessageType.cs index 8b851042..a3aed3d3 100644 --- a/src/Backdash/Network/Messages/MessageType.cs +++ b/src/Backdash/Network/Messages/MessageType.cs @@ -13,4 +13,5 @@ enum MessageType : ushort InputAck, ConsistencyCheckRequest, ConsistencyCheckReply, + ConsistencyCheckFail, } diff --git a/src/Backdash/Network/Messages/ProtocolMessage.cs b/src/Backdash/Network/Messages/ProtocolMessage.cs index 3cc0ac3c..3b72e58e 100644 --- a/src/Backdash/Network/Messages/ProtocolMessage.cs +++ b/src/Backdash/Network/Messages/ProtocolMessage.cs @@ -34,6 +34,9 @@ struct ProtocolMessage(MessageType type = MessageType.Unknown) : IEquatable SyncReply.ToString(), MessageType.ConsistencyCheckRequest => ConsistencyCheckRequest.ToString(), MessageType.ConsistencyCheckReply => ConsistencyCheckReply.ToString(), + MessageType.ConsistencyCheckFail => ConsistencyCheckFail.ToString(), MessageType.Input => Input.ToString(), MessageType.QualityReport => QualityReport.ToString(), MessageType.QualityReply => QualityReply.ToString(), @@ -148,6 +158,7 @@ public readonly bool TryFormat( MessageType.SyncReply => writer.Write(in SyncReply), MessageType.ConsistencyCheckRequest => writer.Write(in ConsistencyCheckRequest), MessageType.ConsistencyCheckReply => writer.Write(in ConsistencyCheckReply), + MessageType.ConsistencyCheckFail => writer.Write(in ConsistencyCheckFail), MessageType.QualityReply => writer.Write(in QualityReply), MessageType.QualityReport => writer.Write(in QualityReport), MessageType.InputAck => writer.Write(in InputAck), @@ -165,6 +176,7 @@ public readonly bool Equals(in ProtocolMessage other) => MessageType.SyncReply => SyncReply.Equals(other.SyncReply), MessageType.ConsistencyCheckRequest => ConsistencyCheckRequest.Equals(other.ConsistencyCheckRequest), MessageType.ConsistencyCheckReply => ConsistencyCheckReply.Equals(other.ConsistencyCheckReply), + MessageType.ConsistencyCheckFail => ConsistencyCheckFail.Equals(other.ConsistencyCheckFail), MessageType.Input => Input.Equals(in other.Input), MessageType.QualityReport => QualityReport.Equals(other.QualityReport), MessageType.QualityReply => QualityReply.Equals(other.QualityReply), diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index 19d399b1..61ab1cf1 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -103,7 +103,8 @@ bool HandleMessage(ref readonly ProtocolMessage message, out ProtocolMessage rep MessageType.QualityReply => OnQualityReply(in message), MessageType.InputAck => OnInputAck(in message), MessageType.ConsistencyCheckRequest => OnConsistencyCheckRequest(in message, ref replyMsg), - MessageType.ConsistencyCheckReply => OnConsistencyCheckReply(in message), + MessageType.ConsistencyCheckReply => OnConsistencyCheckReply(in message, ref replyMsg), + MessageType.ConsistencyCheckFail => OnConsistencyCheckFail(in message), MessageType.KeepAlive => true, MessageType.Unknown => throw new NetcodeException($"Unknown UDP protocol message received: {message.Header.Type}"), @@ -289,7 +290,26 @@ public bool OnSyncRequest(ref readonly ProtocolMessage msg, ref ProtocolMessage return true; } - bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) + bool OnConsistencyCheckRequest(ref readonly ProtocolMessage message, ref ProtocolMessage replyMsg) + { + var checkFrame = message.ConsistencyCheckRequest.Frame; + var checksum = checksumStore.Get(checkFrame); + + logger.Write(LogLevel.Debug, $"Received consistency request check for: {checkFrame} (reply {checksum})"); + + if (checksum.IsEmpty) + { + logger.Write(LogLevel.Warning, $"Unable to find requested local checksum for {checkFrame}"); + return false; + } + + replyMsg.Header.Type = MessageType.ConsistencyCheckReply; + replyMsg.ConsistencyCheckReply.Frame = checkFrame; + replyMsg.ConsistencyCheckReply.Checksum = checksum; + return true; + } + + bool OnConsistencyCheckReply(ref readonly ProtocolMessage message, ref ProtocolMessage replyMsg) { var checkFrame = message.ConsistencyCheckReply.Frame; var checksum = message.ConsistencyCheckReply.Checksum; @@ -308,6 +328,11 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) logger.Write(LogLevel.Error, $"Invalid remote checksum on frame {checkFrame}, {localChecksum} != {checksum}"); + replyMsg.Header.Type = MessageType.ConsistencyCheckFail; + replyMsg.ConsistencyCheckFail.Frame = checkFrame; + replyMsg.ConsistencyCheckFail.RemoteChecksum = checksum; + replyMsg.ConsistencyCheckFail.LocalChecksum = localChecksum; + networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) { ChecksumMismatch = new( @@ -318,33 +343,31 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message) } ); - return false; + return true; } logger.Write(LogLevel.Debug, $"Finish consistency-check request check for {checkFrame} #{checksum}"); state.Consistency.LastCheck = Stopwatch.GetTimestamp(); state.Consistency.AskedFrame = Frame.Null; state.Consistency.AskedChecksum = Checksum.Empty; - return true; } - bool OnConsistencyCheckRequest(ref readonly ProtocolMessage message, ref ProtocolMessage replyMsg) + bool OnConsistencyCheckFail(ref readonly ProtocolMessage message) { - var checkFrame = message.ConsistencyCheckRequest.Frame; - var checksum = checksumStore.Get(checkFrame); - - logger.Write(LogLevel.Debug, $"Received consistency request check for: {checkFrame} (reply {checksum})"); + var body = message.ConsistencyCheckFail; + logger.Write(LogLevel.Warning, $"Failure in consistency-check for {body}"); - if (checksum.IsEmpty) - { - logger.Write(LogLevel.Warning, $"Unable to find requested local checksum for {checkFrame}"); - return false; - } + networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) + { + ChecksumMismatch = new( + MismatchFrame: body.Frame, + LocalChecksum: body.LocalChecksum, + RemoteChecksum: body.RemoteChecksum + ), + } + ); - replyMsg.Header.Type = MessageType.ConsistencyCheckReply; - replyMsg.ConsistencyCheckReply.Frame = checkFrame; - replyMsg.ConsistencyCheckReply.Checksum = checksum; return true; } } diff --git a/tests/Backdash.Tests/Specs/Unit/Network/MessageSerializationTests.cs b/tests/Backdash.Tests/Specs/Unit/Network/MessageSerializationTests.cs index 5c1e0c52..89bbbd8e 100644 --- a/tests/Backdash.Tests/Specs/Unit/Network/MessageSerializationTests.cs +++ b/tests/Backdash.Tests/Specs/Unit/Network/MessageSerializationTests.cs @@ -75,4 +75,11 @@ internal bool ConsistencyCheckReplySerialize(ConsistencyCheckReply value) => (ref ConsistencyCheckReply v, BinarySpanWriter w) => v.Serialize(w), (ref ConsistencyCheckReply v, BinaryBufferReader r) => v.Deserialize(r) ); + + [PropertyTest] + internal bool ConsistencyCheckFailSerialize(ConsistencyCheckFail value) => + AssertThat.Serialization.IsValid(ref value, + (ref ConsistencyCheckFail v, BinarySpanWriter w) => v.Serialize(w), + (ref ConsistencyCheckFail v, BinaryBufferReader r) => v.Deserialize(r) + ); } diff --git a/tests/Backdash.Tests/TestUtils/TestGenerators.cs b/tests/Backdash.Tests/TestUtils/TestGenerators.cs index 3df0c75f..786ff58e 100644 --- a/tests/Backdash.Tests/TestUtils/TestGenerators.cs +++ b/tests/Backdash.Tests/TestUtils/TestGenerators.cs @@ -255,6 +255,18 @@ from checksum in Generate() } ); + public static Arbitrary ConsistencyCheckFailGenerator() => Arb.From( + from frame in Generate() + from localChecksum in Generate() + from remoteChecksum in Generate() + select new ConsistencyCheckFail + { + Frame = frame, + LocalChecksum = localChecksum, + RemoteChecksum = remoteChecksum, + } + ); + public static Arbitrary InputMsgGenerator( Arbitrary connectStatusGenerator ) => @@ -290,7 +302,8 @@ public static Arbitrary UpdMsgGenerator( Arbitrary keepAliveArb, Arbitrary inputAckArb, Arbitrary consistencyCheckReqArb, - Arbitrary consistencyCheckReplyArb + Arbitrary consistencyCheckReplyArb, + Arbitrary consistencyCheckFailArb ) => headerArb.Generator .Where(h => h.Type is not MessageType.Unknown) @@ -344,6 +357,12 @@ Arbitrary consistencyCheckReplyArb Header = header, ConsistencyCheckReply = x, }), + MessageType.ConsistencyCheckFail => + consistencyCheckFailArb.Generator.Select(x => new ProtocolMessage + { + Header = header, + ConsistencyCheckFail = x, + }), MessageType.InputAck => inputAckArb.Generator.Select(x => new ProtocolMessage { From 601fe313a2dfa9a061768852e2723dd36c491880 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 20:35:20 -0300 Subject: [PATCH 12/14] wip --- samples/SpaceWar.Shared/GameSession.cs | 2 +- src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs | 6 ++++-- src/Backdash/Session/INetcodeSession.cs | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/samples/SpaceWar.Shared/GameSession.cs b/samples/SpaceWar.Shared/GameSession.cs index 3d4aecfb..64f5c868 100644 --- a/samples/SpaceWar.Shared/GameSession.cs +++ b/samples/SpaceWar.Shared/GameSession.cs @@ -156,7 +156,7 @@ public void OnPeerEvent(NetcodePlayer player, in PeerEventInfo evt) nonGameState.SetConnectState(player, PlayerConnectState.Disconnected); break; case PeerEvent.ChecksumMismatch: - Log($"=> CHECKSUM MISMATCH: {evt.ChecksumMismatch}"); + Log($"=> CHECKSUM MISMATCH: {player.EndPoint} => {evt.ChecksumMismatch}"); break; } } diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index 61ab1cf1..b703c15c 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -358,12 +358,14 @@ bool OnConsistencyCheckFail(ref readonly ProtocolMessage message) var body = message.ConsistencyCheckFail; logger.Write(LogLevel.Warning, $"Failure in consistency-check for {body}"); + var localChecksum = body.RemoteChecksum; + var remoteChecksum = body.LocalChecksum; networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) { ChecksumMismatch = new( MismatchFrame: body.Frame, - LocalChecksum: body.LocalChecksum, - RemoteChecksum: body.RemoteChecksum + LocalChecksum: localChecksum, + RemoteChecksum: remoteChecksum ), } ); diff --git a/src/Backdash/Session/INetcodeSession.cs b/src/Backdash/Session/INetcodeSession.cs index f857c9a7..d829ec89 100644 --- a/src/Backdash/Session/INetcodeSession.cs +++ b/src/Backdash/Session/INetcodeSession.cs @@ -299,6 +299,9 @@ bool TryGetPlayerByCustomId(int customId, [NotNullWhen(true)] out NetcodePlayer? /// NetcodePlayer? GetPlayerByCustomId(int customId) => TryGetPlayerByCustomId(customId, out var player) ? player : null; + + /// + NetcodePlayer? GetLocalPlayer() => TryGetLocalPlayer(out var player) ? player : null; } /// From b6980bef3b539dfb06a383e31f53948910972030 Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 20:37:51 -0300 Subject: [PATCH 13/14] WIP --- .../Core/Builders/SpanStringBuilder.cs | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/src/Backdash/Core/Builders/SpanStringBuilder.cs b/src/Backdash/Core/Builders/SpanStringBuilder.cs index b34fcf53..31c9560a 100644 --- a/src/Backdash/Core/Builders/SpanStringBuilder.cs +++ b/src/Backdash/Core/Builders/SpanStringBuilder.cs @@ -103,36 +103,4 @@ public void AppendFormatted(T value, string? format) where T : ISpanFormattab succeeded = succeeded && builder.Write(value, format); } } - - public readonly ref struct ObjectBuilder - { - readonly SpanStringBuilder writer; - readonly int firstOffset; - readonly ref int offset; - - public ObjectBuilder( - in Span destination, - ref int writtenCount, - IFormatProvider? formatProvider = null - ) - { - offset = ref writtenCount; - writer = new(in destination, ref offset, formatProvider); - writer.Write('{'); - firstOffset = offset; - } - - public bool Write( - in T value, - ReadOnlySpan format = default, - [CallerArgumentExpression(nameof(value))] - string name = "" - ) where T : ISpanFormattable - { - if (firstOffset != offset && !writer.Write(", ")) return false; - return writer.Write(name) && writer.Write(": ") && writer.Write(value, format); - } - - public void Dispose() => writer.Write("}"); - } } From 888d31e90ca5e2477b21dae2d8116cfac7b1bf5f Mon Sep 17 00:00:00 2001 From: Lucas Teles Date: Wed, 15 Apr 2026 20:38:58 -0300 Subject: [PATCH 14/14] lint fix --- .../Network/Protocol/Comm/ProtocolInbox.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs index b703c15c..d78210bc 100644 --- a/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs +++ b/src/Backdash/Network/Protocol/Comm/ProtocolInbox.cs @@ -263,12 +263,12 @@ bool OnSyncReply(ref readonly ProtocolMessage msg, ref ProtocolMessage replyMsg) else { networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.Synchronizing) - { - Synchronizing = new( + { + Synchronizing = new( TotalSteps: options.NumberOfSyncRoundTrips, CurrentStep: options.NumberOfSyncRoundTrips - state.Sync.RemainingRoundTrips ), - } + } ); sync.CreateRequestMessage(ref replyMsg); } @@ -334,13 +334,13 @@ bool OnConsistencyCheckReply(ref readonly ProtocolMessage message, ref ProtocolM replyMsg.ConsistencyCheckFail.LocalChecksum = localChecksum; networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) - { - ChecksumMismatch = new( + { + ChecksumMismatch = new( MismatchFrame: checkFrame, LocalChecksum: localChecksum, RemoteChecksum: checksum ), - } + } ); return true; @@ -361,13 +361,13 @@ bool OnConsistencyCheckFail(ref readonly ProtocolMessage message) var localChecksum = body.RemoteChecksum; var remoteChecksum = body.LocalChecksum; networkEvents.OnNetworkEvent(state.Player, new(PeerEvent.ChecksumMismatch) - { - ChecksumMismatch = new( + { + ChecksumMismatch = new( MismatchFrame: body.Frame, LocalChecksum: localChecksum, RemoteChecksum: remoteChecksum ), - } + } ); return true;