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.
dotnet add package RecursiveAI.OneScope.Sdk dotnet add package RecursiveAI.OnePhysics.Scope
Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
run.oppr is included in the zip.
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);
3Unzip the example, 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.
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:
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:
var minimal = new History(source, new ScopeHistoryOptions { MaxKeyframes = 2 }); minimal.Seek(120); Console.WriteLine(minimal.KeyframeCount);
2Related recipes
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 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 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.