---
title: "Limit replay keyframe memory in C#"
url: https://recursiveai.org/onescope/cookbook/bound-replay-keyframe-memory/
description: "Keyframes are saved world snapshots used when you seek through a replay. Limit their retained count without changing the replay’s state."
version: "1.0.0"
---

# Limit replay keyframe memory 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.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneScope.Sdk) · [GitHub](https://github.com/recursive-ai-oss/onescope-csharp) · [Docs](https://recursiveai.org/onescope/docs/) · [Download example (.zip)](https://recursiveai.org/onescope/cookbook/bound-replay-keyframe-memory/bound-replay-keyframe-memory.zip)

Limit the saved world snapshots retained while seeking a replay. This bounds OneScope’s snapshot cache for recordings from OneScope modules such as OnePhysics (`.oppr`), not recordings in arbitrary C# applications.

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

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

`run.oppr` is included in the zip.

```csharp
using OneScope.Sdk;
using RecursiveAI.OnePhysics.Scope;

var source = new OnePhysicsModule().Open(
    File.ReadAllBytes("run.oppr"), "run.oppr");
var history = new History(source,
    new ScopeHistoryOptions {
        KeyframeInterval = 8, MaxKeyframes = 3
    });
history.Seek(120);
Console.WriteLine(history.KeyframeCount);
```

```text
3
```

Unzip [the example](https://recursiveai.org/onescope/cookbook/bound-replay-keyframe-memory/bound-replay-keyframe-memory.zip), then `dotnet run`.

## The full program

The example compares each seek with linear playback after filling a three-snapshot cache. The original sample contains a static 20 by 1 ground box centered at (0, −1) and a dynamic circle of radius 0.5 starting at (0, 4), run for 120 steps at 1/60 second with OnePhysics 1.0.0.

```csharp
using OneScope.Sdk;
using RecursiveAI.OnePhysics.Scope;

var source = new OnePhysicsModule().Open(
    File.ReadAllBytes("run.oppr"), "run.oppr");
var history = new History(source, new ScopeHistoryOptions
{
    KeyframeInterval = 8,
    MaxKeyframes = 3
});
var linear = new History(source);
var hashes = new Dictionary<long, ulong> { [0] = linear.StateHash };
while (linear.Step()) hashes[linear.CurrentStep] = linear.StateHash;
foreach (long step in new long[] { 120, 16, 96, 0, 64, 120 })
{
    history.Seek(step);
    bool equal = history.StateHash == hashes[step];
    if (!equal || history.KeyframeCount > 3)
        throw new InvalidOperationException("Replay or cache bound failed.");
    Console.WriteLine($"Step {step}: match={equal}, keyframes={history.KeyframeCount}");
}
Console.WriteLine($"Retained snapshot bytes: {history.KeyframeBytes}");
```

`dotnet run` prints:

```text
Step 120: match=True, keyframes=3
Step 16: match=True, keyframes=3
Step 96: match=True, keyframes=3
Step 0: match=True, keyframes=3
Step 64: match=True, keyframes=3
Step 120: match=True, keyframes=3
Retained snapshot bytes: 3370
```

Invalid cache options throw `ArgumentOutOfRangeException`; seeking outside the recording’s range also throws.

## Variations

### Keep only two snapshots

After the snippet at the top, with the same `source`:

```csharp
var minimal = new History(source,
    new ScopeHistoryOptions { MaxKeyframes = 2 });
minimal.Seek(120);
Console.WriteLine(minimal.KeyframeCount);
```

```text
2
```

## Related recipes

- [View a physics replay](https://recursiveai.org/onescope/cookbook/view-physics-replay/)
- [Inspect navigation-mesh build stages](https://recursiveai.org/onescope/cookbook/inspect-navmesh-build-stages/)

## The short answer

Set `ScopeHistoryOptions.MaxKeyframes` when constructing `History`. Set `KeyframeInterval` separately to choose how often played steps produce snapshots. This bounds the retained snapshot count, not the total memory of your process or a graphics device. The replay remains seekable after eviction: OneScope restores an earlier retained state and runs the recorded inputs again.

## How do I limit memory usage?

`MaxKeyframes` limits the number of retained snapshots; it is not a byte budget. `KeyframeBytes` reports the sum of their serialized byte lengths. It excludes the recording, the current world, scratch buffers, timeline samples and findings. It also excludes object and collection overhead. Do not interpret a snapshot-byte reading as the application’s working set.

The [history options](https://github.com/recursive-ai-oss/onescope-csharp/blob/d681be56420836ea0d8874416367bc8181235813/src/OneScope.Sdk/ScopeHistoryOptions.cs) default to an interval of 64 steps and at most 1024 keyframes. The interval must be at least 1 and the count at least 2. The two-frame minimum allows the start snapshot and the newest keyframe to stay available. Choose the interval and count when you create the history; the options expose init-only properties.

For a recording with changing world size, equal snapshot counts need not mean equal retained bytes. Read `KeyframeBytes` on your actual workload. If timeline data is also a concern, the same options have independent `MaxTrackSamples` and `MaxFindings` controls. Reducing those is not a replacement for bounding the snapshot cache.

## What happens when keyframes are evicted?

C#’s garbage collector cannot release objects that are still referenced. A bounded cache removes references to old entries; it does not promise an immediate decrease in the operating system’s memory reading. You do not need to force a collection each time you seek.

In OneScope, eviction removes an interior keyframe whose neighbors are closest together. The start step and newest keyframe remain. The [history documentation](https://recursiveai.org/onescope/docs/history/) guarantees the same `StateHash` as linear playback even after keyframes are evicted. The full program exercises that behavior by filling the cache, going backward, returning forward, and comparing against hashes collected by stepping from the beginning.

The first visit to a stretch still simulates that stretch. Keyframes are created as playback reaches them; an interval is not a request to precompute the entire replay. Eviction can leave wider gaps between retained frames, so a backward seek may need to replay more steps than a seek in a region with closely spaced snapshots. Use `Scan()` when you want to run through the recording and fill its timeline, not when you merely want to set a memory limit.

A replay also carries its setup and step inputs. Bounding snapshots does not truncate those inputs, shorten the replay or change the file. For recordings that begin at a live-attach step, use `StartStep` and `EndStep` instead of assuming that the first cursor position is zero.
