feat(replay): add replay timeline

This commit is contained in:
dualfroz committed 2026-08-26 13:51:57 +02:00
1 parent fe85a30407
commit 7317326166
1 file changed
+140
+140
View File
@@ -0,0 +1,140 @@
using System;
using Chip8.Core;
namespace Chip8.Replay;
public sealed class Timeline
{
public const int defaultKeyframeInterval = 8;
public const int defaultCapacity = 256;
public Machine machine { get; }
public InputMovie movie { get; }
public RewindBuffer rewind { get; }
public int keyframeInterval { get; }
public int currentFrame { get; private set; }
int nextKeyframeFrame;
Timeline(Machine machine, InputMovie movie, int keyframeInterval, int capacity)
{
this.machine = machine;
this.movie = movie;
this.keyframeInterval = keyframeInterval;
rewind = new RewindBuffer(capacity);
currentFrame = 0;
nextKeyframeFrame = 0;
}
public static Timeline forMovie(InputMovie movie, byte[] rom,
int keyframeInterval = defaultKeyframeInterval, int capacity = defaultCapacity)
{
if (keyframeInterval < 1)
{
throw new ArgumentOutOfRangeException(nameof(keyframeInterval));
}
var machine = MachineBuilder.fromMovie(movie, rom);
return new Timeline(machine, movie, keyframeInterval, capacity);
}
public static Timeline forMachine(Machine machine, InputMovie movie,
int keyframeInterval = defaultKeyframeInterval, int capacity = defaultCapacity)
{
if (keyframeInterval < 1)
{
throw new ArgumentOutOfRangeException(nameof(keyframeInterval));
}
return new Timeline(machine, movie, keyframeInterval, capacity);
}
public int instructionsPerFrame => movie.instructionsPerFrame;
public void advanceFrame()
{
maybeRecordKeyframe();
stepForwardOnce();
}
public void advanceFrames(int count)
{
for (int frame = 0; frame < count; frame++)
{
advanceFrame();
}
}
public void advanceTo(int frame)
{
if (frame < currentFrame)
{
throw new ArgumentOutOfRangeException(nameof(frame), "advanceTo cannot move backward; use rewindTo");
}
while (currentFrame < frame)
{
advanceFrame();
}
}
public void rewindTo(int frame)
{
if (frame < 0)
{
throw new ArgumentOutOfRangeException(nameof(frame));
}
if (frame > currentFrame)
{
throw new ArgumentOutOfRangeException(nameof(frame), "rewindTo cannot move forward; use advanceTo");
}
if (frame == currentFrame)
{
return;
}
TimelineSnapshot? keyframe = rewind.nearestAtOrBefore(frame);
if (keyframe is null)
{
throw new InvalidOperationException(
"frame " + frame + " is older than the retained rewind window (oldest keyframe "
+ rewind.oldestFrame + ")");
}
machine.loadState(keyframe.state);
currentFrame = keyframe.frameIndex;
while (currentFrame < frame)
{
stepForwardOnce();
}
}
public bool stepBack()
{
if (currentFrame == 0)
{
return false;
}
rewindTo(currentFrame - 1);
return true;
}
public byte[] currentState() => machine.saveState();
public ReplayOutcome outcome() => ReplayOutcome.capture(machine, currentFrame);
void stepForwardOnce()
{
KeyMask injected = movie.keysAt(currentFrame);
injected.applyTo(machine.keypad);
machine.runFrame(instructionsPerFrame);
currentFrame++;
}
void maybeRecordKeyframe()
{
if (currentFrame >= nextKeyframeFrame)
{
rewind.store(currentFrame, machine.saveState());
nextKeyframeFrame = currentFrame + keyframeInterval;
}
}
}