diff --git a/src/core/movie.cpp b/src/core/movie.cpp
index 97c96ba3c..f680e9226 100644
--- a/src/core/movie.cpp
+++ b/src/core/movie.cpp
@@ -3,10 +3,12 @@
 // Refer to the license.txt file included.
 
 #include <cstring>
+#include <stdexcept>
 #include <string>
 #include <vector>
 #include <boost/optional.hpp>
 #include <cryptopp/hex.h>
+#include <cryptopp/osrng.h>
 #include "common/bit_field.h"
 #include "common/common_types.h"
 #include "common/file_util.h"
@@ -117,12 +119,61 @@ struct CTMHeader {
     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
     u64_le clock_init_time;      /// The init time of the system clock
+    // Unique identifier of the movie, used to support separate savestate slots for TASing
+    u64_le id;
 
-    std::array<u8, 216> reserved; /// Make heading 256 bytes so it has consistent size
+    std::array<u8, 208> reserved; /// Make heading 256 bytes so it has consistent size
 };
 static_assert(sizeof(CTMHeader) == 256, "CTMHeader should be 256 bytes");
 #pragma pack(pop)
 
+template <class Archive>
+void Movie::serialize(Archive& ar, const unsigned int file_version) {
+    // Only serialize what's needed to make savestates useful for TAS:
+    u64 _current_byte = static_cast<u64>(current_byte);
+    ar& _current_byte;
+    current_byte = static_cast<std::size_t>(_current_byte);
+
+    std::vector<u8> recorded_input_ = recorded_input;
+    ar& recorded_input_;
+
+    ar& init_time;
+
+    if (file_version > 0) {
+        if (Archive::is_loading::value) {
+            u64 savestate_movie_id;
+            ar& savestate_movie_id;
+            if (id != savestate_movie_id) {
+                if (savestate_movie_id == 0) {
+                    throw std::runtime_error("You must close your movie to load this state");
+                } else {
+                    throw std::runtime_error("You must load the same movie to load this state");
+                }
+            }
+        } else {
+            ar& id;
+        }
+    }
+
+    if (Archive::is_loading::value && id != 0) {
+        if (read_only) { // Do not replace the previously recorded input.
+            if (play_mode == PlayMode::Recording) {
+                SaveMovie();
+            }
+            if (current_byte >= recorded_input.size()) {
+                throw std::runtime_error(
+                    "This savestate was created at a later point and must be loaded in R+W mode");
+            }
+            play_mode = PlayMode::Playing;
+        } else {
+            recorded_input = std::move(recorded_input_);
+            play_mode = PlayMode::Recording;
+        }
+    }
+}
+
+SERIALIZE_IMPL(Movie)
+
 bool Movie::IsPlayingInput() const {
     return play_mode == PlayMode::Playing;
 }
@@ -135,6 +186,7 @@ void Movie::CheckInputEnd() {
         LOG_INFO(Movie, "Playback finished");
         play_mode = PlayMode::None;
         init_time = 0;
+        id = 0;
         playback_completion_callback();
     }
 }
