1 //          Copyright Ferdinand Majerech 2014.
2 // Distributed under the Boost Software License, Version 1.0.
3 //    (See accompanying file LICENSE_1_0.txt or copy at
4 //          http://www.boost.org/LICENSE_1_0.txt)
5 
6 
7 /// Code used to handle recording and replaying of InputDevice input.
8 module platform.inputrecording;
9 
10 
11 import std.array;
12 import std.conv;
13 import std.exception;
14 import std.typecons;
15 
16 import io.yaml;
17 
18 import platform.inputdevice;
19 
20 /// Enumerates possible recording states of an InputRecordingDevice.
21 enum RecordingState
22 {
23     /// The InputRecordingDevice is not recording.
24     NotRecording,
25     /** The InputRecordingDevice about to start recording.
26      *
27      * InputRecordingDevice doesn't record the first frame after a startRecording() call
28      * to avoid recording the input that caused the recording to start.
29      */
30     FirstFrame,
31     /// The InputRecordingDevice is recording.
32     Recording
33 }
34 
35 
36 /** Records input received by an InputDevice. Used to generate recorded input benchmarking
37  *  demos (and possibly input macros in future?).
38  */
39 final class InputRecordingDevice
40 {
41 private:
42     /// Current recording state (are we recording?).
43     RecordingState state_ = RecordingState.NotRecording;
44 
45     import std.container;
46     /// Recorded keyboard input is copied here whenever mouseRecorder_ runs out of space.
47     Array!ubyte recordedDataMouse_;
48     /// Recorded keyboard input is copied here whenever keyboardRecorder_ runs out of space.
49     Array!ubyte recordedDataKeyboard_;
50 
51     /// Buffer used by mouseRecorder_ to record data to.
52     ubyte[] mouseRecordBuffer_;
53     /// Buffer used by keyboardRecorder_ to record data to.
54     ubyte[] keyboardRecordBuffer_;
55 
56     /// Records mouse input.
57     Recorder!Mouse    mouseRecorder_;
58     /// Records keyboard input.
59     Recorder!Keyboard keyboardRecorder_;
60 
61     /// Input device to record input from.
62     const InputDevice input_;
63 
64 public:
65 nothrow:
66     /// Construct a recording device capable of recording input from specified input device.
67     this(const InputDevice input) @trusted
68     {
69         input_ = input;
70         enum recBufLength = 64 * 1024;
71         import core.stdc.stdlib: malloc;
72         mouseRecordBuffer_    = (cast(ubyte*)malloc(recBufLength))[0 .. recBufLength];
73         keyboardRecordBuffer_ = (cast(ubyte*)malloc(recBufLength))[0 .. recBufLength];
74     }
75 
76     /** Destroy a recording device.
77      *
78      * Must be called to ensure deletion of any buffers used.
79      */
80     ~this() @trusted @nogc
81     {
82         import core.stdc.stdlib: free;
83         free(mouseRecordBuffer_.ptr);
84         free(keyboardRecordBuffer_.ptr);
85     }
86 
87     /** Start recording.
88      *
89      * Clears previously recorded data, if any, and starts recording from scratch. The
90      * frame immediately after startRecording() will not be recorded to avoid recording
91      * the input that triggered recording in the first place. Actual recording will start
92      * with the second frame.
93      */
94     void startRecording() @trusted
95     {
96         assert(state_ == RecordingState.NotRecording,
97                "Trying to start recording when we're already recording");
98         state_ = RecordingState.FirstFrame;
99         mouseRecorder_    = Recorder!Mouse(mouseRecordBuffer_);
100         keyboardRecorder_ = Recorder!Keyboard(keyboardRecordBuffer_);
101         recordedDataMouse_.reserve(256 * 1024).assumeWontThrow;
102         recordedDataKeyboard_.reserve(256 * 1024).assumeWontThrow;
103     }
104 
105     /** Stop recording.
106      *
107      * Can be called only after startRecording(), and only once per a startRecording()
108      * call.
109      */
110     void stopRecording() @trusted
111     {
112         assert(state_ != RecordingState.NotRecording,
113                "Trying to end recording when we're not recording");
114 
115         delegate
116         {
117             recordedDataMouse_    ~= mouseRecorder_.recordedData;
118             recordedDataKeyboard_ ~= keyboardRecorder_.recordedData;
119             mouseRecorder_.reset();
120             keyboardRecorder_.reset();
121             destroy(mouseRecorder_);
122             destroy(keyboardRecorder_);
123         }().assumeWontThrow;
124         state_ = RecordingState.NotRecording;
125     }
126 
127     /// Get the current recording state (are we recording? first frame before recording?).
128     RecordingState state() @safe pure const @nogc
129     {
130         return state_;
131     }
132 
133     /// Get input recorded from the mouse since the last startRecording() call.
134     Recording!Mouse mouseRecording() @safe
135     {
136         return new BinaryRecording!Mouse(recordedDataMouse_);
137     }
138 
139     /// Get input recorded from the keyboard since the last startRecording() call.
140     Recording!Keyboard keyboardRecording() @safe
141     {
142         return new BinaryRecording!Keyboard(recordedDataKeyboard_);
143     }
144 
145     /// Update the recording device. If recording enabled, record input for the current frame.
146     void update() @trusted
147     {
148         if(state_ == RecordingState.Recording)
149         {
150             record(mouseRecorder_, input_.mouse, recordedDataMouse_);
151             record(keyboardRecorder_, input_.keyboard, recordedDataKeyboard_);
152         }
153         if(state_ == RecordingState.FirstFrame) { state_ = RecordingState.Recording; }
154     }
155 
156 private:
157     /** Record input for current frame from specified source.
158      *
159      * Params:
160      *
161      * recorder     = Recorder to record into.
162      * input        = Source of input to record (e.g. Keyboard or Mouse).
163      * recordedData = Sink to write recorded data to if recorder runs out of space/
164      */
165     static void record(Input)(ref Recorder!Input recorder, const Input input, ref Array!ubyte recordedData)
166         @system
167     {
168         if(recorder.notEnoughSpace)
169         {
170             (recordedData ~= recorder.recordedData).assumeWontThrow;
171             recorder.reset();
172         }
173         recorder.recordFrame(input);
174     }
175 }
176 
177 
178 /** Convert input data from all sources recorded by an InputDevice to YAML.
179  */
180 YAMLNode recordingAsYAML(InputRecordingDevice recorder) @safe nothrow
181 {
182     return YAMLNode(["mouse", "keyboard"],
183                     [recorder.mouseRecording.toYAML, recorder.keyboardRecording.toYAML]);
184 }
185 
186 /** Replay input data from YAML as generated by recordingAsWAML.
187  *
188  * Will fail with a logged warning (in the InputDevice log) if YAML didn't store a valid
189  * recording.
190  *
191  * Params:
192  *
193  * input = InputDevice to replay the input.
194  * yaml  = YAML to load input to replay from.
195  * block = Should the real input sources be blocked while replaying?
196  *         (E.g. blocking the actual mouse while replaying mouse input).
197  */
198 void replayFromYAML(InputDevice input, YAMLNode yaml, Flag!"block" block) @safe nothrow
199 {
200     enum baseMsg = "Failed to load input replay from YAML: ";
201     try
202     {
203         auto mouseYAML    = yaml["mouse"];
204         auto keyboardYAML = yaml["keyboard"];
205         auto mouseRecording    = new YAMLRecording!Mouse(mouseYAML);
206         auto keyboardRecording = new YAMLRecording!Keyboard(keyboardYAML);
207         input.replay(mouseRecording, block ? Yes.blockMouse : No.blockMouse);
208         input.replay(keyboardRecording, block ? Yes.blockKeyboard : No.blockKeyboard);
209     }
210     catch(YAMLException e) { input.log_.warning(baseMsg, e.msg).assumeWontThrow; }
211     catch(ConvException e) { input.log_.warning(baseMsg, e.msg).assumeWontThrow; }
212     catch(Exception e) { assert(false, "Unexpected exception in replayFromYAML"); }
213 }
214 
215 
216 /** Convert an input recording to YAML.
217  *
218  * Params:
219  *
220  * recording = Recording to convert. Will be (or should be assumed to be) consumed.
221  */
222 YAMLNode toYAML(Input)(Recording!Input recording) @trusted nothrow
223 {
224     Input.BaseState lastState;
225     // Not particularly GC-efficient, can be optimized (prealloc) if needed.
226     string[] keys;
227     YAMLNode[] values;
228     foreach(state; recording)
229     {
230         if(state == lastState)
231         {
232             keys   ~= "NoChange";
233             values ~= YAMLNode(YAMLNull());
234             continue;
235         }
236 
237         lastState = state;
238         keys   ~= "Change";
239         values ~= state.toYAML();
240     }
241     return YAMLNode(keys, values, "tag:yaml.org,2002:pairs");
242 }
243 
244 
245 /** Base class for input recordings of specified Input type (Mouse or Keyboard).
246  *
247  * Input type must define a BaseState type defining all state to be recorded (all input
248  * state in Input should be either in BaseState or calculated from BaseState data).
249  *
250  * Acts as an input range of Input.BaseState.
251  */
252 abstract class Recording(Input)
253 {
254 protected:
255     // Input for the current frame in the recording.
256     Input.BaseState inputState_;
257 
258 public:
259     /// Move to the next frame in the recording.
260     void popFront() @safe nothrow;
261 
262     /// Get input for the current frame in the recording.
263     final ref const(Input.BaseState) front() @safe pure nothrow const @nogc
264     {
265         return inputState_;
266     }
267 
268     /// Is the recording at the end? (no more recorded frames of input)
269     bool empty() @safe pure nothrow const @nogc;
270 }
271 
272 package:
273 
274 /** Records input of an Input type (Mouse or Keyboard).
275  *
276  * Input type must define a BaseState type defining all state to be recorded (all input
277  * state in Input should be either in BaseState or calculated from BaseState data).
278  *
279  * Data is recorded by passing a buffer to a Recorder constructor, and repeatedly checking
280  * if there's enough space using $(D notEnoughSpace()), recording input using
281  * $(D recordFrame()) when there's enough space and dumping or copying $(D recordedData()) 
282  * followed by a $(D reset()) when there's not enough space.
283  */
284 struct Recorder(Input)
285 {
286     enum minStorageBytes = Event.sizeof + Input.BaseState.sizeof;
287 private:
288     // Buffer used to store recorded data (as raw bytes).
289     ubyte[] storage_;
290 
291     // Size of used data in storage_ in bytes.
292     size_t used_;
293 
294     // Recording event IDs.
295     enum Event: ubyte
296     {
297         // No change in input state since the last frame. Reuse previous state.
298         NoChange = 0,
299         // Input has changed since the last frame. Rewrite input with new state.
300         Change = 1
301     }
302 
303     // Last recorded state. Used by recordFrame() to determine if the state has changed.
304     Input.BaseState lastState_;
305 
306     /* True if the recorder has just been constructed/reset and there is no recorded data yet.
307      *
308      * Forces the first recorded event to be a 'Change' event so we record the initial
309      * input state.
310      */
311     bool start_ = true;
312 
313 public:
314 pure nothrow @nogc:
315     /** Construct a Recorder with specified storage buffer.
316      *
317      * Params:
318      *
319      * storage = Buffer to store recorded data. Must be deallocated *after* the Recorder
320      *           is destroyed. Must be at least Recorder!Input.minStorageBytes long.
321      */
322     this(ubyte[] storage) @safe
323     {
324         storage_ = storage;
325         assert(!notEnoughSpace, "Too little memory passed to Mouse.Recorder constructor");
326     }
327 
328 
329     /** Record input from a frame (game update).
330      *
331      * Params:
332      *
333      * input = Current state of the input (Mouse or Keyboard).
334      *
335      * Must not be called if notEnoughSpace() is true.
336      */
337     void recordFrame(const(Input) input) @system
338     {
339         assert(!notEnoughSpace,
340                "Recorder.recordFrame() called even though we need more space.");
341 
342         if(lastState_ == input.baseState_ && !start_)
343         {
344             storage_[used_++] = Event.NoChange;
345             return;
346         }
347 
348         start_ = false;
349         storage_[used_++] = Event.Change;
350         const size = input.baseState_.sizeof;
351         storage_[used_ .. used_ + size] = (cast(ubyte*)(&input.baseState_))[0 .. size];
352         lastState_ = input.baseState_;
353         used_ += size;
354     }
355 
356     /** If true, there is not enough space to continue recording. Must be checked by user.
357      *
358      * Once notEnoughSpace() is true, recordFrame() must not be called and the only way
359      * to continue recording is to copy recordedData() elsewhere and reset() the Recorder.
360      */
361     bool notEnoughSpace() @safe const
362     {
363         return storage_.length - used_ < minStorageBytes;
364     }
365 
366     /// Reset the recorder, clearing recorded data and reusing the storage buffer.
367     void reset() @safe
368     {
369         used_      = 0;
370         storage_[] = 0;
371         start_     = true;
372     }
373 
374     /// Get the (raw binary) data recorded so far.
375     const(ubyte)[] recordedData() @safe const
376     {
377         return storage_[0 .. used_];
378     }
379 }
380 
381 /** Recording that iterates over events recorded as binary data by a Recorder.
382  *
383  * Used for replaying recordings recorded during current game and for serializing
384  * recordings for writing to YAML.
385  */
386 final class BinaryRecording(Input): Recording!Input
387 {
388 private:
389     alias Event = Recorder!Input.Event;
390 
391     import std.container;
392 
393     // Raw binary recording.
394     Array!ubyte data_;
395 
396     // Range to the part of data_ for the remainder of the BinaryRecording range.
397     data_.Range dataRange_;
398 
399 package:
400 nothrow:
401     /** Construct a BinaryRecording from recorded data.
402      *
403      * Only InputDevice code can construct a BinaryRecording.
404      *
405      * Params:
406      *
407      * data = Recorded data.
408      */
409     this(ref Array!ubyte data)
410         @trusted //pure @nogc
411     {
412         data_      = data.dup.assumeWontThrow;
413         dataRange_ = data_[].assumeWontThrow;
414         updateFront();
415     }
416 
417 protected:
418     override void popFront() @trusted //pure @nogc
419     {
420         assert(!empty, "Calling popFront() on an empty range");
421 
422         // Skip current Event in binary data, and also skip BaseState after Change events.
423         const event = cast(Event)dataRange_.front;
424         if(event == Event.NoChange) { dataRange_.popFront(); }
425         else
426         {
427             dataRange_.popFront();
428             delegate
429             {
430                 dataRange_ = dataRange_[Input.BaseState.sizeof .. $];
431             }().assumeWontThrow;
432         }
433         updateFront();
434     }
435 
436     override bool empty() @safe pure const @nogc
437     {
438         return dataRange_.empty;
439     }
440 
441 private:
442     /// Update front of the range (inputState_). Called by constructor/popFront().
443     void updateFront() @trusted
444     {
445         if(empty) { return; }
446         // If current event is Change, read BaseState from binary data.
447         const event = cast(Event)dataRange_.front;
448         if(event != Event.NoChange)
449         {
450             assert(event == Event.Change, "unknown recorder event ID");
451             // `1` skips the event ID.
452             inputState_ = *cast(Input.BaseState*)&(dataRange_[1]);
453         }
454     }
455 }
456 
457 /// Recording that reads recorded input from YAML.
458 final class YAMLRecording(Input): Recording!Input
459 {
460 private:
461     alias Event = Recorder!Input.Event;
462 
463     // Currently GC-based, but shouldn't be an issue as recordings are mostly for
464     // debugging, and even otherwise should be used infrequently (e.g. once per game,
465     // as opposed to once per some fixed time period)
466 
467     // Recording events (Change, NoChange).
468     Event[] events_;
469     // States of recorded data to change to for every Change event in events_.
470     Input.BaseState[] states_;
471 
472 package:
473     /** Construct a YAMLRecording from a YAML node (produced by toYAML(Recording)).
474      *
475      * Throws:
476      *
477      * ConvException if any value in the YAML has unexpected format.
478      * YAMLException if the YAML has unexpected layout.
479      */
480     this(YAMLNode data) @trusted
481     {
482         foreach(string key, ref YAMLNode value; data)
483         {
484             events_.assumeSafeAppend;
485             events_ ~= to!Event(key);
486             if(events_.back == Event.NoChange) { continue; }
487             states_ ~= Input.BaseState.fromYAML(value);
488             states_.assumeSafeAppend;
489         }
490         updateFront();
491     }
492 
493 protected:
494 @safe pure nothrow @nogc:
495     override void popFront()
496     {
497         assert(!empty, "Calling popFront() on an empty range");
498 
499         const event = events_.front;
500         if(event != Event.NoChange)
501         {
502             assert(event == Event.Change, "unknown recorder event ID");
503             states_.popFront();
504         }
505         events_.popFront();
506         updateFront();
507     }
508 
509     override bool empty() const { return events_.empty; }
510 
511 private:
512     /// Update front of the range (inputState_). Called by constructor/popFront().
513     void updateFront()
514     {
515         if(empty) { return; }
516         const event = events_.front;
517         if(event != Event.NoChange)
518         {
519             assert(event == Event.Change, "unknown recorder event ID");
520             inputState_ = states_.front;
521         }
522     }
523 }