Replays and digests: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: live (line 85), agree (line 134), diverge (line 139), wire (line 144).
1/// A number that says whether two runs of the same simulation are the same
2/// run, taken every so many steps so a divergence can be bracketed to where
3/// it happened.
4///
5/// A `Demo` is the file this rides in: a level, the state a run started from,
6/// its tape of inputs and this checkpoint trace, so a server can replay a
7/// submitted run and know exactly where it stopped agreeing.
8///
9/// Quoted by `replay_digest.md` and shown whole in the Source tab.
10library;
12import 'package:flutter3d/flutter3d.dart';
13import 'package:flutter3d_showcase/src/demo/demo.dart';
14import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
15import 'package:flutter3d_sim/flutter3d_sim.dart';
16import 'package:vector_math/vector_math.dart';
18final class ReplayDigestDemo extends ShowcaseDemo {
19 late final String _report;
21 double driftAt = 9.0;
22 bool _dirty = true;
23 late final List<BarGauge> _original;
24 late final List<BarGauge> _replay;
25 late final List<MeshNode> _lamps;
27 static const int _checkpoints = 5;
30 void configureView(DemoContext context) {
31 context.orbit
32 ..distance = 12.0
33 ..pitch = 0.45
34 ..yaw = 0.0;
35 context.orbit.target.setValues(0.0, 1.4, 0.0);
36 }
39 Scene build(DemoContext context) {
40 _report = _run();
41 BarGauge tower(String name, Vector4 ink, double x, double z) => BarGauge(
42 context,
43 name,
44 ink,
45 Vector3(x, 0.0, z),
46 height: 3.0,
47 width: 0.8,
48 vertical: true,
49 );
50 _original = <BarGauge>[
51 for (var k = 0; k < _checkpoints; k++)
52 tower(
53 'original $k',
54 Vector4(0.45, 0.65, 0.95, 1.0),
55 -4.0 + k * 2.0,
56 -0.8,
57 ),
58 ];
59 _replay = <BarGauge>[
60 for (var k = 0; k < _checkpoints; k++)
61 tower('replay $k', Vector4(0.95, 0.65, 0.3, 1.0), -4.0 + k * 2.0, 0.8),
62 ];
63 _lamps = <MeshNode>[
64 for (var k = 0; k < _checkpoints; k++)
65 ballNode(
66 context,
67 'lamp $k',
68 0.3,
69 Vector4(0.3, 0.3, 0.33, 1.0),
70 at: Vector3(-4.0 + k * 2.0, 3.7, 0.0),
71 ),
72 ];
73 return sceneOf(<SceneNode>[
74 floorNode(context, width: 12.0, depth: 5.0),
75 for (final BarGauge g in _original) ...g.nodes,
76 for (final BarGauge g in _replay) ...g.nodes,
77 ..._lamps,
78 ]);
79 }
82 void update(DemoContext context, double dt) {
83 if (!_dirty) return;
84 _dirty = false;
85 // The original, and a replay that drifts by one from the chosen step on.
86 // A checkpoint every four steps: the tower is the state there, and its
87 // lamp is green while the two digests agree.
88 final DigestTrace original = _record(20, (int step) => step);
89 final DigestTrace replay = _record(
90 20,
91 (int step) => step >= driftAt.round() ? step + 1 : step,
92 );
93 for (var k = 0; k < _checkpoints; k++) {
94 final int step = (k + 1) * 4;
95 _original[k].set(step / 22.0);
96 _replay[k].set((step >= driftAt.round() ? step + 1 : step) / 22.0);
97 final bool same = original.digests[k] == replay.digests[k];
98 _lamps[k].material.baseColor.setValues(
99 same ? 0.35 : 0.9,
100 same ? 0.85 : 0.3,
101 same ? 0.4 : 0.3,
102 1.0,
103 );
104 }
105 }
108 List<DemoControl> controls(DemoContext context) => <DemoControl>[
109 SliderControl(
110 'Drift starts at step',
111 min: 1,
112 max: 21,
113 value: () => driftAt,
114 onChanged: (double v) {
115 driftAt = v.roundToDouble();
116 _dirty = true;
117 },
118 format: (double v) => v.round() > 20 ? 'never' : v.round().toString(),
119 ),
120 ];
122 /// A toy step: a counter that walks forward, standing in for a simulation.
123 static Map<String, Object?> _stepOf(int x) => <String, Object?>{'x': x};
125 static DigestTrace _record(int steps, int Function(int step) valueAt) {
126 final trace = DigestTrace(every: 4);
127 for (var step = 1; step <= steps; step++) {
128 trace.observe(step, _stepOf(valueAt(step)));
129 }
130 return trace;
131 }
133 static String _run() {
134 // Two recordings of the same run agree at every checkpoint.
135 final original = _record(20, (int step) => step);
136 final replay = _record(20, (int step) => step);
137 final agree = replay.divergenceFrom(original.digests) == null;
139 // A replay whose state drifts from step 9 onward parts company with the
140 // original at the first checkpoint after that.
141 final drifted = _record(20, (int step) => step >= 9 ? step + 1 : step);
142 final where = drifted.divergenceFrom(original.digests);
144 // What a `.f3drun` actually carries: the trace as hex digits, written
145 // out and read back.
146 final wireForm = original.toJson();
147 final reread = DigestTrace.fromJson(wireForm);
148 final roundTrips = reread.divergenceFrom(original.digests) == null;
150 return 'a faithful replay agrees: $agree\n'
151 'a drifted one diverges at $where\n'
152 'a trace written to JSON and read back still agrees: $roundTrips';
153 }
156 void verify(Scene scene, FrameResult frame) {
157 if (frame.drawCalls < 1) {
158 throw StateError('the marker was not drawn');
159 }
160 if (!_report.contains('agrees: true')) {
161 throw StateError('two recordings of the same run must agree');
162 }
163 // The drift starts at step 9; the checkpoint every 4 steps that first
164 // covers it is step 12.
165 if (!_report.contains('step 12')) {
166 throw StateError(
167 'the divergence should be reported at the first '
168 'checkpoint that covers the drift',
169 );
170 }
171 if (!_report.contains('still agrees: true')) {
172 throw StateError('a trace should read back exactly what it wrote');
173 }
174 }
175}