<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Halil Coşgun</title>
    <description>The latest articles on DEV Community by Halil Coşgun (@cosgunhalil).</description>
    <link>https://dev.to/cosgunhalil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4108682%2F28db5d76-1128-4571-8b1f-d82475a7c5b4.jpg</url>
      <title>DEV Community: Halil Coşgun</title>
      <link>https://dev.to/cosgunhalil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cosgunhalil"/>
    <language>en</language>
    <item>
      <title>Tickwise in Unity</title>
      <dc:creator>Halil Coşgun</dc:creator>
      <pubDate>Mon, 14 Sep 2026 20:11:05 +0000</pubDate>
      <link>https://dev.to/cosgunhalil/tickwise-in-unity-10mj</link>
      <guid>https://dev.to/cosgunhalil/tickwise-in-unity-10mj</guid>
      <description>&lt;h2&gt;
  
  
  The part I actually cared about
&lt;/h2&gt;

&lt;p&gt;When I started Tickwise, the plan said the Rust core comes first and the engine bridges come later. That order was deliberate. The core had to be shaped for a language boundary it had not crossed yet, so the probe interface stayed at three plain methods with no generics, and the state dump stayed a flat list of named fields instead of a tree. Nothing about the first release needed those constraints. The second one did.&lt;/p&gt;

&lt;p&gt;I have spent most of my career in Unity, so this is the bridge I wanted to get right more than any other. It is now in the repository, validated by hand in a real editor, and this post walks through both halves: how to put it in your project, and what the sample proves when you run it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvoybpsx7p4vdfx8l84ky.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvoybpsx7p4vdfx8l84ky.png" alt="Figure 1: Unity Package Manager with Tickwise installed" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the bridge actually is
&lt;/h2&gt;

&lt;p&gt;There is no Tickwise engine, no Tickwise loop, no component you drop on a GameObject that takes over your simulation. The package is a thin C# layer over the same native library the Rust crate uses. You keep your own fixed step, and once per tick you hand Tickwise two things: the input bytes for that tick, and an object that can hash your gameplay state.&lt;/p&gt;

&lt;p&gt;The whole contract is one interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDeterminismProbe&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;ulong&lt;/span&gt; &lt;span class="nf"&gt;LightHash&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// every tick, must be cheap&lt;/span&gt;
    &lt;span class="kt"&gt;ulong&lt;/span&gt; &lt;span class="nf"&gt;FullHash&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// every N ticks, must cover everything&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you also want the field-level diff, which names the value that diverged rather than just the tick, you implement a second small interface on the same object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ITickwiseStateWriter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;WriteState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TickwiseDump&lt;/span&gt; &lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire surface you are required to touch. Everything else is configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the package
&lt;/h2&gt;

&lt;p&gt;Install by git URL from the Package Manager window, or add one line to &lt;code&gt;Packages/manifest.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"com.cosgunhalil.tickwise"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/cosgunhalil/Tickwise.git#unity/v0.1.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Released versions live on the &lt;code&gt;upm&lt;/code&gt; branch with the native binaries already inside, under tags shaped like &lt;code&gt;unity/vX.Y.Z&lt;/code&gt;. That is the path you want. Installing from &lt;code&gt;main&lt;/code&gt; is possible but it points at the package sources under &lt;code&gt;bridges/unity/&lt;/code&gt;, which carry no binaries, so you would have to build the native library yourself with the script in &lt;code&gt;bridges/tickwise-ffi/scripts/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Requirements are short: Unity 2022.3 or newer, validated by hand on Unity 6000.3, and the &lt;code&gt;tickwise&lt;/code&gt; command line tool for the comparison step. The tool comes from &lt;code&gt;cargo install tickwise-cli&lt;/code&gt; or from a release binary in the repository. The comparison happens outside the editor because it operates on two finished recordings, usually from two different machines.&lt;/p&gt;

&lt;p&gt;The native library ships per platform: Windows, macOS as a universal binary, Linux, three Android ABIs, and iOS as a static library. The C# side is identical everywhere except iOS, where the import name has to be &lt;code&gt;__Internal&lt;/code&gt; because the library is linked statically into the app rather than loaded at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import the sample
&lt;/h2&gt;

&lt;p&gt;In the Package Manager window, select Tickwise, open the Samples tab, and import Deterministic Mini Game. You get a scene with one object and one script.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fntq45w55joix5l3iavaj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fntq45w55joix5l3iavaj.png" alt="Figure 2: the Samples tab with the Deterministic Mini Game import button" width="799" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sample is eight balls bouncing in a box with integer math and no engine physics at all. That is not laziness, it is the point. A deterministic simulation should be a plain class that steps on demand and can hash itself, with rendering layered on top. If your gameplay code needs &lt;code&gt;Time.deltaTime&lt;/code&gt; to decide what happens, no recording tool can save it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MiniGameSim&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDeterminismProbe&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;chaosNoise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* integer physics */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;ulong&lt;/span&gt; &lt;span class="nf"&gt;LightHash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* score, rng, tick, position sum */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;ulong&lt;/span&gt; &lt;span class="nf"&gt;FullHash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* everything */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner drives it from &lt;code&gt;FixedUpdate&lt;/code&gt; with a fixed step of one sixtieth of a second, feeds it scripted inputs rather than the keyboard so both runs see identical input, and records every tick.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F16giuew0jp5r062shyhd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F16giuew0jp5r062shyhd.png" alt="Figure 3: Tickwise Sample Runner with Session Name, Ticks To Record, Seed, Inject Chaos and Chaos At Tick" width="799" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording, in about ten lines
&lt;/h2&gt;

