---
title: "Compare library versions"
url: https://recursiveai.org/onescope/cookbook/compare-library-versions/
description: "Load two copies of a OneScope module, compare their inspector rows on the same replay, and align differently numbered simulation steps."
version: "1.0.0"
---

# Compare library versions

**OneScope** is a free, open-source viewer for simulation recordings, with a desktop window or command-line control, from Recursive AI. MIT or Apache-2.0.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneScope.Tool) · [GitHub](https://github.com/recursive-ai-oss/onescope-csharp) · [Docs](https://recursiveai.org/onescope/docs/) · [Download example (.zip)](https://recursiveai.org/onescope/cookbook/compare-library-versions/compare-library-versions.zip)

A module is the DLL that teaches OneScope to read a library’s replays, one per library version. OneScope debugs recorded simulation steps, such as physics; a fork is a branched copy of a recording.

1. Unzip the example.
2. Run `dotnet run` in its folder; it builds `v1.dll` and `v2.dll` and compares them. This zip path installs a private copy of the tool automatically; no global tool install is needed.
3. To run the `onescope` command below directly, install the global tool, then run the command in the example folder.

```shell
dotnet tool install --global RecursiveAI.OneScope.Tool
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

The examples use `--headless`, which renders offscreen but still needs a graphics device and driver. On Linux, a Vulkan software renderer such as lavapipe can supply it; `ONEGAME_GPU_ADAPTER=software` selects software rendering. Remove `--headless` to open the window.

```shell
onescope open full.sim --headless \
  --module v1.dll \
  --run 'open {"path":"full.sim",
    "moduleAssembly":"v2.dll"}' \
  --run 'compare {"source":0,
    "other":1,"kind":"versions",
    "goto":"first"}'
```

`full.sim` and the module sources are in the download.

The command moves to step 1. Its `difference` result is:

```json
{
  "entity": 2,
  "field": "Value",
  "source": "1",
  "other": "2"
}
```

## The full program

This is a controlled example with two builds of a tiny counter module, not a comparison of two released physics libraries. Both builds use `RecursiveAI.OneScope.Sdk` 1.0.0. The second build defines `CHANGED`, which adds one to each child counter’s inspected value after step 0. Both module assemblies identify themselves as `counter`; OneScope loads them into separate contexts.

To compare two versions of your own library:

1. Replace the demonstration `Module.cs` with your library’s viewer module implementation and reference it from the two module projects, adapting the launcher’s replay filename as needed. Keep the same module id in both builds.
2. Give `modules/v1/Module.csproj` and `modules/v2/Module.csproj` package references to the respective released versions of your library. Keep each module’s dependencies in its own output folder; point the first `--module` and the second `moduleAssembly` at those output DLLs rather than the sample’s copied counter DLLs.


`full.sim`, the entire replay description, is:

```json
{"entities":3,"bias":0}
```

The sample module interprets this as three child entities under one world root and three empty step inputs. Each step increments the counter. The root reports the entity count. `Module.cs` and both module project files are in the zip; `Program.cs` builds them and drives the tool. To build your own module project, install its SDK dependency:

```shell
dotnet add package RecursiveAI.OneScope.Sdk
```

```csharp
using System.Diagnostics;
using System.Text.Json;

string toolDir = Path.GetFullPath("obj/onescope-tool");
string tool = Path.Combine(toolDir,
    OperatingSystem.IsWindows() ? "onescope.exe" : "onescope");
if (!File.Exists(tool))
    Run("dotnet", "tool", "install", "RecursiveAI.OneScope.Tool",
        "--version", "1.0.0", "--tool-path", toolDir);

foreach (string version in new[] { "v1", "v2" })
    Run("dotnet", "build", $"modules/{version}/Module.csproj", "--nologo", "-v:q");
string first = Path.GetFullPath("modules/v1/bin/Debug/net10.0/CounterModule.dll");
string second = Path.GetFullPath("modules/v2/bin/Debug/net10.0/CounterModule.dll");
File.Copy(first, "v1.dll", true);
File.Copy(second, "v2.dll", true);
var results = Open("full.sim", "open " + JsonSerializer.Serialize(new
    { path = "full.sim", moduleAssembly = second }),
    "compare {\"source\":0,\"other\":1,\"kind\":\"versions\",\"goto\":\"first\"}");
var comparison = results[^1];
Console.WriteLine($"Kind: {comparison.GetProperty("kind")}");
Console.WriteLine($"First divergent step: {comparison.GetProperty("agreement").GetProperty("firstDivergent")}");
Console.WriteLine($"Field: {comparison.GetProperty("difference").GetProperty("field")}");
Console.WriteLine($"Offset: {comparison.GetProperty("offset")}");

