From 83d4d1f8ea4c01fc4b0555603c1f69d1e6359d63 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Sun, 1 Mar 2026 17:45:02 -0600 Subject: [PATCH] Improving cache invalidation during playback, to add an epoch counter to Timeline, so we know when an update took place outside of timeline (i.e. in our video cache thread) and can rescan our entire cache range for potentially missing frames after cache invalidation). --- src/Clip.cpp | 1 + src/Qt/VideoCacheThread.cpp | 32 ++++++++++++++++++++++++++++++++ src/Qt/VideoCacheThread.h | 5 ++++- src/Timeline.cpp | 20 ++++++++++++++++++-- src/Timeline.h | 13 +++++++++---- 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/Clip.cpp b/src/Clip.cpp index 937f955c..f8c46590 100644 --- a/src/Clip.cpp +++ b/src/Clip.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #ifdef USE_IMAGEMAGICK #include "MagickUtilities.h" diff --git a/src/Qt/VideoCacheThread.cpp b/src/Qt/VideoCacheThread.cpp index a3c9aabd..2fdb6e3d 100644 --- a/src/Qt/VideoCacheThread.cpp +++ b/src/Qt/VideoCacheThread.cpp @@ -40,6 +40,8 @@ namespace openshot , reader(nullptr) , force_directional_cache(false) , last_cached_index(0) + , seen_timeline_cache_epoch(0) + , timeline_cache_epoch_initialized(false) { } @@ -130,6 +132,15 @@ namespace openshot return !isThreadRunning(); } + void VideoCacheThread::Reader(ReaderBase* new_reader) + { + std::lock_guard guard(seek_state_mutex); + reader = new_reader; + seen_timeline_cache_epoch = 0; + timeline_cache_epoch_initialized = false; + Play(); + } + void VideoCacheThread::Seek(int64_t new_position, bool start_preroll) { bool should_mark_seek = false; @@ -471,6 +482,27 @@ namespace openshot last_dir.store(dir); } + // If timeline-side cache invalidation occurred (e.g. ApplyJsonDiff / SetJson), + // restart fill from the active playhead window so invalidated gaps self-heal. + if (timeline) { + bool epoch_changed = false; + { + std::lock_guard guard(seek_state_mutex); + const uint64_t timeline_epoch = timeline->CacheEpoch(); + if (!timeline_cache_epoch_initialized) { + seen_timeline_cache_epoch = timeline_epoch; + timeline_cache_epoch_initialized = true; + } + else if (timeline_epoch != seen_timeline_cache_epoch) { + seen_timeline_cache_epoch = timeline_epoch; + epoch_changed = true; + } + } + if (epoch_changed) { + handleUserSeek(playhead, dir); + } + } + // Compute bytes_per_frame, max_bytes, and capacity once int64_t bytes_per_frame = getBytes( (timeline->preview_width ? timeline->preview_width : reader->info.width), diff --git a/src/Qt/VideoCacheThread.h b/src/Qt/VideoCacheThread.h index 58b7308f..2959725d 100644 --- a/src/Qt/VideoCacheThread.h +++ b/src/Qt/VideoCacheThread.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -84,7 +85,7 @@ namespace openshot * @brief Attach a ReaderBase (e.g. Timeline, FFmpegReader) and begin caching. * @param new_reader */ - void Reader(ReaderBase* new_reader) { reader = new_reader; Play(); } + void Reader(ReaderBase* new_reader); protected: /// Thread entry point: loops until threadShouldExit() is true. @@ -198,6 +199,8 @@ namespace openshot ReaderBase* reader; ///< The source reader (e.g., Timeline, FFmpegReader). bool force_directional_cache; ///< (Reserved for future use). + uint64_t seen_timeline_cache_epoch; ///< Last observed Timeline cache invalidation epoch. + bool timeline_cache_epoch_initialized; ///< True once an initial epoch snapshot has been taken. std::atomic last_cached_index; ///< Index of the most recently cached frame. mutable std::mutex seek_state_mutex; ///< Protects coherent seek state updates/consumption. diff --git a/src/Timeline.cpp b/src/Timeline.cpp index d5821ca9..a43f1484 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,7 @@ using namespace openshot; // Default Constructor for the timeline (which sets the canvas width and height) Timeline::Timeline(int width, int height, Fraction fps, int sample_rate, int channels, ChannelLayout channel_layout) : - is_open(false), auto_map_clips(true), managed_cache(true), path(""), max_time(0.0) + is_open(false), auto_map_clips(true), managed_cache(true), path(""), max_time(0.0), cache_epoch(0) { // Create CrashHandler and Attach (incase of errors) CrashHandler::Instance(); @@ -81,7 +82,7 @@ Timeline::Timeline(const ReaderInfo info) : Timeline::Timeline( // Constructor for the timeline (which loads a JSON structure from a file path, and initializes a timeline) Timeline::Timeline(const std::string& projectPath, bool convert_absolute_paths) : - is_open(false), auto_map_clips(true), managed_cache(true), path(projectPath), max_time(0.0) { + is_open(false), auto_map_clips(true), managed_cache(true), path(projectPath), max_time(0.0), cache_epoch(0) { // Create CrashHandler and Attach (incase of errors) CrashHandler::Instance(); @@ -1344,6 +1345,9 @@ void Timeline::SetJsonValue(const Json::Value root) { // Re-open if needed if (was_open) Open(); + + // Timeline content changed: notify cache clients to rescan active window. + BumpCacheEpoch(); } // Apply a special formatted JSON object, which represents a change to the timeline (insert, update, delete) @@ -1374,6 +1378,11 @@ void Timeline::ApplyJsonDiff(std::string value) { apply_json_to_timeline(change); } + + // Timeline content changed: notify cache clients to rescan active window. + if (!root.empty()) { + BumpCacheEpoch(); + } } catch (const std::exception& e) { @@ -1382,6 +1391,10 @@ void Timeline::ApplyJsonDiff(std::string value) { } } +void Timeline::BumpCacheEpoch() { + cache_epoch.fetch_add(1, std::memory_order_relaxed); +} + // Apply JSON diff to clips void Timeline::apply_json_to_clips(Json::Value change) { @@ -1791,6 +1804,9 @@ void Timeline::ClearAllCache(bool deep) { } catch (const ReaderClosed & e) { // ... } + + // Cache content changed: notify cache clients to rebuild their window baseline. + BumpCacheEpoch(); } // Set Max Image Size (used for performance optimization). Convenience function for setting diff --git a/src/Timeline.h b/src/Timeline.h index 83db07ec..98ba875c 100644 --- a/src/Timeline.h +++ b/src/Timeline.h @@ -15,11 +15,9 @@ #include #include -#include #include -#include -#include -#include +#include +#include #include "TimelineBase.h" #include "ReaderBase.h" @@ -167,6 +165,7 @@ namespace openshot { std::string path; ///< Optional path of loaded UTF-8 OpenShot JSON project file double max_time; ///> The max duration (in seconds) of the timeline, based on the furthest clip (right edge) double min_time; ///> The min duration (in seconds) of the timeline, based on the position of the first clip (left edge) + std::atomic cache_epoch; ///< Monotonic cache-invalidation epoch for playback cache reconciliation. std::map> tracked_objects; ///< map of TrackedObjectBBoxes and their IDs @@ -211,6 +210,9 @@ namespace openshot { /// Update the list of 'opened' clips void update_open_clips(openshot::Clip *clip, bool does_clip_intersect); + /// Increment the cache invalidation epoch. + void BumpCacheEpoch(); + public: /// @brief Constructor for the timeline (which configures the default frame properties) @@ -313,6 +315,9 @@ namespace openshot { /// of this cache object though (Timeline will not delete it for you). void SetCache(openshot::CacheBase* new_cache); + /// Return the current cache invalidation epoch. + uint64_t CacheEpoch() const { return cache_epoch.load(std::memory_order_relaxed); }; + /// Get an openshot::Frame object for a specific frame number of this timeline. /// /// @returns The requested frame (containing the image)