Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_threads.h"
#include "COM_result.hh"
#include "DRW_engine.hh"
#include "compositor_cache.hh"
namespace blender::seq {
CompositorCache::~CompositorCache()
{
bool use_main_context = false;
if (this->last_evaluation_used_gpu) {
/* Free resources with GPU context enabled. Cleanup may happen from the main thread, and we
* must use the main context there. */
BLI_assert(BLI_thread_is_main() || this->secondary_gpu_context.ghost_context != nullptr);
use_main_context = BLI_thread_is_main();
if (use_main_context) {
DRW_gpu_context_enable();
}
else {
gpu::GPU_activate_secondary_context(this->secondary_gpu_context);
}
}
this->cache_manager.free();
/* See comment above on context enabling. */
if (this->last_evaluation_used_gpu) {
if (use_main_context) {
DRW_gpu_context_disable();
}
else {
gpu::GPU_deactivate_secondary_context(this->secondary_gpu_context);
}
}
}
void CompositorCache::recreate_if_needed(bool gpu,
compositor::ResultPrecision precision,
const gpu::GPUSecondaryContextData &gpu_context)
{
this->secondary_gpu_context = gpu ? gpu_context : gpu::GPUSecondaryContextData();
if (this->last_evaluation_used_gpu == gpu && this->last_evaluation_precision == precision) {
return;
}
this->cache_manager.free();
this->last_evaluation_used_gpu = gpu;
this->last_evaluation_precision = precision;
}
} // namespace blender::seq

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_static_cache_manager.hh"
#include "GPU_context.hh"
class GHOST_IContext;
namespace blender::seq {
class CompositorCache {
private:
compositor::StaticCacheManager cache_manager;
bool last_evaluation_used_gpu = false;
compositor::ResultPrecision last_evaluation_precision = compositor::ResultPrecision::Half;
gpu::GPUSecondaryContextData secondary_gpu_context = {};
public:
~CompositorCache();
compositor::StaticCacheManager &get_cache_manager()
{
return cache_manager;
}
void recreate_if_needed(bool gpu,
compositor::ResultPrecision precision,
const gpu::GPUSecondaryContextData &gpu_context);
};
} // namespace blender::seq

View File