string Run(string command, params string[] arguments)
{
    var start = new ProcessStartInfo(command)
    {
        RedirectStandardOutput = true, RedirectStandardError = true
    };
    foreach (string argument in arguments) start.ArgumentList.Add(argument);
    using var process = Process.Start(start)!;
    var output = process.StandardOutput.ReadToEndAsync();
    var errors = process.StandardError.ReadToEndAsync();
    process.WaitForExit();
    if (process.ExitCode != 0)
        throw new InvalidOperationException(output.Result + errors.Result);
    return output.Result;
}
JsonElement[] Open(string file, params string[] operations)
{
    var arguments = new List<string>
    {
        "open", file, "--headless", "--size", "640x480",
        "--artifacts", "output", "--module", first
    };
    foreach (string operation in operations)
        arguments.AddRange(["--run", operation]);
    return Run(tool, arguments.ToArray())
        .Split('\n', StringSplitOptions.RemoveEmptyEntries)
        .Select(line => JsonDocument.Parse(line).RootElement.Clone()).ToArray();
}
```

`dotnet run` prints:

```text
Kind: versions
First divergent step: 1
Field: Value
Offset: 0
```

If a file cannot be opened by a loaded module, the operation returns an error instead of a comparison; inspect the error code and message before reading comparison fields.

## Variations

To set the alignment explicitly and return only one differing row, replace the final `compare` operation above with:

```text
compare {"source":0,"other":1,
  "kind":"versions","offset":0,
  "limit":1,"goto":"first"}
```

This is the operation string passed to `--run`, not a standalone shell command. In this example `rowCount` is 3, the returned `rows` contains one entry, and `truncated` is `true`.

## Related recipes

- [Save and reopen a debugging session](https://recursiveai.org/onescope/cookbook/save-and-reopen-debug-session/)
- [Compare shared entities](https://recursiveai.org/onescope/cookbook/compare-shared-entities/)
- [Overlay a reference pose cache](https://recursiveai.org/onescope/cookbook/overlay-reference-pose-cache/)
- [Write a reference pose cache](https://recursiveai.org/onescope/cookbook/write-reference-pose-cache/)

## The short answer

Open the same simulation replay twice, choosing each module assembly with `moduleAssembly`, then run `compare` with `kind:"versions"`. OneScope loads module copies into separate load contexts. It compares the worlds through the rows each module exposes to the inspector rather than handing one module a world object created by the other.

This is useful when the format is readable by both versions but a change in the simulation may alter its results. Each module needs to run the library version you intend to test. The demonstration here changes a counter module’s implementation so that the comparison has a known difference. It is not evidence about compatibility between any two published library releases.

## How do I compare simulation replays across library versions?

Keep each module and its version-specific dependencies in a separate folder. The module id must identify the same module on both sides. Open the first recording with one assembly and the second with the other. Explicitly naming `moduleAssembly` also lets you select that module when a bundled module would otherwise accept the file.

The sample compares identical input data. At step 0, both builds report the same child values. From step 1 onward, the changed build reports one more. The result identifies entity 2 and its `Value` field as the first difference. The inspector is the comparison surface: a value a module never exposes as a row cannot appear as a differing inspector row.

The result also contains world hashes and an agreement history. Read `agreement.basis` rather than assuming every kind of comparison has the same meaning. In this version comparison it is `world hash`. The first and last divergent steps summarize the scanned interval; `rows` describes the selected step. A first differing row and a first divergent simulation step answer different questions.

## How do I align replay steps when versions start at different numbers?

The version pair can align recordings whose start step differs but whose step counts match. If one numbers its first state 0 and the other 1, the comparison can account for that difference. Steps in the result use the first source’s numbering, and `otherStep` gives the second source’s step. A row that merely reports each world’s own step number is not treated as a difference.

Pass `offset` to set the alignment explicitly. First check the recordings’ start and end ranges and what a step means in each module. The shared timeline cursor still puts sources at one step number; the version comparison’s alignment describes how the pair is compared.

Use `goto:"first"` to move to the first divergent step, or request the next, previous or last one. `moved` tells you whether a destination was found. Bound the returned inspector rows with `limit`; if `truncated` is true, `rowCount` is the full count, not the size of the returned array. The private agreement scan has a documented ceiling of 100,000 steps, so check `agreement.complete` before treating it as a scan of the whole recording. See [comparison operations](https://recursiveai.org/onescope/docs/shell/#compare).