@@ -394,6 +446,7 @@ void Movie::SaveMovie() {
     CTMHeader header = {};
     header.filetype = header_magic_bytes;
     header.clock_init_time = init_time;
+    header.id = id;
 
     Core::System::GetInstance().GetAppLoader().ReadProgramId(header.program_id);
 
@@ -421,10 +474,14 @@ void Movie::StartPlayback(const std::string& movie_file,
         save_record.ReadArray(&header, 1);
         if (ValidateHeader(header) != ValidationResult::Invalid) {
             play_mode = PlayMode::Playing;
+            record_movie_file = movie_file;
             recorded_input.resize(size - sizeof(CTMHeader));
             save_record.ReadArray(recorded_input.data(), recorded_input.size());
             current_byte = 0;
+            id = header.id;
             playback_completion_callback = completion_callback;
+
+            LOG_INFO(Movie, "Loaded Movie, ID: {:016X}", id);
         }
     } else {
         LOG_ERROR(Movie, "Failed to playback movie: Unable to open '{}'", movie_file);
@@ -432,9 +489,18 @@ void Movie::StartPlayback(const std::string& movie_file,
 }
 
 void Movie::StartRecording(const std::string& movie_file) {
-    LOG_INFO(Movie, "Enabling Movie recording");
     play_mode = PlayMode::Recording;
     record_movie_file = movie_file;
+
+    // Generate a random ID
+    CryptoPP::AutoSeededRandomPool rng;
+    rng.GenerateBlock(reinterpret_cast<CryptoPP::byte*>(&id), sizeof(id));
+
+    LOG_INFO(Movie, "Enabling Movie recording, ID: {:016X}", id);
+}
+
+void Movie::SetReadOnly(bool read_only_) {
+    read_only = read_only_;
 }
 
 static boost::optional<CTMHeader> ReadHeader(const std::string& movie_file) {
@@ -496,6 +562,7 @@ void Movie::Shutdown() {
     record_movie_file.clear();
     current_byte = 0;
     init_time = 0;
+    id = 0;
 }
 
 template <typename... Targs>
diff --git a/src/core/movie.h b/src/core/movie.h
index e578b909c..0a4ab410a 100644
--- a/src/core/movie.h
+++ b/src/core/movie.h
@@ -46,6 +46,18 @@ public:
         const std::string& movie_file, std::function<void()> completion_callback = [] {});
     void StartRecording(const std::string& movie_file);
 
+    /**
+     * Sets the read-only status.
+     * When true, movies will be opened in read-only mode. Loading a state will resume playback
+     * from that state.
+     * When false, movies will be opened in read/write mode. Loading a state will start recording
+     * from that state (rerecording). To start rerecording without loading a state, one can save
+     * and then immediately load while in R/W.
+     *
+     * The default is true.
+     */
+    void SetReadOnly(bool read_only);
+
     /// Prepare to override the clock before playing back movies
     void PrepareForPlayback(const std::string& movie_file);
 
@@ -58,6 +70,11 @@ public:
     u64 GetOverrideInitTime() const;
     u64 GetMovieProgramID(const std::string& movie_file) const;
 
+    /// Get the current movie's unique ID. Used to provide separate savestate slots for movies.
+    u64 GetCurrentMovieID() const {
+        return id;
+    }
+
     void Shutdown();
 
     /**
@@ -133,16 +150,13 @@ private:
     u64 init_time;
     std::function<void()> playback_completion_callback;
     std::size_t current_byte = 0;
+    u64 id = 0; // ID of the current movie loaded
+    bool read_only = true;
 
     template <class Archive>
-    void serialize(Archive& ar, const unsigned int) {
-        // Only serialize what's needed to make savestates useful for TAS:
-        u64 _current_byte = static_cast<u64>(current_byte);
-        ar& _current_byte;
-        current_byte = static_cast<std::size_t>(_current_byte);
-        ar& recorded_input;
-        ar& init_time;
-    }
+    void serialize(Archive& ar, const unsigned int file_version);
     friend class boost::serialization::access;
 };
-} // namespace Core
\ No newline at end of file
+} // namespace Core
+
+BOOST_CLASS_VERSION(Core::Movie, 1)
diff --git a/src/core/savestate.cpp b/src/core/savestate.cpp
index 26fc0cbed..a5bdb49d8 100644
--- a/src/core/savestate.cpp
+++ b/src/core/savestate.cpp
@@ -11,6 +11,7 @@
 #include "common/zstd_compression.h"
 #include "core/cheats/cheats.h"
 #include "core/core.h"
+#include "core/movie.h"
 #include "core/savestate.h"
 #include "network/network.h"
 #include "video_core/video_core.h"
@@ -37,8 +38,15 @@ static_assert(sizeof(CSTHeader) == 256, "CSTHeader should be 256 bytes");
 constexpr std::array<u8, 4> header_magic_bytes{{'C', 'S', 'T', 0x1B}};
 
 std::string GetSaveStatePath(u64 program_id, u32 slot) {
-    return fmt::format("{}{:016X}.{:02d}.cst", FileUtil::GetUserPath(FileUtil::UserPath::StatesDir),
-                       program_id, slot);
+    const u64 movie_id = Movie::GetInstance().GetCurrentMovieID();
+    if (movie_id) {
+        return fmt::format("{}{:016X}.movie{:016X}.{:02d}.cst",
+                           FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id,
+                           movie_id, slot);
+    } else {
+        return fmt::format("{}{:016X}.{:02d}.cst",
+                           FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), program_id, slot);
+    }
 }
 
 std::vector<SaveStateInfo> ListSaveStates(u64 program_id) {