Files
test2/source/blender/sequencer/intern/thumbnail_cache.cc
Jacques Lucke b7a1325c3c BLI: use blender::Mutex by default which wraps tbb::mutex
This patch adds a new `BLI_mutex.hh` header which adds `blender::Mutex` as alias
for either `tbb::mutex` or `std::mutex` depending on whether TBB is enabled.

Description copied from the patch:
```
/**
 * blender::Mutex should be used as the default mutex in Blender. It implements a subset of the API
 * of std::mutex but has overall better guaranteed properties. It can be used with RAII helpers
 * like std::lock_guard. However, it is not compatible with e.g. std::condition_variable. So one
 * still has to use std::mutex for that case.
 *
 * The mutex provided by TBB has these properties:
 * - It's as fast as a spin-lock in the non-contended case, i.e. when no other thread is trying to
 *   lock the mutex at the same time.
 * - In the contended case, it spins a couple of times but then blocks to avoid draining system
 *   resources by spinning for a long time.
 * - It's only 1 byte large, compared to e.g. 40 bytes when using the std::mutex of GCC. This makes
 *   it more feasible to have many smaller mutexes which can improve scalability of algorithms
 *   compared to using fewer larger mutexes. Also it just reduces "memory slop" across Blender.
 * - It is *not* a fair mutex, i.e. it's not guaranteed that a thread will ever be able to lock the
 *   mutex when there are always more than one threads that try to lock it. In the majority of
 *   cases, using a fair mutex just causes extra overhead without any benefit. std::mutex is not
 *   guaranteed to be fair either.
 */
 ```

The performance benchmark suggests that the impact is negilible in almost
all cases. The only benchmarks that show interesting behavior are the once
testing foreach zones in Geometry Nodes. These tests are explicitly testing
overhead, which I still have to reduce over time. So it's not unexpected that
changing the mutex has an impact there. What's interesting is that on macos the
performance improves a lot while on linux it gets worse. Since that overhead
should eventually be removed almost entirely, I don't really consider that
blocking.

Links:
* Documentation of different mutex flavors in TBB:
  https://www.intel.com/content/www/us/en/docs/onetbb/developer-guide-api-reference/2021-12/mutex-flavors.html
* Older implementation of a similar mutex by me:
  https://archive.blender.org/developer/differential/0016/0016711/index.html
* Interesting read regarding how a mutex can be this small:
  https://webkit.org/blog/6161/locking-in-webkit/

Pull Request: https://projects.blender.org/blender/blender/pulls/138370
2025-05-07 04:53:16 +02:00

622 lines
19 KiB
C++

