Recursive AI

Write a pose cache in C#

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.

A pose cache records positions and rotations without simulation state. OneScope debugs recorded simulation steps, such as physics; a fork is a branched copy of a recording.

Terminal
dotnet add package RecursiveAI.OneScope.Sdk

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using System.Buffers;
using OneScope.Sdk;
var bytes = new ArrayBufferWriter<byte>();
PoseCache.Create(
    info: new(Dimensions: 2, Unit: LengthUnit.Metre,
        StartStep: 0, ModuleId: "", LibraryVersion: "",
        Scene: "", Replay: "", Label: ""),
    entities: [new(Id: new(2), Name: "",
        Shape: PoseShape.Point, Size: default)],
    // Position x,y,z; rotation x,y,z,w.
    poses: [0, 4, 0, 0, 0, 0, 1]).Write(bytes);
File.WriteAllBytes("reference.ospc",
    bytes.WrittenSpan.ToArray());

Writes reference.ospc; there is no console output. Dimensions: 2 means 2D, and new(2) is entity id 2. Keep position Z at 0 for 2D drawing, as specified by the module drawing contract; the file still stores all three position components.

Unzip the example, then dotnet run.

The full program

The snippet at the top writes a point at (0, 4, 0) with identity rotation. This full program gives it a sphere shape of radius 0.5 meters and the name “Ball”, and sets the label to “Reference”. Both use entity id 2 in 2D at step 0; all origin strings are empty. Neither needs an input file.

C#
using System.Buffers;
using System.Numerics;
using OneScope.Sdk;
 
var cache = PoseCache.Create(
    info: new(Dimensions: 2, Unit: LengthUnit.Metre,
        StartStep: 0, ModuleId: "", LibraryVersion: "",
        Scene: "", Replay: "", Label: "Reference"),
    entities: [new(Id: new(2), Name: "Ball",
        Shape: PoseShape.Sphere, Size: new Vector3(0.5f, 0, 0))],
    // Position x,y,z; rotation x,y,z,w. Z is 0 for 2D.
    poses: [0, 4, 0, 0, 0, 0, 1]);
var bytes = new ArrayBufferWriter<byte>();
cache.Write(bytes);
File.WriteAllBytes("reference.ospc",
    bytes.WrittenSpan.ToArray());
Console.WriteLine("Wrote reference.ospc");

dotnet run prints:

Text
Wrote reference.ospc

Invalid cache parts passed to Create raise ArgumentException; malformed file bytes passed to Parse raise ScopeFormatException naming the input and expected format.

Variations

Open the generated file as a ghost (a read-only pose reference). Install the viewer tool, then open the file:

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.

Terminal
dotnet tool install --global RecursiveAI.OneScope.Tool
onescope open reference.ospc --headless \
  --run 'ghost'

The ghost result is:

JSON
{
  "ghosts": [
    {
      "source": 0,
      "name": "reference.ospc",
      "step": 0,
      "startStep": 0,
      "endStep": 0,
      "views": []
    }
  ]
}

There are no overlay views because no compatible simulation source is open. The cache has its own views. To overlay it, open a replay first.

Record an absent entity. This complete replacement program writes two frames starting at step 10; the sphere is absent in the second frame:

C#
using System.Buffers;
using System.Numerics;
using OneScope.Sdk;
 
var cache = PoseCache.Create(
    info: new(Dimensions: 2, Unit: LengthUnit.Metre,
        StartStep: 10, ModuleId: "", LibraryVersion: "",
        Scene: "", Replay: "", Label: "Brief clip"),
    entities: [new(Id: new(2), Name: "Ball",
        Shape: PoseShape.Sphere, Size: new Vector3(0.5f, 0, 0))],
    // Each frame: position x,y,z; rotation x,y,z,w.
    poses: [0, 4, 0, 0, 0, 0, 1,
     float.NaN, 0, 0, 0, 0, 0, 0]);
var bytes = new ArrayBufferWriter<byte>();
cache.Write(bytes);
File.WriteAllBytes("brief.ospc", bytes.WrittenSpan.ToArray());
Console.WriteLine($"Frames: {cache.FrameCount}");
Console.WriteLine($"Present at frame 1: {cache.TryGetPose(1, 0, out _, out _)}");
Text
Frames: 2
Present at frame 1: False

The short answer

Use OneScope.Sdk.PoseCache.Create with cache metadata, a fixed entity list, and a flat array of pose floats. Call Write with an IBufferWriter<byte>, then save those bytes. OneScope opens the resulting .ospc file as a ghost: a read-only sequence of poses that can be scrubbed or drawn over a compatible simulation source.

This is a pose export, not a full simulation replay. It stores where things are and how they are rotated, not the inputs or internal world state needed to simulate them again. Use it for known positions from a reference implementation or a baked clip. The example uses literal data so the output is reproducible without another engine or a recording service.

How do I write a simulation pose cache in C#?

Define entities once, with unique nonzero ids, display names, shapes and sizes. The same entity list applies to every frame. For a sphere, the size’s X component is the radius. For a box, the vector contains half extents; for a capsule, X is radius and Y is half the segment length along local Y. A point does not require a solid shape.

Supply seven floats per entity per frame: position X, Y, Z, then rotation X, Y, Z, W. Place every entity’s pose for one frame before the next frame’s poses. Frame zero belongs to StartStep; each following frame advances one step. The example writes one frame beginning at zero. For several entities, accidentally grouping all frames of entity A before all frames of entity B would change which transforms are read together.

The metadata declares 2D or 3D coordinates and a length unit. Store positions and sizes in that unit. The viewer does not rescale a ghost to match another source, so normalize the exported data yourself when the reference and target simulations use different units. Empty origin strings are allowed; they do not prevent loading the cache.

How do I represent missing entities and check cache limits?

Keep the fixed entity entry, but put NaN in position X for a frame where it is absent. Rotation is then ignored. For a present entity, values must be finite; a zero rotation reads as the identity. The second program demonstrates absence and checks it with TryGetPose, which returns false for that entity and frame.

The format permits 1 to 65,536 entities and 1 to 4,194,304 frames, but the file limit is 256 MiB. Each entity contributes 28 pose bytes per frame, before metadata and entity definitions. Those count limits do not mean their maximum product fits into one file. Text fields are limited to 1,024 UTF-8 bytes. See the pose-cache format for the exact field order and limits.

The reader checks lengths against the remaining data before allocation. A file must end after exactly the declared poses; truncation or extra pose bytes are not silently accepted. Parse provides format errors with the input name and offset. When opening the cache in the viewer, remember that a successful parse establishes a readable pose file, not equivalence with a simulation: ghosts cannot be forked, poked, or used in state diffs.

All onescope recipes