From 04541150b1ee00644dfab79f93cc25ab063c496f Mon Sep 17 00:00:00 2001
From: danzel <danzel@localhost.geek.nz>
Date: Sun, 17 Dec 2017 16:43:09 +1300
Subject: [PATCH] Movie (recorded inputs) playback and recording. SDL has
 command lines to control it.

---
 src/citra/citra.cpp                   |  37 +-
 src/common/logging/backend.cpp        |   1 +
 src/common/logging/log.h              |   1 +
 src/core/CMakeLists.txt               |   2 +
 src/core/core.cpp                     |   3 +
 src/core/hle/service/hid/hid.cpp      |  11 +
 src/core/hle/service/ir/extra_hid.cpp |  21 +-
 src/core/hle/service/ir/extra_hid.h   |  18 +
 src/core/hle/service/ir/ir_rst.cpp    |  20 +-
 src/core/hle/service/ir/ir_rst.h      |  15 +
 src/core/movie.cpp                    | 469 ++++++++++++++++++++++++++
 src/core/movie.h                      |  64 ++++
 src/core/settings.h                   |   4 +
 13 files changed, 625 insertions(+), 41 deletions(-)
 create mode 100644 src/core/movie.cpp
 create mode 100644 src/core/movie.h

diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp
index 85d7ab099..474b42a60 100644
--- a/src/citra/citra.cpp
+++ b/src/citra/citra.cpp
@@ -43,13 +43,16 @@
 #include "network/network.h"
 
 static void PrintHelp(const char* argv0) {
-    std::cout << "Usage: " << argv0 << " [options] <filename>\n"
-                                       "-g, --gdbport=NUMBER Enable gdb stub on port NUMBER\n"
-                                       "-i, --install=FILE    Installs a specified CIA file\n"
-                                       "-m, --multiplayer=nick:password@address:port"
-                                       " Nickname, password, address and port for multiplayer\n"
-                                       "-h, --help           Display this help and exit\n"
-                                       "-v, --version        Output version information and exit\n";
+    std::cout << "Usage: " << argv0
+              << " [options] <filename>\n"
+                 "-g, --gdbport=NUMBER Enable gdb stub on port NUMBER\n"
+                 "-i, --install=FILE    Installs a specified CIA file\n"
+                 "-m, --multiplayer=nick:password@address:port"
+                 " Nickname, password, address and port for multiplayer\n"
+                 "-r, --movie-record=[file]  Record a movie (game inputs) to the given file\n"
+                 "-p, --movie-play=[file]    Playback the movie (game inputs) from the given file\n"
+                 "-h, --help           Display this help and exit\n"
+                 "-v, --version        Output version information and exit\n";
 }
 
 static void PrintVersion() {
@@ -109,6 +112,9 @@ int main(int argc, char** argv) {
     int option_index = 0;
     bool use_gdbstub = Settings::values.use_gdbstub;
     u32 gdb_port = static_cast<u32>(Settings::values.gdbstub_port);
+    std::string movie_record;
+    std::string movie_play;
+
     char* endarg;
 #ifdef _WIN32
     int argc_w;
@@ -129,12 +135,13 @@ int main(int argc, char** argv) {
 
     static struct option long_options[] = {
         {"gdbport", required_argument, 0, 'g'},     {"install", required_argument, 0, 'i'},
-        {"multiplayer", required_argument, 0, 'm'}, {"help", no_argument, 0, 'h'},
+        {"multiplayer", required_argument, 0, 'm'}, {"movie-record", required_argument, 0, 'r'},
+        {"movie-play", required_argument, 0, 'p'},  {"help", no_argument, 0, 'h'},
         {"version", no_argument, 0, 'v'},           {0, 0, 0, 0},
     };
 
     while (optind < argc) {
-        char arg = getopt_long(argc, argv, "g:i:m:hv", long_options, &option_index);
+        char arg = getopt_long(argc, argv, "g:i:m:r:p:hv", long_options, &option_index);
         if (arg != -1) {
             switch (arg) {
             case 'g':
@@ -194,6 +201,12 @@ int main(int argc, char** argv) {
                 }
                 break;
             }
+            case 'r':
+                movie_record = optarg;
+                break;
+            case 'p':
+                movie_play = optarg;
+                break;
             case 'h':
                 PrintHelp(argv[0]);
                 return 0;
@@ -226,11 +239,17 @@ int main(int argc, char** argv) {
         return -1;
     }
 
+    if (!movie_record.empty() && !movie_play.empty()) {
+        LOG_CRITICAL(Frontend, "Cannot both play and record a movie");
+    }
+
     log_filter.ParseFilterString(Settings::values.log_filter);
 
     // Apply the command line arguments
     Settings::values.gdbstub_port = gdb_port;
     Settings::values.use_gdbstub = use_gdbstub;
+    Settings::values.movie_play = std::move(movie_play);
+    Settings::values.movie_record = std::move(movie_record);
     Settings::Apply();
 
     std::unique_ptr<EmuWindow_SDL2> emu_window{std::make_unique<EmuWindow_SDL2>()};
diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp
index 0bc611649..7e58314a3 100644
--- a/src/common/logging/backend.cpp
+++ b/src/common/logging/backend.cpp
@@ -74,6 +74,7 @@ namespace Log {
     SUB(Audio, Sink)                                                                               \
     CLS(Input)                                                                                     \
     CLS(Network)                                                                                   \
+    CLS(Movie)                                                                                     \
     CLS(Loader)                                                                                    \
     CLS(WebService)
 
diff --git a/src/common/logging/log.h b/src/common/logging/log.h
index f36642c38..f602c3ee6 100644
--- a/src/common/logging/log.h
+++ b/src/common/logging/log.h
@@ -92,6 +92,7 @@ enum class Class : ClassType {
     Loader,            ///< ROM loader
     Input,             ///< Input emulation
     Network,           ///< Network emulation
+    Movie,             ///< Movie (Input Recording) Playback
     WebService,        ///< Interface to Citra Web Services
     Count              ///< Total number of logging classes
 };
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index b8d22bd1a..8d3641e4f 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -388,6 +388,8 @@ add_library(core STATIC
     memory.h
     memory_setup.h
     mmio.h
+    movie.cpp
+    movie.h
     perf_stats.cpp
     perf_stats.h
     settings.cpp
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 0c658d1ff..653e33c42 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -19,6 +19,7 @@
 #include "core/hw/hw.h"
 #include "core/loader/loader.h"
 #include "core/memory_setup.h"
+#include "core/movie.h"
 #include "core/settings.h"
 #include "network/network.h"
 #include "video_core/video_core.h"
@@ -160,6 +161,7 @@ System::ResultStatus System::Init(EmuWindow* emu_window, u32 system_mode) {
     Service::Init();
     AudioCore::Init();
     GDBStub::Init();
+    Movie::Init();
 
     if (!VideoCore::Init(emu_window)) {
         return ResultStatus::ErrorVideoCore;
@@ -185,6 +187,7 @@ void System::Shutdown() {
                          perf_results.frametime * 1000.0);
 
     // Shutdown emulation session
+    Movie::Shutdown();
     GDBStub::Shutdown();
     AudioCore::Shutdown();
     VideoCore::Shutdown();
diff --git a/src/core/hle/service/hid/hid.cpp b/src/core/hle/service/hid/hid.cpp
index 9074cd2b0..85bc54c4d 100644
--- a/src/core/hle/service/hid/hid.cpp
+++ b/src/core/hle/service/hid/hid.cpp
@@ -19,6 +19,8 @@
 #include "core/hle/service/hid/hid_spvr.h"
 #include "core/hle/service/hid/hid_user.h"
 #include "core/hle/service/service.h"
+#include "core/movie.h"
+#include "video_core/video_core.h"
 
 namespace Service {
 namespace HID {
@@ -135,6 +137,9 @@ static void UpdatePadCallback(u64 userdata, int cycles_late) {
     constexpr int MAX_CIRCLEPAD_POS = 0x9C; // Max value for a circle pad position
     s16 circle_pad_x = static_cast<s16>(circle_pad_x_f * MAX_CIRCLEPAD_POS);
     s16 circle_pad_y = static_cast<s16>(circle_pad_y_f * MAX_CIRCLEPAD_POS);
+
+    Movie::HandlePadAndCircleStatus(state, circle_pad_x, circle_pad_y);
+
     const DirectionState direction = GetStickDirectionState(circle_pad_x, circle_pad_y);
     state.circle_up.Assign(direction.up);
     state.circle_down.Assign(direction.down);
@@ -180,6 +185,8 @@ static void UpdatePadCallback(u64 userdata, int cycles_late) {
     touch_entry.y = static_cast<u16>(y * Core::kScreenBottomHeight);
     touch_entry.valid.Assign(pressed ? 1 : 0);
 
+    Movie::HandleTouchStatus(touch_entry);
+
     // TODO(bunnei): We're not doing anything with offset 0xA8 + 0x18 of HID SharedMemory, which
     // supposedly is "Touch-screen entry, which contains the raw coordinate data prior to being
     // converted to pixel coordinates." (http://3dbrew.org/wiki/HID_Shared_Memory#Offset_0xA8).
@@ -218,6 +225,8 @@ static void UpdateAccelerometerCallback(u64 userdata, int cycles_late) {
     accelerometer_entry.y = static_cast<s16>(accel.y);
     accelerometer_entry.z = static_cast<s16>(accel.z);
 
+    Movie::HandleAccelerometerStatus(accelerometer_entry);
+
     // Make up "raw" entry
     // TODO(wwylele):
     // From hardware testing, the raw_entry values are approximately, but not exactly, as twice as
@@ -256,6 +265,8 @@ static void UpdateGyroscopeCallback(u64 userdata, int cycles_late) {
     gyroscope_entry.y = static_cast<s16>(gyro.y);
     gyroscope_entry.z = static_cast<s16>(gyro.z);
 
+    Movie::HandleGyroscopeStatus(gyroscope_entry);
+
     // Make up "raw" entry
     mem->gyroscope.raw_entry.x = gyroscope_entry.x;
     mem->gyroscope.raw_entry.z = -gyroscope_entry.y;
diff --git a/src/core/hle/service/ir/extra_hid.cpp b/src/core/hle/service/ir/extra_hid.cpp
index e7acc17a5..1d3df1a4b 100644
--- a/src/core/hle/service/ir/extra_hid.cpp
+++ b/src/core/hle/service/ir/extra_hid.cpp
@@ -3,10 +3,10 @@
 // Refer to the license.txt file included.
 
 #include "common/alignment.h"
-#include "common/bit_field.h"
 #include "common/string_util.h"
 #include "core/core_timing.h"
 #include "core/hle/service/ir/extra_hid.h"
+#include "core/movie.h"
 #include "core/settings.h"
 
 namespace Service {
@@ -176,22 +176,6 @@ void ExtraHID::SendHIDStatus() {
     if (is_device_reload_pending.exchange(false))
         LoadInputDevices();
 
-    struct {
-        union {
-            BitField<0, 8, u32_le> header;
-            BitField<8, 12, u32_le> c_stick_x;
-            BitField<20, 12, u32_le> c_stick_y;
-        } c_stick;
-        union {
-            BitField<0, 5, u8> battery_level;
-            BitField<5, 1, u8> zl_not_held;
-            BitField<6, 1, u8> zr_not_held;
-            BitField<7, 1, u8> r_not_held;
-        } buttons;
-        u8 unknown;
-    } response;
-    static_assert(sizeof(response) == 6, "HID status response has wrong size!");
-
     constexpr int C_STICK_CENTER = 0x800;
     // TODO(wwylele): this value is not accurately measured. We currently assume that the axis can
     // take values in the whole range of a 12-bit integer.
@@ -200,6 +184,7 @@ void ExtraHID::SendHIDStatus() {
     float x, y;
     std::tie(x, y) = c_stick->GetStatus();
 
+    ExtraHIDResponse response;
     response.c_stick.header.Assign(static_cast<u8>(ResponseID::PollHID));
     response.c_stick.c_stick_x.Assign(static_cast<u32>(C_STICK_CENTER + C_STICK_RADIUS * x));
     response.c_stick.c_stick_y.Assign(static_cast<u32>(C_STICK_CENTER + C_STICK_RADIUS * y));
@@ -209,6 +194,8 @@ void ExtraHID::SendHIDStatus() {
     response.buttons.r_not_held.Assign(1);
     response.unknown = 0;
 
+    Movie::HandleExtraHidResponse(response);
+
     std::vector<u8> response_buffer(sizeof(response));
     memcpy(response_buffer.data(), &response, sizeof(response));
     Send(response_buffer);
diff --git a/src/core/hle/service/ir/extra_hid.h b/src/core/hle/service/ir/extra_hid.h
index 8f4cf5010..0949334bf 100644
--- a/src/core/hle/service/ir/extra_hid.h
+++ b/src/core/hle/service/ir/extra_hid.h
@@ -6,6 +6,8 @@
 
 #include <array>
 #include <atomic>
+#include "common/bit_field.h"
+#include "common/swap.h"
 #include "core/frontend/input.h"
 #include "core/hle/service/ir/ir_user.h"
 
@@ -16,6 +18,22 @@ struct EventType;
 namespace Service {
 namespace IR {
 
+struct ExtraHIDResponse {
+    union {
+        BitField<0, 8, u32_le> header;
+        BitField<8, 12, u32_le> c_stick_x;
+        BitField<20, 12, u32_le> c_stick_y;
+    } c_stick;
+    union {
+        BitField<0, 5, u8> battery_level;
+        BitField<5, 1, u8> zl_not_held;
+        BitField<6, 1, u8> zr_not_held;
+        BitField<7, 1, u8> r_not_held;
+    } buttons;
+    u8 unknown;
+};
+static_assert(sizeof(ExtraHIDResponse) == 6, "HID status response has wrong size!");
+
 /**
  * An IRDevice emulating Circle Pad Pro or New 3DS additional HID hardware.
  * This device sends periodic udates at a rate configured by the 3DS, and sends calibration data if
diff --git a/src/core/hle/service/ir/ir_rst.cpp b/src/core/hle/service/ir/ir_rst.cpp
index 84690aecf..4f5af8b02 100644
--- a/src/core/hle/service/ir/ir_rst.cpp
+++ b/src/core/hle/service/ir/ir_rst.cpp
@@ -2,30 +2,18 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
-#include "common/bit_field.h"
 #include "core/core_timing.h"
 #include "core/hle/ipc_helpers.h"
 #include "core/hle/kernel/event.h"
 #include "core/hle/kernel/shared_memory.h"
 #include "core/hle/service/hid/hid.h"
 #include "core/hle/service/ir/ir_rst.h"
+#include "core/movie.h"
 #include "core/settings.h"
 
 namespace Service {
 namespace IR {
 
-union PadState {
-    u32_le hex{};
-
-    BitField<14, 1, u32_le> zl;
-    BitField<15, 1, u32_le> zr;
-
-    BitField<24, 1, u32_le> c_stick_right;
-    BitField<25, 1, u32_le> c_stick_left;
-    BitField<26, 1, u32_le> c_stick_up;
-    BitField<27, 1, u32_le> c_stick_down;
-};
-
 struct PadDataEntry {
     PadState current_state;
     PadState delta_additions;
@@ -74,8 +62,10 @@ void IR_RST::UpdateCallback(u64 userdata, int cycles_late) {
     float c_stick_x_f, c_stick_y_f;
     std::tie(c_stick_x_f, c_stick_y_f) = c_stick->GetStatus();
     constexpr int MAX_CSTICK_RADIUS = 0x9C; // Max value for a c-stick radius
-    const s16 c_stick_x = static_cast<s16>(c_stick_x_f * MAX_CSTICK_RADIUS);
-    const s16 c_stick_y = static_cast<s16>(c_stick_y_f * MAX_CSTICK_RADIUS);
+    s16 c_stick_x = static_cast<s16>(c_stick_x_f * MAX_CSTICK_RADIUS);
+    s16 c_stick_y = static_cast<s16>(c_stick_y_f * MAX_CSTICK_RADIUS);
+
+    Movie::HandleIrRst(state, c_stick_x, c_stick_y);
 
     if (!raw_c_stick) {
         const HID::DirectionState direction = HID::GetStickDirectionState(c_stick_x, c_stick_y);
diff --git a/src/core/hle/service/ir/ir_rst.h b/src/core/hle/service/ir/ir_rst.h
index 621c1b51c..882b3e4c1 100644
--- a/src/core/hle/service/ir/ir_rst.h
+++ b/src/core/hle/service/ir/ir_rst.h
@@ -6,6 +6,9 @@
 
 #include <atomic>
 #include <memory>
+#include "common/bit_field.h"
+#include "common/common_types.h"
+#include "common/swap.h"
 #include "core/frontend/input.h"
 #include "core/hle/kernel/kernel.h"
 #include "core/hle/service/service.h"
@@ -22,6 +25,18 @@ class EventType;
 namespace Service {
 namespace IR {
 
+union PadState {
+    u32_le hex{};
+
+    BitField<14, 1, u32_le> zl;
+    BitField<15, 1, u32_le> zr;
+
+    BitField<24, 1, u32_le> c_stick_right;
+    BitField<25, 1, u32_le> c_stick_left;
+    BitField<26, 1, u32_le> c_stick_up;
+    BitField<27, 1, u32_le> c_stick_down;
+};
+
 /// Interface to "ir:rst" service
 class IR_RST final : public ServiceFramework<IR_RST> {
 public:
diff --git a/src/core/movie.cpp b/src/core/movie.cpp
new file mode 100644
index 000000000..0a6e82b63
--- /dev/null
+++ b/src/core/movie.cpp
@@ -0,0 +1,469 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <cstring>
+#include <string>
+#include <vector>
+#include <cryptopp/hex.h>
+#include "common/bit_field.h"
+#include "common/common_types.h"
+#include "common/file_util.h"
+#include "common/logging/log.h"
+#include "common/scm_rev.h"
+#include "common/string_util.h"
+#include "common/swap.h"
+#include "core/core.h"
+#include "core/hle/service/hid/hid.h"
+#include "core/hle/service/ir/extra_hid.h"
+#include "core/hle/service/ir/ir_rst.h"
+#include "core/movie.h"
+
+namespace Movie {
+
+enum class PlayMode { None, Recording, Playing };
+
+enum class ControllerStateType : u8 {
+    PadAndCircle,
+    Touch,
+    Accelerometer,
+    Gyroscope,
+    IrRst,
+    ExtraHidResponse
+};
+
+#pragma pack(push, 1)
+struct ControllerState {
+    ControllerStateType type;
+
+    union {
+        struct {
+            union {
+                u16_le hex;
+
+                BitField<0, 1, u16_le> a;
+                BitField<1, 1, u16_le> b;
+                BitField<2, 1, u16_le> select;
+                BitField<3, 1, u16_le> start;
+                BitField<4, 1, u16_le> right;
+                BitField<5, 1, u16_le> left;
+                BitField<6, 1, u16_le> up;
+                BitField<7, 1, u16_le> down;
+                BitField<8, 1, u16_le> r;
+                BitField<9, 1, u16_le> l;
+                BitField<10, 1, u16_le> x;
+                BitField<11, 1, u16_le> y;
+                // Bits 12-15 are currently unused
+            };
+            s16_le circle_pad_x;
+            s16_le circle_pad_y;
+        } pad_and_circle;
+
+        struct {
+            u16_le x;
+            u16_le y;
+            // This is a bool, u8 for platform compatibility
+            u8 valid;
+        } touch;
+
+        struct {
+            s16_le x;
+            s16_le y;
+            s16_le z;
+        } accelerometer;
+
+        struct {
+            s16_le x;
+            s16_le y;
+            s16_le z;
+        } gyroscope;
+
+        struct {
+            s16_le x;
+            s16_le y;
+            // These are bool, u8 for platform compatibility
+            u8 zl;
+            u8 zr;
+        } ir_rst;
+
+        struct {
+            union {
+                u32_le hex;
+
+                BitField<0, 5, u32_le> battery_level;
+                BitField<5, 1, u32_le> zl_not_held;
+                BitField<6, 1, u32_le> zr_not_held;
+                BitField<7, 1, u32_le> r_not_held;
+                BitField<8, 12, u32_le> c_stick_x;
+                BitField<20, 12, u32_le> c_stick_y;
+            };
+        } extra_hid_response;
+    };
+};
+static_assert(sizeof(ControllerState) == 7, "ControllerState should be 7 bytes");
+#pragma pack(pop)
+
+constexpr std::array<u8, 4> header_magic_bytes{{'C', 'T', 'M', 0x1B}};
+
+#pragma pack(push, 1)
+struct CTMHeader {
+    std::array<u8, 4> filetype;  /// Unique Identifier to check the file type (always "CTM"0x1B)
+    u64_le program_id;           /// ID of the ROM being executed. Also called title_id
+    std::array<u8, 20> revision; /// Git hash of the revision this movie was created with
+
+    std::array<u8, 224> reserved; /// Make heading 256 bytes so it has consistent size
+};
+static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes");
+#pragma pack(pop)
+
+static PlayMode play_mode = PlayMode::None;
+static std::vector<u8> recorded_input;
+static size_t current_byte = 0;
+
+static bool IsPlayingInput() {
+    return play_mode == PlayMode::Playing;
+}
+static bool IsRecordingInput() {
+    return play_mode == PlayMode::Recording;
+}
+
+static void CheckInputEnd() {
+    if (current_byte + sizeof(ControllerState) > recorded_input.size()) {
+        LOG_INFO(Movie, "Playback finished");
+        play_mode = PlayMode::None;
+    }
+}
+
+static void Play(Service::HID::PadState& pad_state, s16& circle_pad_x, s16& circle_pad_y) {
+    ControllerState s;
+    std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState));
+    current_byte += sizeof(ControllerState);
+
+    if (s.type != ControllerStateType::PadAndCircle) {
+        LOG_ERROR(Movie,
+                  "Expected to read type %d, but found %d. Your playback will be out of sync",
+                  ControllerStateType::PadAndCircle, s.type);
+        return;
+    }
+
+    pad_state.a.Assign(s.pad_and_circle.a);
+    pad_state.b.Assign(s.pad_and_circle.b);
+    pad_state.select.Assign(s.pad_and_circle.select);
+    pad_state.start.Assign(s.pad_and_circle.start);
+    pad_state.right.Assign(s.pad_and_circle.right);
+    pad_state.left.Assign(s.pad_and_circle.left);
+    pad_state.up.Assign(s.pad_and_circle.up);
+    pad_state.down.Assign(s.pad_and_circle.down);
+    pad_state.r.Assign(s.pad_and_circle.r);
+    pad_state.l.Assign(s.pad_and_circle.l);
+    pad_state.x.Assign(s.pad_and_circle.x);
+    pad_state.y.Assign(s.pad_and_circle.y);
+
+    circle_pad_x = s.pad_and_circle.circle_pad_x;
+    circle_pad_y = s.pad_and_circle.circle_pad_y;
+}
+
+static void Play(Service::HID::TouchDataEntry& touch_data) {
+    ControllerState s;
+    std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState));
+    current_byte += sizeof(ControllerState);
+
+    if (s.type != ControllerStateType::Touch) {
+        LOG_ERROR(Movie,
+                  "Expected to read type %d, but found %d. Your playback will be out of sync",
+                  ControllerStateType::Touch, s.type);
+        return;
+    }
+
+    touch_data.x = s.touch.x;
+    touch_data.y = s.touch.y;
+    touch_data.valid.Assign(s.touch.valid);
+}
+
+static void Play(Service::HID::AccelerometerDataEntry& accelerometer_data) {
+    ControllerState s;
+    std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState));
+    current_byte += sizeof(ControllerState);
+
+    if (s.type != ControllerStateType::Accelerometer) {
+        LOG_ERROR(Movie,
+                  "Expected to read type %d, but found %d. Your playback will be out of sync",
+                  ControllerStateType::Accelerometer, s.type);
+        return;
+    }
+
+    accelerometer_data.x = s.accelerometer.x;
+    accelerometer_data.y = s.accelerometer.y;
+    accelerometer_data.z = s.accelerometer.z;
+}
+
+static void Play(Service::HID::GyroscopeDataEntry& gyroscope_data) {
+    ControllerState s;
+    std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState));
+    current_byte += sizeof(ControllerState);
+
+    if (s.type != ControllerStateType::Gyroscope) {
+        LOG_ERROR(Movie,
+                  "Expected to read type %d, but found %d. Your playback will be out of sync",
+                  ControllerStateType::Gyroscope, s.type);
+        return;
+    }
+
+    gyroscope_data.x = s.gyroscope.x;
+    gyroscope_data.y = s.gyroscope.y;
+    gyroscope_data.z = s.gyroscope.z;
+}
+
+static void Play(Service::IR::PadState& pad_state, s16& c_stick_x, s16& c_stick_y) {
+    ControllerState s;
+    std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState));
+    current_byte += sizeof(ControllerState);
+
+    if (s.type != ControllerStateType::IrRst) {
+        LOG_ERROR(Movie,
+                  "Expected to read type %d, but found %d. Your playback will be out of sync",
+                  ControllerStateType::IrRst, s.type);
+        return;
+    }
+
+    c_stick_x = s.ir_rst.x;
+    c_stick_y = s.ir_rst.y;
+    pad_state.zl.Assign(s.ir_rst.zl);
+    pad_state.zr.Assign(s.ir_rst.zr);
+}
+
+static void Play(Service::IR::ExtraHIDResponse& extra_hid_response) {
+    ControllerState s;
+    std::memcpy(&s, &recorded_input[current_byte], sizeof(ControllerState));
+    current_byte += sizeof(ControllerState);
+
+    if (s.type != ControllerStateType::ExtraHidResponse) {
+        LOG_ERROR(Movie,
+                  "Expected to read type %d, but found %d. Your playback will be out of sync",
+                  ControllerStateType::ExtraHidResponse, s.type);
+        return;
+    }
+
+    extra_hid_response.buttons.battery_level.Assign(s.extra_hid_response.battery_level);
+    extra_hid_response.c_stick.c_stick_x.Assign(s.extra_hid_response.c_stick_x);
+    extra_hid_response.c_stick.c_stick_y.Assign(s.extra_hid_response.c_stick_y);
+    extra_hid_response.buttons.r_not_held.Assign(s.extra_hid_response.r_not_held);
+    extra_hid_response.buttons.zl_not_held.Assign(s.extra_hid_response.zl_not_held);
+    extra_hid_response.buttons.zr_not_held.Assign(s.extra_hid_response.zr_not_held);
+}
+
+static void Record(const ControllerState& controller_state) {
+    recorded_input.resize(current_byte + sizeof(ControllerState));
+    std::memcpy(&recorded_input[current_byte], &controller_state, sizeof(ControllerState));
+    current_byte += sizeof(ControllerState);
+}
+
+static void Record(const Service::HID::PadState& pad_state, const s16& circle_pad_x,
+                   const s16& circle_pad_y) {
+    ControllerState s;
+    s.type = ControllerStateType::PadAndCircle;
+
+    s.pad_and_circle.a.Assign(static_cast<u16>(pad_state.a));
+    s.pad_and_circle.b.Assign(static_cast<u16>(pad_state.b));
+    s.pad_and_circle.select.Assign(static_cast<u16>(pad_state.select));
+    s.pad_and_circle.start.Assign(static_cast<u16>(pad_state.start));
+    s.pad_and_circle.right.Assign(static_cast<u16>(pad_state.right));
+    s.pad_and_circle.left.Assign(static_cast<u16>(pad_state.left));
+    s.pad_and_circle.up.Assign(static_cast<u16>(pad_state.up));
+    s.pad_and_circle.down.Assign(static_cast<u16>(pad_state.down));
+    s.pad_and_circle.r.Assign(static_cast<u16>(pad_state.r));
+    s.pad_and_circle.l.Assign(static_cast<u16>(pad_state.l));
+    s.pad_and_circle.x.Assign(static_cast<u16>(pad_state.x));
+    s.pad_and_circle.y.Assign(static_cast<u16>(pad_state.y));
+
+    s.pad_and_circle.circle_pad_x = circle_pad_x;
+    s.pad_and_circle.circle_pad_y = circle_pad_y;
+
+    Record(s);
+}
+
+static void Record(const Service::HID::TouchDataEntry& touch_data) {
+    ControllerState s;
+    s.type = ControllerStateType::Touch;
+
+    s.touch.x = touch_data.x;
+    s.touch.y = touch_data.y;
+    s.touch.valid = static_cast<u8>(touch_data.valid);
+
+    Record(s);
+}
+
+static void Record(const Service::HID::AccelerometerDataEntry& accelerometer_data) {
+    ControllerState s;
+    s.type = ControllerStateType::Accelerometer;
+
+    s.accelerometer.x = accelerometer_data.x;
+    s.accelerometer.y = accelerometer_data.y;
+    s.accelerometer.z = accelerometer_data.z;
+
+    Record(s);
+}
+
+static void Record(const Service::HID::GyroscopeDataEntry& gyroscope_data) {
+    ControllerState s;
+    s.type = ControllerStateType::Gyroscope;
+
+    s.gyroscope.x = gyroscope_data.x;
+    s.gyroscope.y = gyroscope_data.y;
+    s.gyroscope.z = gyroscope_data.z;
+
+    Record(s);
+}
+
+static void Record(const Service::IR::PadState& pad_state, const s16& c_stick_x,
+                   const s16& c_stick_y) {
+    ControllerState s;
+    s.type = ControllerStateType::IrRst;
+
+    s.ir_rst.x = c_stick_x;
+    s.ir_rst.y = c_stick_y;
+    s.ir_rst.zl = static_cast<u8>(pad_state.zl);
+    s.ir_rst.zr = static_cast<u8>(pad_state.zr);
+
+    Record(s);
+}
+
+static void Record(const Service::IR::ExtraHIDResponse& extra_hid_response) {
+    ControllerState s;
+    s.type = ControllerStateType::ExtraHidResponse;
+
+    s.extra_hid_response.battery_level.Assign(extra_hid_response.buttons.battery_level);
+    s.extra_hid_response.c_stick_x.Assign(extra_hid_response.c_stick.c_stick_x);
+    s.extra_hid_response.c_stick_y.Assign(extra_hid_response.c_stick.c_stick_y);
+    s.extra_hid_response.r_not_held.Assign(extra_hid_response.buttons.r_not_held);
+    s.extra_hid_response.zl_not_held.Assign(extra_hid_response.buttons.zl_not_held);
+    s.extra_hid_response.zr_not_held.Assign(extra_hid_response.buttons.zr_not_held);
+
+    Record(s);
+}
+
+static bool ValidateHeader(const CTMHeader& header) {
+    if (header_magic_bytes != header.filetype) {
+        LOG_ERROR(Movie, "Playback file does not have valid header");
+        return false;
+    }
+
+    std::string revision =
+        Common::ArrayToString(header.revision.data(), header.revision.size(), 21, false);
+    revision = Common::ToLower(revision);
+
+    if (revision != Common::g_scm_rev) {
+        LOG_WARNING(Movie,
+                    "This movie was created on a different version of Citra, playback may desync");
+    }
+
+    u64 program_id;
+    Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id);
+    if (program_id != header.program_id) {
+        LOG_WARNING(Movie, "This movie was recorded using a ROM with a different program id");
+    }
+
+    return true;
+}
+
+static void SaveMovie() {
+    LOG_INFO(Movie, "Saving movie");
+    FileUtil::IOFile save_record(Settings::values.movie_record, "wb");
+
+    if (!save_record.IsGood()) {
+        LOG_ERROR(Movie, "Unable to open file to save movie");
+        return;
+    }
+
+    CTMHeader header = {};
+    header.filetype = header_magic_bytes;
+
+    Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id);
+
+    std::string rev_bytes;
+    CryptoPP::StringSource(Common::g_scm_rev, true,
+                           new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes)));
+    std::memcpy(header.revision.data(), rev_bytes.data(), sizeof(CTMHeader::revision));
+
+    save_record.WriteBytes(&header, sizeof(CTMHeader));
+    save_record.WriteBytes(recorded_input.data(), recorded_input.size());
+
+    if (!save_record.IsGood()) {
+        LOG_ERROR(Movie, "Error saving movie");
+    }
+}
+
+void Init() {
+    if (!Settings::values.movie_play.empty()) {
+        LOG_INFO(Movie, "Loading Movie for playback");
+        FileUtil::IOFile save_record(Settings::values.movie_play, "rb");
+        u64 size = save_record.GetSize();
+
+        if (save_record.IsGood() && size > sizeof(CTMHeader)) {
+            CTMHeader header;
+            save_record.ReadArray(&header, 1);
+            if (ValidateHeader(header)) {
+                play_mode = PlayMode::Playing;
+                recorded_input.resize(size - sizeof(CTMHeader));
+                save_record.ReadArray(recorded_input.data(), recorded_input.size());
+                current_byte = 0;
+            }
+        } else {
+            LOG_ERROR(Movie, "Failed to playback movie: Unable to open '%s'",
+                      Settings::values.movie_play.c_str());
+        }
+    }
+
+    if (!Settings::values.movie_record.empty()) {
+        LOG_INFO(Movie, "Enabling Movie recording");
+        play_mode = PlayMode::Recording;
+    }
+}
+
+void Shutdown() {
+    if (!IsRecordingInput()) {
+        return;
+    }
+
+    SaveMovie();
+
+    play_mode = PlayMode::None;
+    recorded_input.resize(0);
+    current_byte = 0;
+}
+
+template <typename... Targs>
+static void Handle(Targs&... Fargs) {
+    if (IsPlayingInput()) {
+        Play(Fargs...);
+        CheckInputEnd();
+    } else if (IsRecordingInput()) {
+        Record(Fargs...);
+    }
+}
+
+void HandlePadAndCircleStatus(Service::HID::PadState& pad_state, s16& circle_pad_x,
+                              s16& circle_pad_y) {
+    Handle(pad_state, circle_pad_x, circle_pad_y);
+}
+
+void HandleTouchStatus(Service::HID::TouchDataEntry& touch_data) {
+    Handle(touch_data);
+}
+
+void HandleAccelerometerStatus(Service::HID::AccelerometerDataEntry& accelerometer_data) {
+    Handle(accelerometer_data);
+}
+
+void HandleGyroscopeStatus(Service::HID::GyroscopeDataEntry& gyroscope_data) {
+    Handle(gyroscope_data);
+}
+
+void HandleIrRst(Service::IR::PadState& pad_state, s16& c_stick_x, s16& c_stick_y) {
+    Handle(pad_state, c_stick_x, c_stick_y);
+}
+
+void HandleExtraHidResponse(Service::IR::ExtraHIDResponse& extra_hid_response) {
+    Handle(extra_hid_response);
+}
+}
diff --git a/src/core/movie.h b/src/core/movie.h
new file mode 100644
index 000000000..44b1978a2
--- /dev/null
+++ b/src/core/movie.h
@@ -0,0 +1,64 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "common/common_types.h"
+
+namespace Service {
+namespace HID {
+struct AccelerometerDataEntry;
+struct GyroscopeDataEntry;
+struct PadState;
+struct TouchDataEntry;
+}
+namespace IR {
+struct ExtraHIDResponse;
+union PadState;
+}
+}
+
+namespace Movie {
+
+void Init();
+
+void Shutdown();
+
+/**
+ * When recording: Takes a copy of the given input states so they can be used for playback
+ * When playing: Replaces the given input states with the ones stored in the playback file
+ */
+void HandlePadAndCircleStatus(Service::HID::PadState& pad_state, s16& circle_pad_x,
+                              s16& circle_pad_y);
+
+/**
+* When recording: Takes a copy of the given input states so they can be used for playback
+* When playing: Replaces the given input states with the ones stored in the playback file
+*/
+void HandleTouchStatus(Service::HID::TouchDataEntry& touch_data);
+
+/**
+* When recording: Takes a copy of the given input states so they can be used for playback
+* When playing: Replaces the given input states with the ones stored in the playback file
+*/
+void HandleAccelerometerStatus(Service::HID::AccelerometerDataEntry& accelerometer_data);
+
+/**
+* When recording: Takes a copy of the given input states so they can be used for playback
+* When playing: Replaces the given input states with the ones stored in the playback file
+*/
+void HandleGyroscopeStatus(Service::HID::GyroscopeDataEntry& gyroscope_data);
+
+/**
+* When recording: Takes a copy of the given input states so they can be used for playback
+* When playing: Replaces the given input states with the ones stored in the playback file
+*/
+void HandleIrRst(Service::IR::PadState& pad_state, s16& c_stick_x, s16& c_stick_y);
+
+/**
+* When recording: Takes a copy of the given input states so they can be used for playback
+* When playing: Replaces the given input states with the ones stored in the playback file
+*/
+void HandleExtraHidResponse(Service::IR::ExtraHIDResponse& extra_hid_response);
+}
diff --git a/src/core/settings.h b/src/core/settings.h
index 8d78cb424..b8fa3f05a 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -130,6 +130,10 @@ struct Values {
     bool use_gdbstub;
     u16 gdbstub_port;
 
+    // Movie
+    std::string movie_play;
+    std::string movie_record;
+
     // WebService
     bool enable_telemetry;
     std::string telemetry_endpoint_url;