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 }