&lt;p&gt;Here is the shape of it, lifted from the sample and trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RecorderConfig&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GameId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tickwise-mini-game"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;BuildHash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Platform&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;TickRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RngSeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FullHashInterval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SnapshotEvery&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;StampCreatedAt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;_recorder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TickwiseRecorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// in FixedUpdate&lt;/span&gt;
&lt;span class="n"&gt;_sim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chaosNoise&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;_recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RecordTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_sim&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth pointing out.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BuildHash&lt;/code&gt; and &lt;code&gt;Platform&lt;/code&gt; go into the recording header, and they matter more than they look. Comparing two recordings from different builds is meaningless, and having the build stamped in the file is what lets you notice that before you spend an afternoon chasing a divergence that was really a version mismatch.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FullHashInterval&lt;/code&gt; is set to 50 in the sample rather than the default 300, because the sample only runs 600 ticks. In a real game the default is the better starting point. The light hash runs every tick and needs to stay cheap; the full hash runs occasionally and needs to cover everything. Anything left out of the full hash is a blind spot where a desync can hide, and Tickwise tells you when a divergence was caught by the full hash but missed by the light one, which is information about your hash coverage rather than about your bug.&lt;/p&gt;

&lt;p&gt;Recordings land in &lt;code&gt;Application.persistentDataPath&lt;/code&gt;, which is the right place: it works in the editor and in a player build, on desktop and on device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The planted bug
&lt;/h2&gt;