@@ -0,0 +1,293 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_hash.hh"
#include "BLI_map.hh"
#include "BLI_mutex.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BKE_scene.hh"
#include "IMB_imbuf.hh"
#include "SEQ_relations.hh"
#include "SEQ_sequencer.hh"
#include "final_image_cache.hh"
#include "prefetch.hh"
namespace blender::seq {
static Mutex final_image_cache_mutex;
struct FinalImageCache {
struct Key {
int timeline_frame;
int view_id;
int display_channel;
int2 image_size;
uint64_t hash() const
{
return get_default_hash(timeline_frame, view_id, display_channel, image_size);
}
bool operator==(const Key &other) const
{
return timeline_frame == other.timeline_frame && view_id == other.view_id &&
display_channel == other.display_channel && image_size == image_size;
}
};
Map<Key, ImBuf *> map_;
~FinalImageCache()
{
clear();
}
void clear()
{
for (ImBuf *item : map_.values()) {
IMB_freeImBuf(item);
}
map_.clear();
}
};
static FinalImageCache *ensure_final_image_cache(Scene *scene)
{
FinalImageCache **cache = &scene->ed->runtime->final_image_cache;
if (*cache == nullptr) {
*cache = MEM_new<FinalImageCache>(__func__);
}
return *cache;
}
static FinalImageCache *query_final_image_cache(const Scene *scene)
{
if (scene == nullptr || scene->ed == nullptr) {
return nullptr;
}
return scene->ed->runtime->final_image_cache;
}
ImBuf *final_image_cache_get(Scene *scene,
float timeline_frame,
int view_id,
int display_channel,
int2 image_size,
bool is_render)
{
if (is_render) {
return nullptr;
}
const FinalImageCache::Key key = {
int(math::round(timeline_frame)), view_id, display_channel, image_size};
ImBuf *res = nullptr;
{
std::lock_guard lock(final_image_cache_mutex);
FinalImageCache *cache = query_final_image_cache(scene);
if (cache == nullptr) {
return nullptr;
}
res = cache->map_.lookup_default(key, nullptr);
}
if (res) {
IMB_refImBuf(res);
}
return res;
}
void final_image_cache_put(Scene *scene,
float timeline_frame,
int view_id,
int display_channel,
int2 image_size,
bool is_render,
ImBuf *image)
{
if (is_render) {
return;
}
const FinalImageCache::Key key = {
int(math::round(timeline_frame)), view_id, display_channel, image_size};
IMB_refImBuf(image);
std::lock_guard lock(final_image_cache_mutex);
FinalImageCache *cache = ensure_final_image_cache(scene);
cache->map_.add_or_modify(
key,
[&](ImBuf **value) { *value = image; },
[&](ImBuf **existing) {
if (*existing) {
IMB_freeImBuf(*existing);
}
*existing = image;
});
}
void final_image_cache_invalidate_frame_range(Scene *scene,
const float timeline_frame_start,
const float timeline_frame_end)
{
std::lock_guard lock(final_image_cache_mutex);
FinalImageCache *cache = query_final_image_cache(scene);
if (cache == nullptr) {
return;
}
const int key_start = int(math::floor(timeline_frame_start));
const int key_end = int(math::ceil(timeline_frame_end));
for (auto it = cache->map_.items().begin(); it != cache->map_.items().end(); it++) {
const int key = (*it).key.timeline_frame;
if (key >= key_start && key <= key_end) {
IMB_freeImBuf((*it).value);
cache->map_.remove(it);
}
}
}
void final_image_cache_clear(Scene *scene)
{
std::lock_guard lock(final_image_cache_mutex);
FinalImageCache *cache = query_final_image_cache(scene);
if (cache != nullptr) {
scene->ed->runtime->final_image_cache->clear();
}
}
void final_image_cache_destroy(Scene *scene)
{
std::lock_guard lock(final_image_cache_mutex);
FinalImageCache *cache = query_final_image_cache(scene);
if (cache != nullptr) {
BLI_assert(cache == scene->ed->runtime->final_image_cache);
MEM_delete(scene->ed->runtime->final_image_cache);
scene->ed->runtime->final_image_cache = nullptr;
}
}
void final_image_cache_iterate(Scene *scene,
void *userdata,
void callback_iter(void *userdata, int timeline_frame))
{
std::lock_guard lock(final_image_cache_mutex);
FinalImageCache *cache = query_final_image_cache(scene);
if (cache == nullptr) {
return;
}
for (const FinalImageCache::Key &frame_view : cache->map_.keys()) {
callback_iter(userdata, frame_view.timeline_frame);
}
}
size_t final_image_cache_calc_memory_size(const Scene *scene)
{
std::lock_guard lock(final_image_cache_mutex);
FinalImageCache *cache = query_final_image_cache(scene);
if (cache == nullptr) {
return 0;
}
size_t size = 0;
for (ImBuf *frame : cache->map_.values()) {
size += IMB_get_size_in_memory(frame);
}
return size;
}
size_t final_image_cache_get_image_count(const Scene *scene)
{
std::lock_guard lock(final_image_cache_mutex);
FinalImageCache *cache = query_final_image_cache(scene);
if (cache == nullptr) {
return 0;
}
return cache->map_.size();
}
bool final_image_cache_evict(Scene *scene)
{
std::lock_guard lock(final_image_cache_mutex);
FinalImageCache *cache = query_final_image_cache(scene);
if (cache == nullptr) {
return false;
}
/* Find which entry to remove -- we pick the one that is furthest from the current frame,
* biasing the ones that are behind the current frame.
*
* However, do not try to evict entries from the current prefetch job range -- we need to
* be able to fully fill the cache from prefetching, and then actually stop the job when it
* is full and no longer can evict anything. */
int cur_prefetch_start = std::numeric_limits<int>::min();
int cur_prefetch_end = std::numeric_limits<int>::min();
if (scene->ed->cache_flag & SEQ_CACHE_STORE_FINAL_OUT) {
/* Only activate the prefetch guards if the cache is active. */
seq_prefetch_get_time_range(scene, &cur_prefetch_start, &cur_prefetch_end);
}
const bool prefetch_loops_around = cur_prefetch_start > cur_prefetch_end;
const int timeline_start = scene->playback_start();
const int timeline_end = scene->playback_end();
/* If we wrap around, treat the timeline start as the playback head position.
* This is to try to mitigate un-needed cache evictions. */
const int cur_frame = prefetch_loops_around ? timeline_start : scene->r.cfra;
FinalImageCache::Key best_key = {};
ImBuf *best_item = nullptr;
int best_score = 0;
for (const auto &item : cache->map_.items()) {
const int item_frame = item.key.timeline_frame;
if (prefetch_loops_around) {
if (item_frame >= timeline_start && item_frame <= cur_prefetch_end) {
continue; /* Within active prefetch range, do not try to remove it. */
}
if (item_frame >= cur_prefetch_start && item_frame <= timeline_end) {
continue; /* Within active prefetch range, do not try to remove it. */
}
}
else if (item_frame >= cur_prefetch_start && item_frame <= cur_prefetch_end) {
continue; /* Within active prefetch range, do not try to remove it. */
}
/* Score for removal is distance to current frame; 2x that if behind current frame. */
int score = 0;
if (item_frame < cur_frame) {
score = (cur_frame - item_frame) * 2;
}
else if (item_frame > cur_frame) {
score = item_frame - cur_frame;
}
if (score > best_score) {
best_key = item.key;
best_item = item.value;
best_score = score;
}
}
/* Remove if we found one. */
if (best_item != nullptr) {
IMB_freeImBuf(best_item);
cache->map_.remove(best_key);
return true;
}
/* Did not find anything to remove. */
return false;
}
} // namespace blender::seq