/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_map.hh"
#include "BLI_math_base.h"
#include "BLI_mutex.hh"
#include "BLI_path_utils.hh"
#include "BLI_set.hh"
#include "BLI_task.hh"
#include "BLI_vector.hh"
#include "BKE_context.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
#include "MOV_read.hh"
#include "SEQ_render.hh"
#include "SEQ_thumbnail_cache.hh"
#include "SEQ_time.hh"
#include "WM_api.hh"
#include "render.hh"
namespace blender::seq {
static constexpr int MAX_THUMBNAILS = 5000;
// #define DEBUG_PRINT_THUMB_JOB_TIMES
static Mutex thumb_cache_mutex;
/* Thumbnail cache is a map keyed by media file path, with values being
* the various thumbnails that are loaded for it (mostly images would contain just
* one thumbnail frame, but movies can contain multiple).
*
* File entries and individual frame entries also record the timestamp when they were
* last accessed, so that when the cache is full, some of the old entries can be removed.
*
* Thumbnails that are requested but do not have an exact match in the cache, are added
* to the "requests" set. The requests are processed in the background by a WM job. */
struct ThumbnailCache {
struct FrameEntry {
int frame_index = 0; /* Frame index (for movies) or image index (for image sequences). */
int stream_index = 0; /* Stream index (only for multi-stream movies). */
ImBuf *thumb = nullptr;
int64_t used_at = 0;
};
struct FileEntry {
Vector<FrameEntry> frames;
int64_t used_at = 0;
};
struct Request {
explicit Request(const std::string &path,
int frame,
int stream,
StripType type,
int64_t logical_time,
float time_frame,
int ch,
int width,
int height)
: file_path(path),
frame_index(frame),
stream_index(stream),
strip_type(type),
requested_at(logical_time),
timeline_frame(time_frame),
channel(ch),
full_width(width),
full_height(height)
{
}
/* These determine request uniqueness (for equality/hash in a Set). */
std::string file_path;
int frame_index = 0; /* Frame index (for movies) or image index (for image sequences). */
int stream_index = 0; /* Stream index (only for multi-stream movies). */
StripType strip_type = STRIP_TYPE_IMAGE;
/* The following members are payload and do not contribute to uniqueness. */
int64_t requested_at = 0;
float timeline_frame = 0;
int channel = 0;
int full_width = 0;
int full_height = 0;
uint64_t hash() const
{
return get_default_hash(file_path, frame_index, stream_index, strip_type);
}
bool operator==(const Request &o) const
{
return frame_index == o.frame_index && stream_index == o.stream_index &&
strip_type == o.strip_type && file_path == o.file_path;
}
};
Map<std::string, FileEntry> map_;
Set<Request> requests_;
int64_t logical_time_ = 0;
~ThumbnailCache()
{
clear();
}
void clear()
{
for (const auto &item : map_.items()) {
for (const auto &thumb : item.value.frames) {
IMB_freeImBuf(thumb.thumb);
}
}
map_.clear();
requests_.clear();
logical_time_ = 0;
}
void remove_entry(const std::string &path)
{
FileEntry *entry = map_.lookup_ptr(path);
if (entry == nullptr) {
return;
}
for (const auto &thumb : entry->frames) {
IMB_freeImBuf(thumb.thumb);
}
map_.remove_contained(path);
}
};
static ThumbnailCache *ensure_thumbnail_cache(Scene *scene)
{
ThumbnailCache **cache = &scene->ed->runtime.thumbnail_cache;
if (*cache == nullptr) {
*cache = MEM_new<ThumbnailCache>(__func__);
}
return *cache;
}
static ThumbnailCache *query_thumbnail_cache(Scene *scene)
{
if (scene == nullptr || scene->ed == nullptr) {
return nullptr;
}
return scene->ed->runtime.thumbnail_cache;
}
bool strip_can_have_thumbnail(const Scene *scene, const Strip *strip)
{
if (scene == nullptr || scene->ed == nullptr || strip == nullptr) {
return false;
}
if (!ELEM(strip->type, STRIP_TYPE_MOVIE, STRIP_TYPE_IMAGE)) {
return false;
}
const StripElem *se = strip->data->stripdata;
if (se->orig_height == 0 || se->orig_width == 0) {
return false;
}
return true;
}
static std::string get_path_from_strip(Scene *scene, const Strip *strip, float timeline_frame)
{
char filepath[FILE_MAX];
filepath[0] = 0;
switch (strip->type) {
case STRIP_TYPE_IMAGE: {
const StripElem *s_elem = render_give_stripelem(scene, strip, timeline_frame);
if (s_elem != nullptr) {
BLI_path_join(filepath, sizeof(filepath), strip->data->dirpath, s_elem->filename);
BLI_path_abs(filepath, ID_BLEND_PATH_FROM_GLOBAL(&scene->id));
}
} break;
case STRIP_TYPE_MOVIE:
BLI_path_join(
filepath, sizeof(filepath), strip->data->dirpath, strip->data->stripdata->filename);
BLI_path_abs(filepath, ID_BLEND_PATH_FROM_GLOBAL(&scene->id));
break;
}
return filepath;
}
static void image_size_to_thumb_size(int &r_width, int &r_height)
{
float aspect = float(r_width) / float(r_height);
if (r_width > r_height) {
r_width = THUMB_SIZE;
r_height = round_fl_to_int(THUMB_SIZE / aspect);
}
else {
r_height = THUMB_SIZE;
r_width = round_fl_to_int(THUMB_SIZE * aspect);
}
}
static ImBuf *make_thumb_for_image(const Scene *scene, const ThumbnailCache::Request &request)
{
ImBuf *ibuf = IMB_thumb_load_image(
request.file_path.c_str(), THUMB_SIZE, nullptr, IMBThumbLoadFlags::LoadLargeFiles);
if (ibuf == nullptr) {
return nullptr;
}
/* Keep only float buffer if we have both byte & float. */
if (ibuf->float_buffer.data != nullptr && ibuf->byte_buffer.data != nullptr) {
IMB_free_byte_pixels(ibuf);
}
seq_imbuf_to_sequencer_space(scene, ibuf, false);
seq_imbuf_assign_spaces(scene, ibuf);
return ibuf;
}
static void scale_to_thumbnail_size(ImBuf *ibuf)
{
if (ibuf == nullptr) {
return;
}
int width = ibuf->x;
int height = ibuf->y;
image_size_to_thumb_size(width, height);
IMB_scale(ibuf, width, height, IMBScaleFilter::Nearest, false);
}
/* Background job that processes in-flight thumbnail requests. */
class ThumbGenerationJob {
Scene *scene_ = nullptr;
ThumbnailCache *cache_ = nullptr;
public:
ThumbGenerationJob(Scene *scene, ThumbnailCache *cache) : scene_(scene), cache_(cache) {}
static void ensure_job(const bContext *C, ThumbnailCache *cache);
private:
static void run_fn(void *customdata, wmJobWorkerStatus *worker_status);
static void end_fn(void *customdata);
static void free_fn(void *customdata);
};
void ThumbGenerationJob::ensure_job(const bContext *C, ThumbnailCache *cache)
{
wmWindowManager *wm = CTX_wm_manager(C);
wmWindow *win = CTX_wm_window(C);
Scene *scene = CTX_data_scene(C);
wmJob *wm_job = WM_jobs_get(
wm, win, scene, "Strip Thumbnails", eWM_JobFlag(0), WM_JOB_TYPE_SEQ_DRAW_THUMBNAIL);
if (!WM_jobs_is_running(wm_job)) {
ThumbGenerationJob *tj = MEM_new<ThumbGenerationJob>("ThumbGenerationJob", scene, cache);
WM_jobs_customdata_set(wm_job, tj, free_fn);
WM_jobs_timer(wm_job, 0.1, NC_SCENE | ND_SEQUENCER, NC_SCENE | ND_SEQUENCER);
WM_jobs_callbacks(wm_job, run_fn, nullptr, nullptr, end_fn);
WM_jobs_start(wm, wm_job);
}
}
void ThumbGenerationJob::free_fn(void *customdata)
{
ThumbGenerationJob *job = static_cast<ThumbGenerationJob *>(customdata);
MEM_delete(job);
}
void ThumbGenerationJob::run_fn(void *customdata, wmJobWorkerStatus *worker_status)
{
#ifdef DEBUG_PRINT_THUMB_JOB_TIMES
clock_t t0 = clock();
std::atomic<int> total_thumbs = 0, total_images = 0, total_movies = 0;
#endif
ThumbGenerationJob *job = static_cast<ThumbGenerationJob *>(customdata);
Vector<ThumbnailCache::Request> requests;
while (!worker_status->stop) {
/* Under cache mutex lock: copy all current requests into a vector for processing.
* NOTE: keep the requests set intact! We don't want to add new requests for same
* items while we are processing them. They will be removed from the set once
* they are finished, one by one. */
{
std::scoped_lock lock(thumb_cache_mutex);
requests.clear();
requests.reserve(job->cache_->requests_.size());
for (const auto &request : job->cache_->requests_) {
requests.append(request);
}
}
if (requests.is_empty()) {
break;
}
/* Sort requests by file, stream and increasing frame index. */
std::sort(requests.begin(),
requests.end(),
[](const ThumbnailCache::Request &a, const ThumbnailCache::Request &b) {
if (a.file_path != b.file_path) {
return a.file_path < b.file_path;
}
if (a.stream_index != b.stream_index) {
return a.stream_index < b.stream_index;
}
return a.frame_index < b.frame_index;
});
/* Process the requests in parallel. Split requests into approximately 4 groups:
* we don't want to go too wide since that would potentially mean that a single input
* movie gets assigned to more than one thread, and the thumbnail loading itself
* is somewhat-threaded already. */
int64_t grain_size = math::max<int64_t>(8, requests.size() / 4);
threading::parallel_for(requests.index_range(), grain_size, [&](IndexRange range) {
/* Often the same movie file is chopped into multiple strips next to each other.
* Since the requests are sorted by file path and frame index, we can reuse MovieReader
* objects between them for performance. */
MovieReader *cur_anim = nullptr;
std::string cur_anim_path;
int cur_stream = 0;
for (const int i : range) {
const ThumbnailCache::Request &request = requests[i];
if (worker_status->stop) {
break;
}
#ifdef DEBUG_PRINT_THUMB_JOB_TIMES
++total_thumbs;
#endif
ImBuf *thumb = nullptr;
if (request.strip_type == STRIP_TYPE_IMAGE) {
/* Load thumbnail for an image. */
#ifdef DEBUG_PRINT_THUMB_JOB_TIMES
++total_images;
#endif
thumb = make_thumb_for_image(job->scene_, request);
}
else if (request.strip_type == STRIP_TYPE_MOVIE) {
/* Load thumbnail for an movie. */
#ifdef DEBUG_PRINT_THUMB_JOB_TIMES
++total_movies;
#endif
/* Are we switching to a different movie file / stream? */
if (request.file_path != cur_anim_path || request.stream_index != cur_stream) {
if (cur_anim != nullptr) {
MOV_close(cur_anim);
cur_anim = nullptr;
}
cur_anim_path = request.file_path;
cur_stream = request.stream_index;
cur_anim = MOV_open_file(cur_anim_path.c_str(), IB_byte_data, cur_stream, nullptr);
}
/* Decode the movie frame. */
if (cur_anim != nullptr) {
thumb = MOV_decode_frame(cur_anim, request.frame_index, IMB_TC_NONE, IMB_PROXY_NONE);
if (thumb != nullptr) {
seq_imbuf_assign_spaces(job->scene_, thumb);
}
}
}
else {
BLI_assert_unreachable();
}
scale_to_thumbnail_size(thumb);
/* Add result into the cache (under cache mutex lock). */
{
std::scoped_lock lock(thumb_cache_mutex);
ThumbnailCache::FileEntry *val = job->cache_->map_.lookup_ptr(request.file_path);
if (val != nullptr) {
val->used_at = math::max(val->used_at, request.requested_at);
val->frames.append(
{request.frame_index, request.stream_index, thumb, request.requested_at});
}
else {
IMB_freeImBuf(thumb);
}
/* Remove the request from original set. */
job->cache_->requests_.remove(request);
}
if (thumb) {
worker_status->do_update = true;
}
}
if (cur_anim != nullptr) {
MOV_close(cur_anim);
cur_anim = nullptr;
}
});
}
#ifdef DEBUG_PRINT_THUMB_JOB_TIMES
clock_t t1 = clock();
printf("VSE thumb job: %i thumbs (%i img, %i movie) in %.3f sec\n",
total_thumbs.load(),
total_images.load(),
total_movies.load(),
double(t1 - t0) / CLOCKS_PER_SEC);
#endif
}
void ThumbGenerationJob::end_fn(void *customdata)
{
ThumbGenerationJob *job = static_cast<ThumbGenerationJob *>(customdata);
WM_main_add_notifier(NC_SCENE | ND_SEQUENCER, job->scene_);
}
static ImBuf *query_thumbnail(ThumbnailCache &cache,
const std::string &key,
int frame_index,
float timeline_frame,
const bContext *C,
const Strip *strip)
{
int64_t cur_time = cache.logical_time_;
ThumbnailCache::FileEntry *val = cache.map_.lookup_ptr(key);
if (val == nullptr) {
/* Nothing in cache for this path yet. */
ThumbnailCache::FileEntry value;
value.used_at = cur_time;
cache.map_.add_new(key, value);
val = cache.map_.lookup_ptr(key);
}
BLI_assert_msg(val != nullptr, "Thumbnail cache value should never be null here");
/* Search thumbnail entries of this file for closest match to the frame we want. */
int64_t best_index = -1;
int best_score = INT_MAX;
for (int64_t index = 0; index < val->frames.size(); index++) {
if (strip->streamindex != val->frames[index].stream_index) {
continue; /* Different video stream than what we need, ignore. */
}
int score = math::abs(frame_index - val->frames[index].frame_index);
if (score < best_score) {
best_score = score;
best_index = index;
if (score == 0) {
break;
}
}
}
if (best_score > 0) {
/* We do not have an exact frame match, add a thumb generation request. */
const StripElem *se = strip->data->stripdata;
int img_width = se->orig_width;
int img_height = se->orig_height;
ThumbnailCache::Request request(key,
frame_index,
strip->streamindex,
StripType(strip->type),
cur_time,
timeline_frame,
strip->machine,
img_width,
img_height);
cache.requests_.add(request);
ThumbGenerationJob::ensure_job(C, &cache);
}
if (best_index < 0) {
return nullptr;
}
/* Return the closest thumbnail fit we have so far. */
val->used_at = math::max(val->used_at, cur_time);
val->frames[best_index].used_at = math::max(val->frames[best_index].used_at, cur_time);
return val->frames[best_index].thumb;
}
ImBuf *thumbnail_cache_get(const bContext *C,
Scene *scene,
const Strip *strip,
float timeline_frame)
{
if (!strip_can_have_thumbnail(scene, strip)) {
return nullptr;
}
timeline_frame = math::round(timeline_frame);
const std::string key = get_path_from_strip(scene, strip, timeline_frame);
int frame_index = give_frame_index(scene, strip, timeline_frame);
if (strip->type == STRIP_TYPE_MOVIE) {
frame_index += strip->anim_startofs;
}
ImBuf *res = nullptr;
{
std::scoped_lock lock(thumb_cache_mutex);
ThumbnailCache *cache = ensure_thumbnail_cache(scene);
res = query_thumbnail(*cache, key, frame_index, timeline_frame, C, strip);
}
if (res) {
IMB_refImBuf(res);
}
return res;
}
void thumbnail_cache_invalidate_strip(Scene *scene, const Strip *strip)
{
if (!strip_can_have_thumbnail(scene, strip)) {
return;
}
std::scoped_lock lock(thumb_cache_mutex);
ThumbnailCache *cache = query_thumbnail_cache(scene);
if (cache != nullptr) {
if (ELEM((strip)->type, STRIP_TYPE_MOVIE, STRIP_TYPE_IMAGE)) {
const StripElem *elem = strip->data->stripdata;
if (elem != nullptr) {
int paths_count = 1;
if (strip->type == STRIP_TYPE_IMAGE) {
/* Image strip has array of file names. */
paths_count = int(MEM_allocN_len(elem) / sizeof(*elem));
}
char filepath[FILE_MAX];
const char *basepath = strip->scene ? ID_BLEND_PATH_FROM_GLOBAL(&strip->scene->id) :
BKE_main_blendfile_path_from_global();
for (int i = 0; i < paths_count; i++, elem++) {
BLI_path_join(filepath, sizeof(filepath), strip->data->dirpath, elem->filename);
BLI_path_abs(filepath, basepath);
cache->remove_entry(filepath);
}
}
}
}
}
void thumbnail_cache_maintain_capacity(Scene *scene)
{
std::scoped_lock lock(thumb_cache_mutex);
ThumbnailCache *cache = query_thumbnail_cache(scene);
if (cache != nullptr) {
cache->logical_time_++;
/* Count total number of thumbnails, and track which one is the least recently used file. */
int64_t entries = 0;
std::string oldest_file;
/* Do not remove thumbnails for files used within last 10 updates. */
int64_t oldest_time = cache->logical_time_ - 10;
int64_t oldest_entries = 0;
for (const auto &item : cache->map_.items()) {
entries += item.value.frames.size();
if (item.value.used_at < oldest_time) {
oldest_file = item.key;
oldest_time = item.value.used_at;
oldest_entries = item.value.frames.size();
}
}
/* If we're beyond capacity and have a long-unused file, remove that. */
if (entries > MAX_THUMBNAILS && !oldest_file.empty()) {
cache->remove_entry(oldest_file);
entries -= oldest_entries;
}
/* If we're still beyond capacity, remove individual long-unused (but not within
* last 100 updates) individual frames. */
if (entries > MAX_THUMBNAILS) {
for (const auto &item : cache->map_.items()) {
for (int64_t i = 0; i < item.value.frames.size(); i++) {
if (item.value.frames[i].used_at < cache->logical_time_ - 100) {
IMB_freeImBuf(item.value.frames[i].thumb);
item.value.frames.remove_and_reorder(i);
}
}
}
}
}
}
void thumbnail_cache_discard_requests_outside(Scene *scene, const rctf &rect)
{
std::scoped_lock lock(thumb_cache_mutex);
ThumbnailCache *cache = query_thumbnail_cache(scene);
if (cache != nullptr) {
cache->requests_.remove_if([&](const ThumbnailCache::Request &request) {
return request.timeline_frame < rect.xmin || request.timeline_frame > rect.xmax ||
request.channel < rect.ymin || request.channel > rect.ymax;
});
}
}
void thumbnail_cache_clear(Scene *scene)
{
std::scoped_lock lock(thumb_cache_mutex);
ThumbnailCache *cache = query_thumbnail_cache(scene);
if (cache != nullptr) {
scene->ed->runtime.thumbnail_cache->clear();
}
}
void thumbnail_cache_destroy(Scene *scene)
{
std::scoped_lock lock(thumb_cache_mutex);
ThumbnailCache *cache = query_thumbnail_cache(scene);
if (cache != nullptr) {
BLI_assert(cache == scene->ed->runtime.thumbnail_cache);
MEM_delete(scene->ed->runtime.thumbnail_cache);
scene->ed->runtime.thumbnail_cache = nullptr;
}
}
} // namespace blender::seq