&lt;p&gt;The sample has a chaos toggle, and what it injects is the most realistic non-determinism bug I could think of for an engine context: the wall clock leaking into the simulation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;injectChaos&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;_tick&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ulong&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;chaosAtTick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;chaosNoise&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)((&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;realtimeSinceStartupAsDouble&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From tick 421 onward, a value derived from real elapsed time is added to the first ball's position. It is never the same on two runs. This is exactly the class of bug that survives code review, because every individual line looks reasonable, and it only shows itself when two machines compare notes.&lt;/p&gt;

&lt;p&gt;Run the scene once with Session Name set to &lt;code&gt;clean&lt;/code&gt; and Inject Chaos off. Run it again with &lt;code&gt;chaotic&lt;/code&gt; and the toggle on. Two files, same seed, same scripted inputs, one difference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxp17ahmnx8nz60deqvfy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxp17ahmnx8nz60deqvfy.png" alt="Figure 4: Chaotic run" width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it pays off
&lt;/h2&gt;

&lt;p&gt;Two recordings, one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tickwise compare clean.rec chaotic.rec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  verdict        first divergence at tick 421, caught by the light hash,
                 last agreement at tick 420
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tick 421. Not "somewhere in the second half of the match", not "after the third round". The exact tick, found offline, in milliseconds, from two files.&lt;/p&gt;

&lt;p&gt;That number is not a coincidence, and that is the part worth dwelling on. The bug was planted at tick 421 and the tool reports tick 421, because the light hash in this sample covers the ball positions. Drop the position sum from the light hash and the same bug is still caught, but later, when a shifted bounce finally changes the score. Same bug, worse report, and the gap between them is entirely a function of what you chose to hash.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjv9eif88khy7d71p2rjs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjv9eif88khy7d71p2rjs.png" alt="Figure 5: the terminal output of tickwise compare with the verdict lines visible" width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bit I am proudest of
&lt;/h2&gt;

&lt;p&gt;The sample scene is validated by hand, which is what I promised in the launch post: editors cannot run on a continuous integration machine, so no one should be asked to trust an engine bridge that was only ever compiled. But I wanted more than a manual checklist, so the validation is also a Unity play mode test that loads the sample scene, runs it twice at twenty times speed, shells out to the command line tool, and asserts on the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;StringAssert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"first divergence at tick 421"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;StringAssert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"last agreement at tick 420"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test that fails if the bridge, the native library, the recording format, and the comparison tool ever stop agreeing with each other. The C# wrapper itself is tested separately without an editor at all: the &lt;code&gt;Runtime/&lt;/code&gt; sources have no reference to &lt;code&gt;UnityEngine&lt;/code&gt;, so they compile in a plain .NET project and run in CI on Windows, macOS, and Linux.&lt;/p&gt;

&lt;p&gt;That separation was not an accident either. A runtime layer that does not depend on the engine is a layer you can test everywhere, and the parts that genuinely need an editor stay small enough to check by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into your own game
&lt;/h2&gt;

&lt;p&gt;Three steps, in order of how much they will teach you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One, find your simulation.&lt;/strong&gt; If your gameplay state lives across a dozen MonoBehaviours and advances in &lt;code&gt;Update&lt;/code&gt; with a variable delta, that is the real work, and Tickwise cannot do it for you. Deterministic multiplayer needs a simulation that steps on a fixed tick and holds its own state. The sample is deliberately shaped to show what that looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two, implement the probe.&lt;/strong&gt; Start with a &lt;code&gt;FullHash&lt;/code&gt; that covers everything you can serialize, and a &lt;code&gt;LightHash&lt;/code&gt; over the few values most likely to reveal a divergence: entity count, player state, the random seed, the score. The repository has a checklist for this question, because it is the one every reader asks second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three, record and compare.&lt;/strong&gt; One line in your tick, two runs, one command. Start with the self-check, which is the same recording compared against a replay of itself on one machine. If your simulation cannot reproduce its own recording, no amount of cross-machine comparison will help yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;The Unity bridge sits on a C ABI that the other engine bridges share, which is why the repository now also carries Unreal, Godot, Bevy, cocos2d-x, and plain C++ layers over the same native library. They are at different stages of validation and I am working through them one at a time, in real editors, because that is the only honest way to say a bridge works.&lt;/p&gt;

&lt;p&gt;The package is at version 0.1.0 and the format is still allowed to break until 1.0. If you try it on a real project, especially one with an existing lockstep or rollback implementation, I would rather hear that it broke than hear nothing.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/cosgunhalil/Tickwise" rel="noopener noreferrer"&gt;github.com/cosgunhalil/Tickwise&lt;/a&gt;, dual licensed MIT or Apache-2.0.&lt;/p&gt;

&lt;p&gt;Unity tutorial: &lt;a href="https://github.com/cosgunhalil/Tickwise/blob/main/docs/unity-tutorial.md" rel="noopener noreferrer"&gt;find your first desync in Unity in 15 minutes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for finding this worth your time.&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>gamedev</category>
      <category>netcode</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Before You Hunt a Desync, Prove Your Own Simulation Is Deterministic</title>
      <dc:creator>Halil Coşgun</dc:creator>
      <pubDate>Sat, 12 Sep 2026 14:16:49 +0000</pubDate>
      <link>https://dev.to/cosgunhalil/before-you-hunt-a-desync-prove-your-own-simulation-is-deterministic-4igf</link>
      <guid>https://dev.to/cosgunhalil/before-you-hunt-a-desync-prove-your-own-simulation-is-deterministic-4igf</guid>
      <description>&lt;h2&gt;
  
  
  The test nobody runs first
&lt;/h2&gt;

&lt;p&gt;When the system catches a desync between two players, the match dies, someone files a bug, and the hunt begins. Which client was wrong? Which subsystem? Which tick? Two machines, two builds, two network stacks, two sets of hardware, and somewhere in there one value went a different way.&lt;/p&gt;

&lt;p&gt;The hardest part of a desync is the hunt itself. What makes it worse is that a large share of those bugs were never about the second machine at all. The simulation was not deterministic on one machine either, and nobody had checked.&lt;/p&gt;

&lt;p&gt;There is a test for that. It needs one computer, one recording, and one command. I call it the self-check, and it is the first thing I would run on any simulation that claims to be deterministic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the self-check actually is
&lt;/h2&gt;

&lt;p&gt;Determinism means a simple thing: same starting state, same inputs, same result. Every time. On any machine.&lt;/p&gt;

&lt;p&gt;The multi-machine version of that promise is what lockstep netcode depends on. But there is a weaker version hiding inside it, and the weaker version is much easier to test:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Same starting state, same inputs, same result, &lt;strong&gt;on the same machine, twice in a row.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your simulation cannot pass that, it will never pass the hard version. And unlike the hard version, you can test it alone, offline, in a few seconds, before any player is involved.&lt;/p&gt;

&lt;p&gt;The procedure is three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Play a session and record it. Tickwise writes the inputs and a hash of every tick into a &lt;code&gt;.rec&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Feed those same recorded inputs back through your simulation and record that run too.&lt;/li&gt;
&lt;li&gt;Compare the two recordings.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tickwise compare original.rec replayed.rec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the verdict is anything but identical, your simulation is not deterministic, and you just found out before your players did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this catches so much
&lt;/h2&gt;

&lt;p&gt;The interesting part is what fails this test. Almost none of it involves the network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iteration order.&lt;/strong&gt; You store entities in a &lt;code&gt;HashMap&lt;/code&gt; and iterate it during the update. Rust randomizes hash seeds per process, so the second run walks the same entities in a different order. If anything in that loop is order-sensitive, and in a physics step it usually is, the two runs diverge. The same class of bug exists in every language with unordered containers, and it is one of the four chaos modes shipped with Tickwise for exactly this reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time leaking into the simulation.&lt;/strong&gt; Somewhere deep in a subsystem, someone reads the wall clock or a frame delta instead of the fixed tick. The first run took 16.2 milliseconds on that frame, the replay took 15.9, and the simulation quietly took a different branch. This one hides well, because on a fast machine the numbers look close enough to be invisible until they are not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uninitialized or stale state.&lt;/strong&gt; A scratch buffer that is not cleared between ticks, a value carried over from the previous run, a lazily built cache that exists on the second pass but not the first. The replay starts from a slightly different world than the recording did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global mutable state.&lt;/strong&gt; A static counter, a shared random generator, a data or system that survives between sessions. The first run leaves fingerprints that the second run reads.&lt;/p&gt;

&lt;p&gt;Every one of these fails on a single machine. None of them needs a second client, a network, or another player. Which means every one of them can be found before a match is ever played.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching it fail on purpose
&lt;/h2&gt;

&lt;p&gt;Tickwise ships with a reference simulation and a &lt;code&gt;--chaos&lt;/code&gt; flag that injects a known class of non-determinism at a tick you choose. That flag exists so the tool can prove it works, and it doubles as a way to see the self-check fail without breaking your own code first.&lt;/p&gt;

&lt;p&gt;Record a clean session, then record a second one with chaos turned on from tick 4021:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tickwise-refsim &lt;span class="nt"&gt;--example&lt;/span&gt; record_demo &lt;span class="nt"&gt;--&lt;/span&gt; clean.rec
cargo run &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tickwise-refsim &lt;span class="nt"&gt;--example&lt;/span&gt; record_demo &lt;span class="nt"&gt;--&lt;/span&gt; chaotic.rec &lt;span class="nt"&gt;--chaos&lt;/span&gt; stale-value 4021
tickwise compare clean.rec chaotic.rec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  verdict        first divergence at tick 4021, caught by the light hash,
                 confirmed by the full hash at tick 4200, last agreement at tick 4020
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two recordings that should have been identical, one exact tick where they stopped being identical. That is the whole shape of the self-check, whether the cause is a deliberate chaos flag or a real bug in your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stricter version
&lt;/h2&gt;

&lt;p&gt;Comparing two recordings tells you that a divergence happened. There is a second mode that tells you the moment it happens, while the replay is still running.&lt;/p&gt;

&lt;p&gt;When you replay a recording, Tickwise can verify every tick against the hashes stored in the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Replayer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"session.rec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ReplayConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;verify_hashes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="nn"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rep&lt;/span&gt;&lt;span class="nf"&gt;.next_step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;my_sim&lt;/span&gt;&lt;span class="nf"&gt;.apply_inputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="nf"&gt;.inputs&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;my_sim&lt;/span&gt;&lt;span class="nf"&gt;.tick&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;rep&lt;/span&gt;&lt;span class="nf"&gt;.after_tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;verify_hashes&lt;/code&gt; on, the replay fails at the first tick where the live hash does not match the recorded one. No second file, no comparison step, no waiting until the end. This is the version worth wiring into CI: record one canonical session, commit it, and replay it on every push. The day someone adds a &lt;code&gt;HashMap&lt;/code&gt; iteration to the physics step, the build tells them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the self-check stops
&lt;/h2&gt;

&lt;p&gt;Honesty about limits, because a test that overpromises is worse than no test.&lt;/p&gt;

&lt;p&gt;The self-check proves that your simulation is deterministic &lt;strong&gt;on one machine, with one build, in one process.&lt;/strong&gt; That is the floor, not the ceiling. Passing it does not mean two different machines will agree. Cross-platform float behavior, compiler flags, CPU differences, and thread counts all live above this line, and the multi-machine comparison is what covers them.&lt;/p&gt;

&lt;p&gt;It also only covers what your hashes cover. If a field is not in &lt;code&gt;light_hash&lt;/code&gt; or &lt;code&gt;full_hash&lt;/code&gt;, the self-check cannot see it drift. This is why Tickwise reports which hash caught a divergence: when the full hash fires and the light hash saw nothing, you have learned something about your hash coverage, not just about your bug. The repository has a checklist for that question.&lt;/p&gt;

&lt;p&gt;So the self-check is not the whole story. It is the cheapest part of the story, and the part almost everyone skips.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start here
&lt;/h2&gt;

&lt;p&gt;If you are building anything that depends on determinism, whether that is lockstep netcode, rollback, replay files, or a deterministic test suite, run the weak version of the promise first. One machine, one recording, one command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;tickwise-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository is at &lt;a href="https://github.com/cosgunhalil/Tickwise" rel="noopener noreferrer"&gt;github.com/cosgunhalil/Tickwise&lt;/a&gt;, dual licensed MIT or Apache-2.0. The &lt;a href="https://github.com/cosgunhalil/Tickwise/blob/main/docs/tutorial.md" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt; walks through the full workflow in about fifteen minutes, and the &lt;a href="https://github.com/cosgunhalil/Tickwise/blob/main/docs/hash-coverage.md" rel="noopener noreferrer"&gt;hash coverage checklist&lt;/a&gt; answers the question this post keeps circling: what belongs in each hash.&lt;/p&gt;

&lt;p&gt;If you run the self-check on your own simulation and it fails, I would genuinely like to hear what caught it. Those stories are how the chaos mode list grows.&lt;/p&gt;

&lt;p&gt;Thank you for finding this worth your time.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>gamedev</category>
      <category>netcode</category>
      <category>testing</category>
    </item>
    <item>
      <title>Find Your First Desync in 15 Minutes</title>
      <dc:creator>Halil Coşgun</dc:creator>
      <pubDate>Sat, 05 Sep 2026 16:15:59 +0000</pubDate>
      <link>https://dev.to/cosgunhalil/find-your-first-desync-in-15-minutes-14df</link>
      <guid>https://dev.to/cosgunhalil/find-your-first-desync-in-15-minutes-14df</guid>
      <description>&lt;h2&gt;
  
  
  The bravest corner of game networking
&lt;/h2&gt;

&lt;p&gt;One corner of game networking always struck me as braver than the rest: fighting games. Multiplayer architecture was my main interest for most of my career, before mobile gaming pulled me toward other problems, and I never picked a favorite between authoritative servers, relay servers, and peer-to-peer. Each fits some game and not the game next door. But fighting games ask for something the others do not.&lt;/p&gt;

&lt;p&gt;A fighting game cannot wait for a server round trip before showing the local player's own punch. One extra frame between the button and the visible response and the game feels wrong, and players can tell. So that community built on deterministic simulation synchronized by inputs: every machine runs the same simulation from the same inputs and is expected to reach the same state. Two ways of handling latency sit on top. Delay-based netcode holds inputs for a few frames so everyone has them in time. Rollback netcode predicts the remote player's input, keeps going, and when the prediction turns out wrong it rewinds and replays those frames before you notice.&lt;/p&gt;

&lt;p&gt;The more I read about this, the braver it looked. You are betting the match on two machines reaching exactly the same state, tick after tick, down to the bit. Floating-point behavior, iteration order, random-number generation: any of them can break the agreement.&lt;/p&gt;

&lt;p&gt;A pattern kept appearing in the articles I read. The bet loses. A desync. Two players are suddenly in different realities, and the author describes a long hunt. The Factorio team put it best in an early &lt;a href="https://www.factorio.com/blog/post/fff-63" rel="noopener noreferrer"&gt;Friday Facts&lt;/a&gt; post about their desync fixing: "You never know if the thing you just solved is the last one, or there are 1500 more waiting." Riot's &lt;a href="https://technology.riotgames.com/news/determinism-league-legends-introduction" rel="noopener noreferrer"&gt;series on making League of Legends deterministic&lt;/a&gt; describes an entire team built around the same fight.&lt;/p&gt;

&lt;p&gt;I don't have that story. I have never shipped a lockstep or rollback game, and I have never lost that week. What I had was curiosity, and a suspicion: the studios doing this must have built some version of the same three tools for themselves. Record the inputs and a hash of every tick. Find the first tick where two recordings disagree. When the run reproduces, dump and diff the game state at that tick. I went looking for an open version of that toolkit that I could use with my own game and could not find one. So I built it, partly to learn Rust properly, and partly because writing the debugger for a problem is the most honest way I know to understand the problem.&lt;/p&gt;

&lt;p&gt;It's called Tickwise. Here's what it does, and how to trace an injected desync with it in the next fifteen minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a desync costs a week
&lt;/h2&gt;

&lt;p&gt;Most bugs tell you where they are. A null reference points at a line. A crash may leave a useful stack trace. A desync can leave neither, and three reasons compound to make it expensive.&lt;/p&gt;

&lt;p&gt;The symptom shows up far from the cause. Say one client rounds a physics value differently at tick 4,021. Nothing visible happens. The two simulations are a hair apart and stay that way until the difference reaches something that matters: a projectile that hits on one screen and misses on the other. By the time a human notices, the match is minutes past the moment that mattered, and nothing on screen points back to it.&lt;/p&gt;

&lt;p&gt;Reproduction is most of the cost. Reproducing a desync means replaying the exact inputs, in the exact order, with the exact random seed. Without them the exact run is usually unrecoverable, and what reaches you is a report from a player in another country that says "it desynced around the fourth round, I think."&lt;/p&gt;

&lt;p&gt;And the tooling, as far as I can see from the outside, tends to stay where it was born. I want to be careful here, because I have not worked inside these studios and most say little about their internals. What I can see is the public record: postmortems, conference talks, engine documentation. They describe the same three ideas again and again. But when I looked for those three things as a small package I could download and use with my own game, engine-agnostic, with replay verification and field-level diffs, I found the workflow inside one commercial engine and otherwise not at all. Excellent versions may well exist behind closed doors. In the public tools I reviewed, the ideas are shared and the code is not.&lt;/p&gt;

&lt;p&gt;That gap is what made me want to try something. The game-specific parts of the problem look small to me: how to hash your state, and how to describe it as fields. If those two things are all a game has to provide, the recorder, the comparison, and the diff might live outside any engine and any project, built once, in the open.&lt;/p&gt;

&lt;p&gt;So here is the belief I would like to break. Debugging a desync is not inherently a week of print statements. It is a week when the recording and comparison tooling is not there. Once the inputs and hashes are on disk, finding the first divergent tick is a comparison, not an investigation, and pointing at the field that diverged is one more command.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Tickwise is, in one screen
&lt;/h2&gt;

&lt;p&gt;Tickwise is a Rust library and a command line tool. The library sits inside your game loop and watches. It never runs your simulation and never touches your netcode. You call it once per tick, it writes a file, and the command line tool reads those files later.&lt;/p&gt;

&lt;p&gt;The entire contract between your game and Tickwise is one trait with three methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;DeterminismProbe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// Called every tick. Must be cheap: a digest of the state most&lt;/span&gt;
    &lt;span class="cd"&gt;/// likely to reveal a desync, not the whole world.&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;light_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/// Called every N ticks. Should cover everything that can&lt;/span&gt;
    &lt;span class="cd"&gt;/// influence a future tick.&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;full_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/// Called only during replay, at the ticks you ask for. May be&lt;/span&gt;
    &lt;span class="cd"&gt;/// expensive. Returns the state as a flat list of named fields.&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;state_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;StateDump&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The split between a light hash and a full hash is the design idea I would keep if everything else were thrown away. Hashing the entire state every tick can be too expensive in a large simulation, so you hash a small digest every tick and the whole thing every few hundred ticks. The light hash catches a desync the moment it touches something in the digest. The full hash catches divergences the light hash was not looking at, and in that case Tickwise reports the window rather than pretending to know the tick. A first divergent tick is exact for state the light hash covers, and a bounded interval for state only the full hash covers. The tool says which of the two you got.&lt;/p&gt;

&lt;p&gt;If your state type already derives &lt;code&gt;serde::Serialize&lt;/code&gt;, you do not write the trait at all. An automatic probe hashes the serialized bytes and turns the type into named fields by walking its structure. The hand-written trait is there for when you want control over the cost, and for the engine bridges coming later, since three plain functions cross a language boundary and a generic trait does not.&lt;/p&gt;

&lt;p&gt;The workflow has two passes, and the command line tool has one command for each plus one for looking inside a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tickwise inspect session.rec     &lt;span class="c"&gt;# what is in a recording&lt;/span&gt;
tickwise compare a.rec b.rec     &lt;span class="c"&gt;# the first tick where two recordings disagree&lt;/span&gt;
tickwise diff a.dump b.dump      &lt;span class="c"&gt;# which fields differ at that tick, and how&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pass one, while you play&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every client records inputs and per-tick hashes into a &lt;code&gt;.rec&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tickwise compare a.rec b.rec&lt;/code&gt; reports the first tick where the recorded hashes disagree, and which hash caught it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pass two, afterwards&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replay each recording in your own loop, dumping the state at the reported tick into a &lt;code&gt;.dump&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tickwise diff a.dump b.dump&lt;/code&gt; lists every field that differs, labeled structural, exact, or a float that drifted by less than your chosen epsilon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The diff classifies rather than judges, because whether floats belong in your simulation is your decision.&lt;/p&gt;

&lt;p&gt;Since "cheap" means nothing in a frame budget without a number, here are the measured ones, from the criterion benchmarks in the repository on a desktop machine. The recorder itself costs about 20 nanoseconds per tick with steady inputs and about 100 when inputs change every tick, because it encodes into a reused buffer and writes through a buffered file. The reference simulation's hand-written light hash takes 32 nanoseconds regardless of world size, which is just under one percent of that simulation's 3.3 microsecond tick at a thousand entities. Its full hash takes 14 microseconds and runs every few hundred ticks. On disk, the 6000 tick demo recording is 283 KiB, about 48 bytes per tick with inputs that change constantly, and far less when players hold a direction. The automatic serde probe is the one to measure yourself, since it serializes your whole state on every call and its cost scales with that; the repository's budget guide explains how.&lt;/p&gt;

&lt;p&gt;What Tickwise is not, so nobody is disappointed later: it is not netcode, not a rollback engine, and it will not make your simulation deterministic for you. It tells you where determinism broke. Fixing it is still your job.&lt;/p&gt;

&lt;p&gt;A note on how it was built, since I want to be upfront about it. I developed Tickwise with Claude, Anthropic's AI, as a programming partner. I wrote the design document, made the architectural and licensing decisions, and reviewed and committed every change. Claude produced much of the initial code and pushed back on several of my decisions, sometimes correctly. That workflow, including where it went wrong, deserves its own post, and it will get one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch a bug with me
&lt;/h2&gt;

&lt;p&gt;Enough description. Let's break something and find it.&lt;/p&gt;

&lt;p&gt;The repository ships a small reference simulation: a few balls bouncing around an arena, two players nudging them, a score that goes up on every bounce. It is deliberately boring and deterministic to the bit. It also has a &lt;code&gt;--chaos&lt;/code&gt; flag that sabotages it on purpose with a controlled model of one of four classic desync-producing bugs, starting at a tick you choose. Today's bug is &lt;code&gt;stale-value&lt;/code&gt;: from the chosen tick on, the simulation reads a scratch value left over from the previous tick, one that should have been reset and was not, and folds it into the score. Safe Rust cannot read truly uninitialized memory, so this is a simulation of the stale-cache bug as it actually appears in production: the value is initialized, it is simply from the wrong moment. It is the kind of thing that passes code review because the code looks fine.&lt;/p&gt;

&lt;p&gt;You need Rust installed and a few minutes of compile time on the first run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;tickwise-cli
git clone https://github.com/cosgunhalil/Tickwise.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Tickwise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Record two sessions. Same seed, same inputs, six thousand ticks each. The second one carries the bug from tick 4021.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tickwise-refsim &lt;span class="nt"&gt;--example&lt;/span&gt; record_demo &lt;span class="nt"&gt;--&lt;/span&gt; clean.rec
cargo run &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tickwise-refsim &lt;span class="nt"&gt;--example&lt;/span&gt; record_demo &lt;span class="nt"&gt;--&lt;/span&gt; chaotic.rec &lt;span class="nt"&gt;--chaos&lt;/span&gt; stale-value 4021
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now pretend these came from two players' machines and you have no idea what happened. Ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tickwise compare clean.rec chaotic.rec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  first          6000 ticks, game tickwise-refsim, seed 0xddba11
  second         6000 ticks, game tickwise-refsim, seed 0xddba11

  verdict        first divergence at tick 4021, caught by the light hash, confirmed by the full hash at tick 4200, last agreement at tick 4020

  next           Pass 2: replay each recording in your own loop with
                 dump_at_ticks = [4021] to produce two .dump files, then run
                 tickwise diff a.dump b.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is pass one, and it took a few milliseconds. The last line is deliberate: the output says what to do next, because nobody reads documentation with a desync open in another window.&lt;/p&gt;

&lt;p&gt;Pass two. Replay both recordings and dump the state at tick 4021. The replayer checks every recorded hash against the live simulation as it goes, so if your simulation could not reproduce its own recording, you would find out here, before hunting anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tickwise-refsim &lt;span class="nt"&gt;--example&lt;/span&gt; replay_demo &lt;span class="nt"&gt;--&lt;/span&gt; clean.rec clean.dump &lt;span class="nt"&gt;--dump-at&lt;/span&gt; 4021
cargo run &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; tickwise-refsim &lt;span class="nt"&gt;--example&lt;/span&gt; replay_demo &lt;span class="nt"&gt;--&lt;/span&gt; chaotic.rec chaotic.dump &lt;span class="nt"&gt;--dump-at&lt;/span&gt; 4021 &lt;span class="nt"&gt;--chaos&lt;/span&gt; stale-value 4021
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wrote clean.dump with the state at tick 4021
replayed ticks 0 to 5999, every hash matched the recording
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing to be clear about, because it is the hardest part of a real desync. This example is deliberately reproducible: passing the same chaos mode during replay recreates the faulty execution. A genuinely nondeterministic bug, a race, unrecorded external state, platform-specific float behavior, may fail that hash verification instead. In that case Tickwise still identifies the first recorded divergence, but it cannot reconstruct the original state from inputs alone, and the field-level diff is out of reach until you either reproduce the divergent execution or capture state during the original run. The tutorial shows exactly this happening with two of the other chaos modes. Recording periodic state dumps during play, so that two machines' dumps can be diffed without reproducing anything, is the natural next step for the recorder, and it is on the list.&lt;/p&gt;

&lt;p&gt;And the question we actually care about: what is different?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tickwise diff clean.dump chaotic.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tick 4021       1 difference over 41 fields: 0 structural, 1 exact, 0 sub-epsilon float drift
  exact          score: 3317 versus 4811663725493808200

  verdict        1 difference across 1 compared tick
  next           an exact difference at the first divergent tick is your lead. Trace that field's last write backwards through the tick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One field out of forty-one. The score on one side is 3317. On the other it is a nineteen-digit number, which immediately suggests that an invalid or stale value entered the score calculation. The other forty fields agree. The first differing field is not automatically the root cause, since a corrupted random state or a reordered loop can surface somewhere downstream, but it is a sharply narrowed lead: the writes that can affect &lt;code&gt;score&lt;/code&gt; during tick 4021. In this simulation that is one function.&lt;/p&gt;

&lt;p&gt;Four commands. The full tutorial in the repository walks through this with more explanation, then through the other three chaos modes, because each teaches something different.&lt;/p&gt;

&lt;p&gt;The one worth previewing is &lt;code&gt;float-drift&lt;/code&gt;, which nudges one ball's velocity by a single bit every tick from the strike onward. Run the same steps and &lt;code&gt;compare&lt;/code&gt; says something new:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  verdict        divergence caught by the full hash at tick 4200, while the light hash saw nothing: the light hash has a blind spot, and the real divergence happened at or before this tick, last agreement at tick 3900
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The light hash in this simulation covers the score, the random state, and the player positions, not ball velocities, so a one-bit velocity change is invisible to it. The full hash fires at its next scheduled check, tick 4200, and Tickwise tells you plainly that the light hash missed something and the real divergence lies in the 300 ticks before. Dump just after the last agreement and the diff shows one field, one bit, classified as sub-epsilon drift; dump at 4200 and the same field has compounded into an exact difference. A light hash is only as good as what you put in it, and the repository has a checklist for exactly that question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I learned building it
&lt;/h2&gt;

&lt;p&gt;This project was partly an excuse to learn Rust properly, and partly an excuse to make a few mistakes in public and write them down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for the language you have not written yet.&lt;/strong&gt; From the first day, the plan was that Tickwise would eventually reach Unity and other engines through a C ABI. That one constraint shaped everything. The probe trait has three plain methods and no generics, because three function pointers cross a language boundary and a generic trait does not. The state dump is a flat list of named fields built with insert calls, not a tree, because a sequence of push calls is far simpler to expose across a C ABI than a Rust-owned recursive tree. None of the FFI code exists yet, and the core is already shaped for it. Designing for a second language turned out to be a good way to design a clean API for the first one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A file format that must never panic will find your bugs for you.&lt;/strong&gt; Recordings will come from other people's machines, sometimes half-written when a game crashed. So the rule was that malformed input produces an error, never a panic, and the tests enforce it: one truncates a valid recording at every possible byte length, another flips every single byte, and a fuzzer runs on every push. The truncation sweep found a real bug on the day the format was written: a bounds check on the seek index could underflow on a crafted file and panic in debug builds. It never reached a user, because the test written to catch that class of mistake existed before the code it tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My favorite example did not exist.&lt;/strong&gt; The design document opened with the dream output, &lt;code&gt;players[2].velocity.x: 3.5 vs 3.5000001&lt;/code&gt;, a sub-epsilon float drift. When I wrote the test for it, the compiler's lint pointed out that &lt;code&gt;3.5000001&lt;/code&gt; is not representable as an f32. It rounds to exactly 3.5. The nearest single-precision value above 3.5 is 3.5000002, one unit in the last place away. I had carried a number in my head for weeks that could not exist in the type I was writing about. The diff engine now compares float bits, never decimal strings, and the tests use the real neighbor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is going
&lt;/h2&gt;

&lt;p&gt;Version 0.2.2 is on crates.io: the recorder, the two file formats, all three commands, the replayer, the serde layer, and two integrations in the repository, one with GGRS's example game and one with the Bones ECS framework. It is the first complete Rust-only release.&lt;/p&gt;

&lt;p&gt;The next step is leaving Rust. First a C ABI: one crate, &lt;code&gt;tickwise-ffi&lt;/code&gt;, built as a shared and a static library with a generated C header, exposing the recorder and the dump builder as plain functions, with prebuilt binaries for Windows, macOS, Linux, Android, and iOS published from CI. Every engine bridge after it will be a thin wrapper over that one surface, which is why it comes first and gets the most careful review.&lt;/p&gt;

&lt;p&gt;Then Unity, as a package you add from the repository URL: a C# interface mirroring the three probe methods, a recorder that works in any loop, and a sample scene with a deterministic mini game and a chaos toggle. I have spent most of my career around Unity, so this is the bridge I care most about getting right, and since the Unity editor cannot run on a CI machine, it will be validated by hand in a real editor before anyone is asked to trust it. Each bridge will start with recording and compare, with replay and dumps following once recording is proven in that engine.&lt;/p&gt;

&lt;p&gt;More engines follow on the same C ABI, in whatever order people actually ask for. No dates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep reading
&lt;/h2&gt;

&lt;p&gt;If this made you curious about the field rather than just the tool, these are the pieces I learned the most from, roughly in reading order.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://gafferongames.com/post/deterministic_lockstep/" rel="noopener noreferrer"&gt;Deterministic Lockstep&lt;/a&gt; by Glenn Fiedler. The clearest explanation of the model, and the source of the standard: "Not close. Not near enough. Exactly the same."&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.gabrielgambetta.com/client-server-game-architecture.html" rel="noopener noreferrer"&gt;Fast-Paced Multiplayer&lt;/a&gt; by Gabriel Gambetta. The authoritative-server side: client-side prediction and server reconciliation. Read it to see what lockstep gives up and gets back.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.gamedeveloper.com/programming/1500-archers-on-a-28-8-network-programming-in-age-of-empires-and-beyond" rel="noopener noreferrer"&gt;1500 Archers on a 28.8&lt;/a&gt; by Paul Bettner and Mark Terrano. The 2001 Age of Empires paper, still one of the best accounts of why lockstep exists.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pond3r/ggpo" rel="noopener noreferrer"&gt;GGPO&lt;/a&gt; by Tony Cannon. The rollback library that changed fighting games.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arstechnica.com/gaming/2019/10/explaining-how-fighting-games-use-delay-based-and-rollback-netcode/" rel="noopener noreferrer"&gt;Explaining how fighting games use delay-based and rollback netcode&lt;/a&gt; by Ricky Pusch at Ars Technica. The explainer that fixed the vocabulary.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://technology.riotgames.com/news/determinism-league-legends-introduction" rel="noopener noreferrer"&gt;Determinism in League of Legends&lt;/a&gt; by Riot Games. Making an existing engine deterministic after the fact.&lt;/li&gt;
&lt;li&gt;The Factorio team's Friday Facts posts on desyncs, starting with &lt;a href="https://www.factorio.com/blog/post/fff-63" rel="noopener noreferrer"&gt;#63, The endless struggle&lt;/a&gt;. Search the archive for "desync"; few teams have written as candidly about hunting them.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.rs/ggrs" rel="noopener noreferrer"&gt;GGRS&lt;/a&gt;, the Rust rollback library, and its SyncTest session. Tickwise is designed to sit next to it, not replace it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add tickwise &lt;span class="nt"&gt;--features&lt;/span&gt; serde
cargo &lt;span class="nb"&gt;install &lt;/span&gt;tickwise-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository is at &lt;a href="https://github.com/cosgunhalil/Tickwise" rel="noopener noreferrer"&gt;github.com/cosgunhalil/Tickwise&lt;/a&gt;, dual licensed MIT or Apache-2.0. The &lt;a href="https://github.com/cosgunhalil/Tickwise/blob/main/docs/tutorial.md" rel="noopener noreferrer"&gt;fifteen minute tutorial&lt;/a&gt; is the longer version of what you just read, and the &lt;a href="https://github.com/cosgunhalil/Tickwise/blob/main/docs/hash-coverage.md" rel="noopener noreferrer"&gt;hash coverage checklist&lt;/a&gt; answers the question every reader asks next: what to put in each hash.&lt;/p&gt;

&lt;p&gt;If you have a desync story, or better, a recording of one, the issue tracker has a template for it. Real recordings from real games are the most valuable thing this project can receive right now, and I would rather learn from your bug than from another one of mine.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>gamedev</category>
      <category>netcode</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