View File

@@ -0,0 +1,56 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*
* Cache of final rendered frames.
* - Keyed by (timeline frame, view_id).
* - When full, cache eviction policy is to remove frames furthest
* from the current-frame, biasing towards removal of
* frames behind the current-frame.
* - Invalidated fairly often while editing, basically whenever any
* strip overlapping that frame changes.
*/
#pragma once
#include "BLI_math_vector_types.hh"
namespace blender {
struct ImBuf;
struct Strip;
struct Scene;
namespace seq {
void final_image_cache_put(Scene *scene,
float timeline_frame,
int view_id,
int display_channel,
int2 image_size,
bool is_render,
ImBuf *image);
ImBuf *final_image_cache_get(Scene *scene,
float timeline_frame,
int view_id,
int display_channel,
int2 image_size,
bool is_render);
void final_image_cache_invalidate_frame_range(Scene *scene,
const float timeline_frame_start,
const float timeline_frame_end);
void final_image_cache_clear(Scene *scene);
void final_image_cache_destroy(Scene *scene);
bool final_image_cache_evict(Scene *scene);
size_t final_image_cache_get_image_count(const Scene *scene);
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,192 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_map.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
#include "SEQ_sequencer.hh"
#include "intra_frame_cache.hh"
namespace blender::seq {
struct StripImageMap {
Map<const Strip *, SeqResult> map_;
SeqResult get(const Strip *strip) const;
void put(const Strip *strip, const SeqResult &result);
void invalidate(const Strip *strip);
void clear();
};
struct IntraFrameCache {
StripImageMap preprocessed;
StripImageMap composite;
float timeline_frame = -1.0f;
int view_id = -1;
int width = -1;
int height = -1;
bool is_render = false;
~IntraFrameCache()
{
preprocessed.clear();
composite.clear();
}
};
static IntraFrameCache *query_intra_frame_cache(Scene *scene)
{
if (scene == nullptr || scene->ed == nullptr) {
return nullptr;
}
return scene->ed->runtime->intra_frame_cache;
}
void intra_frame_cache_invalidate(Scene *scene)
{
IntraFrameCache *cache = query_intra_frame_cache(scene);
if (cache != nullptr) {
cache->preprocessed.clear();
cache->composite.clear();
cache->timeline_frame = -1.0f;
cache->view_id = -1;
cache->width = -1;
cache->height = -1;
cache->is_render = false;
}
}
void intra_frame_cache_invalidate(Scene *scene, const Strip *strip)
{
if (strip == nullptr) {
return;
}
IntraFrameCache *cache = query_intra_frame_cache(scene);
if (cache != nullptr) {
cache->preprocessed.invalidate(strip);
cache->composite.invalidate(strip);
}
}
void StripImageMap::invalidate(const Strip *strip)
{
/* Invalidate this strip, and all strips that are above it. */
for (auto it = this->map_.items().begin(); it != this->map_.items().end(); it++) {
const Strip *key = (*it).key;
if (key == strip || key->channel >= strip->channel) {
IMB_freeImBuf((*it).value.image);
this->map_.remove(it);
}
}
}
SeqResult StripImageMap::get(const Strip *strip) const
{
SeqResult result = this->map_.lookup_default(strip, {});
if (result.is_valid()) {
IMB_refImBuf(result.image);
}
return result;
}
void StripImageMap::put(const Strip *strip, const SeqResult &result)
{
BLI_assert(strip != nullptr);
if (!result.is_valid()) {
return;
}
SeqResult existing = this->map_.lookup_default(strip, {});
if (existing.is_valid()) {
IMB_freeImBuf(existing.image);
}
this->map_.add_overwrite(strip, result);
IMB_refImBuf(result.image);
}
void StripImageMap::clear()
{
for (const auto &item : this->map_.items()) {
IMB_freeImBuf(item.value.image);
}
this->map_.clear();
}
SeqResult intra_frame_cache_get_preprocessed(Scene *scene, const Strip *strip)
{
IntraFrameCache *cache = query_intra_frame_cache(scene);
if (strip == nullptr || cache == nullptr) {
return {};
}
return cache->preprocessed.get(strip);
}
SeqResult intra_frame_cache_get_composite(Scene *scene, const Strip *strip)
{
IntraFrameCache *cache = query_intra_frame_cache(scene);
if (strip == nullptr || cache == nullptr) {
return {};
}
return cache->composite.get(strip);
}
void intra_frame_cache_put_preprocessed(Scene *scene, const Strip *strip, const SeqResult &result)
{
if (scene == nullptr || scene->ed == nullptr || strip == nullptr || !result.is_valid()) {
return;
}
IntraFrameCache *&cache = scene->ed->runtime->intra_frame_cache;
if (cache == nullptr) {
cache = MEM_new<IntraFrameCache>(__func__);
}
cache->preprocessed.put(strip, result);
}
void intra_frame_cache_put_composite(Scene *scene, const Strip *strip, const SeqResult &result)
{
if (scene == nullptr || scene->ed == nullptr || strip == nullptr || !result.is_valid()) {
return;
}
IntraFrameCache *&cache = scene->ed->runtime->intra_frame_cache;
if (cache == nullptr) {
cache = MEM_new<IntraFrameCache>(__func__);
}
cache->composite.put(strip, result);
}
void intra_frame_cache_destroy(Scene *scene)
{
IntraFrameCache *cache = query_intra_frame_cache(scene);
if (cache != nullptr) {
MEM_SAFE_DELETE(scene->ed->runtime->intra_frame_cache);
}
}
void intra_frame_cache_set_cur_frame(
Scene *scene, float frame, int view_id, int width, int height, bool is_render)
{
IntraFrameCache *cache = query_intra_frame_cache(scene);
if (cache != nullptr) {
if (cache->timeline_frame != frame || cache->view_id != view_id || cache->width != width ||
cache->height != height || cache->is_render != is_render)
{
cache->timeline_frame = frame;
cache->view_id = view_id;
cache->width = width;
cache->height = height;
cache->is_render = is_render;
cache->preprocessed.clear();
cache->composite.clear();
}
}
}
} // namespace blender::seq

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*
* Cached intermediate images used while rendering one sequencer frame.
* - For each strip, "preprocessed" (strip source, possibly
* transformed, with modifiers applied) and "composite" (result of
* blending this strip with image underneath) images are cached.
* - Whenever going to a different frame, the cached content of previous
* frame is cleared.
* - Primary reason for having this cache at all, is when the whole frame
* is a complex stack of things, and you want to tweak settings of one
* of the involved strips. You don't want to be re-calculating all the
* strips that are "below" your tweaked strip, for better interactivity.
*/
#pragma once
#include "render.hh"
namespace blender {
struct Strip;
struct Scene;
namespace seq {
SeqResult intra_frame_cache_get_preprocessed(Scene *scene, const Strip *strip);
SeqResult intra_frame_cache_get_composite(Scene *scene, const Strip *strip);
void intra_frame_cache_put_preprocessed(Scene *scene, const Strip *strip, const SeqResult &result);
void intra_frame_cache_put_composite(Scene *scene, const Strip *strip, const SeqResult &result);
void intra_frame_cache_destroy(Scene *scene);
void intra_frame_cache_invalidate(Scene *scene, const Strip *strip);
void intra_frame_cache_invalidate(Scene *scene);
void intra_frame_cache_set_cur_frame(
Scene *scene, float frame, int view_id, int width, int height, bool is_render);
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,215 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "GPU_texture.hh"
#include "SEQ_preview_cache.hh"
#include "SEQ_sequencer.hh"
namespace blender::seq {
struct PreviewCacheItem {
int64_t last_used = -1;
int timeline_frame = -1;
int display_channel = -1;
int width = -1;
int height = -1;
gpu::Texture *texture = nullptr;
gpu::Texture *scope_texture = nullptr;
void clear()
{
last_used = -1;
timeline_frame = -1;
width = -1;
height = -1;
GPU_TEXTURE_FREE_SAFE(texture);
GPU_TEXTURE_FREE_SAFE(scope_texture);
}
};
struct PreviewCache {
static constexpr int cache_size = 4;
PreviewCacheItem items[cache_size];
int64_t tick_count = 0;
~PreviewCache()
{
clear();
}
void clear()
{
for (PreviewCacheItem &item : this->items) {
item.clear();
}
}
};
static PreviewCache *query_preview_cache(Scene *scene)
{
if (scene == nullptr || scene->ed == nullptr) {
return nullptr;
}
return scene->ed->runtime->preview_cache;
}
static PreviewCache *ensure_preview_cache(Scene *scene)
{
if (scene == nullptr || scene->ed == nullptr) {
return nullptr;
}
PreviewCache *&cache = scene->ed->runtime->preview_cache;
if (cache == nullptr) {
cache = MEM_new<PreviewCache>(__func__);
}
return cache;
}
gpu::Texture *preview_cache_get_gpu_texture(
Scene *scene, int timeline_frame, int display_channel, int width, int height)
{
PreviewCache *cache = query_preview_cache(scene);
if (cache == nullptr) {
return nullptr;
}
cache->tick_count++;
for (PreviewCacheItem &item : cache->items) {
if (item.timeline_frame == timeline_frame && item.display_channel == display_channel &&
item.width == width && item.height == height && item.texture != nullptr)
{
item.last_used = cache->tick_count;
return item.texture;
}
}
return nullptr;
}
gpu::Texture *preview_cache_get_gpu_scope_texture(
Scene *scene, int timeline_frame, int display_channel, int width, int height)
{
PreviewCache *cache = query_preview_cache(scene);
if (cache == nullptr) {
return nullptr;
}
cache->tick_count++;
for (PreviewCacheItem &item : cache->items) {
if (item.timeline_frame == timeline_frame && item.display_channel == display_channel &&
item.width == width && item.height == height && item.scope_texture != nullptr)
{
item.last_used = cache->tick_count;
return item.scope_texture;
}
}
return nullptr;
}
static PreviewCacheItem *find_slot(
PreviewCache *cache, int timeline_frame, int display_channel, int width, int height)
{
cache->tick_count++;
/* Try to find an exact frame match. */
for (PreviewCacheItem &item : cache->items) {
if (item.timeline_frame == timeline_frame && item.display_channel == display_channel &&
item.width == width && item.height == height)
{
return &item;
}
}
/* Find unused or least recently used slot. */
PreviewCacheItem *best_slot = nullptr;
int64_t best_score = -1;
for (PreviewCacheItem &item : cache->items) {
if (item.texture == nullptr && item.scope_texture == nullptr) {
return &item;
}
int64_t score = cache->tick_count - item.last_used;
if (score >= best_score) {
best_score = score;
best_slot = &item;
}
}
return best_slot;
}
void preview_cache_set_gpu_texture(Scene *scene,
int timeline_frame,
int display_channel,
gpu::Texture *texture)
{
PreviewCache *cache = ensure_preview_cache(scene);
if (cache == nullptr || texture == nullptr) {
return;
}
const int width = GPU_texture_width(texture);
const int height = GPU_texture_height(texture);
PreviewCacheItem *slot = find_slot(cache, timeline_frame, display_channel, width, height);
if (slot == nullptr) {
return;
}
slot->timeline_frame = timeline_frame;
slot->display_channel = display_channel;
slot->width = width;
slot->height = height;
slot->last_used = cache->tick_count;
GPU_TEXTURE_FREE_SAFE(slot->texture);
/* Free the display-space texture of this slot too. */
GPU_TEXTURE_FREE_SAFE(slot->scope_texture);
slot->texture = texture;
}
void preview_cache_set_gpu_scope_texture(Scene *scene,
int timeline_frame,
int display_channel,
gpu::Texture *texture)
{
PreviewCache *cache = ensure_preview_cache(scene);
if (cache == nullptr || texture == nullptr) {
return;
}
const int width = GPU_texture_width(texture);
const int height = GPU_texture_height(texture);
PreviewCacheItem *slot = find_slot(cache, timeline_frame, display_channel, width, height);
if (slot == nullptr) {
return;
}
slot->timeline_frame = timeline_frame;
slot->display_channel = display_channel;
slot->width = width;
slot->height = height;
slot->last_used = cache->tick_count;
GPU_TEXTURE_FREE_SAFE(slot->scope_texture);
slot->scope_texture = texture;
}
void preview_cache_invalidate(Scene *scene)
{
PreviewCache *cache = query_preview_cache(scene);
if (cache != nullptr) {
cache->clear();
}
}
void preview_cache_destroy(Scene *scene)
{
PreviewCache *cache = query_preview_cache(scene);
if (cache != nullptr) {
MEM_SAFE_DELETE(scene->ed->runtime->preview_cache);
}
}
} // namespace blender::seq

View File

@@ -0,0 +1,370 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_map.hh"
#include "BLI_mutex.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BKE_scene.hh"
#include "IMB_imbuf.hh"
#include "SEQ_relations.hh"
#include "SEQ_render.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_time.hh"
#include "prefetch.hh"
#include "source_image_cache.hh"
namespace blender::seq {
static Mutex source_image_cache_mutex;
struct SourceImageCache {
struct FrameEntry {
SeqResult image;
/**
* Frame in timeline, relative to strip start. Used to determine which
* entries to evict (furthest from the play-head). Due to reversed
* frames, playback rate, retiming the relationship between source frame
* index and timeline frame is not a simple one.
*/
float strip_frame = 0;
};
struct Key {
float source_frame = 0.0f;
int view_id = 0;
eDrawType scene_draw_type = OB_SOLID;
uint64_t hash() const
{
return get_default_hash(source_frame, view_id, scene_draw_type);
}
friend bool operator==(const Key &a, const Key &b) = default;
};
struct StripEntry {
Map<Key, FrameEntry> frames;
};
Map<const Strip *, StripEntry> map_;
~SourceImageCache()
{
clear();
}
void clear()
{
for (const auto &item : map_.items()) {
for (const auto &frame : item.value.frames.values()) {
IMB_freeImBuf(frame.image.image);
}
}
map_.clear();
}
void remove_entry(const Strip *strip)
{
StripEntry *entry = map_.lookup_ptr(strip);
if (entry == nullptr) {
return;
}
for (const auto &frame : entry->frames.values()) {
IMB_freeImBuf(frame.image.image);
}
map_.remove_contained(strip);
}
};
static SourceImageCache *ensure_source_image_cache(Scene *scene)
{
SourceImageCache **cache = &scene->ed->runtime->source_image_cache;
if (*cache == nullptr) {
*cache = MEM_new<SourceImageCache>(__func__);
}
return *cache;
}
static SourceImageCache *query_source_image_cache(const Scene *scene)
{
if (scene == nullptr || scene->ed == nullptr) {
return nullptr;
}
return scene->ed->runtime->source_image_cache;
}
static float give_cache_frame_index(const Scene *scene, const Strip *strip, float timeline_frame)
{
float frame_index = give_frame_index(scene, strip, timeline_frame);
if (strip->type != STRIP_TYPE_SCENE) {
/* Scene strips that are slowed down need fractional frame index for animation interpolation;
* for others use integer index for better cache hit rates. */
frame_index = std::trunc(frame_index);
}
if (strip->type == STRIP_TYPE_MOVIE) {
frame_index += strip->anim_startofs;
}
return frame_index;
}
static SourceImageCache::Key get_key(const RenderData *context,
const Scene *scene,
const Strip *strip,
float timeline_frame)
{
const float frame_index = give_cache_frame_index(scene, strip, timeline_frame);
eDrawType draw_type = OB_RENDER;
if (!context->render && strip->type == STRIP_TYPE_SCENE) {
draw_type = eDrawType(scene->r.seq_prev_type);
}
return {frame_index, context->view_id, draw_type};
}
SeqResult source_image_cache_get(const RenderData *context,
const Strip *strip,
float timeline_frame)
{
if (context->skip_cache || strip == nullptr) {
return {};
}
Scene *scene = prefetch_get_original_scene_and_strip(context, strip);
timeline_frame = math::round(timeline_frame);
const SourceImageCache::Key key = get_key(context, scene, strip, timeline_frame);
SeqResult res;
{
std::lock_guard lock(source_image_cache_mutex);
SourceImageCache *cache = query_source_image_cache(scene);
if (cache == nullptr) {
return res;
}
SourceImageCache::StripEntry *val = cache->map_.lookup_ptr(strip);
if (val == nullptr) {
/* Nothing in cache for this strip yet. */
return res;
}
/* Search entries for the frame we want. */
SourceImageCache::FrameEntry *frame = val->frames.lookup_ptr(key);
if (frame != nullptr) {
res = frame->image;
}
/* For effect, meta, and scene strips, check if the cached result matches our current
* render resolution. If it does not, remove stale source entries for this strip. */
if (res.is_valid() &&
(strip->is_effect() || strip->type == STRIP_TYPE_SCENE || strip->type == STRIP_TYPE_META))
{
if (res.image->x != context->rectx || res.image->y != context->recty) {
cache->remove_entry(strip);
return {};
}
}
}
if (res.is_valid()) {
IMB_refImBuf(res.image);
}
return res;
}
void source_image_cache_put(const RenderData *context,
const Strip *strip,
float timeline_frame,
const SeqResult &image)
{
if (context->skip_cache || strip == nullptr || !image.is_valid()) {
return;
}
Scene *scene = prefetch_get_original_scene_and_strip(context, strip);
timeline_frame = math::round(timeline_frame);
const SourceImageCache::Key key = get_key(context, scene, strip, timeline_frame);
IMB_refImBuf(image.image);
std::lock_guard lock(source_image_cache_mutex);
SourceImageCache *cache = ensure_source_image_cache(scene);
SourceImageCache::StripEntry *val = cache->map_.lookup_ptr(strip);
if (val == nullptr) {
/* Nothing in cache for this strip yet. */
cache->map_.add_new(strip, {});
val = cache->map_.lookup_ptr(strip);
}
BLI_assert_msg(val != nullptr, "Source image cache value should never be null here");
SourceImageCache::FrameEntry &frame = val->frames.lookup_or_add_default(key);
if (frame.image.is_valid()) {
IMB_freeImBuf(frame.image.image);
}
frame.strip_frame = timeline_frame - strip->start;
frame.image = image;
}
void source_image_cache_invalidate_strip(Scene *scene, const Strip *strip)
{
std::lock_guard lock(source_image_cache_mutex);
SourceImageCache *cache = query_source_image_cache(scene);
if (cache != nullptr) {
cache->remove_entry(strip);
}
}
void source_image_cache_clear(Scene *scene)
{
std::lock_guard lock(source_image_cache_mutex);
SourceImageCache *cache = query_source_image_cache(scene);
if (cache != nullptr) {
scene->ed->runtime->source_image_cache->clear();
}
}
void source_image_cache_destroy(Scene *scene)
{
std::lock_guard lock(source_image_cache_mutex);
SourceImageCache *cache = query_source_image_cache(scene);
if (cache != nullptr) {
BLI_assert(cache == scene->ed->runtime->source_image_cache);
MEM_delete(scene->ed->runtime->source_image_cache);
scene->ed->runtime->source_image_cache = nullptr;
}
}
void source_image_cache_iterate(Scene *scene,
void *userdata,
void callback_iter(void *userdata,
const Strip *strip,
int timeline_frame))
{
std::lock_guard lock(source_image_cache_mutex);
SourceImageCache *cache = query_source_image_cache(scene);
if (cache == nullptr) {
return;
}
for (const auto &[strip, frames] : cache->map_.items()) {
for (const auto &[frame_key, frame] : frames.frames.items()) {
const float timeline_frame = strip->start + frame.strip_frame;
callback_iter(userdata, strip, int(timeline_frame));
}
}
}
size_t source_image_cache_calc_memory_size(const Scene *scene)
{
std::lock_guard lock(source_image_cache_mutex);
SourceImageCache *cache = query_source_image_cache(scene);
if (cache == nullptr) {
return 0;
}
size_t size = 0;
for (const SourceImageCache::StripEntry &entry : cache->map_.values()) {
for (const SourceImageCache::FrameEntry &frame : entry.frames.values()) {
if (frame.image.is_valid()) {
size += IMB_get_size_in_memory(frame.image.image);
}
}
}
return size;
}
size_t source_image_cache_get_image_count(const Scene *scene)
{
std::lock_guard lock(source_image_cache_mutex);
SourceImageCache *cache = query_source_image_cache(scene);
if (cache == nullptr) {
return 0;
}
size_t count = 0;
for (const SourceImageCache::StripEntry &entry : cache->map_.values()) {
count += entry.frames.size();
}
return count;
}
bool source_image_cache_evict(Scene *scene)
{
std::lock_guard lock(source_image_cache_mutex);
SourceImageCache *cache = query_source_image_cache(scene);
if (cache == nullptr) {
return false;
}
/* Find which entry to remove -- we pick the one that is furthest from the current frame,
* biasing the ones that are behind the current frame.
*
* However, do not try to evict entries from the current prefetch job range -- we need to
* be able to fully fill the cache from prefetching, and then actually stop the job when it
* is full and no longer can evict anything. */
int cur_prefetch_start = std::numeric_limits<int>::min();
int cur_prefetch_end = std::numeric_limits<int>::min();
if (scene->ed->cache_flag & SEQ_CACHE_STORE_RAW) {
/* Only activate the prefetch guards if the cache is active. */
seq_prefetch_get_time_range(scene, &cur_prefetch_start, &cur_prefetch_end);
}
const bool prefetch_loops_around = cur_prefetch_start > cur_prefetch_end;
const int timeline_start = scene->playback_start();
const int timeline_end = scene->playback_end();
/* If we wrap around, treat the timeline start as the playback head position.
* This is to try to mitigate un-needed cache evictions. */
const int cur_frame = prefetch_loops_around ? timeline_start : scene->r.cfra;
SourceImageCache::StripEntry *best_strip = nullptr;
SourceImageCache::Key best_key;
int best_score = 0;
for (const auto &strip : cache->map_.items()) {
for (const auto &entry : strip.value.frames.items()) {
const int item_frame = int(strip.key->start + entry.value.strip_frame);
if (prefetch_loops_around) {
if (item_frame >= timeline_start && item_frame <= cur_prefetch_end) {
continue; /* Within active prefetch range, do not try to remove it. */
}
if (item_frame >= cur_prefetch_start && item_frame <= timeline_end) {
continue; /* Within active prefetch range, do not try to remove it. */
}
}
else if (item_frame >= cur_prefetch_start && item_frame <= cur_prefetch_end) {
continue; /* Within active prefetch range, do not try to remove it. */
}
/* Score for removal is distance to current frame; 2x that if behind current frame. */
int score = 0;
if (item_frame < cur_frame) {
score = (cur_frame - item_frame) * 2;
}
else if (item_frame > cur_frame) {
score = item_frame - cur_frame;
}
if (score > best_score) {
best_strip = &strip.value;
best_key = entry.key;
best_score = score;
}
}
}
/* Remove if we found one. */
if (best_strip != nullptr) {
IMB_freeImBuf(best_strip->frames.lookup(best_key).image.image);
best_strip->frames.remove(best_key);
return true;
}
return false;
}
} // namespace blender::seq

View File

@@ -0,0 +1,51 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*
* Cache source images for strips.
* - Keyed by (strip + frame index within strip media + view ID + scene strip draw type).
* - Caching is only done for strips that are independent of
* any other strips (images, movies, no-input effect strips like
* Text and Color).
* - When full, cache eviction policy is to remove frames furthest
* from the current-frame, biasing towards removal of
* frames behind the current-frame.
* - Invalidated fairly rarely, since the cached items only change
* when the source content changes.
*/
#pragma once
#include "render.hh"
namespace blender {
struct Strip;
struct Scene;
struct RenderData;
namespace seq {
void source_image_cache_put(const RenderData *context,
const Strip *strip,
float timeline_frame,
const SeqResult &image);
SeqResult source_image_cache_get(const RenderData *context,
const Strip *strip,
float timeline_frame);
void source_image_cache_invalidate_strip(Scene *scene, const Strip *strip);
void source_image_cache_clear(Scene *scene);
void source_image_cache_destroy(Scene *scene);
bool source_image_cache_evict(Scene *scene);
size_t source_image_cache_get_image_count(const Scene *scene);
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,646 @@
/* 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_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_sequencer.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;
default:
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_data() != nullptr && ibuf->byte_data() != nullptr) {
IMB_free_byte_pixels(ibuf);
}
ensure_ibuf_is_sequencer_space(scene, ibuf, false);
return ibuf;
}
static void scale_to_thumbnail_size(ImBuf *ibuf)
{
if (ibuf == nullptr) {
return;
}
/* We only need byte thumbnails. */
if (ibuf->float_data()) {
if (ibuf->byte_data() == nullptr) {
IMB_byte_from_float(ibuf);
}
IMB_free_float_pixels(ibuf);
}
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_sequencer_scene(C);
wmJob *wm_job = WM_jobs_get(wm,
win,
scene,
"Generating 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::ranges::sort(requests,
[](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;
});
/* Note: we could process thumbnail cache requests somewhat in parallel,
* but let's not do that so that UI responsiveness is not affected much.
* Some of video/image loading code parts are multi-threaded internally already,
* and that does provide some parallelism. */
{
/* 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;
IMB_Proxy_Size cur_proxy_size = IMB_PROXY_NONE;
for (const ThumbnailCache::Request &request : requests) {
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(), ImBufFlags::Zero, cur_stream, true, nullptr);
cur_proxy_size = IMB_PROXY_NONE;
if (cur_anim != nullptr) {
/* Find the lowest proxy resolution available.
* `x & -x` leaves only the lowest bit set. */
int proxies_mask = MOV_get_existing_proxies(cur_anim);
cur_proxy_size = IMB_Proxy_Size(proxies_mask & -proxies_mask);
}
}
/* Decode the movie frame. */
if (cur_anim != nullptr) {
thumb = MOV_decode_frame(cur_anim, request.frame_index, cur_proxy_size);
if (thumb == nullptr && cur_proxy_size != IMB_PROXY_NONE) {
/* Broken proxy file, switch to non-proxy. */
cur_proxy_size = IMB_PROXY_NONE;
thumb = MOV_decode_frame(cur_anim, request.frame_index, cur_proxy_size);
}
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,
strip->type,
cur_time,
timeline_frame,
strip->channel,
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 = ID_BLEND_PATH_FROM_GLOBAL(&scene->id);
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);
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