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,203 @@
/* SPDX-FileCopyrightText: 2022 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <cstring>
#include "DNA_anim_types.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BKE_fcurve.hh"
#include "BLI_listbase.h"
#include "DEG_depsgraph.hh"
#include "SEQ_animation.hh"
namespace blender::seq {
bool animation_keyframes_exist(const Scene *scene)
{
return scene->adt != nullptr && scene->adt->action != nullptr &&
scene->adt->action->wrap().has_keyframes(scene->adt->slot_handle);
}
bool animation_drivers_exist(Scene *scene)
{
return scene->adt != nullptr && !scene->adt->drivers.is_empty();
}
bool fcurve_matches(const Strip &strip, const FCurve &fcurve)
{
return animrig::fcurve_matches_collection_path(
fcurve, "sequence_editor.strips_all[", strip.name + 2);
}
void offset_animdata(const Scene *scene, Strip *strip, float ofs)
{
if (!animation_keyframes_exist(scene) || ofs == 0.0f) {
return;
}
Vector<FCurve *> fcurves = animrig::fcurves_in_action_slot_filtered(
scene->adt->action, scene->adt->slot_handle, [&](const FCurve &fcurve) {
return fcurve_matches(*strip, fcurve);
});
for (FCurve *fcu : fcurves) {
uint i;
if (fcu->bezt) {
for (i = 0; i < fcu->totvert; i++) {
BezTriple *bezt = &fcu->bezt[i];
bezt->vec[0][0] += ofs;
bezt->vec[1][0] += ofs;
bezt->vec[2][0] += ofs;
}
}
if (fcu->fpt) {
for (i = 0; i < fcu->totvert; i++) {
FPoint *fpt = &fcu->fpt[i];
fpt->vec[0] += ofs;
}
}
}
DEG_id_tag_update(&scene->adt->action->id, ID_RECALC_ANIMATION);
}
void free_animdata(Scene *scene, Strip *strip)
{
if (!animation_keyframes_exist(scene)) {
return;
}
Vector<FCurve *> fcurves = animrig::fcurves_in_action_slot_filtered(
scene->adt->action, scene->adt->slot_handle, [&](const FCurve &fcurve) {
return fcurve_matches(*strip, fcurve);
});
animrig::Action &action = scene->adt->action->wrap();
for (FCurve *fcu : fcurves) {
action_fcurve_remove(action, *fcu);
}
}
void animation_backup_original(Scene *scene, AnimationBackup *backup)
{
if (animation_keyframes_exist(scene)) {
animrig::Action &action = scene->adt->action->wrap();
assert_baklava_phase_1_invariants(action);
if (animrig::Channelbag *channelbag = animrig::channelbag_for_action_slot(
action, scene->adt->slot_handle))
{
animrig::channelbag_fcurves_move(backup->channelbag, *channelbag);
}
}
if (animation_drivers_exist(scene)) {
BLI_movelisttolist(&backup->drivers, &scene->adt->drivers);
}
}
void animation_restore_original(Scene *scene, AnimationBackup *backup)
{
if (!backup->channelbag.fcurves().is_empty()) {
BLI_assert(scene->adt != nullptr && scene->adt->action != nullptr);
animrig::Action &action = scene->adt->action->wrap();
assert_baklava_phase_1_invariants(action);
animrig::Channelbag *channelbag = animrig::channelbag_for_action_slot(action,
scene->adt->slot_handle);
/* The channel bag should exist if we got here, because otherwise the
* backup channel bag would have been empty. */
BLI_assert(channelbag != nullptr);
animrig::channelbag_fcurves_move(*channelbag, backup->channelbag);
}
if (!backup->drivers.is_empty()) {
BLI_assert(scene->adt != nullptr);
BLI_movelisttolist(&scene->adt->drivers, &backup->drivers);
}
}
/**
* Duplicate the animation in `src` that matches items in `strip` into `dst`.
*/
static void strip_animation_duplicate(Strip *strip,
animrig::Action &dst,
const animrig::slot_handle_t dst_slot_handle,
AnimationBackup *src)
{
if (strip->type == STRIP_TYPE_META) {
for (Strip &meta_child : strip->seqbase) {
strip_animation_duplicate(&meta_child, dst, dst_slot_handle, src);
}
}
Vector<FCurve *> fcurves = animrig::fcurves_in_span_filtered(
src->channelbag.fcurves(),
[&](const FCurve &fcurve) { return fcurve_matches(*strip, fcurve); });
for (const FCurve *fcu : fcurves) {
FCurve *fcu_copy = BKE_fcurve_copy(fcu);
/* Handling groups properly requires more work, so we ignore them for now.
*
* Note that when legacy actions are deprecated, then we can handle channel
* groups way more easily because we know they're stored in the
* already-duplicated channelbag in `src`, and we therefore don't have to
* worry that they might have already been freed. */
fcu_copy->grp = nullptr;
animrig::action_fcurve_attach(dst, dst_slot_handle, *fcu_copy, std::nullopt);
}
}
/**
* Duplicate the drivers in `src` that matches items in `strip` into `dst`.
*/
static void strip_drivers_duplicate(Strip *strip, AnimData *dst, AnimationBackup *src)
{
if (strip->type == STRIP_TYPE_META) {
for (Strip &meta_child : strip->seqbase) {
strip_drivers_duplicate(&meta_child, dst, src);
}
}
Vector<FCurve *> fcurves = animrig::fcurves_in_listbase_filtered(
src->drivers, [&](const FCurve &fcurve) { return fcurve_matches(*strip, fcurve); });
for (const FCurve *fcu : fcurves) {
FCurve *fcu_cpy = BKE_fcurve_copy(fcu);
BLI_addtail(&dst->drivers, fcu_cpy);
}
}
void animation_duplicate_backup_to_scene(Scene *scene, Strip *strip, AnimationBackup *backup)
{
BLI_assert(scene != nullptr);
if (!backup->channelbag.fcurves().is_empty()) {
BLI_assert(scene->adt != nullptr);
BLI_assert(scene->adt->action != nullptr);
strip_animation_duplicate(strip, scene->adt->action->wrap(), scene->adt->slot_handle, backup);
}
if (!backup->drivers.is_empty()) {
BLI_assert(scene->adt != nullptr);
strip_drivers_duplicate(strip, scene->adt, backup);
}
}
} // namespace blender::seq

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

View File

@@ -0,0 +1,75 @@
/* SPDX-FileCopyrightText: 2022 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "MEM_guardedalloc.h"
#include "DNA_listBase.h"
#include "DNA_sequence_types.h"
#include "BLI_listbase.h"
#include "BLI_string_utf8.h"
#include "BLT_translation.hh"
#include "sequencer.hh"
#include "SEQ_channels.hh"
#include "SEQ_sequencer.hh"
namespace blender::seq {
ListBaseT<SeqTimelineChannel> *channels_displayed_get(const Editing *ed)
{
return ed ? ed->current_channels() : nullptr;
}
void channels_ensure(ListBaseT<SeqTimelineChannel> *channels)
{
/* Allocate channels. Channel 0 is never used, but allocated to prevent off by 1 issues. */
for (int i = 0; i < MAX_CHANNELS + 1; i++) {
SeqTimelineChannel *channel = MEM_new<SeqTimelineChannel>("seq timeline channel");
SNPRINTF_UTF8(channel->name, DATA_("Channel %d"), i);
channel->index = i;
BLI_addtail(channels, channel);
}
}
void channels_duplicate(ListBaseT<SeqTimelineChannel> *channels_dst,
ListBaseT<SeqTimelineChannel> *channels_src)
{
for (SeqTimelineChannel &channel : *channels_src) {
SeqTimelineChannel *channel_duplicate = static_cast<SeqTimelineChannel *>(
MEM_dupalloc(&channel));
BLI_addtail(channels_dst, channel_duplicate);
}
}
void channels_free(ListBaseT<SeqTimelineChannel> *channels)
{
for (SeqTimelineChannel &channel : channels->items_mutable()) {
MEM_delete(&channel);
}
}
SeqTimelineChannel *channel_get_by_index(const ListBaseT<SeqTimelineChannel> *channels,
const int channel_index)
{
return static_cast<SeqTimelineChannel *>(BLI_findlink(channels, channel_index));
}
ListBaseT<SeqTimelineChannel> *get_channels_by_strip(Editing *ed, const Strip *strip)
{
Strip *strip_owner = lookup_meta_by_strip(ed, strip);
if (strip_owner != nullptr) {
return &strip_owner->channels;
}
return &ed->channels;
}
} // namespace blender::seq

View File

@@ -0,0 +1,256 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BKE_node_runtime.hh"
#include "PRF_profile.hh"
#include "COM_algorithm_parallel_reduction.hh"
#include "COM_ocio_color_space_conversion_shader.hh"
#include "COM_realize_on_domain_operation.hh"
#include "COM_utilities.hh"
#include "GPU_state.hh"
#include "GPU_texture_pool.hh"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "compositor.hh"
namespace blender::seq {
compositor::ResultPrecision CompositorContext::get_precision() const
{
switch (this->render_data_.scene->r.compositor_precision) {
case SCE_COMPOSITOR_PRECISION_AUTO:
/* Auto uses full precision for final renders and half precision otherwise. */
return this->render_data_.render ? compositor::ResultPrecision::Full :
compositor::ResultPrecision::Half;
case SCE_COMPOSITOR_PRECISION_FULL:
return compositor::ResultPrecision::Full;
}
BLI_assert_unreachable();
return compositor::ResultPrecision::Half;
}
void CompositorContext::create_result_from_input(compositor::Result &result, ImBuf &input)
{
PRF_scope_with_name("SeqCreateCompInput", ProfileCategory::Draw);
const bool gpu = this->use_gpu();
const int2 size = int2(input.x, input.y);
if (!gpu) {
/* CPU path: ensure input is linear float. */
ensure_ibuf_is_linear_space(&input, true);
BLI_assert(input.float_data());
result.share_data(input.float_data(), size);
return;
}
/* GPU path: do necessary color space conversions (if any) to linear space on the GPU. */
const bool input_is_byte = input.float_data() == nullptr;
const char *input_colorspace = input_is_byte ? IMB_colormanagement_get_byte_colorspace(&input) :
IMB_colormanagement_get_float_colorspace(&input);
const char *linear_colorspace = IMB_colormanagement_role_colorspace_name_get(
COLOR_ROLE_SCENE_LINEAR);
bool use_fallback = true;
if (input_is_byte || !STREQ(input_colorspace, linear_colorspace)) {
/* Need to convert data format or colorspace: upload input into temporary texture,
* convert into compositor result. */
/* Get the conversion shader. */
compositor::OCIOColorSpaceConversionShader &ocio_shader =
this->cache_manager().ocio_color_space_conversion_shaders.get(
*this, input_colorspace, linear_colorspace);
gpu::Shader *shader = ocio_shader.bind_shader_and_resources();
if (shader) {
/* Upload input image into a GPU texture. */
gpu::TexturePool &pool = gpu::TexturePool::get();
gpu::Texture *input_tex = pool.acquire_texture_2d(size,
1,
input_is_byte ?
gpu::TextureFormat::UNORM_8_8_8_8 :
gpu::TextureFormat::SFLOAT_32_32_32_32,
GPU_TEXTURE_USAGE_SHADER_READ,
"seq_comp_input");
if (input_tex) {
if (input_is_byte) {
GPU_texture_update(input_tex, GPU_DATA_UBYTE, input.byte_data());
}
else {
GPU_texture_update(input_tex, GPU_DATA_FLOAT, input.float_data());
}
/* Allocate compositor result texture. We use global compositor precision even
* for byte inputs. In theory Half precision should be enough, but that leads to potential
* small differences between CPU & GPU paths. */
result.set_precision(get_precision());
result.allocate_texture(size);
/* Convert input texture into the compositor result texture. */
GPU_texture_bind(input_tex,
GPU_shader_get_sampler_binding(shader, ocio_shader.input_sampler_name()));
result.bind_as_image(shader, ocio_shader.output_image_name());
GPU_shader_uniform_1b(shader, "premultiply_output", input_is_byte);
compositor::compute_dispatch_threads_at_least(shader, size);
GPU_texture_unbind(input_tex);
result.unbind_as_image();
pool.release_texture(input_tex);
use_fallback = false;
}
ocio_shader.unbind_shader_and_resources();
}
}
/* Colorspace conversion was not needed or failed: upload input float data into
* compositor result. */
if (use_fallback) {
/* This is a no-op if input is already linear float; otherwise this step might be needed
* if conversion above has failed. */
ensure_ibuf_is_linear_space(&input, true);
result.allocate_texture(size);
GPU_texture_update(result, GPU_DATA_FLOAT, input.float_data());
}
}
void CompositorContext::write_viewer_impl(const compositor::Result &result, ImBuf &image)
{
using namespace compositor;
/* Realize the transforms if needed. */
const InputDescriptor input_descriptor = {ResultType::Color,
InputRealizationMode::OperationDomain};
SimpleOperation *realization_operation = RealizeOnDomainOperation::construct_if_needed(
*this, result, input_descriptor, result.domain());
if (realization_operation) {
Result realize_input = this->create_result(ResultType::Color, result.precision());
realize_input.share_data(result);
realization_operation->map_input_to_result(&realize_input);
realization_operation->evaluate();
Result &realized_viewer_result = realization_operation->get_result();
this->write_output(realized_viewer_result, image);
realized_viewer_result.release();
viewer_was_written_ = true;
delete realization_operation;
return;
}
this->write_output(result, image);
viewer_was_written_ = true;
}
void CompositorContext::write_output(const compositor::Result &result, ImBuf &image)
{
/* Do not write the output if the viewer output was already written. */
if (viewer_was_written_) {
return;
}
PRF_scope_with_name("SeqCompWriteOutput", ProfileCategory::Draw);
if (result.is_single_value()) {
compositor::Color color = result.get_single_value<compositor::Color>();
IMB_rectfill(&image, color);
image.color_mode = color.a < 1.0f ? ImColorMode::RGBA : ImColorMode::RGB;
return;
}
result_translation_ = result.domain().transformation.location();
const int output_size_x = result.domain().data_size.x;
const int output_size_y = result.domain().data_size.y;
if (output_size_x != image.x || output_size_y != image.y || !image.float_buffer.data) {
/* Output size is different (e.g. image is blurred with expanded bounds);
* need to allocate appropriately sized buffer. */
IMB_free_all_data(&image);
image.x = output_size_x;
image.y = output_size_y;
}
compositor::Color min_color = compositor::minimum_color(*this, result);
image.color_mode = min_color.a < 1.0f ? ImColorMode::RGBA : ImColorMode::RGB;
if (this->use_gpu()) {
PRF_scope_with_name("SeqCompositorGPUReadback", ProfileCategory::Draw);
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
IMB_alloc_float_pixels(&image, 4, false);
GPU_texture_read(result.gpu_texture(), GPU_DATA_FLOAT, 0, image.float_data_for_write());
}
else if (result.sharing_info()) {
image.channels = 4;
image.float_buffer = ImBufFloatBuffer{
.data = static_cast<const float *>(result.cpu_data().data()),
.sharing_info = result.sharing_info(),
.colorspace = nullptr};
}
else if (result.cpu_data().data() != image.float_data()) {
IMB_alloc_float_pixels(&image, 4, false);
std::memcpy(image.float_data_for_write(),
result.cpu_data().data(),
IMB_get_pixel_count(&image) * sizeof(float) * 4);
}
const char *to_colorspace = IMB_colormanagement_role_colorspace_name_get(
COLOR_ROLE_SCENE_LINEAR);
IMB_colormanagement_assign_float_colorspace(&image, to_colorspace);
}
void CompositorContext::write_outputs(const bNodeTree &node_group,
compositor::NodeGroupOperation &node_group_operation,
ImBuf &output_image)
{
using namespace compositor;
for (const bNodeTreeInterfaceSocket *output_socket : node_group.interface_outputs()) {
Result &output_result = node_group_operation.get_result(output_socket->identifier);
if (!output_result.should_compute()) {
continue;
}
/* Realize the output transforms if needed. */
const InputDescriptor input_descriptor = {ResultType::Color,
InputRealizationMode::OperationDomain};
SimpleOperation *realization_operation = RealizeOnDomainOperation::construct_if_needed(
*this, output_result, input_descriptor, output_result.domain());
if (realization_operation) {
realization_operation->map_input_to_result(&output_result);
realization_operation->evaluate();
Result &realized_output_result = realization_operation->get_result();
this->write_output(realized_output_result, output_image);
realized_output_result.release();
delete realization_operation;
continue;
}
this->write_output(output_result, output_image);
output_result.release();
}
}
void CompositorContext::set_output_refcount(const bNodeTree &node_group,
compositor::NodeGroupOperation &node_group_operation)
{
using namespace compositor;
/* Set the reference count for the outputs, only the first color output is actually needed,
* while the rest are ignored. */
node_group.ensure_interface_cache();
for (const bNodeTreeInterfaceSocket *output_socket : node_group.interface_outputs()) {
const bool is_first_output = output_socket == node_group.interface_outputs().first();
Result &output_result = node_group_operation.get_result(output_socket->identifier);
const bool is_color = output_result.type() == ResultType::Color;
output_result.set_reference_count(is_first_output && is_color ? 1 : 0);
}
}
} // namespace blender::seq

View File

@@ -0,0 +1,91 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#pragma once
#include "COM_context.hh"
#include "COM_node_group_operation.hh"
#include "SEQ_render.hh"
namespace blender::seq {
class CompositorContext : public compositor::Context {
protected:
const RenderData &render_data_;
const Strip *strip_ = nullptr;
float2 result_translation_ = float2(0, 0);
/* Identifies if the output of the viewer was written. */
bool viewer_was_written_ = false;
/* True if GPU compute is supported and can be used, if false, we fallback to CPU. */
bool gpu_supported_ = true;
public:
CompositorContext(compositor::StaticCacheManager &cache_manager,
const RenderData &render_data,
const Strip &strip)
: compositor::Context(cache_manager), render_data_(render_data), strip_(&strip)
{
}
const Main &get_main() const override
{
return *render_data_.bmain;
}
const Scene &get_scene() const override
{
return *render_data_.scene;
}
bool treat_viewer_as_group_output() const override
{
return true;
}
const Strip *get_strip() const override
{
return strip_;
}
void set_gpu_supported(const bool supported)
{
gpu_supported_ = supported;
}
bool use_gpu() const override
{
return gpu_supported_ &&
this->render_data_.scene->r.compositor_device == SCE_COMPOSITOR_DEVICE_GPU;
}
compositor::ResultPrecision get_precision() const override;
float2 get_result_translation() const
{
return result_translation_;
}
protected:
compositor::NodeGroupOutputTypes needed_outputs() const
{
compositor::NodeGroupOutputTypes needed_outputs =
compositor::NodeGroupOutputTypes::GroupOutputNode;
if (!render_data_.render) {
needed_outputs |= compositor::NodeGroupOutputTypes::ViewerNode;
}
return needed_outputs;
}
void create_result_from_input(compositor::Result &result, ImBuf &input);
void write_viewer_impl(const compositor::Result &result, ImBuf &image);
void write_output(const compositor::Result &result, ImBuf &image);
void write_outputs(const bNodeTree &node_group,
compositor::NodeGroupOperation &node_group_operation,
ImBuf &output_image);
void set_output_refcount(const bNodeTree &node_group,
compositor::NodeGroupOperation &node_group_operation);
};
} // namespace blender::seq

View File

@@ -0,0 +1,181 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
#include "PRF_profile.hh"
#include "SEQ_render.hh"
#include "effects.hh"
namespace blender::seq {
/* -------------------------------------------------------------------- */
/* Color Add Effect */
struct AddEffectOp {
template<typename T> void apply(const T *src1, const T *src2, T *dst, int64_t size) const
{
const float fac = this->factor;
int ifac = int(256.0f * fac);
for (int64_t idx = 0; idx < size; idx++) {
if constexpr (std::is_same_v<T, uchar>) {
const int f = ifac * int(src2[3]);
dst[0] = min_ii(src1[0] + ((f * src2[0]) >> 16), 255);
dst[1] = min_ii(src1[1] + ((f * src2[1]) >> 16), 255);
dst[2] = min_ii(src1[2] + ((f * src2[2]) >> 16), 255);
}
else {
const float f = (1.0f - (src1[3] * (1.0f - fac))) * src2[3];
dst[0] = src1[0] + f * src2[0];
dst[1] = src1[1] + f * src2[1];
dst[2] = src1[2] + f * src2[2];
}
dst[3] = src1[3];
src1 += 4;
src2 += 4;
dst += 4;
}
}
float factor;
};
static SeqResult do_add_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip * /*strip*/,
float /*timeline_frame*/,
float fac,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxAdd", ProfileCategory::Draw);
SeqResult dst = prepare_effect_imbufs(context, src1, src2);
AddEffectOp op;
op.factor = fac;
apply_effect_op(op, src1.image, src2.image, dst.image);
/* Destination uses alpha from src1 */
dst.is_opaque_before_transform = !src1.image->can_contain_alpha();
return dst;
}
/* -------------------------------------------------------------------- */
/* Color Subtract Effect */
struct SubEffectOp {
template<typename T> void apply(const T *src1, const T *src2, T *dst, int64_t size) const
{
const float fac = this->factor;
int ifac = int(256.0f * fac);
for (int64_t idx = 0; idx < size; idx++) {
if constexpr (std::is_same_v<T, uchar>) {
const int f = ifac * int(src2[3]);
dst[0] = max_ii(src1[0] - ((f * src2[0]) >> 16), 0);
dst[1] = max_ii(src1[1] - ((f * src2[1]) >> 16), 0);
dst[2] = max_ii(src1[2] - ((f * src2[2]) >> 16), 0);
}
else {
const float f = (1.0f - (src1[3] * (1.0f - fac))) * src2[3];
dst[0] = max_ff(src1[0] - f * src2[0], 0.0f);
dst[1] = max_ff(src1[1] - f * src2[1], 0.0f);
dst[2] = max_ff(src1[2] - f * src2[2], 0.0f);
}
dst[3] = src1[3];
src1 += 4;
src2 += 4;
dst += 4;
}
}
float factor;
};
static SeqResult do_sub_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip * /*strip*/,
float /*timeline_frame*/,
float fac,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxSub", ProfileCategory::Draw);
SeqResult dst = prepare_effect_imbufs(context, src1, src2);
SubEffectOp op;
op.factor = fac;
apply_effect_op(op, src1.image, src2.image, dst.image);
/* Destination uses alpha from src1 */
dst.is_opaque_before_transform = !src1.image->can_contain_alpha();
return dst;
}
/* -------------------------------------------------------------------- */
/* Multiply Effect */
struct MulEffectOp {
template<typename T> void apply(const T *src1, const T *src2, T *dst, int64_t size) const
{
const float fac = this->factor;
int ifac = int(256.0f * fac);
for (int64_t idx = 0; idx < size; idx++) {
/* Formula: `fac * (a * b) + (1-fac) * a => fac * a * (b - 1) + a` */
if constexpr (std::is_same_v<T, uchar>) {
dst[0] = src1[0] + ((ifac * src1[0] * (src2[0] - 255)) >> 16);
dst[1] = src1[1] + ((ifac * src1[1] * (src2[1] - 255)) >> 16);
dst[2] = src1[2] + ((ifac * src1[2] * (src2[2] - 255)) >> 16);
dst[3] = src1[3] + ((ifac * src1[3] * (src2[3] - 255)) >> 16);
}
else {
dst[0] = src1[0] + fac * src1[0] * (src2[0] - 1.0f);
dst[1] = src1[1] + fac * src1[1] * (src2[1] - 1.0f);
dst[2] = src1[2] + fac * src1[2] * (src2[2] - 1.0f);
dst[3] = src1[3] + fac * src1[3] * (src2[3] - 1.0f);
}
src1 += 4;
src2 += 4;
dst += 4;
}
}
float factor;
};
static SeqResult do_mul_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip * /*strip*/,
float /*timeline_frame*/,
float fac,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxMul", ProfileCategory::Draw);
SeqResult dst = prepare_effect_imbufs(context, src1, src2);
MulEffectOp op;
op.factor = fac;
apply_effect_op(op, src1.image, src2.image, dst.image);
return dst;
}
void add_effect_get_handle(EffectHandle &rval)
{
rval.execute = do_add_effect;
rval.early_out = early_out_mul_input2;
}
void sub_effect_get_handle(EffectHandle &rval)
{
rval.execute = do_sub_effect;
rval.early_out = early_out_mul_input2;
}
void mul_effect_get_handle(EffectHandle &rval)
{
rval.execute = do_mul_effect;
rval.early_out = early_out_mul_input2;
}
} // namespace blender::seq

View File

@@ -0,0 +1,92 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "PRF_profile.hh"
#include "DNA_sequence_types.h"
#include "SEQ_channels.hh"
#include "SEQ_render.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_utils.hh"
#include "effects.hh"
#include "render.hh"
namespace blender::seq {
static StripEarlyOut early_out_adjustment(const Strip * /*strip*/, float /*fac*/)
{
return StripEarlyOut::NoInput;
}
static SeqResult do_adjustment_impl(const RenderData *context,
SeqRenderState *state,
Strip *strip,
float timeline_frame)
{
SeqResult out;
Editing *ed = context->scene->ed;
ListBaseT<Strip> *seqbasep = get_seqbase_by_strip(context->scene, strip);
ListBaseT<SeqTimelineChannel> *channels = get_channels_by_strip(ed, strip);
/* Clamp timeline_frame to strip range so it behaves as if it had "still frame" offset (last
* frame is static after end of strip). This is how most strips behave. This way transition
* effects that doesn't overlap or speed effect can't fail rendering outside of strip range. */
timeline_frame = clamp_i(
timeline_frame, strip->left_handle(), strip->right_handle(context->scene) - 1);
if (strip->channel > 1) {
out = seq_render_give_ibuf_seqbase(
context, state, timeline_frame, strip->channel - 1, channels, seqbasep);
}
/* Found nothing? Then work our way up the meta-strip stack, as this adjustment strip might be
* inside a nested meta-strip and affect strips below that meta-strip.
*
* NOTE: we should NOT walk past the stack level that the user is currently tabbed into,
* otherwise the adjustment layer can leak content from outside the meta context. */
if (!out.is_valid()) {
Strip *meta = lookup_meta_by_strip(ed, strip);
if (meta && meta != ed->current_meta_strip) {
out = do_adjustment_impl(context, state, meta, timeline_frame);
}
}
return out;
}
static SeqResult do_adjustment(const RenderData *context,
SeqRenderState *state,
Strip *strip,
float timeline_frame,
float /*fac*/,
const SeqResult & /*ibuf1*/,
const SeqResult & /*ibuf2*/)
{
PRF_scope_with_name("SeqFxAdjustment", ProfileCategory::Draw);
Editing *ed = context->scene->ed;
if (!ed || state->strips_in_progress.contains(strip)) {
return {};
}
state->strips_in_progress.add(strip);
SeqResult out = do_adjustment_impl(context, state, strip, timeline_frame);
state->strips_in_progress.remove(strip);
return out;
}
void adjustment_effect_get_handle(EffectHandle &rval)
{
rval.early_out = early_out_adjustment;
rval.execute = do_adjustment;
}
} // namespace blender::seq

View File

@@ -0,0 +1,415 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_math_color_blend.h"
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
#include "PRF_profile.hh"
#include "SEQ_render.hh"
#include "effects.hh"
namespace blender::seq {
/* -------------------------------------------------------------------- */
/* Alpha Over Effect */
static void init_alpha_over_or_under(Strip *strip)
{
Strip *input1 = strip->input1;
Strip *input2 = strip->input2;
strip->input2 = input1;
strip->input1 = input2;
}
static bool alpha_opaque(uchar alpha)
{
return alpha == 255;
}
static bool alpha_opaque(float alpha)
{
return alpha >= 1.0f;
}
/* dst = src1 over src2 (alpha from src1) */
struct AlphaOverEffectOp {
template<typename T> void apply(const T *src1, const T *src2, T *dst, int64_t size) const
{
const float fac = this->factor;
if (fac <= 0.0f) {
memcpy(dst, src2, sizeof(T) * 4 * size);
return;
}
for (int64_t idx = 0; idx < size; idx++) {
if (std::is_same_v<T, uchar> && src1[3] == 0) {
/* Optimization for fully transparent pixels: copy src2. Only do this for byte images;
* in floats alpha=0 can still have pure emissive color. */
memcpy(dst, src2, sizeof(T) * 4);
}
else if (fac == 1.0f && alpha_opaque(src1[3])) {
/* No change to `src1` as `fac == 1` and fully opaque. */
memcpy(dst, src1, sizeof(T) * 4);
}
else {
float4 col1 = load_premul_pixel(src1);
float mfac = 1.0f - fac * col1.w;
float4 col2 = load_premul_pixel(src2);
float4 col = fac * col1 + mfac * col2;
store_premul_pixel(col, dst);
}
src1 += 4;
src2 += 4;
dst += 4;
}
}
float factor;
};
static SeqResult do_alphaover_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip * /*strip*/,
float /*timeline_frame*/,
float fac,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxOver", ProfileCategory::Draw);
SeqResult dst = prepare_effect_imbufs(context, src1, src2);
AlphaOverEffectOp op;
op.factor = fac;
apply_effect_op(op, src1.image, src2.image, dst.image);
return dst;
}
/* -------------------------------------------------------------------- */
/* Alpha Under Effect */
/* dst = src1 under src2 (alpha from src2) */
struct AlphaUnderEffectOp {
template<typename T> void apply(const T *src1, const T *src2, T *dst, int64_t size) const
{
const float fac = this->factor;
if (fac <= 0.0f) {
memcpy(dst, src2, sizeof(T) * 4 * size);
return;
}
for (int64_t idx = 0; idx < size; idx++) {
if (src2[3] <= 0.0f && fac >= 1.0f) {
memcpy(dst, src1, sizeof(T) * 4);
}
else if (alpha_opaque(src2[3])) {
memcpy(dst, src2, sizeof(T) * 4);
}
else {
float4 col2 = load_premul_pixel(src2);
float mfac = fac * (1.0f - col2.w);
float4 col1 = load_premul_pixel(src1);
float4 col = mfac * col1 + col2;
store_premul_pixel(col, dst);
}
src1 += 4;
src2 += 4;
dst += 4;
}
}
float factor;
};
static SeqResult do_alphaunder_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip * /*strip*/,
float /*timeline_frame*/,
float fac,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxUnder", ProfileCategory::Draw);
SeqResult dst = prepare_effect_imbufs(context, src1, src2);
AlphaUnderEffectOp op;
op.factor = fac;
apply_effect_op(op, src1.image, src2.image, dst.image);
return dst;
}
/* -------------------------------------------------------------------- */
/* Blend Mode Effect */
/* blend_function has to be: void (T* dst, const T *src1, const T *src2) */
template<typename T, typename Func>
static void apply_blend_function(
float fac, int64_t size, const T *src1, const T *src2, T *dst, Func blend_function)
{
for (int64_t i = 0; i < size; i++) {
T achannel = src2[3];
(static_cast<T *>(const_cast<T *>(src2)))[3] = T(achannel * fac);
blend_function(dst, src1, src2);
(static_cast<T *>(const_cast<T *>(src2)))[3] = achannel;
dst[3] = src1[3];
src1 += 4;
src2 += 4;
dst += 4;
}
}
static void do_blend_effect_float(float fac,
int64_t size,
const float *rect1,
const float *rect2,
StripBlendMode btype,
float *out)
{
switch (btype) {
case STRIP_BLEND_ADD:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_add_float);
break;
case STRIP_BLEND_SUB:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_sub_float);
break;
case STRIP_BLEND_MUL:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_mul_float);
break;
case STRIP_BLEND_DARKEN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_darken_float);
break;
case STRIP_BLEND_COLOR_BURN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_burn_float);
break;
case STRIP_BLEND_LINEAR_BURN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_linearburn_float);
break;
case STRIP_BLEND_SCREEN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_screen_float);
break;
case STRIP_BLEND_LIGHTEN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_lighten_float);
break;
case STRIP_BLEND_DODGE:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_dodge_float);
break;
case STRIP_BLEND_OVERLAY:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_overlay_float);
break;
case STRIP_BLEND_SOFT_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_softlight_float);
break;
case STRIP_BLEND_HARD_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_hardlight_float);
break;
case STRIP_BLEND_PIN_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_pinlight_float);
break;
case STRIP_BLEND_LIN_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_linearlight_float);
break;
case STRIP_BLEND_VIVID_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_vividlight_float);
break;
case STRIP_BLEND_BLEND_COLOR:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_color_float);
break;
case STRIP_BLEND_HUE:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_hue_float);
break;
case STRIP_BLEND_SATURATION:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_saturation_float);
break;
case STRIP_BLEND_VALUE:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_luminosity_float);
break;
case STRIP_BLEND_DIFFERENCE:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_difference_float);
break;
case STRIP_BLEND_EXCLUSION:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_exclusion_float);
break;
default:
break;
}
}
static void do_blend_effect_byte(float fac,
int64_t size,
const uchar *rect1,
const uchar *rect2,
StripBlendMode btype,
uchar *out)
{
switch (btype) {
case STRIP_BLEND_ADD:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_add_byte);
break;
case STRIP_BLEND_SUB:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_sub_byte);
break;
case STRIP_BLEND_MUL:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_mul_byte);
break;
case STRIP_BLEND_DARKEN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_darken_byte);
break;
case STRIP_BLEND_COLOR_BURN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_burn_byte);
break;
case STRIP_BLEND_LINEAR_BURN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_linearburn_byte);
break;
case STRIP_BLEND_SCREEN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_screen_byte);
break;
case STRIP_BLEND_LIGHTEN:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_lighten_byte);
break;
case STRIP_BLEND_DODGE:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_dodge_byte);
break;
case STRIP_BLEND_OVERLAY:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_overlay_byte);
break;
case STRIP_BLEND_SOFT_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_softlight_byte);
break;
case STRIP_BLEND_HARD_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_hardlight_byte);
break;
case STRIP_BLEND_PIN_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_pinlight_byte);
break;
case STRIP_BLEND_LIN_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_linearlight_byte);
break;
case STRIP_BLEND_VIVID_LIGHT:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_vividlight_byte);
break;
case STRIP_BLEND_BLEND_COLOR:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_color_byte);
break;
case STRIP_BLEND_HUE:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_hue_byte);
break;
case STRIP_BLEND_SATURATION:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_saturation_byte);
break;
case STRIP_BLEND_VALUE:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_luminosity_byte);
break;
case STRIP_BLEND_DIFFERENCE:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_difference_byte);
break;
case STRIP_BLEND_EXCLUSION:
apply_blend_function(fac, size, rect1, rect2, out, blend_color_exclusion_byte);
break;
default:
break;
}
}
struct BlendModeEffectOp {
template<typename T> void apply(const T *src1, const T *src2, T *dst, int64_t size) const
{
if constexpr (std::is_same_v<T, float>) {
do_blend_effect_float(this->factor, size, src1, src2, this->blend_mode, dst);
}
else {
do_blend_effect_byte(this->factor, size, src1, src2, this->blend_mode, dst);
}
}
StripBlendMode blend_mode;
float factor;
};
static SeqResult do_blend_mode_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip *strip,
float /*timeline_frame*/,
float fac,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxBlend", ProfileCategory::Draw);
SeqResult dst = prepare_effect_imbufs(context, src1, src2);
BlendModeEffectOp op;
op.factor = fac;
op.blend_mode = strip->blend_mode;
apply_effect_op(op, src1.image, src2.image, dst.image);
return dst;
}
/* -------------------------------------------------------------------- */
/* Color Mix Effect */
static void init_colormix_effect(Strip *strip)
{
ColorMixVars *data = MEM_new<ColorMixVars>("colormixvars");
strip->effectdata = data;
data->blend_effect = STRIP_BLEND_OVERLAY;
data->factor = 1.0f;
}
static void free_colormix_effect(Strip *strip, const bool /*do_id_user*/)
{
if (strip->effectdata) {
MEM_delete(static_cast<ColorMixVars *>(strip->effectdata));
strip->effectdata = nullptr;
}
}
static SeqResult do_colormix_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip *strip,
float /*timeline_frame*/,
float /*fac*/,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxColorMix", ProfileCategory::Draw);
SeqResult dst = prepare_effect_imbufs(context, src1, src2);
const ColorMixVars *data = static_cast<const ColorMixVars *>(strip->effectdata);
BlendModeEffectOp op;
op.blend_mode = data->blend_effect;
op.factor = data->factor;
apply_effect_op(op, src1.image, src2.image, dst.image);
return dst;
}
void blend_mode_effect_get_handle(EffectHandle &rval)
{
rval.execute = do_blend_mode_effect;
rval.early_out = early_out_mul_input2;
}
void color_mix_effect_get_handle(EffectHandle &rval)
{
rval.init = init_colormix_effect;
rval.free = free_colormix_effect;
rval.execute = do_colormix_effect;
rval.early_out = early_out_mul_input2;
}
void alpha_over_effect_get_handle(EffectHandle &rval)
{
rval.init = init_alpha_over_or_under;
rval.execute = do_alphaover_effect;
rval.early_out = early_out_mul_input1;
}
void alpha_under_effect_get_handle(EffectHandle &rval)
{
rval.init = init_alpha_over_or_under;
rval.execute = do_alphaunder_effect;
}
} // namespace blender::seq

View File

@@ -0,0 +1,204 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BKE_node_runtime.hh"
#include "COM_domain.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_sequence_types.h"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "PRF_profile.hh"
#include "SEQ_sequencer.hh"
#include "cache/compositor_cache.hh"
#include "compositor.hh"
#include "effects.hh"
namespace blender::seq {
class CompositorEffectContext : public CompositorContext {
bNodeTree *node_group_;
ImBuf *input_1_;
ImBuf *input_2_;
ImBuf *output_;
float factor_;
public:
CompositorEffectContext(compositor::StaticCacheManager &cache_manager,
const RenderData &render_data,
bNodeTree *node_tree,
ImBuf *input_1,
ImBuf *input_2,
ImBuf *output,
float factor,
const Strip &strip)
: CompositorContext(cache_manager, render_data, strip),
node_group_(node_tree),
input_1_(input_1),
input_2_(input_2),
output_(output),
factor_(factor)
{
}
compositor::Domain get_compositing_domain() const override
{
return compositor::Domain(int2(this->output_->x, this->output_->y));
}
void write_viewer(compositor::Result &viewer_result) override
{
write_viewer_impl(viewer_result, *this->output_);
}
void evaluate()
{
using namespace compositor;
const bNodeTree &node_group = *DEG_get_evaluated<bNodeTree>(render_data_.depsgraph,
node_group_);
const bke::DataBlockComputeContext compute_context(nullptr, this->get_scene().id);
NodeGroupOperation node_group_operation(*this,
node_group,
this->needed_outputs(),
node_group.active_viewer_key,
bke::NODE_INSTANCE_KEY_BASE,
compute_context);
set_output_refcount(node_group, node_group_operation);
/* Map the inputs to the operation. */
Vector<std::unique_ptr<Result>> inputs;
int float_counter = 0;
int color_counter = 0;
for (const bNodeTreeInterfaceSocket *input_socket : node_group.interface_inputs()) {
const bke::bNodeSocketType *typeinfo = input_socket->socket_typeinfo();
Result *input_result = nullptr;
if (typeinfo && typeinfo->type == SOCK_FLOAT && float_counter == 0) {
/* First float input is factor. */
input_result = new Result(this->create_result(ResultType::Float, ResultPrecision::Full));
input_result->allocate_single_value();
input_result->set_single_value(this->factor_);
float_counter++;
}
else if (color_counter == 0 && this->input_1_) {
/* First input image. */
input_result = new Result(this->create_result(ResultType::Color, ResultPrecision::Full));
create_result_from_input(*input_result, *this->input_1_);
color_counter++;
}
else if (color_counter == 1 && this->input_2_) {
/* Second input image. */
input_result = new Result(this->create_result(ResultType::Color, ResultPrecision::Full));
create_result_from_input(*input_result, *this->input_2_);
color_counter++;
}
else {
/* Unsupported sockets. */
input_result = new Result(this->create_result(ResultType::Color, ResultPrecision::Full));
input_result->allocate_invalid();
}
node_group_operation.map_input_to_result(input_socket->identifier, input_result);
inputs.append(std::unique_ptr<Result>(input_result));
}
node_group_operation.evaluate();
this->write_outputs(node_group, node_group_operation, *this->output_);
}
};
static SeqResult do_compositor_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip *strip,
float /*timeline_frame*/,
float fac,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxCompositor", ProfileCategory::Draw);
const int x = context->rectx;
const int y = context->recty;
SeqResult out;
out.image = IMB_allocImBuf(x, y, ImBufFlags::FloatData | ImBufFlags::UninitializedPixels);
IMB_colormanagement_assign_float_colorspace(
out.image, IMB_colormanagement_role_colorspace_name_get(COLOR_ROLE_SCENE_LINEAR));
CompositorEffectVars *data = static_cast<CompositorEffectVars *>(strip->effectdata);
if (!data || !data->node_group) {
IMB_rectfill(out.image, float4(0, 0, 0, 1));
out.image->color_mode = ImColorMode::RGB;
out.is_opaque_before_transform = true;
}
else {
CompositorCache &com_cache = context->scene->ed->runtime->ensure_compositor_cache();
CompositorEffectContext com_context(com_cache.get_cache_manager(),
*context,
data->node_group,
src1.image,
src2.image,
out.image,
fac,
*strip);
if (com_context.use_gpu()) {
com_context.set_gpu_supported(render_begin_gpu(*context));
}
com_cache.recreate_if_needed(
com_context.use_gpu(), com_context.get_precision(), context->gpu_context);
com_context.evaluate();
com_context.cache_manager().reset();
if (com_context.use_gpu()) {
render_end_gpu(*context);
}
out.translation += com_context.get_result_translation();
out.is_opaque_before_transform = !out.image->can_contain_alpha();
}
return out;
}
static void init_compositor_effect(Strip *strip)
{
CompositorEffectVars *data = MEM_new<CompositorEffectVars>(__func__);
strip->effectdata = data;
}
static void free_compositor_effect(Strip *strip, const bool /*do_id_user*/)
{
if (strip->effectdata) {
CompositorEffectVars *data = static_cast<CompositorEffectVars *>(strip->effectdata);
MEM_delete(data);
strip->effectdata = nullptr;
}
}
static StripEarlyOut early_out_compositor(const Strip *strip, float /*fac*/)
{
/* No inputs: compositor generates the result. */
if (strip->input1 == nullptr) {
return StripEarlyOut::NoInput;
}
/* One or two inputs: do the effect. */
return StripEarlyOut::DoEffect;
}
void compositor_effect_get_handle(EffectHandle &rval)
{
rval.init = init_compositor_effect;
rval.free = free_compositor_effect;
rval.execute = do_compositor_effect;
rval.early_out = early_out_compositor;
}
} // namespace blender::seq

View File

@@ -0,0 +1,136 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
#include "PRF_profile.hh"
#include "SEQ_render.hh"
#include "effects.hh"
namespace blender::seq {
struct CrossEffectOp {
template<typename T> void apply(const T *src1, const T *src2, T *dst, int64_t size) const
{
const float fac = this->factor;
const float mfac = 1.0f - fac;
const int ifac = int(256.0f * fac);
const int imfac = 256 - ifac;
for (int64_t idx = 0; idx < size; idx++) {
if constexpr (std::is_same_v<T, uchar>) {
dst[0] = (imfac * src1[0] + ifac * src2[0]) >> 8;
dst[1] = (imfac * src1[1] + ifac * src2[1]) >> 8;
dst[2] = (imfac * src1[2] + ifac * src2[2]) >> 8;
dst[3] = (imfac * src1[3] + ifac * src2[3]) >> 8;
}
else {
dst[0] = mfac * src1[0] + fac * src2[0];
dst[1] = mfac * src1[1] + fac * src2[1];
dst[2] = mfac * src1[2] + fac * src2[2];
dst[3] = mfac * src1[3] + fac * src2[3];
}
src1 += 4;
src2 += 4;
dst += 4;
}
}
float factor;
};
static SeqResult do_cross_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip * /*strip*/,
float /*timeline_frame*/,
float fac,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxCross", ProfileCategory::Draw);
SeqResult dst = prepare_effect_imbufs(context, src1, src2);
CrossEffectOp op;
op.factor = fac;
apply_effect_op(op, src1.image, src2.image, dst.image);
dst.is_opaque_before_transform = !src1.image->can_contain_alpha() &&
!src2.image->can_contain_alpha();
return dst;
}
/* One could argue that gamma cross should not be hardcoded to 2.0 gamma,
* but instead either do proper input->linear conversion (often sRGB). Or
* maybe not even that, but do interpolation in some perceptual color space
* like OKLAB. But currently it is fixed to just 2.0 gamma. */
static float gammaCorrect(float c)
{
if (UNLIKELY(c < 0)) {
return -(c * c);
}
return c * c;
}
static float invGammaCorrect(float c)
{
return sqrtf_signed(c);
}
struct GammaCrossEffectOp {
template<typename T> void apply(const T *src1, const T *src2, T *dst, int64_t size) const
{
const float fac = this->factor;
const float mfac = 1.0f - fac;
for (int64_t idx = 0; idx < size; idx++) {
float4 col1 = load_premul_pixel(src1);
float4 col2 = load_premul_pixel(src2);
float4 col;
for (int c = 0; c < 4; ++c) {
col[c] = gammaCorrect(mfac * invGammaCorrect(col1[c]) + fac * invGammaCorrect(col2[c]));
}
store_premul_pixel(col, dst);
src1 += 4;
src2 += 4;
dst += 4;
}
}
float factor;
};
static SeqResult do_gammacross_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip * /*strip*/,
float /*timeline_frame*/,
float fac,
const SeqResult &src1,
const SeqResult &src2)
{
PRF_scope_with_name("SeqFxGammaCross", ProfileCategory::Draw);
SeqResult dst = prepare_effect_imbufs(context, src1, src2);
GammaCrossEffectOp op;
op.factor = fac;
apply_effect_op(op, src1.image, src2.image, dst.image);
dst.is_opaque_before_transform = !src1.image->can_contain_alpha() &&
!src2.image->can_contain_alpha();
return dst;
}
void cross_effect_get_handle(EffectHandle &rval)
{
rval.execute = do_cross_effect;
rval.early_out = early_out_fade;
}
void gamma_cross_effect_get_handle(EffectHandle &rval)
{
rval.early_out = early_out_fade;
rval.execute = do_gammacross_effect;
}
} // namespace blender::seq

View File

@@ -0,0 +1,227 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_math_base.hh"
#include "BLI_task.hh"
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
#include "PRF_profile.hh"
#include "SEQ_render.hh"
#include "effects.hh"
namespace blender::seq {
static void init_gaussian_blur_effect(Strip *strip)
{
GaussianBlurVars *data = MEM_new<GaussianBlurVars>("gaussianblurvars");
strip->effectdata = data;
data->size_x = 9.0f;
data->size_y = 9.0f;
}
static void free_gaussian_blur_effect(Strip *strip, const bool /*do_id_user*/)
{
if (strip->effectdata) {
GaussianBlurVars *data = static_cast<GaussianBlurVars *>(strip->effectdata);
MEM_delete(data);
strip->effectdata = nullptr;
}
}
static StripEarlyOut early_out_gaussian_blur(const Strip *strip, float /*fac*/)
{
GaussianBlurVars *data = static_cast<GaussianBlurVars *>(strip->effectdata);
if (data->size_x == 0.0f && data->size_y == 0) {
return StripEarlyOut::UseInput1;
}
return StripEarlyOut::DoEffect;
}
template<typename T>
static void gaussian_blur_x(const Span<float> gaussian,
int half_size,
int start_line,
int width,
int height,
int /*frame_height*/,
const T *rect,
T *dst)
{
dst += int64_t(start_line) * width * 4;
for (int y = start_line; y < start_line + height; y++) {
for (int x = 0; x < width; x++) {
float4 accum(0.0f);
float accum_weight = 0.0f;
int xmin = math::max(x - half_size, 0);
int xmax = math::min(x + half_size, width - 1);
for (int nx = xmin, index = (xmin - x) + half_size; nx <= xmax; nx++, index++) {
float weight = gaussian[index];
int offset = (y * width + nx) * 4;
accum += float4(rect + offset) * weight;
accum_weight += weight;
}
accum *= (1.0f / accum_weight);
if constexpr (math::is_math_float_type<T>) {
dst[0] = accum[0];
dst[1] = accum[1];
dst[2] = accum[2];
dst[3] = accum[3];
}
else {
dst[0] = accum[0] + 0.5f;
dst[1] = accum[1] + 0.5f;
dst[2] = accum[2] + 0.5f;
dst[3] = accum[3] + 0.5f;
}
dst += 4;
}
}
}
template<typename T>
static void gaussian_blur_y(const Span<float> gaussian,
int half_size,
int start_line,
int width,
int height,
int frame_height,
const T *rect,
T *dst)
{
dst += int64_t(start_line) * width * 4;
for (int y = start_line; y < start_line + height; y++) {
for (int x = 0; x < width; x++) {
float4 accum(0.0f);
float accum_weight = 0.0f;
int ymin = math::max(y - half_size, 0);
int ymax = math::min(y + half_size, frame_height - 1);
for (int ny = ymin, index = (ymin - y) + half_size; ny <= ymax; ny++, index++) {
float weight = gaussian[index];
int offset = (ny * width + x) * 4;
accum += float4(rect + offset) * weight;
accum_weight += weight;
}
accum *= (1.0f / accum_weight);
if constexpr (math::is_math_float_type<T>) {
dst[0] = accum[0];
dst[1] = accum[1];
dst[2] = accum[2];
dst[3] = accum[3];
}
else {
dst[0] = accum[0] + 0.5f;
dst[1] = accum[1] + 0.5f;
dst[2] = accum[2] + 0.5f;
dst[3] = accum[3] + 0.5f;
}
dst += 4;
}
}
}
static SeqResult do_gaussian_blur_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip *strip,
float /*timeline_frame*/,
float /*fac*/,
const SeqResult &ibuf1,
const SeqResult & /*ibuf2*/)
{
PRF_scope_with_name("SeqFxBlur", ProfileCategory::Draw);
/* Create blur kernel weights. */
const GaussianBlurVars *data = static_cast<const GaussianBlurVars *>(strip->effectdata);
const float size_scale = seq::get_render_scale_factor(*context);
const float size_x = data->size_x * size_scale;
const float size_y = data->size_y * size_scale;
const int half_size_x = int(size_x + 0.5f);
const int half_size_y = int(size_y + 0.5f);
Array<float> gaussian_x = make_gaussian_blur_kernel(size_x, half_size_x);
Array<float> gaussian_y = make_gaussian_blur_kernel(size_y, half_size_y);
const int width = context->rectx;
const int height = context->recty;
const bool is_float = ibuf1.image->float_data();
/* Horizontal blur: create output, blur ibuf1 into it. */
SeqResult out = prepare_effect_imbufs(context, ibuf1, {});
threading::parallel_for(IndexRange(context->recty), 32, [&](const IndexRange y_range) {
const int y_first = y_range.first();
const int y_size = y_range.size();
if (is_float) {
gaussian_blur_x(gaussian_x,
half_size_x,
y_first,
width,
y_size,
height,
ibuf1.image->float_data(),
out.image->float_data_for_write());
}
else {
gaussian_blur_x(gaussian_x,
half_size_x,
y_first,
width,
y_size,
height,
ibuf1.image->byte_data(),
out.image->byte_data_for_write());
}
});
/* Vertical blur: create output, blur previous output into it. */
SeqResult vin = out;
out = prepare_effect_imbufs(context, vin, {});
threading::parallel_for(IndexRange(context->recty), 32, [&](const IndexRange y_range) {
const int y_first = y_range.first();
const int y_size = y_range.size();
if (is_float) {
gaussian_blur_y(gaussian_y,
half_size_y,
y_first,
width,
y_size,
height,
vin.image->float_data(),
out.image->float_data_for_write());
}
else {
gaussian_blur_y(gaussian_y,
half_size_y,
y_first,
width,
y_size,
height,
vin.image->byte_data(),
out.image->byte_data_for_write());
}
});
/* Free the first output. */
IMB_freeImBuf(vin.image);
return out;
}
void gaussian_blur_effect_get_handle(EffectHandle &rval)
{
rval.init = init_gaussian_blur_effect;
rval.free = free_gaussian_blur_effect;
rval.early_out = early_out_gaussian_blur;
rval.execute = do_gaussian_blur_effect;
}
} // namespace blender::seq

View File

@@ -0,0 +1,246 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_math_vector.hh"
#include "BLI_task.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "PRF_profile.hh"
#include "SEQ_render.hh"
#include "effects.hh"
namespace blender::seq {
static void glow_blur_bitmap(
const float4 *src, float4 *map, int width, int height, float blur, int quality)
{
/* If we're not really blurring, bail out */
if (blur <= 0) {
return;
}
/* If result would be no blurring, early out. */
const int halfWidth = ((quality + 1) * blur);
if (halfWidth == 0) {
return;
}
Array<float4> temp(width * height);
/* Initialize the gaussian filter.
* TODO: use code from #filter_kernel_value. */
Array<float> filter(halfWidth * 2);
const float k = -1.0f / (2.0f * float(M_PI) * blur * blur);
float weight = 0;
for (int ix = 0; ix < halfWidth; ix++) {
weight = exp(k * (ix * ix));
filter[halfWidth - ix] = weight;
filter[halfWidth + ix] = weight;
}
filter[0] = weight;
/* Normalize the array */
float fval = 0;
for (int ix = 0; ix < halfWidth * 2; ix++) {
fval += filter[ix];
}
for (int ix = 0; ix < halfWidth * 2; ix++) {
filter[ix] /= fval;
}
/* Blur the rows: read map, write temp */
threading::parallel_for(IndexRange(height), 32, [&](const IndexRange y_range) {
for (const int y : y_range) {
for (int x = 0; x < width; x++) {
float4 curColor = float4(0.0f);
int xmin = math::max(x - halfWidth, 0);
int xmax = math::min(x + halfWidth, width);
for (int nx = xmin, index = (xmin - x) + halfWidth; nx < xmax; nx++, index++) {
curColor += map[nx + y * width] * filter[index];
}
temp[x + y * width] = curColor;
}
}
});
/* Blur the columns: read temp, write map */
threading::parallel_for(IndexRange(width), 32, [&](const IndexRange x_range) {
const float4 one = float4(1.0f);
for (const int x : x_range) {
for (int y = 0; y < height; y++) {
float4 curColor = float4(0.0f);
int ymin = math::max(y - halfWidth, 0);
int ymax = math::min(y + halfWidth, height);
for (int ny = ymin, index = (ymin - y) + halfWidth; ny < ymax; ny++, index++) {
curColor += temp[x + ny * width] * filter[index];
}
if (src != nullptr) {
curColor = math::min(one, src[x + y * width] + curColor);
}
map[x + y * width] = curColor;
}
}
});
}
static void blur_isolate_highlights(const float4 *in,
float4 *out,
int width,
int height,
float threshold,
float boost,
float clamp)
{
threading::parallel_for(IndexRange(height), 64, [&](const IndexRange y_range) {
const float4 clampv = float4(clamp);
for (const int y : y_range) {
int index = y * width;
for (int x = 0; x < width; x++, index++) {
/* Isolate the intensity */
float intensity = (in[index].x + in[index].y + in[index].z - threshold);
float4 val;
if (intensity > 0) {
val = math::min(clampv, in[index] * (boost * intensity));
}
else {
val = float4(0.0f);
}
out[index] = val;
}
}
});
}
static void init_glow_effect(Strip *strip)
{
GlowVars *data = MEM_new<GlowVars>("glowvars");
strip->effectdata = data;
data->fMini = 0.25f;
data->fClamp = 1.0f;
data->fBoost = 0.5f;
data->dDist = 3.0f;
data->dQuality = 3;
data->bNoComp = 0;
}
static void free_glow_effect(Strip *strip, const bool /*do_id_user*/)
{
if (strip->effectdata) {
GlowVars *data = static_cast<GlowVars *>(strip->effectdata);
MEM_delete(data);
strip->effectdata = nullptr;
}
}
static void do_glow_effect_byte(Strip *strip,
int render_size,
float fac,
int x,
int y,
const uchar *rect1,
const uchar * /*rect2*/,
uchar *out)
{
GlowVars *glow = static_cast<GlowVars *>(strip->effectdata);
Array<float4> inbuf(x * y);
Array<float4> outbuf(x * y);
IMB_colormanagement_transform_byte_to_float(*inbuf.data(), rect1, x, y, 4, "sRGB", "sRGB");
blur_isolate_highlights(
inbuf.data(), outbuf.data(), x, y, glow->fMini * 3.0f, glow->fBoost * fac, glow->fClamp);
glow_blur_bitmap(glow->bNoComp ? nullptr : inbuf.data(),
outbuf.data(),
x,
y,
glow->dDist * (render_size / 100.0f),
glow->dQuality);
threading::parallel_for(IndexRange(y), 64, [&](const IndexRange y_range) {
size_t offset = y_range.first() * x;
IMB_buffer_byte_from_float(
out + offset * 4, *(outbuf.data() + offset), 4, 0.0f, true, x, y_range.size(), x);
});
}
static void do_glow_effect_float(Strip *strip,
int render_size,
float fac,
int x,
int y,
const float *rect1,
const float * /*rect2*/,
float *out)
{
float4 *outbuf = reinterpret_cast<float4 *>(out);
const float4 *inbuf = reinterpret_cast<const float4 *>(rect1);
GlowVars *glow = static_cast<GlowVars *>(strip->effectdata);
blur_isolate_highlights(
inbuf, outbuf, x, y, glow->fMini * 3.0f, glow->fBoost * fac, glow->fClamp);
glow_blur_bitmap(glow->bNoComp ? nullptr : inbuf,
outbuf,
x,
y,
glow->dDist * (render_size / 100.0f),
glow->dQuality);
}
static SeqResult do_glow_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip *strip,
float /*timeline_frame*/,
float fac,
const SeqResult &ibuf1,
const SeqResult & /*ibuf2*/)
{
PRF_scope_with_name("SeqFxGlow", ProfileCategory::Draw);
SeqResult out = prepare_effect_imbufs(context, ibuf1, {});
int render_size = 100 * context->rectx / context->scene->r.xsch;
if (out.image->float_data()) {
do_glow_effect_float(strip,
render_size,
fac,
context->rectx,
context->recty,
ibuf1.image->float_data(),
nullptr,
out.image->float_data_for_write());
}
else {
do_glow_effect_byte(strip,
render_size,
fac,
context->rectx,
context->recty,
ibuf1.image->byte_data(),
nullptr,
out.image->byte_data_for_write());
}
return out;
}
void glow_effect_get_handle(EffectHandle &rval)
{
rval.init = init_glow_effect;
rval.free = free_glow_effect;
rval.execute = do_glow_effect;
}
} // namespace blender::seq

View File

@@ -0,0 +1,65 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "PRF_profile.hh"
#include "SEQ_channels.hh"
#include "SEQ_render.hh"
#include "SEQ_utils.hh"
#include "effects.hh"
#include "render.hh"
namespace blender::seq {
static StripEarlyOut early_out_multicam(const Strip * /*strip*/, float /*fac*/)
{
return StripEarlyOut::NoInput;
}
static SeqResult do_multicam(const RenderData *context,
SeqRenderState *state,
Strip *strip,
float timeline_frame,
float /*fac*/,
const SeqResult & /*ibuf1*/,
const SeqResult & /*ibuf2*/)
{
PRF_scope_with_name("SeqFxMultiCam", ProfileCategory::Draw);
if (strip->multicam_source == 0 || strip->multicam_source >= strip->channel) {
return {};
}
Editing *ed = context->scene->ed;
if (!ed || state->strips_in_progress.contains(strip)) {
return {};
}
ListBaseT<Strip> *seqbasep = get_seqbase_by_strip(context->scene, strip);
ListBaseT<SeqTimelineChannel> *channels = get_channels_by_strip(ed, strip);
if (!seqbasep) {
return {};
}
state->strips_in_progress.add(strip);
SeqResult out = seq_render_give_ibuf_seqbase(
context, state, timeline_frame, strip->multicam_source, channels, seqbasep);
state->strips_in_progress.remove(strip);
return out;
}
void multi_camera_effect_get_handle(EffectHandle &rval)
{
rval.early_out = early_out_multicam;
rval.execute = do_multicam;
}
} // namespace blender::seq

View File

@@ -0,0 +1,85 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_task.hh"
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
#include "PRF_profile.hh"
#include "effects.hh"
namespace blender::seq {
static void init_solid_color(Strip *strip)
{
SolidColorVars *data = MEM_new<SolidColorVars>("solidcolor");
strip->effectdata = data;
data->col[0] = data->col[1] = data->col[2] = 0.5;
data->width = data->height = 1;
}
static void free_solid_color(Strip *strip, const bool /*do_id_user*/)
{
if (strip->effectdata) {
SolidColorVars *data = static_cast<SolidColorVars *>(strip->effectdata);
MEM_delete(data);
strip->effectdata = nullptr;
}
}
static StripEarlyOut early_out_color(const Strip * /*strip*/, float /*fac*/)
{
return StripEarlyOut::NoInput;
}
static SeqResult do_solid_color(const RenderData * /*context*/,
SeqRenderState * /*state*/,
Strip *strip,
float /*timeline_frame*/,
float /*fac*/,
const SeqResult & /*ibuf1*/,
const SeqResult & /*ibuf2*/)
{
PRF_scope_with_name("SeqFxColor", ProfileCategory::Draw);
SeqResult out;
const SolidColorVars *cv = static_cast<const SolidColorVars *>(strip->effectdata);
out.image = IMB_allocImBuf(cv->width, cv->height, ImBufFlags::ByteData);
uchar color[4];
rgb_float_to_uchar(color, cv->col);
color[3] = 255;
uchar *byte_data = out.image->byte_data_for_write();
threading::parallel_for(IndexRange(out.image->y), 64, [&](const IndexRange y_range) {
uchar *dst = byte_data + y_range.first() * out.image->x * 4;
uchar *dst_end = dst + y_range.size() * out.image->x * 4;
while (dst < dst_end) {
memcpy(dst, color, sizeof(color));
dst += 4;
}
});
out.image->color_mode = ImColorMode::RGB;
out.is_opaque_before_transform = true;
return out;
}
void solid_color_effect_get_handle(EffectHandle &rval)
{
rval.init = init_solid_color;
rval.free = free_solid_color;
rval.early_out = early_out_color;
rval.execute = do_solid_color;
}
} // namespace blender::seq

View File

@@ -0,0 +1,213 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BKE_fcurve.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
#include "PRF_profile.hh"
#include "RNA_prototypes.hh"
#include "SEQ_render.hh"
#include "SEQ_time.hh"
#include "effects.hh"
#include "render.hh"
namespace blender::seq {
static void init_speed_effect(Strip *strip)
{
SpeedControlVars *data = MEM_new<SpeedControlVars>("speedcontrolvars");
strip->effectdata = data;
data->speed_control_type = SEQ_SPEED_STRETCH;
data->speed_fader = 1.0f;
data->speed_fader_length = 0.0f;
data->speed_fader_frame_number = 0.0f;
}
static void free_speed_effect(Strip *strip, const bool /*do_id_user*/)
{
if (strip->effectdata) {
SpeedControlVars *v = static_cast<SpeedControlVars *>(strip->effectdata);
if (v->frameMap) {
MEM_delete(v->frameMap);
}
MEM_delete(v);
strip->effectdata = nullptr;
}
}
static void copy_speed_effect(Strip *dst, const Strip *src, const int /*flag*/)
{
SpeedControlVars *v = MEM_dupalloc(static_cast<SpeedControlVars *>(src->effectdata));
v->frameMap = nullptr;
dst->effectdata = v;
}
static StripEarlyOut early_out_speed(const Strip * /*strip*/, float /*fac*/)
{
return StripEarlyOut::DoEffect;
}
static FCurve *strip_effect_speed_speed_factor_curve_get(Scene *scene, Strip *strip)
{
return id_data_find_fcurve(&scene->id, strip, RNA_Strip, "speed_factor", 0, nullptr);
}
void strip_effect_speed_rebuild_map(Scene *scene, Strip *strip)
{
const int effect_strip_length = strip->right_handle(scene) - strip->left_handle();
if ((strip->input1 == nullptr) || (effect_strip_length < 1)) {
return; /* Make COVERITY happy and check for (CID 598) input strip. */
}
const FCurve *fcu = strip_effect_speed_speed_factor_curve_get(scene, strip);
if (fcu == nullptr) {
return;
}
SpeedControlVars *v = static_cast<SpeedControlVars *>(strip->effectdata);
if (v->frameMap) {
MEM_delete(v->frameMap);
}
v->frameMap = MEM_new_array_uninitialized<float>(size_t(effect_strip_length), __func__);
v->frameMap[0] = 0.0f;
float target_frame = 0;
for (int frame_index = 1; frame_index < effect_strip_length; frame_index++) {
target_frame += evaluate_fcurve(fcu, strip->left_handle() + frame_index);
const int target_frame_max = strip->input1->length(scene);
CLAMP(target_frame, 0, target_frame_max);
v->frameMap[frame_index] = target_frame;
}
}
static void strip_effect_speed_frame_map_ensure(Scene *scene, Strip *strip)
{
const SpeedControlVars *v = static_cast<SpeedControlVars *>(strip->effectdata);
if (v->frameMap != nullptr) {
return;
}
strip_effect_speed_rebuild_map(scene, strip);
}
float strip_speed_effect_target_frame_get(Scene *scene,
Strip *strip_speed,
float timeline_frame,
int input)
{
if (strip_speed->input1 == nullptr) {
return 0.0f;
}
strip_effect_handle_get(strip_speed); /* Ensure, that data are initialized. */
int frame_index = round_fl_to_int(give_frame_index(scene, strip_speed, timeline_frame));
SpeedControlVars *s = static_cast<SpeedControlVars *>(strip_speed->effectdata);
const Strip *source = strip_speed->input1;
float target_frame = 0.0f;
switch (s->speed_control_type) {
case SEQ_SPEED_STRETCH: {
/* Only right handle controls effect speed! */
const float target_content_length = source->length(scene) - source->startofs;
const float speed_effetct_length = strip_speed->right_handle(scene) -
strip_speed->left_handle();
const float ratio = frame_index / speed_effetct_length;
target_frame = target_content_length * ratio;
break;
}
case SEQ_SPEED_MULTIPLY: {
const FCurve *fcu = strip_effect_speed_speed_factor_curve_get(scene, strip_speed);
if (fcu != nullptr) {
strip_effect_speed_frame_map_ensure(scene, strip_speed);
target_frame = s->frameMap[frame_index];
}
else {
target_frame = frame_index * s->speed_fader;
if (s->speed_fader < 0) {
/* Treat `target_frame` as a negative offset from the last frame of the strip. */
target_frame += source->length(scene);
}
}
break;
}
case SEQ_SPEED_LENGTH:
target_frame = source->length(scene) * (s->speed_fader_length / 100.0f);
break;
case SEQ_SPEED_FRAME_NUMBER:
target_frame = s->speed_fader_frame_number;
break;
}
CLAMP(target_frame, 0, source->length(scene));
target_frame += strip_speed->start;
/* No interpolation. */
if ((s->flags & SEQ_SPEED_USE_INTERPOLATION) == 0) {
return target_frame;
}
/* Interpolation is used, switch between current and next frame based on which input is
* requested. */
return input == 0 ? target_frame : ceil(target_frame);
}
static float speed_effect_interpolation_ratio_get(Scene *scene,
Strip *strip_speed,
float timeline_frame)
{
const float target_frame = strip_speed_effect_target_frame_get(
scene, strip_speed, timeline_frame, 0);
return target_frame - floor(target_frame);
}
static SeqResult do_speed_effect(const RenderData *context,
SeqRenderState *state,
Strip *strip,
float timeline_frame,
float fac,
const SeqResult &ibuf1,
const SeqResult &ibuf2)
{
PRF_scope_with_name("SeqFxSpeed", ProfileCategory::Draw);
SeqResult out;
const SpeedControlVars *s = static_cast<SpeedControlVars *>(strip->effectdata);
EffectHandle cross_effect = effect_handle_get(STRIP_TYPE_CROSS);
if (s->flags & SEQ_SPEED_USE_INTERPOLATION) {
fac = speed_effect_interpolation_ratio_get(context->scene, strip, timeline_frame);
/* Current frame is ibuf1, next frame is ibuf2. */
out = cross_effect.execute(context, state, nullptr, timeline_frame, fac, ibuf1, ibuf2);
return out;
}
/* No interpolation. */
out.image = IMB_dupImBuf(ibuf1.image);
out.is_opaque_before_transform = !ibuf1.image->can_contain_alpha();
return out;
}
void speed_effect_get_handle(EffectHandle &rval)
{
rval.init = init_speed_effect;
rval.free = free_speed_effect;
rval.copy = copy_speed_effect;
rval.execute = do_speed_effect;
rval.early_out = early_out_speed;
}
} // namespace blender::seq

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,253 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <algorithm>
#include "BLI_math_vector.hh"
#include "BLI_task.hh"
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
#include "PRF_profile.hh"
#include "SEQ_render.hh"
#include "effects.hh"
namespace blender::seq {
struct WipeData {
WipeData(const WipeVars *wipe, int width, int height, float fac)
{
this->type = wipe->wipetype;
this->forward = wipe->forward != 0;
this->size = float2(width, height);
if (this->type == SEQ_WIPE_SINGLE) {
/* Position that the wipe line goes through: moves along
* the image diagonal. The other diagonal when angle is negative. */
this->pos = this->size * (this->forward ? fac : (1.0f - fac));
if (wipe->angle < 0.0f) {
this->pos.x = this->size.x - this->pos.x;
}
}
if (this->type == SEQ_WIPE_DOUBLE) {
/* For double blend, position goes from center of screen
* along the diagonal. The other blend line position will be
* a mirror of it. */
float2 offset = this->size * (this->forward ? (1.0f - fac) : fac) * 0.5f;
if (wipe->angle < 0.0f) {
offset.x = -offset.x;
}
this->pos = this->size * 0.5f + offset;
}
/* Line direction: (cos(a), sin(a)). Perpendicular: (-sin(a), cos(a)).
* Angle is negative to match previous behavior. */
this->normal.x = -sinf(-wipe->angle);
this->normal.y = cosf(-wipe->angle);
/* Blend zone width. */
float blend_width = wipe->edgeWidth * ((width + height) / 2.0f);
if (ELEM(this->type, SEQ_WIPE_DOUBLE, SEQ_WIPE_IRIS)) {
blend_width *= 0.5f;
}
/* For single/double wipes, make sure the blend zone goes to zero at start & end
* of transition. */
if (ELEM(this->type, SEQ_WIPE_SINGLE, SEQ_WIPE_DOUBLE)) {
blend_width = std::min(blend_width, fac * this->size.y);
blend_width = std::min(blend_width, this->size.y - fac * this->size.y);
}
this->blend_width_inv = math::safe_rcp(blend_width);
if (this->type == SEQ_WIPE_IRIS) {
/* Distance to Iris circle at current factor. */
float2 iris = this->size * 0.5f * (this->forward ? (1.0f - fac) : fac);
this->iris_dist = math::length(iris);
}
if (this->type == SEQ_WIPE_CLOCK) {
float angle_cur = 2.0f * float(M_PI) * (this->forward ? (1.0f - fac) : fac);
float angle_width = wipe->edgeWidth * float(M_PI);
float delta_neg = angle_width * (this->forward ? fac : (1.0f - fac));
float delta_pos = angle_width * (this->forward ? (1.0f - fac) : fac);
this->clock_angles.x = std::max(angle_cur - delta_neg, 0.0f);
this->clock_angles.y = std::min(angle_cur + delta_pos, 2.0f * float(M_PI));
this->clock_angle_inv_dif = math::safe_rcp(this->clock_angles.y - this->clock_angles.x);
}
}
float2 size; /* Image size. */
float2 pos; /* Position that wipe line goes through. */
float2 normal; /* Normal vector to single/double wipe line. */
float blend_width_inv = 0.0f;
float iris_dist = 0.0f;
float2 clock_angles; /* Min, max clock angles at current factor. */
float clock_angle_inv_dif = 0.0f;
eEffectWipeType type;
bool forward = false;
};
static float calc_wipe_band(float dist, float inv_width)
{
if (inv_width == 0.0f) {
return dist < 0.0f ? 0.0f : 1.0f;
}
return dist * inv_width + 0.5f;
}
static float calc_wipe_blend(const WipeData *data, int x, int y)
{
float output = 0.0f;
switch (data->type) {
case SEQ_WIPE_SINGLE: {
/* Distance to line: dot(pixel_pos - line_pos, line_normal). */
float dist = math::dot(float2(x, y) - data->pos, data->normal);
output = calc_wipe_band(dist, data->blend_width_inv);
} break;
case SEQ_WIPE_DOUBLE: {
/* Distance to line: dot(pixel_pos - line_pos, line_normal).
* For double wipe, we have two lines to calculate the distance to. */
float2 pos1 = data->pos;
float2 pos2 = data->size - data->pos;
float dist1 = math::dot(float2(x, y) - pos1, -data->normal);
float dist2 = math::dot(float2(x, y) - pos2, data->normal);
float dist = std::min(dist1, dist2);
output = calc_wipe_band(dist, data->blend_width_inv);
} break;
case SEQ_WIPE_CLOCK: {
float2 offset = float2(x, y) - data->size * 0.5f;
if (math::length_squared(offset) < 1.0e-3f) {
output = 0.0f;
}
else {
float angle;
angle = atan2f(offset.y, offset.x);
if (angle < 0.0f) {
angle += 2.0f * float(M_PI);
}
if (angle < data->clock_angles.x) {
output = 1;
}
else if (angle > data->clock_angles.y) {
output = 0;
}
else {
output = (data->clock_angles.y - angle) * data->clock_angle_inv_dif;
}
}
} break;
case SEQ_WIPE_IRIS: {
float dist = math::distance(float2(x, y), data->size * 0.5f);
output = calc_wipe_band(data->iris_dist - dist, data->blend_width_inv);
} break;
}
if (!data->forward) {
output = 1.0f - output;
}
return output;
}
static void init_wipe_effect(Strip *strip)
{
strip->effectdata = MEM_new<WipeVars>("wipevars");
}
static void free_wipe_effect(Strip *strip, const bool /*do_id_user*/)
{
if (strip->effectdata) {
MEM_delete(static_cast<WipeVars *>(strip->effectdata));
strip->effectdata = nullptr;
}
}
template<typename T>
static void do_wipe_effect(
const Strip *strip, float fac, int width, int height, const T *rect1, const T *rect2, T *out)
{
const WipeVars *wipe = static_cast<const WipeVars *>(strip->effectdata);
const WipeData data(wipe, width, height, fac);
threading::parallel_for(IndexRange(height), 64, [&](const IndexRange y_range) {
const T *cp1 = rect1 + y_range.first() * width * 4;
const T *cp2 = rect2 + y_range.first() * width * 4;
T *rt = out + y_range.first() * width * 4;
for (const int y : y_range) {
for (int x = 0; x < width; x++) {
float blend = calc_wipe_blend(&data, x, y);
if (blend <= 0.0f) {
memcpy(rt, cp2, sizeof(T) * 4);
}
else if (blend >= 1.0f) {
memcpy(rt, cp1, sizeof(T) * 4);
}
else {
float4 col1 = load_premul_pixel(cp1);
float4 col2 = load_premul_pixel(cp2);
float4 col = col1 * blend + col2 * (1.0f - blend);
store_premul_pixel(col, rt);
}
rt += 4;
cp1 += 4;
cp2 += 4;
}
}
});
}
static SeqResult do_wipe_effect(const RenderData *context,
SeqRenderState * /*state*/,
Strip *strip,
float /*timeline_frame*/,
float fac,
const SeqResult &ibuf1,
const SeqResult &ibuf2)
{
PRF_scope_with_name("SeqFxWipe", ProfileCategory::Draw);
SeqResult out = prepare_effect_imbufs(context, ibuf1, ibuf2);
if (out.image->float_data()) {
do_wipe_effect(strip,
fac,
context->rectx,
context->recty,
ibuf1.image->float_data(),
ibuf2.image->float_data(),
out.image->float_data_for_write());
}
else {
do_wipe_effect(strip,
fac,
context->rectx,
context->recty,
ibuf1.image->byte_data(),
ibuf2.image->byte_data(),
out.image->byte_data_for_write());
}
out.is_opaque_before_transform = !ibuf1.image->can_contain_alpha() &&
!ibuf2.image->can_contain_alpha();
return out;
}
void wipe_effect_get_handle(EffectHandle &rval)
{
rval.init = init_wipe_effect;
rval.free = free_wipe_effect;
rval.early_out = early_out_fade;
rval.execute = do_wipe_effect;
}
} // namespace blender::seq

View File

@@ -0,0 +1,383 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2024 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_math_filter.hh"
#include "BKE_fcurve.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "IMB_metadata.hh"
#include "PRF_profile.hh"
#include "RNA_prototypes.hh"
#include "SEQ_render.hh"
#include "effects.hh"
#include "render.hh"
namespace blender::seq {
SeqResult prepare_effect_imbufs(const RenderData *context,
const SeqResult &ibuf1,
const SeqResult &ibuf2,
bool uninitialized_pixels)
{
PRF_scope_with_name("SeqFxPrepareImbufs", ProfileCategory::Draw);
SeqResult out;
Scene *scene = context->scene;
int x = context->rectx;
int y = context->recty;
ImBufFlags base_flags = uninitialized_pixels ? ImBufFlags::UninitializedPixels :
ImBufFlags::Zero;
if (!ibuf1.is_valid() && !ibuf2.is_valid()) {
out.image = IMB_allocImBuf(x, y, ImBufFlags::ByteData | base_flags);
}
else if ((ibuf1.is_valid() && ibuf1.image->float_data()) ||
(ibuf2.is_valid() && ibuf2.image->float_data()))
{
/* if any inputs are float, output is float too */
out.image = IMB_allocImBuf(x, y, ImBufFlags::FloatData | base_flags);
}
else {
out.image = IMB_allocImBuf(x, y, ImBufFlags::ByteData | base_flags);
}
if (out.image->float_data()) {
if (ibuf1.is_valid()) {
ensure_ibuf_is_sequencer_space(scene, ibuf1.image, true);
}
if (ibuf2.is_valid()) {
ensure_ibuf_is_sequencer_space(scene, ibuf2.image, true);
}
IMB_colormanagement_assign_float_colorspace(out.image,
scene->sequencer_colorspace_settings.name);
}
else {
if (ibuf1.is_valid() && !ibuf1.image->byte_data()) {
IMB_byte_from_float(ibuf1.image);
}
if (ibuf2.is_valid() && !ibuf2.image->byte_data()) {
IMB_byte_from_float(ibuf2.image);
}
}
/* If effect only affecting a single channel, forward input's metadata to the output. */
if (ibuf1.is_valid() && ibuf1.image == ibuf2.image) {
IMB_metadata_copy(out.image, ibuf1.image);
}
return out;
}
Array<float> make_gaussian_blur_kernel(float rad, int size)
{
int n = 2 * size + 1;
Array<float> gaussian(n);
float sum = 0.0f;
float fac = (rad > 0.0f ? 1.0f / rad : 0.0f);
for (int i = -size; i <= size; i++) {
float val = math::filter_kernel_value(math::FilterKernel::Gauss, float(i) * fac);
sum += val;
gaussian[i + size] = val;
}
float inv_sum = 1.0f / sum;
for (int i = 0; i < n; i++) {
gaussian[i] *= inv_sum;
}
return gaussian;
}
static void init_noop(Strip * /*strip*/) {}
static void copy_effect_default(Strip *dst, const Strip *src, const int /*flag*/)
{
dst->effectdata = MEM_dupalloc_void(src->effectdata);
}
static StripEarlyOut early_out_noop(const Strip * /*strip*/, float /*fac*/)
{
return StripEarlyOut::DoEffect;
}
StripEarlyOut early_out_fade(const Strip * /*strip*/, float fac)
{
if (fac == 0.0f) {
return StripEarlyOut::UseInput1;
}
if (fac == 1.0f) {
return StripEarlyOut::UseInput2;
}
return StripEarlyOut::DoEffect;
}
StripEarlyOut early_out_mul_input2(const Strip * /*strip*/, float fac)
{
if (fac == 0.0f) {
return StripEarlyOut::UseInput1;
}
return StripEarlyOut::DoEffect;
}
StripEarlyOut early_out_mul_input1(const Strip * /*strip*/, float fac)
{
if (fac == 0.0f) {
return StripEarlyOut::UseInput2;
}
return StripEarlyOut::DoEffect;
}
void effect_ensure_initialized(Strip *strip)
{
if (strip->effectdata == nullptr) {
EffectHandle h = strip_effect_handle_get(strip);
if (h.init != nullptr) {
h.init(strip);
}
}
}
void effect_free(Strip *strip)
{
EffectHandle h = strip_effect_handle_get(strip);
if (h.free != nullptr) {
h.free(strip, true);
BLI_assert(strip->effectdata == nullptr);
}
}
EffectHandle effect_handle_get(StripType strip_type)
{
EffectHandle rval;
rval.init = init_noop;
rval.free = nullptr;
rval.early_out = early_out_noop;
rval.execute = nullptr;
rval.copy = copy_effect_default;
switch (strip_type) {
case STRIP_TYPE_CROSS:
cross_effect_get_handle(rval);
break;
case STRIP_TYPE_GAMCROSS:
gamma_cross_effect_get_handle(rval);
break;
case STRIP_TYPE_COMPOSITOR:
compositor_effect_get_handle(rval);
break;
case STRIP_TYPE_ADD:
add_effect_get_handle(rval);
break;
case STRIP_TYPE_SUB:
sub_effect_get_handle(rval);
break;
case STRIP_TYPE_MUL:
mul_effect_get_handle(rval);
break;
case STRIP_TYPE_COLORMIX:
color_mix_effect_get_handle(rval);
break;
case STRIP_TYPE_ALPHAOVER:
alpha_over_effect_get_handle(rval);
break;
case STRIP_TYPE_ALPHAUNDER:
alpha_under_effect_get_handle(rval);
break;
case STRIP_TYPE_WIPE:
wipe_effect_get_handle(rval);
break;
case STRIP_TYPE_GLOW:
glow_effect_get_handle(rval);
break;
case STRIP_TYPE_SPEED:
speed_effect_get_handle(rval);
break;
case STRIP_TYPE_COLOR:
solid_color_effect_get_handle(rval);
break;
case STRIP_TYPE_MULTICAM:
multi_camera_effect_get_handle(rval);
break;
case STRIP_TYPE_ADJUSTMENT:
adjustment_effect_get_handle(rval);
break;
case STRIP_TYPE_GAUSSIAN_BLUR:
gaussian_blur_effect_get_handle(rval);
break;
case STRIP_TYPE_TEXT:
text_effect_get_handle(rval);
break;
default:
break;
}
return rval;
}
static EffectHandle effect_handle_for_blend_mode_get(StripBlendMode blend)
{
EffectHandle rval;
rval.init = init_noop;
rval.free = nullptr;
rval.early_out = early_out_noop;
rval.execute = nullptr;
rval.copy = nullptr;
switch (blend) {
case STRIP_BLEND_CROSS:
cross_effect_get_handle(rval);
break;
case STRIP_BLEND_ADD:
add_effect_get_handle(rval);
break;
case STRIP_BLEND_SUB:
sub_effect_get_handle(rval);
break;
case STRIP_BLEND_ALPHAOVER:
alpha_over_effect_get_handle(rval);
break;
case STRIP_BLEND_ALPHAUNDER:
alpha_under_effect_get_handle(rval);
break;
case STRIP_BLEND_GAMCROSS:
gamma_cross_effect_get_handle(rval);
break;
case STRIP_BLEND_MUL:
mul_effect_get_handle(rval);
break;
case STRIP_BLEND_SCREEN:
case STRIP_BLEND_LIGHTEN:
case STRIP_BLEND_DODGE:
case STRIP_BLEND_DARKEN:
case STRIP_BLEND_COLOR_BURN:
case STRIP_BLEND_LINEAR_BURN:
case STRIP_BLEND_OVERLAY:
case STRIP_BLEND_HARD_LIGHT:
case STRIP_BLEND_SOFT_LIGHT:
case STRIP_BLEND_PIN_LIGHT:
case STRIP_BLEND_LIN_LIGHT:
case STRIP_BLEND_VIVID_LIGHT:
case STRIP_BLEND_HUE:
case STRIP_BLEND_SATURATION:
case STRIP_BLEND_VALUE:
case STRIP_BLEND_BLEND_COLOR:
case STRIP_BLEND_DIFFERENCE:
case STRIP_BLEND_EXCLUSION:
blend_mode_effect_get_handle(rval);
break;
default:
break;
}
return rval;
}
EffectHandle strip_effect_handle_get(Strip *strip)
{
EffectHandle h = {};
if (strip->is_effect()) {
h = effect_handle_get(strip->type);
}
return h;
}
EffectHandle strip_blend_mode_handle_get(Strip *strip)
{
EffectHandle h = {};
if (strip->blend_mode != STRIP_BLEND_REPLACE) {
h = effect_handle_for_blend_mode_get(strip->blend_mode);
}
return h;
}
static float transition_fader_calc(const Scene *scene, const Strip *strip, float timeline_frame)
{
float fac = float(timeline_frame - strip->left_handle());
/* Compositor with no inputs can have strip->len not be updated,
* since most of existing editing code assumes no-input effects never need the length.
* So for the fader, just calculated it here directly. */
if (strip->type == STRIP_TYPE_COMPOSITOR) {
fac /= strip->enddisp - strip->startdisp;
}
else {
fac /= strip->length(scene);
}
fac = math::clamp(fac, 0.0f, 1.0f);
return fac;
}
float effect_fader_calc(Scene *scene, Strip *strip, float timeline_frame)
{
if (strip->flag & SEQ_USE_EFFECT_DEFAULT_FADE) {
if (effect_is_transition(strip->type)) {
return transition_fader_calc(scene, strip, timeline_frame);
}
return 1.0f;
}
const FCurve *fcu = id_data_find_fcurve(
&scene->id, strip, RNA_Strip, "effect_fader", 0, nullptr);
if (fcu) {
return evaluate_fcurve(fcu, timeline_frame);
}
return strip->effect_fader;
}
int effect_type_get_min_num_inputs(StripType type)
{
if (!strip_type_is_effect(type)) {
return 0;
}
/* Zero input effects. Note: compositor is here too, but it supports
* any input count. */
if (ELEM(type,
STRIP_TYPE_ADJUSTMENT,
STRIP_TYPE_MULTICAM,
STRIP_TYPE_COLOR,
STRIP_TYPE_TEXT,
STRIP_TYPE_COMPOSITOR))
{
return 0;
}
/* One input effects. */
if (ELEM(type, STRIP_TYPE_GAUSSIAN_BLUR, STRIP_TYPE_GLOW, STRIP_TYPE_SPEED)) {
return 1;
}
/* Others are two inputs. */
return 2;
}
bool strip_type_is_effect(StripType type)
{
return (type >= STRIP_TYPE_CROSS && type <= STRIP_TYPE_COMPOSITOR) ||
(type >= STRIP_TYPE_WIPE && type <= STRIP_TYPE_ADJUSTMENT) ||
(type >= STRIP_TYPE_GAUSSIAN_BLUR && type <= STRIP_TYPE_COLORMIX);
}
bool effect_is_transition(StripType type)
{
return ELEM(type, STRIP_TYPE_CROSS, STRIP_TYPE_GAMCROSS, STRIP_TYPE_WIPE, STRIP_TYPE_COMPOSITOR);
}
} // namespace blender::seq

View File

@@ -0,0 +1,173 @@
/* SPDX-FileCopyrightText: 2004 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup sequencer
*/
#include "BLI_array.hh"
#include "BLI_math_color.h"
#include "BLI_math_vector_types.hh"
#include "BLI_task.hh"
#include "IMB_imbuf_types.hh"
#include "SEQ_effects.hh"
#include "render.hh"
namespace blender {
struct ImBuf;
struct Scene;
struct Strip;
namespace seq {
struct SeqRenderState;
struct RenderData;
enum class StripEarlyOut {
NoInput = -1, /* No input needed. */
DoEffect = 0, /* No early out (do the effect). */
UseInput1 = 1, /* Output = input1. */
UseInput2 = 2, /* Output = input2. */
};
struct EffectHandle {
/* #init is only called on first creation, or when changing effect type. */
void (*init)(Strip *strip);
/* duplicate */
void (*copy)(Strip *dst, const Strip *src, int flag);
/* destruct */
void (*free)(Strip *strip, bool do_id_user);
StripEarlyOut (*early_out)(const Strip *strip, float fac);
/* execute the effect */
SeqResult (*execute)(const RenderData *context,
SeqRenderState *state,
Strip *strip,
float timeline_frame,
float fac,
const SeqResult &input1,
const SeqResult &input2);
};
/** Get the effect handle for a given strip.
* If `strip` is not an effect strip, returns empty `EffectHandle`. */
EffectHandle strip_effect_handle_get(Strip *strip);
EffectHandle strip_blend_mode_handle_get(Strip *strip);
/**
* Build frame map when speed in mode #SEQ_SPEED_MULTIPLY is animated.
* This is, because `target_frame` value is integrated over time.
*/
void strip_effect_speed_rebuild_map(Scene *scene, Strip *strip);
/**
* Override timeline_frame when rendering speed effect input.
*/
float strip_speed_effect_target_frame_get(Scene *scene,
Strip *strip_speed,
float timeline_frame,
int input);
SeqResult prepare_effect_imbufs(const RenderData *context,
const SeqResult &ibuf1,
const SeqResult &ibuf2,
bool uninitialized_pixels = true);
Array<float> make_gaussian_blur_kernel(float rad, int size);
inline float4 load_premul_pixel(const uchar *ptr)
{
float4 res;
straight_uchar_to_premul_float(res, ptr);
return res;
}
inline float4 load_premul_pixel(const float *ptr)
{
return float4(ptr);
}
inline void store_premul_pixel(const float4 &pix, uchar *dst)
{
premul_float_to_straight_uchar(dst, pix);
}
inline void store_premul_pixel(const float4 &pix, float *dst)
{
*reinterpret_cast<float4 *>(dst) = pix;
}
StripEarlyOut early_out_mul_input1(const Strip * /*strip*/, float fac);
StripEarlyOut early_out_mul_input2(const Strip * /*strip*/, float fac);
StripEarlyOut early_out_fade(const Strip * /*strip*/, float fac);
EffectHandle effect_handle_get(StripType strip_type);
float effect_fader_calc(Scene *scene, Strip *strip, float timeline_frame);
void add_effect_get_handle(EffectHandle &rval);
void adjustment_effect_get_handle(EffectHandle &rval);
void alpha_over_effect_get_handle(EffectHandle &rval);
void alpha_under_effect_get_handle(EffectHandle &rval);
void blend_mode_effect_get_handle(EffectHandle &rval);
void color_mix_effect_get_handle(EffectHandle &rval);
void compositor_effect_get_handle(EffectHandle &rval);
void cross_effect_get_handle(EffectHandle &rval);
void gamma_cross_effect_get_handle(EffectHandle &rval);
void gaussian_blur_effect_get_handle(EffectHandle &rval);
void glow_effect_get_handle(EffectHandle &rval);
void mul_effect_get_handle(EffectHandle &rval);
void multi_camera_effect_get_handle(EffectHandle &rval);
void solid_color_effect_get_handle(EffectHandle &rval);
void speed_effect_get_handle(EffectHandle &rval);
void sub_effect_get_handle(EffectHandle &rval);
void text_effect_get_handle(EffectHandle &rval);
void transform_effect_get_handle(EffectHandle &rval);
void wipe_effect_get_handle(EffectHandle &rval);
/* Given `OpT` that implements an `apply` function:
*
* template <typename T>
* void apply(const T *src1, const T *src2, T *dst, int64_t size) const;
*
* this function calls the apply() function in parallel
* chunks of the image to process, and with uchar or float types
* All images are expected to have 4 (RGBA) color channels. */
template<typename OpT>
static void apply_effect_op(const OpT &op, const ImBuf *src1, const ImBuf *src2, ImBuf *dst)
{
BLI_assert_msg(src1->channels == 0 || src1->channels == 4,
"Sequencer only supports 4 channel images");
BLI_assert_msg(src2->channels == 0 || src2->channels == 4,
"Sequencer only supports 4 channel images");
BLI_assert_msg(dst->channels == 0 || dst->channels == 4,
"Sequencer only supports 4 channel images");
float *dst_float_data = dst->float_data_for_write();
uchar *dst_byte_data = dst->byte_data_for_write();
threading::parallel_for(IndexRange(size_t(dst->x) * dst->y), 32 * 1024, [&](IndexRange range) {
int64_t offset = range.first() * 4;
if (dst_float_data) {
const float *src1_ptr = src1->float_data() + offset;
const float *src2_ptr = src2->float_data() + offset;
float *dst_ptr = dst_float_data + offset;
op.apply(src1_ptr, src2_ptr, dst_ptr, range.size());
}
else {
const uchar *src1_ptr = src1->byte_data() + offset;
const uchar *src2_ptr = src2->byte_data() + offset;
uchar *dst_ptr = dst_byte_data + offset;
op.apply(src1_ptr, src2_ptr, dst_ptr, range.size());
}
});
}
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,326 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2024 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <cstring>
#include "DNA_sequence_types.h"
#include "BLI_listbase.h"
#include "SEQ_connect.hh"
#include "SEQ_iterator.hh"
#include "SEQ_relations.hh"
#include "SEQ_render.hh"
namespace blender::seq {
static bool strip_for_each_recursive(ListBaseT<Strip> *seqbase,
ForEachFunc callback,
void *user_data)
{
for (Strip &strip : *seqbase) {
if (!callback(&strip, user_data)) {
/* Callback signaled stop, return. */
return false;
}
if (strip.type == STRIP_TYPE_META) {
if (!strip_for_each_recursive(&strip.seqbase, callback, user_data)) {
return false;
}
}
}
return true;
}
static bool strip_for_each_recursive(ListBaseT<Strip> *seqbase,
FunctionRef<bool(Strip *)> callback)
{
for (Strip &strip : *seqbase) {
if (!callback(&strip)) {
/* Callback signaled stop, return. */
return false;
}
if (strip.type == STRIP_TYPE_META) {
if (!strip_for_each_recursive(&strip.seqbase, callback)) {
return false;
}
}
}
return true;
}
void foreach_strip(ListBaseT<Strip> *seqbase, ForEachFunc callback, void *user_data)
{
strip_for_each_recursive(seqbase, callback, user_data);
}
void foreach_strip(ListBaseT<Strip> *seqbase, FunctionRef<bool(Strip *)> callback)
{
strip_for_each_recursive(seqbase, callback);
}
VectorSet<Strip *> query_by_reference(Strip *strip_reference,
ListBaseT<Strip> *seqbase,
void strip_query_func(Strip *strip_reference,
ListBaseT<Strip> *seqbase,
VectorSet<Strip *> &strips))
{
VectorSet<Strip *> strips;
strip_query_func(strip_reference, seqbase, strips);
return strips;
}
void iterator_set_expand(ListBaseT<Strip> *seqbase,
VectorSet<Strip *> &strips,
void strip_query_func(Strip *strip,
ListBaseT<Strip> *seqbase,
VectorSet<Strip *> &strips))
{
/* Collect expanded results for each sequence in provided VectorSet. */
VectorSet<Strip *> query_matches;
for (Strip *strip : strips) {
query_matches.add_multiple(query_by_reference(strip, seqbase, strip_query_func));
}
/* Merge all expanded results in provided VectorSet. */
strips.add_multiple(query_matches);
}
static void query_all_strips_recursive(const ListBaseT<Strip> *seqbase, VectorSet<Strip *> &strips)
{
for (Strip &strip : *seqbase) {
if (strip.type == STRIP_TYPE_META) {
query_all_strips_recursive(&strip.seqbase, strips);
}
strips.add(&strip);
}
}
VectorSet<Strip *> query_all_strips_recursive(const ListBaseT<Strip> *seqbase)
{
VectorSet<Strip *> strips;
query_all_strips_recursive(seqbase, strips);
return strips;
}
static void query_strips_recursive_at_frame(const Scene *scene,
const ListBaseT<Strip> *seqbase,
const int timeline_frame,
VectorSet<Strip *> &strips)
{
for (Strip &strip : *seqbase) {
if (!strip.intersects_frame(scene, timeline_frame)) {
continue;
}
if (strip.type == STRIP_TYPE_META) {
query_strips_recursive_at_frame(scene, &strip.seqbase, timeline_frame, strips);
}
strips.add(&strip);
}
}
VectorSet<Strip *> query_strips_recursive_at_frame(const Scene *scene,
const ListBaseT<Strip> *seqbase,
const int timeline_frame)
{
VectorSet<Strip *> strips;
query_strips_recursive_at_frame(scene, seqbase, timeline_frame, strips);
return strips;
}
VectorSet<Strip *> query_all_strips(ListBaseT<Strip> *seqbase)
{
VectorSet<Strip *> strips;
for (Strip &strip : *seqbase) {
strips.add(&strip);
}
return strips;
}
VectorSet<Strip *> query_selected_strips(ListBaseT<Strip> *seqbase)
{
VectorSet<Strip *> strips;
for (Strip &strip : *seqbase) {
if ((strip.flag & SEQ_SELECT) != 0) {
strips.add(&strip);
}
}
return strips;
}
static VectorSet<Strip *> query_strips_at_frame(const Scene *scene,
ListBaseT<Strip> *seqbase,
const int timeline_frame)
{
VectorSet<Strip *> strips;
for (Strip &strip : *seqbase) {
if (strip.intersects_frame(scene, timeline_frame)) {
strips.add(&strip);
}
}
return strips;
}
static void collection_filter_channel_up_to_incl(VectorSet<Strip *> &strip_stack,
const int channel)
{
strip_stack.remove_if([&](Strip *strip) { return strip->channel > channel; });
}
bool must_render_strip(const VectorSet<Strip *> &strip_stack, Strip *target_strip)
{
bool strip_have_effect_in_stack = false;
for (Strip *strip : strip_stack) {
/* Strips below another strip with replace blending are never directly rendered. */
if (strip->blend_mode == STRIP_BLEND_REPLACE && target_strip->channel < strip->channel) {
return false;
}
if (strip->is_effect() && relation_is_effect_of_strip(strip, target_strip)) {
/* Strips at the same channel or above their effect are rendered. */
if (target_strip->channel >= strip->channel) {
return true;
}
/* Mark that this strip has an effect in the stack that is above the strip. */
strip_have_effect_in_stack = true;
}
}
/* All effects with inputs are rendered assuming they pass the above checks. */
if (target_strip->is_effect_with_inputs()) {
return true;
}
/* If strip has effects in stack, and all effects are above this strip, it is not rendered. */
if (strip_have_effect_in_stack) {
return false;
}
return true;
}
/* Remove strips we don't want to render from VectorSet. */
static void collection_filter_rendered_strips(VectorSet<Strip *> &strip_stack,
ListBaseT<SeqTimelineChannel> *channels)
{
/* Remove sound strips and muted strips from VectorSet, because these are not rendered.
* Function #must_render_strip() don't have to check for these strips anymore. */
strip_stack.remove_if([&](Strip *strip) {
return strip->type == STRIP_TYPE_SOUND || render_is_muted(channels, strip);
});
strip_stack.remove_if([&](Strip *strip) { return !must_render_strip(strip_stack, strip); });
}
VectorSet<Strip *> query_rendered_strips(const Scene *scene,
ListBaseT<SeqTimelineChannel> *channels,
ListBaseT<Strip> *seqbase,
const int timeline_frame,
const int displayed_channel)
{
VectorSet strips = query_strips_at_frame(scene, seqbase, timeline_frame);
if (displayed_channel != 0) {
collection_filter_channel_up_to_incl(strips, displayed_channel);
}
collection_filter_rendered_strips(strips, channels);
return strips;
}
Vector<Strip *> query_rendered_strips_sorted(const Scene *scene,
ListBaseT<SeqTimelineChannel> *channels,
ListBaseT<Strip> *seqbase,
const int timeline_frame,
const int chanshown)
{
VectorSet strips = query_rendered_strips(scene, channels, seqbase, timeline_frame, chanshown);
Vector<Strip *> strips_vec = strips.extract_vector();
/* Sort strips by channel. */
std::ranges::sort(strips_vec,
[](const Strip *a, const Strip *b) { return a->channel < b->channel; });
return strips_vec;
}
VectorSet<Strip *> query_unselected_strips(ListBaseT<Strip> *seqbase)
{
VectorSet<Strip *> strips;
for (Strip &strip : *seqbase) {
if ((strip.flag & SEQ_SELECT) != 0) {
continue;
}
strips.add(&strip);
}
return strips;
}
void query_strip_effect_chain(Strip *strip,
ListBaseT<Strip> *seqbase,
VectorSet<Strip *> &r_strips)
{
if (r_strips.contains(strip)) {
return; /* Strip is already in set, so all effects connected to it are as well. */
}
r_strips.add(strip);
/* Find all input strips for `strip`. */
if (strip->is_effect()) {
if (strip->input1) {
query_strip_effect_chain(strip->input1, seqbase, r_strips);
}
if (strip->input2) {
query_strip_effect_chain(strip->input2, seqbase, r_strips);
}
}
/* Find all effect strips that have `strip` as an input. */
for (Strip &strip_test : *seqbase) {
if (strip_test.input1 == strip || strip_test.input2 == strip) {
query_strip_effect_chain(&strip_test, seqbase, r_strips);
}
}
}
void query_strip_connected_and_effect_chain(Strip *strip,
ListBaseT<Strip> *seqbase,
VectorSet<Strip *> &r_strips)
{
Vector<Strip *> pending;
pending.append(strip);
while (!pending.is_empty()) {
Strip *current = pending.pop_last();
if (r_strips.contains(current)) {
continue;
}
r_strips.add(current);
VectorSet<Strip *> connections = connected_strips_get(current);
for (Strip *connection : connections) {
if (!r_strips.contains(connection)) {
pending.append(connection);
}
}
VectorSet<Strip *> effect_chain;
query_strip_effect_chain(current, seqbase, effect_chain);
for (Strip *effect_strip : effect_chain) {
if (!r_strips.contains(effect_strip)) {
pending.append(effect_strip);
}
}
}
}
} // namespace blender::seq

View File

@@ -0,0 +1,185 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_fileops.h"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_mutex.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "DNA_sound_types.h"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_utils.hh"
namespace blender::seq {
static Mutex presence_lock;
static bool check_sound_media_missing(const bSound *sound)
{
if (sound == nullptr) {
return false;
}
char filepath[FILE_MAX];
STRNCPY(filepath, sound->filepath);
BLI_path_abs(filepath, ID_BLEND_PATH_FROM_GLOBAL(&sound->id));
return !BLI_exists(filepath);
}
static bool check_media_missing(const Scene *scene, const Strip *strip)
{
if (strip == nullptr || strip->data == nullptr) {
return false;
}
/* Images or movies. */
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);
if (!BLI_exists(filepath)) {
return true;
}
}
}
}
/* Recurse into meta strips. */
if (strip->type == STRIP_TYPE_META) {
for (Strip &strip_n : strip->seqbase) {
if (check_media_missing(scene, &strip_n)) {
return true;
}
}
}
/* Nothing is missing. */
return false;
}
struct MediaPresence {
Map<const void *, bool> map_seq;
Map<const bSound *, bool> map_sound;
};
static MediaPresence *get_media_presence_cache(Scene *scene)
{
MediaPresence **presence = &scene->ed->runtime->media_presence;
if (*presence == nullptr) {
*presence = MEM_new<MediaPresence>(__func__);
}
return *presence;
}
bool media_presence_is_missing(Scene *scene, const Strip *strip)
{
if (strip == nullptr || scene == nullptr || scene->ed == nullptr) {
return false;
}
std::scoped_lock lock(presence_lock);
MediaPresence *presence = get_media_presence_cache(scene);
bool missing = false;
/* Strips that reference another data block that has path to media
* (e.g. sound strips) need to key the presence cache on that data
* block. Since it can be used by multiple strips. */
if (strip->type == STRIP_TYPE_SOUND) {
const bSound *sound = strip->sound;
if (sound && sound->packedfile != nullptr) {
/* The sound file has been packed, don't look up the path. */
return false;
}
const bool *val = presence->map_sound.lookup_ptr(sound);
if (val != nullptr) {
missing = *val;
}
else {
missing = check_sound_media_missing(sound);
presence->map_sound.add_new(sound, missing);
}
}
else {
/* Regular strips that point to media directly. */
const bool *val = presence->map_seq.lookup_ptr(strip);
if (val != nullptr) {
missing = *val;
}
else {
missing = check_media_missing(scene, strip);
presence->map_seq.add_new(strip, missing);
}
}
return missing;
}
void media_presence_set_missing(Scene *scene, const Strip *strip, bool missing)
{
if (strip == nullptr || scene == nullptr || scene->ed == nullptr) {
return;
}
std::scoped_lock lock(presence_lock);
MediaPresence *presence = get_media_presence_cache(scene);
if (strip->type == STRIP_TYPE_SOUND) {
const bSound *sound = strip->sound;
presence->map_sound.add_overwrite(sound, missing);
}
else {
presence->map_seq.add_overwrite(strip, missing);
}
}
void media_presence_invalidate_strip(Scene *scene, const Strip *strip)
{
std::scoped_lock lock(presence_lock);
if (scene != nullptr && scene->ed != nullptr && scene->ed->runtime->media_presence != nullptr) {
scene->ed->runtime->media_presence->map_seq.remove(strip);
}
}
void media_presence_invalidate_sound(Scene *scene, const bSound *sound)
{
std::scoped_lock lock(presence_lock);
if (scene != nullptr && scene->ed != nullptr && scene->ed->runtime->media_presence != nullptr) {
scene->ed->runtime->media_presence->map_sound.remove(sound);
}
}
void media_presence_free(Scene *scene)
{
std::scoped_lock lock(presence_lock);
if (scene != nullptr && scene->ed != nullptr && scene->ed->runtime->media_presence != nullptr) {
MEM_delete(scene->ed->runtime->media_presence);
scene->ed->runtime->media_presence = nullptr;
}
}
} // namespace blender::seq

View File

@@ -0,0 +1,128 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <cfloat>
#include "BLI_math_base.h"
#include "BLI_math_vector.hh"
#include "BLT_translation.hh"
#include "DNA_sequence_types.h"
#include "PRF_profile.hh"
#include "SEQ_modifier.hh"
#include "SEQ_render.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "modifier.hh"
#include "render.hh"
namespace blender::seq {
struct BrightContrastApplyOp {
float mul;
float add;
template<typename ImageT, typename MaskSampler>
void apply(ImageT *image, MaskSampler &mask, int image_x, IndexRange y_range)
{
image += y_range.first() * image_x * 4;
for (int64_t y : y_range) {
mask.begin_row(y);
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
/* NOTE: arguably incorrect usage of "raw" values, should be un-premultiplied.
* Not changing behavior for now, but would be good to fix someday. */
float4 input = load_pixel_raw(image);
float4 result;
result = input * this->mul + this->add;
result.w = input.w;
mask.apply_mask(input, result);
store_pixel_raw(result, image);
image += 4;
}
}
}
};
static void brightcontrast_apply(ModifierApplyContext &context, StripModifierData *smd)
{
PRF_scope_with_name("SeqModBrightContrast", ProfileCategory::Draw);
ensure_ibuf_is_sequencer_space(context.render_data.scene, context.result.image, false);
ImBuf *mask = modifier_render_mask_input(context, *smd);
const BrightContrastModifierData *bcmd = reinterpret_cast<BrightContrastModifierData *>(smd);
BrightContrastApplyOp op;
/* The algorithm is by Werner D. Streidt
* (http://visca.com/ffactory/archives/5-99/msg00021.html)
* Extracted from OpenCV `demhist.cpp`. */
const float brightness = bcmd->bright / 100.0f;
const float contrast = bcmd->contrast;
float delta = contrast / 200.0f;
if (contrast > 0) {
op.mul = 1.0f - delta * 2.0f;
op.mul = 1.0f / max_ff(op.mul, FLT_EPSILON);
op.add = op.mul * (brightness - delta);
}
else {
delta *= -1;
op.mul = max_ff(1.0f - delta * 2.0f, 0.0f);
op.add = op.mul * brightness + delta;
}
apply_modifier_op(op, context.result.image, mask, context.transform);
if (mask != nullptr) {
IMB_freeImBuf(mask);
}
}
static void brightcontrast_panel_draw(const bContext *C, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
layout.use_property_split_set(true);
layout.prop(ptr, "bright", UI_ITEM_NONE, std::nullopt, ICON_NONE);
layout.prop(ptr, "contrast", UI_ITEM_NONE, std::nullopt, ICON_NONE);
if (ui::Layout *mask_input_layout = layout.panel_prop(
C, ptr, "open_mask_input_panel", IFACE_("Mask Input")))
{
draw_mask_input_type_settings(C, *mask_input_layout, ptr);
}
}
static void brightcontrast_register(ARegionType *region_type)
{
modifier_panel_register(region_type, eSeqModifierType_BrightContrast, brightcontrast_panel_draw);
}
StripModifierTypeInfo seqModifierType_BrightContrast = {
/*idname*/ "BrightContrast",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Brightness/Contrast"),
/*struct_name*/ "BrightContrastModifierData",
/*struct_size*/ sizeof(BrightContrastModifierData),
/*init_data*/ nullptr,
/*free_data*/ nullptr,
/*copy_data*/ nullptr,
/*apply*/ brightcontrast_apply,
/*panel_register*/ brightcontrast_register,
/*blend_write*/ nullptr,
/*blend_read*/ nullptr,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,396 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_math_base.h"
#include "BLT_translation.hh"
#include "DNA_sequence_types.h"
#include "PRF_profile.hh"
#include "SEQ_modifier.hh"
#include "SEQ_render.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "RNA_access.hh"
#include "modifier.hh"
#include "render.hh"
namespace blender::seq {
/* Lift-Gamma-Gain math. NOTE: lift is actually (2-lift). */
static float color_balance_lgg(
float in, const float lift, const float gain, const float gamma, const float mul)
{
float x = (((in - 1.0f) * lift) + 1.0f) * gain;
/* prevent NaN */
x = std::max(x, 0.0f);
x = powf(x, gamma) * mul;
CLAMP(x, FLT_MIN, FLT_MAX);
return x;
}
/* Slope-Offset-Power (ASC CDL) math, see https://en.wikipedia.org/wiki/ASC_CDL */
static float color_balance_sop(
float in, const float slope, const float offset, const float power, float mul)
{
float x = in * slope + offset;
/* prevent NaN */
x = std::max(x, 0.0f);
x = powf(x, power);
x *= mul;
CLAMP(x, FLT_MIN, FLT_MAX);
return x;
}
/**
* Use a larger lookup table than 256 possible byte values: due to alpha
* pre-multiplication, dark values with low alphas might need more precision.
*/
static constexpr int CB_TABLE_SIZE = 1024;
static void make_cb_table_lgg(
float lift, float gain, float gamma, float mul, float r_table[CB_TABLE_SIZE])
{
for (int i = 0; i < CB_TABLE_SIZE; i++) {
float x = float(i) * (1.0f / (CB_TABLE_SIZE - 1.0f));
r_table[i] = color_balance_lgg(x, lift, gain, gamma, mul);
}
}
static void make_cb_table_sop(
float slope, float offset, float power, float mul, float r_table[CB_TABLE_SIZE])
{
for (int i = 0; i < CB_TABLE_SIZE; i++) {
float x = float(i) * (1.0f / (CB_TABLE_SIZE - 1.0f));
r_table[i] = color_balance_sop(x, slope, offset, power, mul);
}
}
struct ColorBalanceApplyOp {
int method;
float3 lift, gain, gamma;
float3 slope, offset, power;
float multiplier;
float lut[3][CB_TABLE_SIZE];
/* Apply on a byte image via a table lookup. */
template<typename MaskSampler>
void apply(uchar *image, MaskSampler &mask, int image_x, IndexRange y_range)
{
image += y_range.first() * image_x * 4;
for (int64_t y : y_range) {
mask.begin_row(y);
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
float4 input = load_pixel_premul(image);
float4 result;
int p0 = int(input.x * (CB_TABLE_SIZE - 1.0f) + 0.5f);
int p1 = int(input.y * (CB_TABLE_SIZE - 1.0f) + 0.5f);
int p2 = int(input.z * (CB_TABLE_SIZE - 1.0f) + 0.5f);
result.x = this->lut[0][p0];
result.y = this->lut[1][p1];
result.z = this->lut[2][p2];
result.w = input.w;
mask.apply_mask(input, result);
store_pixel_premul(result, image);
image += 4;
}
}
}
/* Apply on a float image by doing full math. */
template<typename MaskSampler>
void apply(float *image, MaskSampler &mask, int image_x, IndexRange y_range)
{
image += y_range.first() * image_x * 4;
for (int64_t y : y_range) {
mask.begin_row(y);
if (this->method == SEQ_COLOR_BALANCE_METHOD_LIFTGAMMAGAIN) {
/* Lift/Gamma/Gain */
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
float4 input = load_pixel_premul(image);
float4 result;
result.x = color_balance_lgg(
input.x, this->lift.x, this->gain.x, this->gamma.x, this->multiplier);
result.y = color_balance_lgg(
input.y, this->lift.y, this->gain.y, this->gamma.y, this->multiplier);
result.z = color_balance_lgg(
input.z, this->lift.z, this->gain.z, this->gamma.z, this->multiplier);
result.w = input.w;
mask.apply_mask(input, result);
store_pixel_premul(result, image);
image += 4;
}
}
else if (this->method == SEQ_COLOR_BALANCE_METHOD_SLOPEOFFSETPOWER) {
/* Slope/Offset/Power */
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
float4 input = load_pixel_premul(image);
float4 result;
result.x = color_balance_sop(
input.x, this->slope.x, this->offset.x, this->power.x, this->multiplier);
result.y = color_balance_sop(
input.y, this->slope.y, this->offset.y, this->power.y, this->multiplier);
result.z = color_balance_sop(
input.z, this->slope.z, this->offset.z, this->power.z, this->multiplier);
result.w = input.w;
mask.apply_mask(input, result);
store_pixel_premul(result, image);
image += 4;
}
}
else {
BLI_assert_unreachable();
}
}
}
void init_lgg(const StripColorBalance &data)
{
BLI_assert(data.method == SEQ_COLOR_BALANCE_METHOD_LIFTGAMMAGAIN);
this->lift = 2.0f - float3(data.lift);
if (data.flag & SEQ_COLOR_BALANCE_INVERSE_LIFT) {
for (int c = 0; c < 3; c++) {
/* tweak to give more subtle results
* values above 1.0 are scaled */
if (this->lift[c] > 1.0f) {
this->lift[c] = powf(this->lift[c] - 1.0f, 2.0f) + 1.0f;
}
this->lift[c] = 2.0f - this->lift[c];
}
}
this->gain = float3(data.gain);
if (data.flag & SEQ_COLOR_BALANCE_INVERSE_GAIN) {
this->gain = math::rcp(math::max(this->gain, float3(1.0e-6f)));
}
this->gamma = float3(data.gamma);
if (!(data.flag & SEQ_COLOR_BALANCE_INVERSE_GAMMA)) {
this->gamma = math::rcp(math::max(this->gamma, float3(1.0e-6f)));
}
}
void init_sop(const StripColorBalance &data)
{
BLI_assert(data.method == SEQ_COLOR_BALANCE_METHOD_SLOPEOFFSETPOWER);
this->slope = float3(data.slope);
if (data.flag & SEQ_COLOR_BALANCE_INVERSE_SLOPE) {
this->slope = math::rcp(math::max(this->slope, float3(1.0e-6f)));
}
this->offset = float3(data.offset) - 1.0f;
if (data.flag & SEQ_COLOR_BALANCE_INVERSE_OFFSET) {
this->offset = -this->offset;
}
this->power = float3(data.power);
if (!(data.flag & SEQ_COLOR_BALANCE_INVERSE_POWER)) {
this->power = math::rcp(math::max(this->power, float3(1.0e-6f)));
}
}
void init(const ColorBalanceModifierData &data, bool byte_image)
{
this->multiplier = data.color_multiply;
this->method = data.color_balance.method;
if (this->method == SEQ_COLOR_BALANCE_METHOD_LIFTGAMMAGAIN) {
init_lgg(data.color_balance);
if (byte_image) {
for (int c = 0; c < 3; c++) {
make_cb_table_lgg(
this->lift[c], this->gain[c], this->gamma[c], this->multiplier, this->lut[c]);
}
}
}
else if (this->method == SEQ_COLOR_BALANCE_METHOD_SLOPEOFFSETPOWER) {
init_sop(data.color_balance);
if (byte_image) {
for (int c = 0; c < 3; c++) {
make_cb_table_sop(
this->slope[c], this->offset[c], this->power[c], this->multiplier, this->lut[c]);
}
}
}
else {
BLI_assert_unreachable();
}
}
};
static void colorBalance_init_data(StripModifierData *smd)
{
ColorBalanceModifierData *cbmd = reinterpret_cast<ColorBalanceModifierData *>(smd);
cbmd->color_multiply = 1.0f;
cbmd->color_balance.method = SEQ_COLOR_BALANCE_METHOD_LIFTGAMMAGAIN;
for (int c = 0; c < 3; c++) {
cbmd->color_balance.lift[c] = 1.0f;
cbmd->color_balance.gamma[c] = 1.0f;
cbmd->color_balance.gain[c] = 1.0f;
cbmd->color_balance.slope[c] = 1.0f;
cbmd->color_balance.offset[c] = 1.0f;
cbmd->color_balance.power[c] = 1.0f;
}
}
static void colorBalance_apply(ModifierApplyContext &context, StripModifierData *smd)
{
PRF_scope_with_name("SeqModColorBalance", ProfileCategory::Draw);
ensure_ibuf_is_sequencer_space(context.render_data.scene, context.result.image, false);
ImBuf *mask = modifier_render_mask_input(context, *smd);
const ColorBalanceModifierData *cbmd = reinterpret_cast<const ColorBalanceModifierData *>(smd);
ColorBalanceApplyOp op;
op.init(*cbmd, context.result.image->byte_data() != nullptr);
apply_modifier_op(op, context.result.image, mask, context.transform);
if (mask != nullptr) {
IMB_freeImBuf(mask);
}
}
static void colorBalance_panel_draw(const bContext *C, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
PointerRNA color_balance = RNA_pointer_get(ptr, "color_balance");
const int correction_method = RNA_enum_get(&color_balance, "correction_method");
layout.use_property_split_set(true);
layout.prop(ptr, "color_multiply", UI_ITEM_NONE, std::nullopt, ICON_NONE);
layout.prop(&color_balance, "correction_method", UI_ITEM_NONE, std::nullopt, ICON_NONE);
ui::Layout &flow = layout.grid_flow(true, 0, true, false, false);
flow.use_property_split_set(false);
if (correction_method == SEQ_COLOR_BALANCE_METHOD_LIFTGAMMAGAIN) {
/* Split into separate scopes to be able to reuse "split" and "col" variable names. */
{
ui::Layout &split = flow.column(false).split(0.35f, false);
ui::Layout &col = split.column(true);
col.label(IFACE_("Lift"), ICON_NONE);
col.separator();
col.separator();
col.prop(&color_balance, "lift", UI_ITEM_NONE, "", ICON_NONE);
col.prop(
&color_balance, "invert_lift", UI_ITEM_NONE, IFACE_("Invert"), ICON_ARROW_LEFTRIGHT);
template_color_picker(&split, &color_balance, "lift", true, false, false, true);
col.separator();
}
{
ui::Layout &split = flow.column(false).split(0.35f, false);
ui::Layout &col = split.column(true);
col.label(IFACE_("Gamma"), ICON_NONE);
col.separator();
col.separator();
col.prop(&color_balance, "gamma", UI_ITEM_NONE, "", ICON_NONE);
col.prop(
&color_balance, "invert_gamma", UI_ITEM_NONE, IFACE_("Invert"), ICON_ARROW_LEFTRIGHT);
template_color_picker(&split, &color_balance, "gamma", true, false, true, true);
col.separator();
}
{
ui::Layout &split = flow.column(false).split(0.35f, false);
ui::Layout &col = split.column(true);
col.label(IFACE_("Gain"), ICON_NONE);
col.separator();
col.separator();
col.prop(&color_balance, "gain", UI_ITEM_NONE, "", ICON_NONE);
col.prop(
&color_balance, "invert_gain", UI_ITEM_NONE, IFACE_("Invert"), ICON_ARROW_LEFTRIGHT);
template_color_picker(&split, &color_balance, "gain", true, false, true, true);
}
}
else if (correction_method == SEQ_COLOR_BALANCE_METHOD_SLOPEOFFSETPOWER) {
{
ui::Layout &split = flow.column(false).split(0.35f, false);
ui::Layout &col = split.column(true);
col.label(IFACE_("Offset"), ICON_NONE);
col.separator();
col.separator();
col.prop(&color_balance, "offset", UI_ITEM_NONE, "", ICON_NONE);
col.prop(
&color_balance, "invert_offset", UI_ITEM_NONE, IFACE_("Invert"), ICON_ARROW_LEFTRIGHT);
template_color_picker(&split, &color_balance, "offset", true, false, false, true);
col.separator();
}
{
ui::Layout &split = flow.column(false).split(0.35f, false);
ui::Layout &col = split.column(true);
col.label(IFACE_("Power"), ICON_NONE);
col.separator();
col.separator();
col.prop(&color_balance, "power", UI_ITEM_NONE, "", ICON_NONE);
col.prop(
&color_balance, "invert_power", UI_ITEM_NONE, IFACE_("Invert"), ICON_ARROW_LEFTRIGHT);
template_color_picker(&split, &color_balance, "power", true, false, false, true);
col.separator();
}
{
ui::Layout &split = flow.column(false).split(0.35f, false);
ui::Layout &col = split.column(true);
col.label(IFACE_("Slope"), ICON_NONE);
col.separator();
col.separator();
col.prop(&color_balance, "slope", UI_ITEM_NONE, "", ICON_NONE);
col.prop(
&color_balance, "invert_slope", UI_ITEM_NONE, IFACE_("Invert"), ICON_ARROW_LEFTRIGHT);
template_color_picker(&split, &color_balance, "slope", true, false, false, true);
}
}
else {
BLI_assert_unreachable();
}
if (ui::Layout *mask_input_layout = layout.panel_prop(
C, ptr, "open_mask_input_panel", IFACE_("Mask Input")))
{
draw_mask_input_type_settings(C, *mask_input_layout, ptr);
}
}
static void colorBalance_register(ARegionType *region_type)
{
modifier_panel_register(region_type, eSeqModifierType_ColorBalance, colorBalance_panel_draw);
}
StripModifierTypeInfo seqModifierType_ColorBalance = {
/*idname*/ "ColorBalance",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Color Balance"),
/*struct_name*/ "ColorBalanceModifierData",
/*struct_size*/ sizeof(ColorBalanceModifierData),
/*init_data*/ colorBalance_init_data,
/*free_data*/ nullptr,
/*copy_data*/ nullptr,
/*apply*/ colorBalance_apply,
/*panel_register*/ colorBalance_register,
/*blend_write*/ nullptr,
/*blend_read*/ nullptr,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,496 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_math_rotation.hh"
#include "BLT_translation.hh"
#include "COM_domain.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "DNA_node_types.h"
#include "DNA_sequence_types.h"
#include "BKE_anim_data.hh"
#include "BKE_animsys.h"
#include "BKE_context.hh"
#include "BKE_idprop.hh"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "DEG_depsgraph_query.hh"
#include "IMB_colormanagement.hh"
#include "NOD_composite.hh"
#include "NOD_compositor_nodes_caller_ui.hh"
#include "NOD_compositor_nodes_srna.hh"
#include "PRF_profile.hh"
#include "SEQ_modifier.hh"
#include "SEQ_select.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_transform.hh"
#include "UI_interface.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "cache/compositor_cache.hh"
#include "compositor.hh"
#include "modifier.hh"
#include "render.hh"
namespace blender::seq {
void compositor_nodes_update_interface(Scene &sequencer_scene,
SequencerCompositorModifierData &cmd)
{
if (!cmd.modifier.system_properties) {
cmd.modifier.system_properties =
bke::idprop::create_group("SequencerCompositorModifierProperties").release();
}
PointerRNA properties_ptr = RNA_pointer_create_discrete(
&sequencer_scene.id, RNA_SequencerCompositorModifierProperties, &cmd);
RNA_ensure_and_sync_system_properties(properties_ptr, *cmd.modifier.system_properties);
DEG_id_tag_update(&sequencer_scene.id, ID_RECALC_SEQUENCER_STRIPS);
}
template<typename T>
static void set_float_array(PointerRNA *input_props_ptr, compositor::Result &result)
{
T value;
RNA_float_get_array(input_props_ptr, "value", value);
result.set_single_value(value);
}
template<typename T>
static void set_int_array(PointerRNA *input_props_ptr, compositor::Result &result)
{
T value;
RNA_int_get_array(input_props_ptr, "value", value);
result.set_single_value(value);
}
static void set_single_input_from_rna_value(PointerRNA *input_props_ptr,
const eNodeSocketDatatype socket_type,
compositor::Result &result,
const std::optional<int> dimensions = {})
{
using namespace nodes;
switch (socket_type) {
case SOCK_FLOAT: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
const float value = RNA_float_get(input_props_ptr, "value");
result.set_single_value(value);
}
break;
}
case SOCK_VECTOR: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
switch (dimensions.value_or(3)) {
case 2: {
set_float_array<float2>(input_props_ptr, result);
break;
}
case 3: {
set_float_array<float3>(input_props_ptr, result);
break;
}
case 4: {
set_float_array<float4>(input_props_ptr, result);
break;
}
default:
BLI_assert_unreachable();
}
}
break;
}
case SOCK_RGBA: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
ColorGeometry4f value;
RNA_float_get_array(input_props_ptr, "value", value);
result.set_single_value(value);
}
break;
}
case SOCK_BOOLEAN: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
const bool value = RNA_boolean_get(input_props_ptr, "value");
result.set_single_value(value);
}
break;
}
case SOCK_INT: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
const int value = RNA_int_get(input_props_ptr, "value");
result.set_single_value(value);
}
break;
}
case SOCK_ROTATION: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
float3 value_euler;
RNA_float_get_array(input_props_ptr, "value", value_euler);
math::Quaternion value_rotation = math::to_quaternion(math::EulerXYZ(value_euler));
result.set_single_value(value_rotation);
}
break;
}
case SOCK_MENU: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
const MenuValue value = MenuValue(RNA_enum_get(input_props_ptr, "value"));
result.set_single_value(value);
}
break;
}
case SOCK_STRING: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
const std::string value = RNA_string_get(input_props_ptr, "value");
result.set_single_value(value);
}
break;
}
case SOCK_INT_VECTOR: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
switch (dimensions.value_or(2)) {
case 2: {
set_int_array<int2>(input_props_ptr, result);
break;
}
case 3: {
set_int_array<int3>(input_props_ptr, result);
break;
}
default:
BLI_assert_unreachable();
}
}
break;
}
case SOCK_OBJECT: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
Object *value = RNA_pointer_get(input_props_ptr, "value").data_as<Object>();
result.set_single_value(value);
}
break;
}
case SOCK_FONT: {
const auto type = CompositorNodesInputType(RNA_enum_get(input_props_ptr, "type"));
if (type == CompositorNodesInputType::Value) {
VFont *value = RNA_pointer_get(input_props_ptr, "value").data_as<VFont>();
result.set_single_value(value);
}
break;
}
case SOCK_IMAGE:
case SOCK_COLLECTION:
case SOCK_TEXTURE:
case SOCK_MATERIAL:
case SOCK_SCENE:
case SOCK_TEXT_ID:
case SOCK_MASK:
case SOCK_SOUND:
case SOCK_GEOMETRY:
case SOCK_MATRIX:
case SOCK_BUNDLE:
case SOCK_CLOSURE:
case SOCK_SHADER:
case SOCK_CUSTOM:
break;
}
}
static std::optional<int> get_socket_dimension(const bNodeTreeInterfaceSocket *socket,
const eNodeSocketDatatype socket_type)
{
if (socket_type == SOCK_VECTOR) {
return static_cast<bNodeSocketValueVector *>(socket->socket_data)->dimensions;
}
else if (socket_type == SOCK_INT_VECTOR) {
return static_cast<bNodeSocketValueIntVector *>(socket->socket_data)->dimensions;
}
return {};
}
class CompositorModifierContext : public CompositorContext {
private:
const ModifierApplyContext &mod_context_;
SequencerCompositorModifierData *modifier_data_;
ImBuf *image_buffer_;
compositor::Result mask_;
ImBuf *mask_buffer_ = nullptr;
int timeline_frame_;
bool owns_mask_ = false;
PointerRNA properties_ptr_;
public:
CompositorModifierContext(const ModifierApplyContext &mod_context,
compositor::StaticCacheManager &cache_manager,
SequencerCompositorModifierData *modifier_data)
: CompositorContext(cache_manager, mod_context.render_data, mod_context.strip),
mod_context_(mod_context),
modifier_data_(modifier_data),
image_buffer_(mod_context.result.image),
mask_(*this, compositor::ResultType::Color, compositor::ResultPrecision::Full),
timeline_frame_(mod_context.timeline_frame)
{
PointerRNA ptr = RNA_pointer_create_discrete(
&mod_context.render_data.scene->id, RNA_SequencerCompositorModifierData, modifier_data);
properties_ptr_ = RNA_pointer_get(&ptr, "properties");
}
void free_resources()
{
IMB_freeImBuf(this->mask_buffer_);
this->mask_buffer_ = nullptr;
if (this->owns_mask_) {
this->mask_.release();
this->owns_mask_ = false;
}
}
compositor::Domain get_compositing_domain() const override
{
return compositor::Domain(int2(image_buffer_->x, image_buffer_->y));
}
void write_viewer(compositor::Result &viewer_result) override
{
write_viewer_impl(viewer_result, *image_buffer_);
}
void evaluate()
{
using namespace compositor;
const StripModifierData &smd = this->modifier_data_->modifier;
const bool is_mask_used = smd.mask_input_type == STRIP_MASK_INPUT_STRIP ?
smd.mask_strip != nullptr :
smd.mask_id != nullptr;
const bNodeTree &node_group = *DEG_get_evaluated<bNodeTree>(render_data_.depsgraph,
modifier_data_->node_group);
const bke::DataBlockComputeContext compute_context(nullptr, this->get_scene().id);
NodeGroupOperation node_group_operation(*this,
node_group,
this->needed_outputs(),
node_group.active_viewer_key,
bke::NODE_INSTANCE_KEY_BASE,
compute_context);
set_output_refcount(node_group, node_group_operation);
node_group.ensure_topology_cache();
PointerRNA inputs_ptr = RNA_pointer_get(&properties_ptr_, "inputs");
BLI_assert(inputs_ptr.data != nullptr);
/* Map the inputs to the operation. */
Vector<std::unique_ptr<Result>> inputs;
const Span<const bNodeTreeInterfaceSocket *> interface_inputs = node_group.interface_inputs();
for (const bNodeTreeInterfaceSocket *input_socket : interface_inputs) {
bke::bNodeSocketType *typeinfo = input_socket->socket_typeinfo();
const eNodeSocketDatatype socket_type = typeinfo ? typeinfo->type : SOCK_CUSTOM;
const bool valid_socket_type = typeinfo && node_group.typeinfo->valid_socket_type(
node_group.typeinfo, typeinfo);
/* Fallback to ResultType::Float for invalid inputs. */
const ResultType result_type = valid_socket_type ?
compositor::get_node_interface_socket_result_type(
*input_socket) :
ResultType::Float;
Result *input_result = new Result(this->create_result(result_type, ResultPrecision::Full));
if (input_socket == interface_inputs[0]) {
if (socket_type == SOCK_RGBA) {
/* First socket is the image input. */
create_result_from_input(*input_result, *image_buffer_);
}
else {
input_result->allocate_invalid();
}
}
else if (is_mask_used && input_socket == interface_inputs[1]) {
if (socket_type == SOCK_RGBA) {
/* Second socket is the mask input. */
render_mask_input(this->mod_context_, this->timeline_frame_);
if (this->mask_.is_allocated()) {
input_result->set_type(this->mask_.type());
input_result->set_precision(this->mask_.precision());
input_result->share_data(this->mask_);
input_result->set_transformation(this->mod_context_.transform_comp_result);
}
else {
input_result->allocate_invalid();
}
}
else {
input_result->allocate_invalid();
}
}
else if (valid_socket_type) {
PointerRNA input_props_ptr = RNA_pointer_get(&inputs_ptr, input_socket->identifier);
input_result->allocate_single_value();
set_single_input_from_rna_value(&input_props_ptr,
socket_type,
*input_result,
get_socket_dimension(input_socket, socket_type));
}
else {
input_result->allocate_invalid();
}
node_group_operation.map_input_to_result(input_socket->identifier, input_result);
inputs.append(std::unique_ptr<Result>(input_result));
}
{
PRF_scope_with_name("SeqCompositorEvaluate", ProfileCategory::Draw);
node_group_operation.evaluate();
}
this->write_outputs(node_group, node_group_operation, *this->image_buffer_);
}
/* Render mask - similar to #modifier_render_mask_input except for the Mask ID
* path we do a more efficient approach than rendering into a full ImBuf. */
void render_mask_input(const ModifierApplyContext &context, int timeline_frame)
{
PRF_scope_with_name("SeqRenderMaskInput", ProfileCategory::Draw);
const StripModifierData &smd = this->modifier_data_->modifier;
if (smd.mask_input_type == STRIP_MASK_INPUT_STRIP && smd.mask_strip) {
this->mask_buffer_ = seq_render_strip(&context.render_data,
&context.render_state,
smd.mask_strip,
timeline_frame)
.image;
if (this->mask_buffer_ != nullptr) {
this->create_result_from_input(this->mask_, *this->mask_buffer_);
this->owns_mask_ = true;
}
}
else if (smd.mask_input_type == STRIP_MASK_INPUT_ID && smd.mask_id) {
int frame_index = 0;
if (smd.mask_time == STRIP_MASK_TIME_RELATIVE) {
frame_index = smd.mask_id->sfra + timeline_frame - context.strip.start;
}
else if (smd.mask_time == STRIP_MASK_TIME_ABSOLUTE) {
frame_index = timeline_frame;
}
/* Mask is a grayscale value, similar to alpha, so conceptually it is already a
* "linear" quantity. However, masks used to be turned into grayscale images and
* interpreted as being in "sequencer working space" (default: sRGB), so keep at least
* that behavior working as before -- if sequencer space is sRGB, convert value to
* linear for the compositor. */
const bool seq_space_is_srgb = IMB_colormanagement_space_name_is_srgb(
context.render_data.scene->sequencer_colorspace_settings.name);
const int width = context.render_data.rectx;
const int height = context.render_data.recty;
this->mask_.set_type(compositor::ResultType::Float);
this->mask_.share_data(
this->cache_manager().cached_masks.get(*this,
smd.mask_id,
compositor::Domain(int2(width, height)),
1.0f,
true,
frame_index,
1,
0.0f,
seq_space_is_srgb));
this->owns_mask_ = false;
}
}
};
static void compositor_modifier_init_data(StripModifierData *strip_modifier_data)
{
SequencerCompositorModifierData *modifier_data =
reinterpret_cast<SequencerCompositorModifierData *>(strip_modifier_data);
modifier_data->node_group = nullptr;
}
static void compositor_modifier_apply(ModifierApplyContext &context,
StripModifierData *strip_modifier_data)
{
PRF_scope_with_name("SeqModCompositor", ProfileCategory::Draw);
SequencerCompositorModifierData *modifier_data =
reinterpret_cast<SequencerCompositorModifierData *>(strip_modifier_data);
if (!modifier_data->node_group) {
return;
}
CompositorCache &com_cache = context.render_data.scene->ed->runtime->ensure_compositor_cache();
CompositorModifierContext com_mod_context(context, com_cache.get_cache_manager(), modifier_data);
if (com_mod_context.use_gpu()) {
com_mod_context.set_gpu_supported(render_begin_gpu(context.render_data));
}
com_cache.recreate_if_needed(
com_mod_context.use_gpu(), com_mod_context.get_precision(), context.render_data.gpu_context);
com_mod_context.evaluate();
com_mod_context.cache_manager().reset();
com_mod_context.free_resources();
if (com_mod_context.use_gpu()) {
render_end_gpu(context.render_data);
}
context.result.translation += com_mod_context.get_result_translation();
}
static PointerRNA *modifier_panel_get_property_pointers(Panel *panel)
{
PointerRNA *ptr = ui::panel_custom_data_get(panel);
BLI_assert(!RNA_pointer_is_null(ptr));
BLI_assert(RNA_struct_is_a(ptr->type, RNA_StripModifier));
ui::panel_context_pointer_set(panel, "modifier", ptr);
return ptr;
}
static void compositor_modifier_panel_draw(const bContext *C, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *modifier_ptr = modifier_panel_get_property_pointers(panel);
nodes::draw_compositor_nodes_modifier_ui(*C, modifier_ptr, layout);
}
static void compositor_modifier_register(ARegionType *region_type)
{
modifier_panel_register(
region_type, eSeqModifierType_Compositor, compositor_modifier_panel_draw);
}
StripModifierTypeInfo seqModifierType_Compositor = {
/*idname*/ "Compositor",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Compositor"),
/*struct_name*/ "SequencerCompositorModifierData",
/*struct_size*/ sizeof(SequencerCompositorModifierData),
/*init_data*/ compositor_modifier_init_data,
/*free_data*/ nullptr,
/*copy_data*/ nullptr,
/*apply*/ compositor_modifier_apply,
/*panel_register*/ compositor_modifier_register,
/*blend_write*/ nullptr,
/*blend_read*/ nullptr,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,147 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BKE_colortools.hh"
#include "BLT_translation.hh"
#include "DNA_curve_enums.h"
#include "DNA_sequence_types.h"
#include "PRF_profile.hh"
#include "SEQ_modifier.hh"
#include "SEQ_render.hh"
#include "UI_interface.hh"
#include "UI_interface_c.hh"
#include "UI_interface_layout.hh"
#include "modifier.hh"
#include "render.hh"
namespace blender::seq {
static void curves_init_data(StripModifierData *smd)
{
CurvesModifierData *cmd = reinterpret_cast<CurvesModifierData *>(smd);
BKE_curvemapping_set_defaults(&cmd->curve_mapping, 4, 0.0f, 0.0f, 1.0f, 1.0f, HD_AUTO);
}
static void curves_free_data(StripModifierData *smd)
{
CurvesModifierData *cmd = reinterpret_cast<CurvesModifierData *>(smd);
BKE_curvemapping_free_data(&cmd->curve_mapping);
}
static void curves_copy_data(StripModifierData *target, StripModifierData *smd)
{
CurvesModifierData *cmd = reinterpret_cast<CurvesModifierData *>(smd);
CurvesModifierData *cmd_target = reinterpret_cast<CurvesModifierData *>(target);
BKE_curvemapping_copy_data(&cmd_target->curve_mapping, &cmd->curve_mapping);
}
struct CurvesApplyOp {
const CurveMapping *curve_mapping;
template<typename ImageT, typename MaskSampler>
void apply(ImageT *image, MaskSampler &mask, int image_x, IndexRange y_range)
{
image += y_range.first() * image_x * 4;
for (int64_t y : y_range) {
mask.begin_row(y);
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
float4 input = load_pixel_premul(image);
float4 result;
BKE_curvemapping_evaluate_premulRGBF(this->curve_mapping, result, input);
result.w = input.w;
mask.apply_mask(input, result);
store_pixel_premul(result, image);
image += 4;
}
}
}
};
static void curves_apply(ModifierApplyContext &context, StripModifierData *smd)
{
PRF_scope_with_name("SeqModCurves", ProfileCategory::Draw);
ensure_ibuf_is_sequencer_space(context.render_data.scene, context.result.image, false);
ImBuf *mask = modifier_render_mask_input(context, *smd);
CurvesModifierData *cmd = reinterpret_cast<CurvesModifierData *>(smd);
const float black[3] = {0.0f, 0.0f, 0.0f};
const float white[3] = {1.0f, 1.0f, 1.0f};
BKE_curvemapping_init(&cmd->curve_mapping);
BKE_curvemapping_premultiply(&cmd->curve_mapping, false);
BKE_curvemapping_set_black_white(&cmd->curve_mapping, black, white);
CurvesApplyOp op;
op.curve_mapping = &cmd->curve_mapping;
apply_modifier_op(op, context.result.image, mask, context.transform);
BKE_curvemapping_premultiply(&cmd->curve_mapping, true);
if (mask != nullptr) {
IMB_freeImBuf(mask);
}
}
static void curves_panel_draw(const bContext *C, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
template_curve_mapping(&layout, ptr, "curve_mapping", 'c', false, false, false, true, false);
if (ui::Layout *mask_input_layout = layout.panel_prop(
C, ptr, "open_mask_input_panel", IFACE_("Mask Input")))
{
draw_mask_input_type_settings(C, *mask_input_layout, ptr);
}
}
static void curves_register(ARegionType *region_type)
{
modifier_panel_register(region_type, eSeqModifierType_Curves, curves_panel_draw);
}
static void curves_write(BlendWriter *writer, const StripModifierData *smd)
{
const CurvesModifierData *cmd = reinterpret_cast<const CurvesModifierData *>(smd);
BKE_curvemapping_blend_write(writer, &cmd->curve_mapping);
}
static void curves_read(BlendDataReader *reader, StripModifierData *smd)
{
CurvesModifierData *cmd = reinterpret_cast<CurvesModifierData *>(smd);
BKE_curvemapping_blend_read(reader, &cmd->curve_mapping);
}
StripModifierTypeInfo seqModifierType_Curves = {
/*idname*/ "Curves",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Curves"),
/*struct_name*/ "CurvesModifierData",
/*struct_size*/ sizeof(CurvesModifierData),
/*init_data*/ curves_init_data,
/*free_data*/ curves_free_data,
/*copy_data*/ curves_copy_data,
/*apply*/ curves_apply,
/*panel_register*/ curves_register,
/*blend_write*/ curves_write,
/*blend_read*/ curves_read,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,174 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_math_color.h"
#include "BKE_colortools.hh"
#include "BLT_translation.hh"
#include "DNA_curve_enums.h"
#include "DNA_sequence_types.h"
#include "PRF_profile.hh"
#include "SEQ_modifier.hh"
#include "SEQ_render.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "modifier.hh"
#include "render.hh"
namespace blender::seq {
static void hue_correct_init_data(StripModifierData *smd)
{
HueCorrectModifierData *hcmd = reinterpret_cast<HueCorrectModifierData *>(smd);
int c;
BKE_curvemapping_set_defaults(&hcmd->curve_mapping, 1, 0.0f, 0.0f, 1.0f, 1.0f, HD_AUTO);
hcmd->curve_mapping.preset = CURVE_PRESET_MID8;
for (c = 0; c < 3; c++) {
CurveMap *cuma = &hcmd->curve_mapping.cm[c];
BKE_curvemap_reset(
cuma, &hcmd->curve_mapping.clipr, hcmd->curve_mapping.preset, CurveMapSlopeType::Positive);
}
/* use wrapping for all hue correct modifiers */
hcmd->curve_mapping.flag |= CUMA_USE_WRAPPING;
/* default to showing Saturation */
hcmd->curve_mapping.cur = 1;
}
static void hue_correct_free_data(StripModifierData *smd)
{
HueCorrectModifierData *hcmd = reinterpret_cast<HueCorrectModifierData *>(smd);
BKE_curvemapping_free_data(&hcmd->curve_mapping);
}
static void hue_correct_copy_data(StripModifierData *target, StripModifierData *smd)
{
HueCorrectModifierData *hcmd = reinterpret_cast<HueCorrectModifierData *>(smd);
HueCorrectModifierData *hcmd_target = reinterpret_cast<HueCorrectModifierData *>(target);
BKE_curvemapping_copy_data(&hcmd_target->curve_mapping, &hcmd->curve_mapping);
}
struct HueCorrectApplyOp {
const CurveMapping *curve_mapping;
template<typename ImageT, typename MaskSampler>
void apply(ImageT *image, MaskSampler &mask, int image_x, IndexRange y_range)
{
image += y_range.first() * image_x * 4;
for (int64_t y : y_range) {
mask.begin_row(y);
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
/* NOTE: arguably incorrect usage of "raw" values, should be un-premultiplied.
* Not changing behavior for now, but would be good to fix someday. */
float4 input = load_pixel_raw(image);
float4 result;
result.w = input.w;
float3 hsv;
rgb_to_hsv(input.x, input.y, input.z, &hsv.x, &hsv.y, &hsv.z);
/* adjust hue, scaling returned default 0.5 up to 1 */
float f;
f = BKE_curvemapping_evaluateF(this->curve_mapping, 0, hsv.x);
hsv.x += f - 0.5f;
/* adjust saturation, scaling returned default 0.5 up to 1 */
f = BKE_curvemapping_evaluateF(this->curve_mapping, 1, hsv.x);
hsv.y *= (f * 2.0f);
/* adjust value, scaling returned default 0.5 up to 1 */
f = BKE_curvemapping_evaluateF(this->curve_mapping, 2, hsv.x);
hsv.z *= (f * 2.0f);
hsv.x = hsv.x - floorf(hsv.x); /* mod 1.0 */
hsv.y = math::clamp(hsv.y, 0.0f, 1.0f);
/* convert back to rgb */
hsv_to_rgb(hsv.x, hsv.y, hsv.z, &result.x, &result.y, &result.z);
mask.apply_mask(input, result);
store_pixel_raw(result, image);
image += 4;
}
}
}
};
static void hue_correct_apply(ModifierApplyContext &context, StripModifierData *smd)
{
PRF_scope_with_name("SeqModHueCorrect", ProfileCategory::Draw);
ensure_ibuf_is_sequencer_space(context.render_data.scene, context.result.image, false);
ImBuf *mask = modifier_render_mask_input(context, *smd);
HueCorrectModifierData *hcmd = reinterpret_cast<HueCorrectModifierData *>(smd);
BKE_curvemapping_init(&hcmd->curve_mapping);
HueCorrectApplyOp op;
op.curve_mapping = &hcmd->curve_mapping;
apply_modifier_op(op, context.result.image, mask, context.transform);
if (mask != nullptr) {
IMB_freeImBuf(mask);
}
}
static void hue_correct_panel_draw(const bContext *C, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
template_curve_mapping(&layout, ptr, "curve_mapping", 'h', false, false, false, false, false);
if (ui::Layout *mask_input_layout = layout.panel_prop(
C, ptr, "open_mask_input_panel", IFACE_("Mask Input")))
{
draw_mask_input_type_settings(C, *mask_input_layout, ptr);
}
}
static void hue_correct_register(ARegionType *region_type)
{
modifier_panel_register(region_type, eSeqModifierType_HueCorrect, hue_correct_panel_draw);
}
static void hue_correct_write(BlendWriter *writer, const StripModifierData *smd)
{
const HueCorrectModifierData *hmd = reinterpret_cast<const HueCorrectModifierData *>(smd);
BKE_curvemapping_blend_write(writer, &hmd->curve_mapping);
}
static void hue_correct_read(BlendDataReader *reader, StripModifierData *smd)
{
HueCorrectModifierData *hmd = reinterpret_cast<HueCorrectModifierData *>(smd);
BKE_curvemapping_blend_read(reader, &hmd->curve_mapping);
}
StripModifierTypeInfo seqModifierType_HueCorrect = {
/*idname*/ "HueCorrect",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Hue Correct"),
/*struct_name*/ "HueCorrectModifierData",
/*struct_size*/ sizeof(HueCorrectModifierData),
/*init_data*/ hue_correct_init_data,
/*free_data*/ hue_correct_free_data,
/*copy_data*/ hue_correct_copy_data,
/*apply*/ hue_correct_apply,
/*panel_register*/ hue_correct_register,
/*blend_write*/ hue_correct_write,
/*blend_read*/ hue_correct_read,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,104 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_math_base.h"
#include "BLI_math_matrix.hh"
#include "BLT_translation.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "PRF_profile.hh"
#include "SEQ_modifier.hh"
#include "SEQ_render.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "modifier.hh"
#include "render.hh"
namespace blender::seq {
struct MaskApplyOp {
template<typename ImageT, typename MaskSampler>
void apply(ImageT *image, MaskSampler &mask, int image_x, IndexRange y_range)
{
image += y_range.first() * image_x * 4;
for (int64_t y : y_range) {
mask.begin_row(y);
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
float m = mask.load_mask_min();
if constexpr (std::is_same_v<ImageT, uchar>) {
/* Byte buffer is straight, so only affect on alpha itself, this is
* the only way to alpha-over byte strip after applying mask modifier. */
image[3] = uchar(image[3] * m);
}
else if constexpr (std::is_same_v<ImageT, float>) {
/* Float buffers are pre-multiplied, so need to pre-multiply color as well to make it
* easy to alpha-over masked strip. */
float4 pix(image);
pix *= m;
*reinterpret_cast<float4 *>(image) = pix;
}
image += 4;
}
}
}
};
static void maskmodifier_apply(ModifierApplyContext &context, StripModifierData *smd)
{
PRF_scope_with_name("SeqModMask", ProfileCategory::Draw);
ImBuf *mask = modifier_render_mask_input(context, *smd);
if (mask != nullptr && (mask->byte_data() != nullptr || mask->float_data() != nullptr)) {
ensure_ibuf_is_sequencer_space(context.render_data.scene, context.result.image, false);
MaskApplyOp op;
apply_modifier_op(op, context.result.image, mask, context.transform);
/* Image has gained transparency. */
context.result.image->color_mode = ImColorMode::RGBA;
}
if (mask != nullptr) {
IMB_freeImBuf(mask);
}
}
static void maskmodifier_panel_draw(const bContext *C, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
draw_mask_input_type_settings(C, layout, ptr);
}
static void maskmodifier_register(ARegionType *region_type)
{
modifier_panel_register(region_type, eSeqModifierType_Mask, maskmodifier_panel_draw);
}
StripModifierTypeInfo seqModifierType_Mask = {
/*idname*/ "Mask",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Mask"),
/*struct_name*/ "SequencerMaskModifierData",
/*struct_size*/ sizeof(SequencerMaskModifierData),
/*init_data*/ nullptr,
/*free_data*/ nullptr,
/*copy_data*/ nullptr,
/*apply*/ maskmodifier_apply,
/*panel_register*/ maskmodifier_register,
/*blend_write*/ nullptr,
/*blend_read*/ nullptr,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,31 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLT_translation.hh"
#include "DNA_sequence_types.h"
#include "SEQ_modifier.hh"
namespace blender::seq {
StripModifierTypeInfo seqModifierType_None = {
/*idname*/ "None",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "None"),
/*struct_name*/ "StripModifierData",
/*struct_size*/ sizeof(StripModifierData),
/*init_data*/ nullptr,
/*free_data*/ nullptr,
/*copy_data*/ nullptr,
/*apply*/ nullptr,
/*panel_register*/ nullptr,
/*blend_write*/ nullptr,
/*blend_read*/ nullptr,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,66 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLT_translation.hh"
#include "DNA_sequence_types.h"
#include "SEQ_modifier.hh"
#include "RNA_access.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "modifier.hh"
namespace blender::seq {
static void echomodifier_init_data(StripModifierData *smd)
{
EchoModifierData *emd = reinterpret_cast<EchoModifierData *>(smd);
emd->delay = 1.0f;
emd->feedback = 0.5f;
emd->mix = 0.5f;
}
static void echomodifier_draw(const bContext * /*C*/, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
layout.use_property_split_set(true);
ui::Layout &col = layout.column(false);
col.prop(ptr, "delay", UI_ITEM_NONE, std::nullopt, ICON_NONE);
col.prop(ptr, "feedback", UI_ITEM_NONE, std::nullopt, ICON_NONE);
col.prop(ptr, "mix", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
static void echomodifier_register(ARegionType *region_type)
{
modifier_panel_register(region_type, eSeqModifierType_Echo, echomodifier_draw);
}
StripModifierTypeInfo seqModifierType_Echo = {
/*idname*/ "Echo",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Echo"),
/*struct_name*/ "EchoModifierData",
/*struct_size*/ sizeof(EchoModifierData),
/*init_data*/ echomodifier_init_data,
/*free_data*/ nullptr,
/*copy_data*/ nullptr,
/*apply*/ nullptr,
/*panel_register*/ echomodifier_register,
/*blend_write*/ nullptr,
/*blend_read*/ nullptr,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,96 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <fmt/format.h>
#include "BKE_colortools.hh"
#include "BLI_listbase.h"
#include "BLO_read_write.hh"
#include "BLT_translation.hh"
#include "DNA_sequence_types.h"
#include "SEQ_modifier.hh"
#include "SEQ_sound.hh"
#include "RNA_access.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "modifier.hh"
namespace blender::seq {
static void sound_equalizermodifier_draw(const bContext * /*C*/, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
layout.use_property_split_set(true);
ui::Layout &flow = layout.grid_flow(true, 0, true, false, false);
RNA_BEGIN (ptr, sound_eq, "graphics") {
PointerRNA curve_mapping = RNA_pointer_get(&sound_eq, "curve_mapping");
const float clip_min_x = RNA_float_get(&curve_mapping, "clip_min_x");
const float clip_max_x = RNA_float_get(&curve_mapping, "clip_max_x");
ui::Layout &col = flow.column(false);
ui::Layout &split = col.split(0.4f, false);
split.label(fmt::format("{:.2f}", clip_min_x), ICON_NONE);
split.label("Hz", ICON_NONE);
split.alignment_set(ui::LayoutAlign::Right);
split.label(fmt::format("{:.2f}", clip_max_x), ICON_NONE);
template_curve_mapping(&col, &sound_eq, "curve_mapping", 0, false, true, true, false, false);
ui::Layout &row = col.row(false);
row.alignment_set(ui::LayoutAlign::Center);
row.label("dB", ICON_NONE);
}
RNA_END;
}
static void sound_equalizermodifier_register(ARegionType *region_type)
{
modifier_panel_register(
region_type, eSeqModifierType_SoundEqualizer, sound_equalizermodifier_draw);
}
static void sound_equalizermodifier_write(BlendWriter *writer, const StripModifierData *smd)
{
const SoundEqualizerModifierData *semd = reinterpret_cast<const SoundEqualizerModifierData *>(
smd);
for (EQCurveMappingData &eqcmd : semd->graphics) {
writer->write_struct_by_name("EQCurveMappingData", &eqcmd);
BKE_curvemapping_blend_write(writer, &eqcmd.curve_mapping);
}
}
static void sound_equalizermodifier_read(BlendDataReader *reader, StripModifierData *smd)
{
SoundEqualizerModifierData *semd = reinterpret_cast<SoundEqualizerModifierData *>(smd);
BLO_read_struct_list(reader, EQCurveMappingData, &semd->graphics);
for (EQCurveMappingData &eqcmd : semd->graphics) {
BKE_curvemapping_blend_read(reader, &eqcmd.curve_mapping);
}
}
StripModifierTypeInfo seqModifierType_SoundEqualizer = {
/*idname*/ "SoundEqualizer",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Equalizer"),
/*struct_name*/ "SoundEqualizerModifierData",
/*struct_size*/ sizeof(SoundEqualizerModifierData),
/*init_data*/ sound_equalizermodifier_init_data,
/*free_data*/ sound_equalizermodifier_free,
/*copy_data*/ sound_equalizermodifier_copy_data,
/*apply*/ nullptr,
/*panel_register*/ sound_equalizermodifier_register,
/*blend_write*/ sound_equalizermodifier_write,
/*blend_read*/ sound_equalizermodifier_read,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,79 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLT_translation.hh"
#include <fmt/format.h>
#include "DNA_sequence_types.h"
#include "SEQ_modifier.hh"
#include "RNA_access.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "modifier.hh"
namespace blender::seq {
static void pitchmodifier_init_data(StripModifierData *smd)
{
PitchModifierData *pmd = reinterpret_cast<PitchModifierData *>(smd);
pmd->mode = ePitchMode::PITCH_MODE_SEMITONES;
pmd->semitones = 0;
pmd->cents = 0;
pmd->ratio = 1;
pmd->preserve_formant = false;
pmd->quality = ePitchQuality::PITCH_QUALITY_HIGH;
}
static void pitchmodifier_draw(const bContext * /*C*/, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
layout.use_property_split_set(true);
ui::Layout &col = layout.column(false);
col.prop(ptr, "mode", UI_ITEM_NONE, std::nullopt, ICON_NONE);
int mode = RNA_enum_get(ptr, "mode");
if (mode == ePitchMode::PITCH_MODE_SEMITONES) {
col.prop(ptr, "semitones", UI_ITEM_NONE, std::nullopt, ICON_NONE);
col.prop(ptr, "cents", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
else if (mode == ePitchMode::PITCH_MODE_RATIO) {
col.prop(ptr, "ratio", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
col.prop(ptr, "preserve_formant", UI_ITEM_NONE, std::nullopt, ICON_NONE);
col.prop(ptr, "quality", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
static void pitchmodifier_register(ARegionType *region_type)
{
modifier_panel_register(region_type, eSeqModifierType_Pitch, pitchmodifier_draw);
}
StripModifierTypeInfo seqModifierType_Pitch = {
/*idname*/ "Pitch",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Pitch"),
/*struct_name*/ "PitchModifierData",
/*struct_size*/ sizeof(PitchModifierData),
/*init_data*/ pitchmodifier_init_data,
/*free_data*/ nullptr,
/*copy_data*/ nullptr,
/*apply*/ nullptr,
/*panel_register*/ pitchmodifier_register,
/*blend_write*/ nullptr,
/*blend_read*/ nullptr,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,377 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_array.hh"
#include "BLT_translation.hh"
#include "DNA_sequence_types.h"
#include "IMB_colormanagement.hh"
#include "PRF_profile.hh"
#include "SEQ_modifier.hh"
#include "SEQ_render.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "RNA_access.hh"
#include "modifier.hh"
#include "render.hh"
namespace blender::seq {
struct AvgLogLum {
const SequencerTonemapModifierData *tmmd;
float al;
float auto_key;
float lav;
float3 cav;
float igm;
};
static void tonemapmodifier_init_data(StripModifierData *smd)
{
SequencerTonemapModifierData *tmmd = reinterpret_cast<SequencerTonemapModifierData *>(smd);
/* Same as tone-map compositor node. */
tmmd->type = SEQ_TONEMAP_RD_PHOTORECEPTOR;
tmmd->key = 0.18f;
tmmd->offset = 1.0f;
tmmd->gamma = 1.0f;
tmmd->intensity = 0.0f;
tmmd->contrast = 0.0f;
tmmd->adaptation = 1.0f;
tmmd->correction = 0.0f;
}
/* Convert chunk of float image pixels to scene linear space, in-place. */
static void pixels_to_scene_linear_float(const ColorSpace *colorspace,
float4 *pixels,
int64_t count)
{
IMB_colormanagement_colorspace_to_scene_linear(
reinterpret_cast<float *>(pixels), int(count), 1, 4, colorspace, false);
}
/* Convert chunk of byte image pixels to scene linear space, into a destination array. */
static void pixels_to_scene_linear_byte(const ColorSpace *colorspace,
const uchar *pixels,
float4 *dst,
int64_t count)
{
const uchar *bptr = pixels;
float4 *dst_ptr = dst;
for (int64_t i = 0; i < count; i++) {
straight_uchar_to_premul_float(*dst_ptr, bptr);
bptr += 4;
dst_ptr++;
}
IMB_colormanagement_colorspace_to_scene_linear(
reinterpret_cast<float *>(dst), int(count), 1, 4, colorspace, false);
}
static void scene_linear_to_image_chunk_byte(float4 *src, ImBuf *ibuf, IndexRange range)
{
const ColorSpace *colorspace = ibuf->byte_buffer.colorspace;
IMB_colormanagement_scene_linear_to_colorspace(
reinterpret_cast<float *>(src), int(range.size()), 1, 4, colorspace);
const float4 *src_ptr = src;
uchar *bptr = ibuf->byte_data_for_write();
for (const int64_t idx : range) {
premul_float_to_straight_uchar(bptr + idx * 4, *src_ptr);
src_ptr++;
}
}
struct AreaLuminance {
int64_t pixel_count = 0;
double sum = 0.0f;
float3 color_sum = {0, 0, 0};
double log_sum = 0.0;
float min = FLT_MAX;
float max = -FLT_MAX;
};
static void scene_linear_to_image_chunk_float(ImBuf *ibuf, IndexRange range)
{
const ColorSpace *colorspace = ibuf->float_buffer.colorspace;
float4 *fptr = reinterpret_cast<float4 *>(ibuf->float_data_for_write());
IMB_colormanagement_scene_linear_to_colorspace(
reinterpret_cast<float *>(fptr + range.first()), int(range.size()), 1, 4, colorspace);
}
template<typename MaskSampler>
static void tonemap_simple(
float4 *scene_linear, MaskSampler &mask, int image_x, IndexRange y_range, const AvgLogLum &avg)
{
for (int64_t y : y_range) {
mask.begin_row(y);
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
float4 input = *scene_linear;
/* Apply correction. */
float3 pixel = input.xyz() * avg.al;
float3 d = pixel + avg.tmmd->offset;
pixel.x /= (d.x == 0.0f) ? 1.0f : d.x;
pixel.y /= (d.y == 0.0f) ? 1.0f : d.y;
pixel.z /= (d.z == 0.0f) ? 1.0f : d.z;
const float igm = avg.igm;
if (igm != 0.0f) {
pixel.x = powf(math::max(pixel.x, 0.0f), igm);
pixel.y = powf(math::max(pixel.y, 0.0f), igm);
pixel.z = powf(math::max(pixel.z, 0.0f), igm);
}
/* Apply mask. */
float4 result(pixel.x, pixel.y, pixel.z, input.w);
mask.apply_mask(input, result);
*scene_linear = result;
scene_linear++;
}
}
}
template<typename MaskSampler>
static void tonemap_rd_photoreceptor(
float4 *scene_linear, MaskSampler &mask, int image_x, IndexRange y_range, const AvgLogLum &avg)
{
const float f = expf(-avg.tmmd->intensity);
const float m = (avg.tmmd->contrast > 0.0f) ? avg.tmmd->contrast :
(0.3f + 0.7f * powf(avg.auto_key, 1.4f));
const float ic = 1.0f - avg.tmmd->correction, ia = 1.0f - avg.tmmd->adaptation;
for (int64_t y : y_range) {
mask.begin_row(y);
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
float4 input = *scene_linear;
/* Apply correction. */
float3 pixel = input.xyz();
const float L = IMB_colormanagement_get_luminance(pixel);
float I_l = pixel.x + ic * (L - pixel.x);
float I_g = avg.cav.x + ic * (avg.lav - avg.cav.x);
float I_a = I_l + ia * (I_g - I_l);
pixel.x /= std::max(pixel.x + powf(f * I_a, m), 1.0e-30f);
I_l = pixel.y + ic * (L - pixel.y);
I_g = avg.cav.y + ic * (avg.lav - avg.cav.y);
I_a = I_l + ia * (I_g - I_l);
pixel.y /= std::max(pixel.y + powf(f * I_a, m), 1.0e-30f);
I_l = pixel.z + ic * (L - pixel.z);
I_g = avg.cav.z + ic * (avg.lav - avg.cav.z);
I_a = I_l + ia * (I_g - I_l);
pixel.z /= std::max(pixel.z + powf(f * I_a, m), 1.0e-30f);
/* Apply mask. */
float4 result(pixel.x, pixel.y, pixel.z, input.w);
mask.apply_mask(input, result);
*scene_linear = result;
scene_linear++;
}
}
}
struct TonemapApplyOp {
AreaLuminance lum;
AvgLogLum data;
eModTonemapType type;
ImBuf *ibuf;
template<typename ImageT, typename MaskSampler>
void apply(ImageT *image, MaskSampler &mask, int image_x, IndexRange y_range)
{
const IndexRange pixel_range(y_range.first() * image_x, y_range.size() * image_x);
if constexpr (std::is_same_v<ImageT, float>) {
/* Float pixels: no need for temporary storage. Luminance calculation already converted
* data to scene linear. */
float4 *pixels = (float4 *)(image + y_range.first() * image_x * 4);
if (this->type == SEQ_TONEMAP_RD_PHOTORECEPTOR) {
tonemap_rd_photoreceptor(pixels, mask, image_x, y_range, data);
}
else {
BLI_assert(this->type == SEQ_TONEMAP_RH_SIMPLE);
tonemap_simple(pixels, mask, image_x, y_range, data);
}
scene_linear_to_image_chunk_float(this->ibuf, pixel_range);
}
else {
/* Byte pixels: temporary storage for scene linear pixel values. */
Array<float4> scene_linear(pixel_range.size());
pixels_to_scene_linear_byte(ibuf->byte_buffer.colorspace,
ibuf->byte_data() + pixel_range.first() * 4,
scene_linear.data(),
pixel_range.size());
if (this->type == SEQ_TONEMAP_RD_PHOTORECEPTOR) {
tonemap_rd_photoreceptor(scene_linear.data(), mask, image_x, y_range, data);
}
else {
BLI_assert(this->type == SEQ_TONEMAP_RH_SIMPLE);
tonemap_simple(scene_linear.data(), mask, image_x, y_range, data);
}
scene_linear_to_image_chunk_byte(scene_linear.data(), this->ibuf, pixel_range);
}
}
};
static void tonemap_calc_chunk_luminance(const int width,
const IndexRange y_range,
const float4 *scene_linear,
AreaLuminance &r_lum)
{
for ([[maybe_unused]] const int y : y_range) {
for (int x = 0; x < width; x++) {
float4 pixel = *scene_linear;
r_lum.pixel_count++;
float L = IMB_colormanagement_get_luminance(pixel);
r_lum.sum += L;
r_lum.color_sum.x += pixel.x;
r_lum.color_sum.y += pixel.y;
r_lum.color_sum.z += pixel.z;
r_lum.log_sum += logf(math::max(L, 0.0f) + 1e-5f);
r_lum.max = math::max(r_lum.max, L);
r_lum.min = math::min(r_lum.min, L);
scene_linear++;
}
}
}
static AreaLuminance tonemap_calc_input_luminance(ImBuf *ibuf)
{
float *float_data = ibuf->float_data_for_write();
AreaLuminance lum;
lum = threading::parallel_reduce(
IndexRange(ibuf->y),
32,
lum,
/* Calculate luminance for a chunk. */
[&](const IndexRange y_range, const AreaLuminance &init) {
AreaLuminance lum = init;
const int64_t chunk_size = y_range.size() * ibuf->x;
/* For float images, convert to scene-linear in place. The rest
* of tone-mapper can then continue with scene-linear values. */
if (float_data != nullptr) {
float4 *fptr = reinterpret_cast<float4 *>(float_data);
fptr += y_range.first() * ibuf->x;
pixels_to_scene_linear_float(ibuf->float_buffer.colorspace, fptr, chunk_size);
tonemap_calc_chunk_luminance(ibuf->x, y_range, fptr, lum);
}
else {
const uchar *bptr = ibuf->byte_data() + y_range.first() * ibuf->x * 4;
Array<float4> scene_linear(chunk_size);
pixels_to_scene_linear_byte(
ibuf->byte_buffer.colorspace, bptr, scene_linear.data(), chunk_size);
tonemap_calc_chunk_luminance(ibuf->x, y_range, scene_linear.data(), lum);
}
return lum;
},
/* Reduce luminance results. */
[&](const AreaLuminance &a, const AreaLuminance &b) {
AreaLuminance res;
res.pixel_count = a.pixel_count + b.pixel_count;
res.sum = a.sum + b.sum;
res.color_sum = a.color_sum + b.color_sum;
res.log_sum = a.log_sum + b.log_sum;
res.min = math::min(a.min, b.min);
res.max = math::max(a.max, b.max);
return res;
});
return lum;
}
static void tonemapmodifier_apply(ModifierApplyContext &context, StripModifierData *smd)
{
PRF_scope_with_name("SeqModTonemap", ProfileCategory::Draw);
ensure_ibuf_is_sequencer_space(context.render_data.scene, context.result.image, false);
ImBuf *mask = modifier_render_mask_input(context, *smd);
const SequencerTonemapModifierData *tmmd =
reinterpret_cast<const SequencerTonemapModifierData *>(smd);
TonemapApplyOp op;
op.type = tmmd->type;
op.ibuf = context.result.image;
op.lum = tonemap_calc_input_luminance(context.result.image);
if (op.lum.pixel_count == 0) {
return; /* Strip is zero size or off-screen. */
}
op.data.tmmd = tmmd;
op.data.lav = op.lum.sum / op.lum.pixel_count;
op.data.cav.x = op.lum.color_sum.x / op.lum.pixel_count;
op.data.cav.y = op.lum.color_sum.y / op.lum.pixel_count;
op.data.cav.z = op.lum.color_sum.z / op.lum.pixel_count;
float maxl = log(double(op.lum.max) + 1e-5f);
float minl = log(double(op.lum.min) + 1e-5f);
float avl = op.lum.log_sum / op.lum.pixel_count;
op.data.auto_key = (maxl > minl) ? ((maxl - avl) / (maxl - minl)) : 1.0f;
float al = exp(double(avl));
op.data.al = (al == 0.0f) ? 0.0f : (tmmd->key / al);
op.data.igm = (tmmd->gamma == 0.0f) ? 1.0f : (1.0f / tmmd->gamma);
apply_modifier_op(op, context.result.image, mask, context.transform);
if (mask != nullptr) {
IMB_freeImBuf(mask);
}
}
static void tonemapmodifier_panel_draw(const bContext *C, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
const int tonemap_type = RNA_enum_get(ptr, "tonemap_type");
layout.use_property_split_set(true);
ui::Layout &col = layout.column(false);
col.prop(ptr, "tonemap_type", UI_ITEM_NONE, std::nullopt, ICON_NONE);
if (tonemap_type == SEQ_TONEMAP_RD_PHOTORECEPTOR) {
col.prop(ptr, "intensity", UI_ITEM_NONE, std::nullopt, ICON_NONE);
col.prop(ptr, "contrast", UI_ITEM_NONE, std::nullopt, ICON_NONE);
col.prop(ptr, "adaptation", UI_ITEM_NONE, std::nullopt, ICON_NONE);
col.prop(ptr, "correction", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
else if (tonemap_type == SEQ_TONEMAP_RH_SIMPLE) {
col.prop(ptr, "key", UI_ITEM_NONE, std::nullopt, ICON_NONE);
col.prop(ptr, "offset", UI_ITEM_NONE, std::nullopt, ICON_NONE);
col.prop(ptr, "gamma", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
else {
BLI_assert_unreachable();
}
if (ui::Layout *mask_input_layout = layout.panel_prop(
C, ptr, "open_mask_input_panel", IFACE_("Mask Input")))
{
draw_mask_input_type_settings(C, *mask_input_layout, ptr);
}
}
static void tonemapmodifier_register(ARegionType *region_type)
{
modifier_panel_register(region_type, eSeqModifierType_Tonemap, tonemapmodifier_panel_draw);
}
StripModifierTypeInfo seqModifierType_Tonemap = {
/*idname*/ "Tonemap",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "Tonemap"),
/*struct_name*/ "SequencerTonemapModifierData",
/*struct_size*/ sizeof(SequencerTonemapModifierData),
/*init_data*/ tonemapmodifier_init_data,
/*free_data*/ nullptr,
/*copy_data*/ nullptr,
/*apply*/ tonemapmodifier_apply,
/*panel_register*/ tonemapmodifier_register,
/*blend_write*/ nullptr,
/*blend_read*/ nullptr,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,122 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_math_vector.h"
#include "BLT_translation.hh"
#include "DNA_sequence_types.h"
#include "PRF_profile.hh"
#include "SEQ_modifier.hh"
#include "SEQ_render.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "modifier.hh"
#include "render.hh"
namespace blender::seq {
static void whiteBalance_init_data(StripModifierData *smd)
{
WhiteBalanceModifierData *cbmd = reinterpret_cast<WhiteBalanceModifierData *>(smd);
copy_v3_fl(cbmd->white_value, 1.0f);
}
struct WhiteBalanceApplyOp {
float multiplier[3];
template<typename ImageT, typename MaskSampler>
void apply(ImageT *image, MaskSampler &mask, int image_x, IndexRange y_range)
{
image += y_range.first() * image_x * 4;
for (int64_t y : y_range) {
mask.begin_row(y);
for ([[maybe_unused]] int64_t x : IndexRange(image_x)) {
float4 input = load_pixel_premul(image);
float4 result;
result.w = input.w;
#if 0
mul_v3_v3(result, multiplier);
#else
/* similar to division without the clipping */
for (int i = 0; i < 3; i++) {
/* Prevent pow argument from being negative. This whole math
* breaks down overall with any HDR colors; would be good to
* revisit and do something more proper. */
float f = max_ff(1.0f - input[i], 0.0f);
result[i] = 1.0f - powf(f, this->multiplier[i]);
}
#endif
mask.apply_mask(input, result);
store_pixel_premul(result, image);
image += 4;
}
}
}
};
static void whiteBalance_apply(ModifierApplyContext &context, StripModifierData *smd)
{
PRF_scope_with_name("SeqModWhiteBalance", ProfileCategory::Draw);
ensure_ibuf_is_sequencer_space(context.render_data.scene, context.result.image, false);
ImBuf *mask = modifier_render_mask_input(context, *smd);
const WhiteBalanceModifierData *data = reinterpret_cast<const WhiteBalanceModifierData *>(smd);
WhiteBalanceApplyOp op;
op.multiplier[0] = (data->white_value[0] != 0.0f) ? 1.0f / data->white_value[0] : FLT_MAX;
op.multiplier[1] = (data->white_value[1] != 0.0f) ? 1.0f / data->white_value[1] : FLT_MAX;
op.multiplier[2] = (data->white_value[2] != 0.0f) ? 1.0f / data->white_value[2] : FLT_MAX;
apply_modifier_op(op, context.result.image, mask, context.transform);
if (mask != nullptr) {
IMB_freeImBuf(mask);
}
}
static void whiteBalance_panel_draw(const bContext *C, Panel *panel)
{
ui::Layout &layout = *panel->layout;
PointerRNA *ptr = ui::panel_custom_data_get(panel);
layout.use_property_split_set(true);
layout.prop(ptr, "white_value", UI_ITEM_NONE, std::nullopt, ICON_NONE);
if (ui::Layout *mask_input_layout = layout.panel_prop(
C, ptr, "open_mask_input_panel", IFACE_("Mask Input")))
{
draw_mask_input_type_settings(C, *mask_input_layout, ptr);
}
}
static void whiteBalance_register(ARegionType *region_type)
{
modifier_panel_register(region_type, eSeqModifierType_WhiteBalance, whiteBalance_panel_draw);
}
StripModifierTypeInfo seqModifierType_WhiteBalance = {
/*idname*/ "WhiteBalance",
/*name*/ CTX_N_(BLT_I18NCONTEXT_ID_SEQUENCE, "White Balance"),
/*struct_name*/ "WhiteBalanceModifierData",
/*struct_size*/ sizeof(WhiteBalanceModifierData),
/*init_data*/ whiteBalance_init_data,
/*free_data*/ nullptr,
/*copy_data*/ nullptr,
/*apply*/ whiteBalance_apply,
/*panel_register*/ whiteBalance_register,
/*blend_write*/ nullptr,
/*blend_read*/ nullptr,
};
}; // namespace blender::seq

View File

@@ -0,0 +1,731 @@
/* SPDX-FileCopyrightText: 2012-2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bke
*/
#include "BLI_array.hh"
#include "BLI_hash.hh"
#include "BLI_listbase.h"
#include "BLI_rand.hh"
#include "BLI_set.hh"
#include "BLI_string_utf8.h"
#include "BLI_string_utils.hh"
#include "BLI_task.hh"
#include "BLT_translation.hh"
#include "DNA_mask_types.h"
#include "DNA_sequence_types.h"
#include "DNA_space_types.h"
#include "BKE_colortools.hh"
#include "BKE_idprop.hh"
#include "BKE_screen.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "SEQ_modifier.hh"
#include "SEQ_modifiertypes.hh"
#include "SEQ_render.hh"
#include "SEQ_select.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_utils.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "BLO_read_write.hh"
#include "WM_api.hh"
#include "modifier.hh"
#include "render.hh"
namespace blender::seq {
/* -------------------------------------------------------------------- */
static bool modifier_has_persistent_uid(const Strip &strip, int uid)
{
for (StripModifierData &smd : strip.modifiers) {
if (smd.persistent_uid == uid) {
return true;
}
}
return false;
}
void modifier_persistent_uid_init(const Strip &strip, StripModifierData &smd)
{
uint64_t hash = get_default_hash(StringRef(smd.name));
RandomNumberGenerator rng{uint32_t(hash)};
while (true) {
const int new_uid = rng.get_int32();
if (new_uid <= 0) {
continue;
}
if (modifier_has_persistent_uid(strip, new_uid)) {
continue;
}
smd.persistent_uid = new_uid;
break;
}
}
bool modifier_persistent_uids_are_valid(const Strip &strip)
{
Set<int> uids;
int modifiers_num = 0;
for (StripModifierData &smd : strip.modifiers) {
if (smd.persistent_uid <= 0) {
return false;
}
uids.add(smd.persistent_uid);
modifiers_num++;
}
if (uids.size() != modifiers_num) {
return false;
}
return true;
}
static void modifier_ops_extra_draw(bContext *C, ui::Layout *layout, void *smd_v)
{
Scene *sequencer_scene = CTX_data_sequencer_scene(C);
Strip *strip = seq::select_active_get(sequencer_scene);
if (!strip) {
return;
}
StripModifierData *smd = static_cast<StripModifierData *>(smd_v);
PointerRNA mod_ptr = RNA_pointer_create_discrete(&sequencer_scene->id, RNA_StripModifier, smd);
PointerRNA op_ptr;
/* Duplicate. */
op_ptr = layout->op("SEQUENCER_OT_strip_modifier_duplicate",
CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Duplicate"),
ICON_DUPLICATE);
RNA_string_set(&op_ptr, "modifier", smd->name);
/* Copy to selected. */
op_ptr = layout->op("SEQUENCER_OT_strip_modifier_copy",
CTX_IFACE_(BLT_I18NCONTEXT_OPERATOR_DEFAULT, "Copy to Selected"),
0);
RNA_enum_set(&op_ptr, "type", /*SEQ_MODIFIER_COPY_APPEND*/ 1);
RNA_string_set(&op_ptr, "modifier", smd->name);
layout->separator();
/* Move to first. */
{
ui::Layout &row = layout->row(false);
op_ptr = row.op("SEQUENCER_OT_strip_modifier_move_to_index",
IFACE_("Move to First"),
ICON_TRIA_UP,
wm::OpCallContext::InvokeDefault,
UI_ITEM_NONE);
RNA_string_set(&op_ptr, "modifier", smd->name);
RNA_int_set(&op_ptr, "index", 0);
row.enabled_set(smd->prev != nullptr);
}
/* Move to last. */
{
ui::Layout &row = layout->row(false);
op_ptr = row.op("SEQUENCER_OT_strip_modifier_move_to_index",
IFACE_("Move to Last"),
ICON_TRIA_DOWN,
wm::OpCallContext::InvokeDefault,
UI_ITEM_NONE);
RNA_string_set(&op_ptr, "modifier", smd->name);
RNA_int_set(&op_ptr, "index", strip->modifiers.count() - 1);
row.enabled_set(smd->next != nullptr);
}
if (smd->type == eSeqModifierType_Compositor) {
layout->separator();
layout->prop(&mod_ptr, "show_group_selector", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
}
static void modifier_panel_header(const bContext * /*C*/, Panel *panel)
{
ui::Layout &layout = *panel->layout;
/* Don't use #modifier_panel_get_property_pointers, we don't want to lock the header. */
PointerRNA *ptr = ui::panel_custom_data_get(panel);
StripModifierData *smd = static_cast<StripModifierData *>(ptr->data);
ui::panel_context_pointer_set(panel, "modifier", ptr);
/* Modifier Icon. */
ui::Layout *sub = &layout.row(true);
sub->emboss_set(ui::EmbossType::None);
PointerRNA active_op_ptr = sub->op(
"SEQUENCER_OT_strip_modifier_set_active", "", RNA_struct_ui_icon(ptr->type));
RNA_string_set(&active_op_ptr, "modifier", smd->name);
ui::Layout &row = layout.row(true);
/* Modifier Name.
* Count how many buttons are added to the header to check if there is enough space. */
int buttons_number = 0;
ui::Layout &name_row = row.row(true);
if (!smd->is_type_sound()) {
sub = &row.row(true);
sub->prop(ptr, "show_preview", UI_ITEM_NONE, "", ICON_NONE);
buttons_number++;
}
sub = &row.row(true);
sub->prop(ptr, "enable", UI_ITEM_NONE, "", ICON_NONE);
buttons_number++;
/* Extra operators menu. */
row.menu_fn("", ICON_DOWNARROW_HLT, modifier_ops_extra_draw, smd);
/* Delete button. */
sub = &row.row(false);
sub->emboss_set(ui::EmbossType::None);
PointerRNA remove_op_ptr = sub->op("SEQUENCER_OT_strip_modifier_remove", "", ICON_X);
RNA_string_set(&remove_op_ptr, "name", smd->name);
buttons_number++;
bool display_name = (panel->sizex / UI_UNIT_X - buttons_number > 5) || (panel->sizex == 0);
if (display_name) {
name_row.prop(ptr, "name", UI_ITEM_NONE, "", ICON_NONE);
}
else {
row.alignment_set(ui::LayoutAlign::Right);
}
/* Extra padding for delete button. */
layout.separator();
}
void draw_mask_input_type_settings(const bContext *C, ui::Layout &layout, PointerRNA *ptr)
{
Scene *sequencer_scene = CTX_data_sequencer_scene(C);
Editing *ed = seq::editing_get(sequencer_scene);
const int input_mask_type = RNA_enum_get(ptr, "input_mask_type");
layout.use_property_split_set(true);
ui::Layout &col = layout.column(false);
ui::Layout *row = &col.row(true);
row->prop(ptr, "input_mask_type", ui::ITEM_R_EXPAND, IFACE_("Type"), ICON_NONE);
if (input_mask_type == STRIP_MASK_INPUT_STRIP) {
PointerRNA sequences_object = RNA_pointer_create_discrete(
&sequencer_scene->id, RNA_SequenceEditor, ed);
col.prop_search(
ptr, "input_mask_strip", &sequences_object, "strips_all", IFACE_("Mask"), ICON_NONE);
}
else {
col.prop(ptr, "input_mask_id", UI_ITEM_NONE, std::nullopt, ICON_NONE);
row = &col.row(true);
row->prop(ptr, "mask_time", ui::ITEM_R_EXPAND, std::nullopt, ICON_NONE);
}
}
bool modifier_ui_poll(const bContext *C, PanelType * /*pt*/)
{
Scene *sequencer_scene = CTX_data_sequencer_scene(C);
if (!sequencer_scene) {
return false;
}
Strip *active_strip = seq::select_active_get(sequencer_scene);
return active_strip != nullptr;
}
/**
* Move a modifier to the index it's moved to after a drag and drop.
*/
static void modifier_reorder(bContext *C, Panel *panel, const int new_index)
{
PointerRNA *smd_ptr = ui::panel_custom_data_get(panel);
StripModifierData *smd = static_cast<StripModifierData *>(smd_ptr->data);
wmOperatorType *ot = WM_operatortype_find("SEQUENCER_OT_strip_modifier_move_to_index", false);
PointerRNA props_ptr = WM_operator_properties_create_ptr(ot);
RNA_string_set(&props_ptr, "modifier", smd->name);
RNA_int_set(&props_ptr, "index", new_index);
WM_operator_name_call_ptr(C, ot, wm::OpCallContext::InvokeDefault, &props_ptr, nullptr);
WM_operator_properties_free(&props_ptr);
}
static short get_strip_modifier_expand_flag(const bContext * /*C*/, Panel *panel)
{
PointerRNA *smd_ptr = ui::panel_custom_data_get(panel);
StripModifierData *smd = static_cast<StripModifierData *>(smd_ptr->data);
return smd->ui_expand_flag;
}
static void set_strip_modifier_expand_flag(const bContext * /*C*/, Panel *panel, short expand_flag)
{
PointerRNA *smd_ptr = ui::panel_custom_data_get(panel);
StripModifierData *smd = static_cast<StripModifierData *>(smd_ptr->data);
smd->ui_expand_flag = expand_flag;
}
PanelType *modifier_panel_register(ARegionType *region_type,
const eStripModifierType type,
PanelDrawFn draw)
{
PanelType *panel_type = MEM_new_zeroed<PanelType>(__func__);
modifier_type_panel_id(type, panel_type->idname);
STRNCPY_UTF8(panel_type->label, "");
STRNCPY_UTF8(panel_type->translation_context, BLT_I18NCONTEXT_DEFAULT_BPYRNA);
STRNCPY_UTF8(panel_type->active_property, "is_active");
STRNCPY_UTF8(panel_type->context, "strip_modifier");
panel_type->draw_header = modifier_panel_header;
panel_type->draw = draw;
panel_type->poll = modifier_ui_poll;
/* Give the panel the special flag that says it was built here and corresponds to a
* modifier rather than a #PanelType. */
panel_type->flag = PANEL_TYPE_HEADER_EXPAND | PANEL_TYPE_INSTANCED;
panel_type->reorder = modifier_reorder;
panel_type->get_list_data_expand_flag = get_strip_modifier_expand_flag;
panel_type->set_list_data_expand_flag = set_strip_modifier_expand_flag;
BLI_addtail(&region_type->paneltypes, panel_type);
return panel_type;
}
/* -------------------------------------------------------------------- */
float4 load_pixel_premul(const uchar *ptr)
{
float4 res;
straight_uchar_to_premul_float(res, ptr);
return res;
}
float4 load_pixel_premul(const float *ptr)
{
return float4(ptr);
}
void store_pixel_premul(float4 pix, uchar *ptr)
{
premul_float_to_straight_uchar(ptr, pix);
}
void store_pixel_premul(float4 pix, float *ptr)
{
*reinterpret_cast<float4 *>(ptr) = pix;
}
float4 load_pixel_raw(const uchar *ptr)
{
float4 res;
rgba_uchar_to_float(res, ptr);
return res;
}
float4 load_pixel_raw(const float *ptr)
{
return float4(ptr);
}
void store_pixel_raw(float4 pix, uchar *ptr)
{
rgba_float_to_uchar(ptr, pix);
}
void store_pixel_raw(float4 pix, float *ptr)
{
*reinterpret_cast<float4 *>(ptr) = pix;
}
ImBuf *modifier_render_mask_input(const ModifierApplyContext &context,
const StripModifierData &smd)
{
ImBuf *mask = nullptr;
if (smd.mask_input_type == STRIP_MASK_INPUT_STRIP) {
if (smd.mask_strip) {
mask = seq_render_strip(&context.render_data,
&context.render_state,
smd.mask_strip,
context.timeline_frame)
.image;
}
}
else if (smd.mask_input_type == STRIP_MASK_INPUT_ID) {
int frame_offset = 0;
if (smd.mask_time == STRIP_MASK_TIME_RELATIVE) {
frame_offset = context.strip.start;
}
else if (smd.mask_time == STRIP_MASK_TIME_ABSOLUTE) {
frame_offset = smd.mask_id ? smd.mask_id->sfra : 0;
}
/* Note that we do not request mask to be float image: if it is that is
* fine, but if it is a byte image then we also just take that without
* extra memory allocations or conversions. All modifiers are expected
* to handle mask being either type. */
mask = seq_render_mask(context.render_data.depsgraph,
context.render_data.rectx,
context.render_data.recty,
smd.mask_id,
context.timeline_frame - frame_offset,
false);
}
return mask;
}
/* -------------------------------------------------------------------- */
/** \name Public Modifier Functions
* \{ */
static StripModifierTypeInfo *modifiersTypes[NUM_STRIP_MODIFIER_TYPES] = {nullptr};
static void modifier_types_init(StripModifierTypeInfo *types[])
{
#define INIT_TYPE(typeName) (types[eSeqModifierType_##typeName] = &seqModifierType_##typeName)
INIT_TYPE(None);
INIT_TYPE(BrightContrast);
INIT_TYPE(ColorBalance);
INIT_TYPE(Compositor);
INIT_TYPE(Curves);
INIT_TYPE(HueCorrect);
INIT_TYPE(Mask);
INIT_TYPE(SoundEqualizer);
INIT_TYPE(Pitch);
INIT_TYPE(Echo);
INIT_TYPE(Tonemap);
INIT_TYPE(WhiteBalance);
#undef INIT_TYPE
}
void modifiers_init()
{
modifier_types_init(modifiersTypes);
}
const StripModifierTypeInfo *modifier_type_info_get(eStripModifierType type)
{
if (type <= 0 || type >= NUM_STRIP_MODIFIER_TYPES) {
return nullptr;
}
return modifiersTypes[type];
}
StripModifierData *modifier_new(Strip *strip, const char *name, eStripModifierType type)
{
StripModifierData *smd;
const StripModifierTypeInfo *smti = modifier_type_info_get(type);
smd = static_cast<StripModifierData *>(MEM_new_zeroed(smti->struct_size, "sequence modifier"));
smd->type = type;
smd->flag |= STRIP_MODIFIER_FLAG_EXPANDED | STRIP_MODIFIER_FLAG_SHOW_PREVIEW;
smd->ui_expand_flag |= UI_PANEL_DATA_EXPAND_ROOT;
smd->runtime = MEM_new<StripModifierDataRuntime>(__func__);
if (!name || !name[0]) {
STRNCPY_UTF8(smd->name, CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, smti->name));
}
else {
STRNCPY_UTF8(smd->name, name);
}
BLI_addtail(&strip->modifiers, smd);
if (ELEM(strip->type, STRIP_TYPE_SOUND, STRIP_TYPE_SOUND_HD)) {
strip->runtime->sound_modifiers_count++;
}
modifier_unique_name(strip, smd);
if (smti->init_data) {
smti->init_data(smd);
}
modifier_set_active(strip, smd);
return smd;
}
bool modifier_remove(Strip *strip, StripModifierData *smd)
{
if (BLI_findindex(&strip->modifiers, smd) == -1) {
return false;
}
if (smd->flag & STRIP_MODIFIER_FLAG_ACTIVE) {
/* Prefer the next modifier but use the previous if this modifier is the last in the list. */
if (smd->next != nullptr) {
modifier_set_active(strip, smd->next);
}
else if (smd->prev != nullptr) {
modifier_set_active(strip, smd->prev);
}
}
BLI_remlink(&strip->modifiers, smd);
modifier_free(smd);
return true;
}
void modifier_clear(Strip *strip)
{
StripModifierData *smd, *smd_next;
for (smd = static_cast<StripModifierData *>(strip->modifiers.first); smd; smd = smd_next) {
smd_next = smd->next;
modifier_free(smd);
}
strip->modifiers.clear_no_delete();
}
void modifier_free(StripModifierData *smd)
{
const StripModifierTypeInfo *smti = modifier_type_info_get(smd->type);
if (smti && smti->free_data) {
smti->free_data(smd);
}
if (smd->runtime) {
MEM_delete(smd->runtime);
}
if (smd->system_properties != nullptr) {
IDP_FreeProperty_ex(smd->system_properties, false);
}
MEM_delete(smd);
}
void modifier_unique_name(Strip *strip, StripModifierData *smd)
{
const StripModifierTypeInfo *smti = modifier_type_info_get(smd->type);
BLI_uniquename(&strip->modifiers,
smd,
CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, smti->name),
'.',
offsetof(StripModifierData, name),
sizeof(smd->name));
}
StripModifierData *modifier_find_by_name(Strip *strip, const char *name)
{
return static_cast<StripModifierData *>(
BLI_findstring(&(strip->modifiers), name, offsetof(StripModifierData, name)));
}
static bool skip_modifier(Scene *scene, const StripModifierData *smd, int timeline_frame)
{
using namespace blender::seq;
if (smd->mask_strip == nullptr) {
return false;
}
const bool strip_has_ended_skip = smd->mask_input_type == STRIP_MASK_INPUT_STRIP &&
smd->mask_time == STRIP_MASK_TIME_RELATIVE &&
!smd->mask_strip->intersects_frame(scene, timeline_frame);
const bool missing_data_skip = !strip_has_valid_data(smd->mask_strip) ||
media_presence_is_missing(scene, smd->mask_strip);
return strip_has_ended_skip || missing_data_skip;
}
void modifier_apply_stack(ModifierApplyContext &context)
{
if (context.strip.modifiers.first == nullptr) {
return;
}
for (StripModifierData &smd : context.strip.modifiers) {
const StripModifierTypeInfo *smti = modifier_type_info_get(smd.type);
/* could happen if modifier is being removed or not exists in current version of blender */
if (!smti) {
continue;
}
const bool show_preview = (smd.flag & STRIP_MODIFIER_FLAG_SHOW_PREVIEW) != 0;
const bool show_render = (smd.flag & STRIP_MODIFIER_FLAG_MUTE) == 0;
if (context.render_data.render && !show_render) {
continue;
}
if (!context.render_data.render && !show_preview) {
continue;
}
if (smti->apply && !skip_modifier(context.render_data.scene, &smd, context.timeline_frame)) {
smti->apply(context, &smd);
}
}
}
StripModifierData *modifier_copy(Strip &strip_dst, StripModifierData *mod_src, const int flag)
{
const StripModifierTypeInfo *smti = modifier_type_info_get(mod_src->type);
StripModifierData *mod_new = MEM_dupalloc(mod_src);
mod_new->system_properties = nullptr;
if (mod_src->system_properties) {
mod_new->system_properties = IDP_CopyProperty_ex(mod_src->system_properties, flag);
}
mod_new->runtime = MEM_new<StripModifierDataRuntime>(__func__);
if (smti && smti->copy_data) {
smti->copy_data(mod_new, mod_src);
}
BLI_addtail(&strip_dst.modifiers, mod_new);
BLI_uniquename(&strip_dst.modifiers,
mod_new,
"Strip Modifier",
'.',
offsetof(StripModifierData, name),
sizeof(StripModifierData::name));
return mod_new;
}
void modifier_list_copy(Strip *strip_new, Strip *strip, const int flag)
{
for (StripModifierData &smd : strip->modifiers) {
modifier_copy(*strip_new, &smd, flag);
}
}
bool strip_supports_modifiers(const Strip *strip)
{
return (strip->type != STRIP_TYPE_SOUND);
}
bool modifier_move_to_index(Strip *strip, StripModifierData *smd, const int new_index)
{
const int current_index = BLI_findindex(&strip->modifiers, smd);
return BLI_listbase_move_index(&strip->modifiers, current_index, new_index);
}
StripModifierData *modifier_get_active(const Strip *strip)
{
/* In debug mode, check for only one active modifier. */
#ifndef NDEBUG
int active_count = 0;
for (StripModifierData &smd : strip->modifiers) {
if (smd.flag & STRIP_MODIFIER_FLAG_ACTIVE) {
active_count++;
}
}
BLI_assert(ELEM(active_count, 0, 1));
#endif
for (StripModifierData &smd : strip->modifiers) {
if (smd.flag & STRIP_MODIFIER_FLAG_ACTIVE) {
return &smd;
}
}
return nullptr;
}
void modifier_set_active(Strip *strip, StripModifierData *smd)
{
for (StripModifierData &smd_iter : strip->modifiers) {
smd_iter.flag &= ~STRIP_MODIFIER_FLAG_ACTIVE;
}
if (smd != nullptr) {
BLI_assert(BLI_findindex(&strip->modifiers, smd) != -1);
smd->flag |= STRIP_MODIFIER_FLAG_ACTIVE;
}
}
void modifier_type_panel_id(eStripModifierType type, char *r_idname)
{
const StripModifierTypeInfo *mti = modifier_type_info_get(type);
BLI_string_join(
r_idname, sizeof(PanelType::idname), STRIP_MODIFIER_TYPE_PANEL_PREFIX, mti->idname);
}
void foreach_strip_modifier_id(Strip *strip, const FunctionRef<void(ID *)> fn)
{
for (StripModifierData &smd : strip->modifiers) {
if (smd.mask_id) {
fn(reinterpret_cast<ID *>(smd.mask_id));
}
if (smd.type == eSeqModifierType_Compositor) {
auto *modifier_data = reinterpret_cast<SequencerCompositorModifierData *>(&smd);
if (modifier_data->node_group) {
fn(reinterpret_cast<ID *>(modifier_data->node_group));
}
}
if (smd.system_properties) {
IDP_foreach_property(smd.system_properties, IDP_TYPE_FILTER_ID, [&](IDProperty *id_prop) {
fn((ID *)id_prop->data.pointer);
});
}
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name .blend File I/O
* \{ */
void modifier_blend_write(BlendWriter *writer, ListBaseT<StripModifierData> *modbase)
{
for (StripModifierData &smd : *modbase) {
const StripModifierTypeInfo *smti = modifier_type_info_get(smd.type);
if (smti) {
if (smd.system_properties) {
IDP_BlendWrite(writer, smd.system_properties);
}
writer->write_struct_by_name(smti->struct_name, &smd);
if (smti->blend_write) {
smti->blend_write(writer, &smd);
}
}
else {
writer->write_struct(&smd);
}
}
}
void modifier_blend_read_data(BlendDataReader *reader, ListBaseT<StripModifierData> *lb)
{
BLO_read_struct_list(reader, StripModifierData, lb);
for (StripModifierData &smd : *lb) {
BLO_read_struct(reader, IDProperty, &smd.system_properties);
IDP_BlendDataRead(reader, &smd.system_properties);
if (smd.mask_strip) {
BLO_read_struct(reader, Strip, &smd.mask_strip);
}
const StripModifierTypeInfo *smti = modifier_type_info_get(smd.type);
if (smti && smti->blend_read) {
smti->blend_read(reader, &smd);
}
smd.runtime = MEM_new<StripModifierDataRuntime>(__func__);
}
}
/** \} */
} // namespace blender::seq

View File

@@ -0,0 +1,353 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup sequencer
*/
#include "BLI_math_color.h"
#include "BLI_math_interp.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.hh"
#include "BLI_task.hh"
#include "DNA_sequence_types.h"
#include "IMB_imbuf.hh"
namespace blender {
struct bContext;
struct ARegionType;
struct ImBuf;
struct Strip;
struct Panel;
struct PanelType;
struct PointerRNA;
namespace ui {
struct Layout;
} // namespace ui
namespace seq {
struct RenderData;
struct SeqRenderState;
struct SeqResult;
struct ModifierApplyContext {
ModifierApplyContext(const RenderData &render_data,
SeqRenderState &render_state,
const Strip &strip,
const float3x3 &transform,
const float3x3 &transform_comp_result,
const float timeline_frame,
SeqResult &result)
: render_data(render_data),
render_state(render_state),
strip(strip),
transform(transform),
transform_comp_result(transform_comp_result),
timeline_frame(timeline_frame),
result(result)
{
}
const RenderData &render_data;
SeqRenderState &render_state;
const Strip &strip;
/* Transformation from strip image local pixel coordinates to the
* full render area pixel coordinates.This is used to sample
* modifier masks (since masks are in full render area space). */
const float3x3 transform;
/* Transformation to apply when sampling masks in compositor modifier. */
const float3x3 transform_comp_result;
/* Timeline frame at which the modifiers are being applied at. */
const float timeline_frame;
SeqResult &result;
};
void modifier_apply_stack(ModifierApplyContext &context);
ImBuf *modifier_render_mask_input(const ModifierApplyContext &context,
const StripModifierData &smd);
bool modifier_persistent_uids_are_valid(const Strip &strip);
void draw_mask_input_type_settings(const bContext *C, ui::Layout &layout, PointerRNA *ptr);
bool modifier_ui_poll(const bContext *C, PanelType *pt);
using PanelDrawFn = void (*)(const bContext *, Panel *);
PanelType *modifier_panel_register(ARegionType *region_type,
const eStripModifierType type,
PanelDrawFn draw);
float4 load_pixel_premul(const uchar *ptr);
float4 load_pixel_premul(const float *ptr);
void store_pixel_premul(const float4 pix, uchar *ptr);
void store_pixel_premul(const float4 pix, float *ptr);
float4 load_pixel_raw(const uchar *ptr);
float4 load_pixel_raw(const float *ptr);
void store_pixel_raw(const float4 pix, uchar *ptr);
void store_pixel_raw(const float4 pix, float *ptr);
/* Mask sampler for #apply_modifier_op: no mask is present. */
struct MaskSamplerNone {
void begin_row(int64_t /*y*/) {}
void apply_mask(const float4 /*input*/, float4 & /*result*/) {}
float load_mask_min()
{
return 0.0f;
}
};
/* Mask sampler for #apply_modifier_op: floating point mask,
* same size as input, no transform. */
struct MaskSamplerDirectFloat {
MaskSamplerDirectFloat(const ImBuf *mask) : mask(mask)
{
BLI_assert(mask && mask->float_data());
}
void begin_row(int64_t y)
{
BLI_assert(y >= 0 && y < mask->y);
ptr = mask->float_data() + y * mask->x * 4;
}
void apply_mask(const float4 input, float4 &result)
{
float3 m(this->ptr);
result.x = math::interpolate(input.x, result.x, m.x);
result.y = math::interpolate(input.y, result.y, m.y);
result.z = math::interpolate(input.z, result.z, m.z);
this->ptr += 4;
}
float load_mask_min()
{
float r = std::min({this->ptr[0], this->ptr[1], this->ptr[2]});
this->ptr += 4;
return r;
}
const float *ptr = nullptr;
const ImBuf *mask;
};
/* Mask sampler for #apply_modifier_op: byte mask,
* same size as input, no transform. */
struct MaskSamplerDirectByte {
MaskSamplerDirectByte(const ImBuf *mask) : mask(mask)
{
BLI_assert(mask && mask->byte_data());
}
void begin_row(int64_t y)
{
BLI_assert(y >= 0 && y < mask->y);
ptr = mask->byte_data() + y * mask->x * 4;
}
void apply_mask(const float4 input, float4 &result)
{
float3 m;
rgb_uchar_to_float(m, this->ptr);
result.x = math::interpolate(input.x, result.x, m.x);
result.y = math::interpolate(input.y, result.y, m.y);
result.z = math::interpolate(input.z, result.z, m.z);
this->ptr += 4;
}
float load_mask_min()
{
float r = float(std::min({this->ptr[0], this->ptr[1], this->ptr[2]})) * (1.0f / 255.0f);
this->ptr += 4;
return r;
}
const uchar *ptr = nullptr;
const ImBuf *mask;
};
/* Mask sampler for #apply_modifier_op: floating point mask,
* sample mask with a transform. */
struct MaskSamplerTransformedFloat {
MaskSamplerTransformedFloat(const ImBuf *mask, const float3x3 &transform)
: mask(mask), transform(transform)
{
BLI_assert(mask && mask->float_data());
start_uv = transform.location().xy();
add_x = transform.x_axis().xy();
add_y = transform.y_axis().xy();
}
void begin_row(int64_t y)
{
this->cur_y = y;
this->cur_x = 0;
/* Sample at pixel centers. */
this->cur_uv_row = this->start_uv + (y + 0.5f) * this->add_y + 0.5f * this->add_x;
}
void apply_mask(const float4 input, float4 &result)
{
float2 uv = this->cur_uv_row + this->cur_x * this->add_x - 0.5f;
float4 m;
math::interpolate_bilinear_border_fl(
this->mask->float_data(), m, this->mask->x, this->mask->y, 4, uv.x, uv.y);
result.x = math::interpolate(input.x, result.x, m.x);
result.y = math::interpolate(input.y, result.y, m.y);
result.z = math::interpolate(input.z, result.z, m.z);
this->cur_x++;
}
float load_mask_min()
{
float2 uv = this->cur_uv_row + this->cur_x * this->add_x - 0.5f;
float4 m;
math::interpolate_bilinear_border_fl(
this->mask->float_data(), m, this->mask->x, this->mask->y, 4, uv.x, uv.y);
float r = std::min({m.x, m.y, m.z});
this->cur_x++;
return r;
}
int64_t cur_x = 0, cur_y = 0;
const ImBuf *mask;
const float3x3 transform;
float2 start_uv, add_x, add_y;
float2 cur_uv_row;
};
/* Mask sampler for #apply_modifier_op: byte mask,
* sample mask with a transform. */
struct MaskSamplerTransformedByte {
MaskSamplerTransformedByte(const ImBuf *mask, const float3x3 &transform)
: mask(mask), transform(transform)
{
BLI_assert(mask && mask->byte_data());
start_uv = transform.location().xy();
add_x = transform.x_axis().xy();
add_y = transform.y_axis().xy();
}
void begin_row(int64_t y)
{
this->cur_y = y;
this->cur_x = 0;
/* Sample at pixel centers. */
this->cur_uv_row = this->start_uv + (y + 0.5f) * this->add_y + 0.5f * this->add_x;
}
void apply_mask(const float4 input, float4 &result)
{
float2 uv = this->cur_uv_row + this->cur_x * this->add_x - 0.5f;
uchar4 mb = math::interpolate_bilinear_border_byte(
this->mask->byte_data(), this->mask->x, this->mask->y, uv.x, uv.y);
float3 m;
rgb_uchar_to_float(m, mb);
result.x = math::interpolate(input.x, result.x, m.x);
result.y = math::interpolate(input.y, result.y, m.y);
result.z = math::interpolate(input.z, result.z, m.z);
this->cur_x++;
}
float load_mask_min()
{
float2 uv = this->cur_uv_row + this->cur_x * this->add_x - 0.5f;
uchar4 m = math::interpolate_bilinear_border_byte(
this->mask->byte_data(), this->mask->x, this->mask->y, uv.x, uv.y);
float r = float(std::min({m.x, m.y, m.z})) * (1.0f / 255.0f);
this->cur_x++;
return r;
}
int64_t cur_x = 0, cur_y = 0;
const ImBuf *mask;
const float3x3 transform;
float2 start_uv, add_x, add_y;
float2 cur_uv_row;
};
/* Given `T` that implements an `apply` function:
*
* template <typename ImageT, typename MaskSampler>
* void apply(ImageT* image, MaskSampler &mask, int image_x, IndexRange y_range);
*
* this function calls the apply() function in parallel
* chunks of the image to process, and with needed
* uchar or float ImageT types, and with appropriate MaskSampler
* instantiated, depending on whether the mask exists, data type
* of the mask, and whether it needs a transformation or can be
* sampled directly.
*
* Both input and mask images are expected to have
* 4 (RGBA) color channels. Input is modified. */
template<typename T>
void apply_modifier_op(T &op, ImBuf *ibuf, const ImBuf *mask, const float3x3 &mask_transform)
{
if (ibuf == nullptr) {
return;
}
BLI_assert_msg(ibuf->channels == 0 || ibuf->channels == 4,
"Sequencer only supports 4 channel images");
BLI_assert_msg(mask == nullptr || mask->channels == 0 || mask->channels == 4,
"Sequencer only supports 4 channel images");
const bool direct_mask_sampling = mask == nullptr || (mask->x == ibuf->x && mask->y == ibuf->y &&
math::is_identity(mask_transform));
const int image_x = ibuf->x;
uchar *image_byte = ibuf->byte_data_for_write();
float *image_float = ibuf->float_data_for_write();
threading::parallel_for(IndexRange(ibuf->y), 16, [&](IndexRange y_range) {
const uchar *mask_byte = mask ? mask->byte_data() : nullptr;
const float *mask_float = mask ? mask->float_data() : nullptr;
/* Instantiate the needed processing function based on image/mask
* data types. */
if (image_byte) {
if (mask_byte) {
if (direct_mask_sampling) {
MaskSamplerDirectByte sampler(mask);
op.apply(image_byte, sampler, image_x, y_range);
}
else {
MaskSamplerTransformedByte sampler(mask, mask_transform);
op.apply(image_byte, sampler, image_x, y_range);
}
}
else if (mask_float) {
if (direct_mask_sampling) {
MaskSamplerDirectFloat sampler(mask);
op.apply(image_byte, sampler, image_x, y_range);
}
else {
MaskSamplerTransformedFloat sampler(mask, mask_transform);
op.apply(image_byte, sampler, image_x, y_range);
}
}
else {
MaskSamplerNone sampler;
op.apply(image_byte, sampler, image_x, y_range);
}
}
else if (image_float) {
if (mask_byte) {
if (direct_mask_sampling) {
MaskSamplerDirectByte sampler(mask);
op.apply(image_float, sampler, image_x, y_range);
}
else {
MaskSamplerTransformedByte sampler(mask, mask_transform);
op.apply(image_float, sampler, image_x, y_range);
}
}
else if (mask_float) {
if (direct_mask_sampling) {
MaskSamplerDirectFloat sampler(mask);
op.apply(image_float, sampler, image_x, y_range);
}
else {
MaskSamplerTransformedFloat sampler(mask, mask_transform);
op.apply(image_float, sampler, image_x, y_range);
}
}
else {
MaskSamplerNone sampler;
op.apply(image_float, sampler, image_x, y_range);
}
}
});
}
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,54 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "DNA_scene_types.h"
#include "BLI_string.h"
#include "BKE_scene.hh"
#include "MOV_read.hh"
#include "multiview.hh"
namespace blender::seq {
void seq_anim_add_suffix(Scene *scene, MovieReader *anim, const int view_id)
{
const char *suffix = BKE_scene_multiview_view_id_suffix_get(&scene->r, view_id);
MOV_set_multiview_suffix(anim, suffix);
}
int seq_num_files(Scene *scene, char views_format, const bool is_multiview)
{
if (!is_multiview) {
return 1;
}
if (views_format == R_IMF_VIEWS_STEREO_3D) {
return 1;
}
/* R_IMF_VIEWS_INDIVIDUAL */
return BKE_scene_multiview_num_views_get(&scene->r);
}
void seq_multiview_name(Scene *scene,
const int view_id,
const char *prefix,
const char *ext,
char *r_path,
size_t r_size)
{
const char *suffix = BKE_scene_multiview_view_id_suffix_get(&scene->r, view_id);
BLI_assert(ext != nullptr && suffix != nullptr && prefix != nullptr);
BLI_snprintf(r_path, r_size, "%s%s%s", prefix, suffix, ext);
}
} // namespace blender::seq

View File

@@ -0,0 +1,29 @@
/* SPDX-FileCopyrightText: 2004 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup sequencer
*/
#include <cstdlib>
namespace blender {
struct MovieReader;
struct Scene;
namespace seq {
void seq_anim_add_suffix(Scene *scene, MovieReader *anim, int view_id);
void seq_multiview_name(
Scene *scene, int view_id, const char *prefix, const char *ext, char *r_path, size_t r_size);
/**
* The number of files will vary according to the stereo format.
*/
int seq_num_files(Scene *scene, char views_format, bool is_multiview);
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,706 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <limits>
#include "MEM_guardedalloc.h"
#include "DNA_scene_types.h"
#include "DNA_screen_types.h"
#include "DNA_sequence_types.h"
#include "DNA_space_types.h"
#include "BLI_threads.h"
#include "BLI_vector_set.hh"
#include "IMB_imbuf.hh"
#include "BKE_anim_data.hh"
#include "BKE_animsys.h"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_layer.hh"
#include "BKE_main.hh"
#include "BKE_scene.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "DEG_depsgraph_debug.hh"
#include "DEG_depsgraph_query.hh"
#include "GPU_context.hh"
#include "SEQ_channels.hh"
#include "SEQ_iterator.hh"
#include "SEQ_prefetch.hh"
#include "SEQ_relations.hh"
#include "SEQ_render.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_time.hh"
#include "prefetch.hh"
#include "render.hh"
namespace blender {
struct RenderResult;
struct Scene;
struct ThreadSlot;
namespace seq {
/* Prefetch several frames before the playhead, so that it is fast to move it a bit backwards. */
static constexpr int before_playhead_frames = 5;
struct PrefetchJob {
PrefetchJob *next = nullptr;
PrefetchJob *prev = nullptr;
Main *bmain = nullptr;
Main *bmain_eval = nullptr;
Scene *scene = nullptr;
Scene *scene_eval = nullptr;
Depsgraph *depsgraph = nullptr;
ThreadMutex prefetch_suspend_mutex = {};
ThreadCondition prefetch_suspend_cond = {};
ListBaseT<ThreadSlot> threads = {};
/* context */
RenderData context = {};
RenderData context_cpy = {};
/* prefetch area */
int cfra = 0;
int timeline_start = 0;
int timeline_end = 0;
int timeline_length = 0;
int num_frames_prefetched = 0;
int cache_flags = 0; /* Only used to detect cache flag changes. */
/* Control: */
/* Set by prefetch. */
bool running = false;
bool waiting = false;
bool stop = false;
/* Set from outside. */
bool is_scrubbing = false;
public:
void init_depsgraph();
void free_depsgraph();
void init_gpu();
void free_gpu();
};
static PrefetchJob *seq_prefetch_job_get(Scene *scene)
{
if (scene && scene->ed) {
return scene->ed->runtime->prefetch_job;
}
return nullptr;
}
bool seq_prefetch_job_is_running(Scene *scene)
{
PrefetchJob *pfjob = seq_prefetch_job_get(scene);
if (!pfjob) {
return false;
}
return pfjob->running;
}
static void seq_prefetch_job_scrubbing_set(Scene *scene, bool is_scrubbing)
{
PrefetchJob *pfjob = seq_prefetch_job_get(scene);
if (!pfjob) {
return;
}
pfjob->is_scrubbing = is_scrubbing;
}
static bool seq_prefetch_job_is_waiting(Scene *scene)
{
PrefetchJob *pfjob = seq_prefetch_job_get(scene);
if (!pfjob) {
return false;
}
return pfjob->waiting;
}
static Strip *original_strip_get(const Strip *strip, ListBaseT<Strip> *seqbase)
{
for (Strip &strip_orig : *seqbase) {
if (STREQ(strip->name, strip_orig.name)) {
return &strip_orig;
}
if (strip_orig.type == STRIP_TYPE_META) {
Strip *match = original_strip_get(strip, &strip_orig.seqbase);
if (match != nullptr) {
return match;
}
}
}
return nullptr;
}
static Strip *original_strip_get(const Strip *strip, Scene *scene)
{
Editing *ed = scene->ed;
return original_strip_get(strip, &ed->seqbase);
}
static RenderData *get_original_context(const RenderData *context)
{
PrefetchJob *pfjob = seq_prefetch_job_get(context->scene);
return pfjob ? &pfjob->context : nullptr;
}
Scene *prefetch_get_original_scene(const RenderData *context)
{
Scene *scene = context->scene;
if (context->is_prefetch_render) {
context = get_original_context(context);
if (context != nullptr) {
scene = context->scene;
}
}
return scene;
}
Scene *prefetch_get_original_scene_and_strip(const RenderData *context, const Strip *&strip)
{
Scene *scene = context->scene;
if (context->is_prefetch_render) {
context = get_original_context(context);
if (context != nullptr) {
scene = context->scene;
strip = original_strip_get(strip, scene);
}
}
return scene;
}
static bool seq_prefetch_is_cache_full(Scene *scene)
{
return evict_caches_if_full(scene);
}
static int seq_prefetch_cfra(PrefetchJob *pfjob)
{
int new_frame = pfjob->cfra + pfjob->num_frames_prefetched;
const ScenePlaybackRange playback_range = BKE_scene_get_playback_range(pfjob->scene);
if (new_frame >= playback_range.end_frame) {
/* Wrap around to where we will jump when we reach the end frame. */
new_frame = playback_range.start_frame + new_frame - playback_range.end_frame;
}
return new_frame;
}
static AnimationEvalContext seq_prefetch_anim_eval_context(PrefetchJob *pfjob)
{
return BKE_animsys_eval_context_construct(pfjob->depsgraph, seq_prefetch_cfra(pfjob));
}
void seq_prefetch_get_time_range(Scene *scene, int *r_start, int *r_end)
{
/* When there is no prefetch job, return "impossible" negative values. */
*r_start = std::numeric_limits<int>::min();
*r_end = std::numeric_limits<int>::min();
PrefetchJob *pfjob = seq_prefetch_job_get(scene);
if (pfjob == nullptr) {
return;
}
if ((scene->ed->cache_flag & SEQ_CACHE_PREFETCH_ENABLE) == 0 || !pfjob->running) {
return;
}
*r_start = pfjob->cfra;
*r_end = seq_prefetch_cfra(pfjob);
}
void PrefetchJob::free_depsgraph()
{
if (this->depsgraph != nullptr) {
DEG_graph_free(this->depsgraph);
}
this->depsgraph = nullptr;
this->scene_eval = nullptr;
}
static void seq_prefetch_update_depsgraph(PrefetchJob *pfjob)
{
DEG_evaluate_on_framechange(pfjob->depsgraph, seq_prefetch_cfra(pfjob));
/* Prevent depsgraph from copying scene data to evaluated scene. It would reset updated frame. */
DEG_ids_clear_recalc(pfjob->depsgraph, false);
}
void PrefetchJob::init_depsgraph()
{
ViewLayer *view_layer = BKE_view_layer_default_render(this->scene);
this->depsgraph = DEG_graph_new(this->bmain_eval, this->scene, view_layer, DAG_EVAL_RENDER);
DEG_debug_name_set(this->depsgraph, "SEQUENCER PREFETCH");
/* Make sure there is a correct evaluated scene pointer. */
DEG_graph_build_for_render_pipeline(this->depsgraph);
/* Update immediately so we have proper evaluated scene. */
seq_prefetch_update_depsgraph(this);
this->scene_eval = DEG_get_evaluated_scene(this->depsgraph);
this->scene_eval->ed->cache_flag = SEQ_CACHE_NONE;
}
void PrefetchJob::init_gpu()
{
this->context_cpy.gpu_context = gpu::GPU_create_secondary_context();
}
void PrefetchJob::free_gpu()
{
if (this->context_cpy.gpu_context.ghost_context != nullptr) {
gpu::GPU_destroy_secondary_context(this->context_cpy.gpu_context);
this->context_cpy.gpu_context = {};
}
}
static void seq_prefetch_update_area(PrefetchJob *pfjob)
{
int cfra = math::max(pfjob->scene->r.cfra - before_playhead_frames, pfjob->timeline_start);
/* rebase */
if (cfra > pfjob->cfra) {
int delta = cfra - pfjob->cfra;
pfjob->cfra = cfra;
pfjob->num_frames_prefetched -= delta;
pfjob->num_frames_prefetched = std::max(pfjob->num_frames_prefetched, 0);
}
/* reset */
if (cfra < pfjob->cfra) {
pfjob->cfra = cfra;
pfjob->num_frames_prefetched = 0;
}
/* timeline span changes */
const ScenePlaybackRange playback_range = BKE_scene_get_playback_range(pfjob->scene);
if (pfjob->timeline_start != playback_range.start_frame ||
pfjob->timeline_end != playback_range.end_frame)
{
pfjob->timeline_start = playback_range.start_frame;
pfjob->timeline_end = playback_range.end_frame;
pfjob->timeline_length = playback_range.end_frame - playback_range.start_frame;
/* Reset the number of prefetched frames as we need to re-evaluate which
* frames to keep in the cache.
*/
pfjob->num_frames_prefetched = 0;
}
/* cache flag changes */
Scene *scene = pfjob->scene;
if (pfjob->cache_flags != scene->ed->cache_flag) {
pfjob->cache_flags = scene->ed->cache_flag;
pfjob->num_frames_prefetched = 0;
}
}
void prefetch_stop_all()
{
/* TODO(Richard): Use wm_jobs for prefetch, or pass main. */
for (Scene *scene = static_cast<Scene *>(G.main->scenes.first); scene;
scene = static_cast<Scene *>(scene->id.next))
{
prefetch_stop(scene);
}
}
void prefetch_stop(Scene *scene)
{
PrefetchJob *pfjob = seq_prefetch_job_get(scene);
if (!pfjob) {
return;
}
pfjob->stop = true;
while (pfjob->running) {
BLI_condition_notify_one(&pfjob->prefetch_suspend_cond);
}
}
static void seq_prefetch_update_context(const RenderData *context)
{
PrefetchJob *pfjob = seq_prefetch_job_get(context->scene);
render_new_render_data(pfjob->bmain_eval,
pfjob->depsgraph,
pfjob->scene_eval,
context->rectx,
context->recty,
context->preview_render_size,
nullptr,
&pfjob->context_cpy);
pfjob->context_cpy.is_prefetch_render = true;
render_new_render_data(pfjob->bmain,
pfjob->depsgraph,
pfjob->scene,
context->rectx,
context->recty,
context->preview_render_size,
nullptr,
&pfjob->context);
pfjob->context.is_prefetch_render = false;
}
static void seq_prefetch_update_scene(Scene *scene)
{
PrefetchJob *pfjob = seq_prefetch_job_get(scene);
if (!pfjob) {
return;
}
pfjob->scene = scene;
pfjob->free_depsgraph();
pfjob->init_depsgraph();
}
static void seq_prefetch_update_active_seqbase(PrefetchJob *pfjob)
{
MetaStack *ms_orig = meta_stack_active_get(editing_get(pfjob->scene));
Editing *ed_eval = editing_get(pfjob->scene_eval);
if (ms_orig != nullptr) {
Strip *meta_eval = original_strip_get(ms_orig->parent_strip, pfjob->scene_eval);
ed_eval->current_meta_strip = meta_eval;
}
else {
ed_eval->current_meta_strip = nullptr;
}
}
static void seq_prefetch_resume(Scene *scene)
{
PrefetchJob *pfjob = seq_prefetch_job_get(scene);
if (pfjob && pfjob->waiting) {
BLI_condition_notify_one(&pfjob->prefetch_suspend_cond);
}
}
void seq_prefetch_free(Scene *scene)
{
PrefetchJob *pfjob = seq_prefetch_job_get(scene);
if (!pfjob) {
return;
}
prefetch_stop(scene);
BLI_threadpool_remove(&pfjob->threads, pfjob);
BLI_threadpool_end(&pfjob->threads);
BLI_mutex_end(&pfjob->prefetch_suspend_mutex);
BLI_condition_end(&pfjob->prefetch_suspend_cond);
pfjob->free_depsgraph();
pfjob->free_gpu();
BKE_main_free(pfjob->bmain_eval);
scene->ed->runtime->prefetch_job = nullptr;
MEM_delete(pfjob);
}
static VectorSet<Strip *> query_scene_strips(Editing *ed)
{
Map<const Scene *, VectorSet<Strip *>> &strips_by_scene = lookup_strips_by_scene_map_get(ed);
VectorSet<Strip *> scene_strips;
for (const VectorSet<Strip *> &strips : strips_by_scene.values()) {
scene_strips.add_multiple(strips);
}
return scene_strips;
}
/* Find whether any scene strips are indirectly rendered, e.g. as mask or effect inputs. */
static bool seq_prefetch_scene_strip_is_rendered(const Scene *scene,
ListBaseT<SeqTimelineChannel> *channels,
ListBaseT<Strip> *seqbase,
Span<Strip *> scene_strips,
int timeline_frame,
SeqRenderState state)
{
Vector<Strip *> rendered_strips = query_rendered_strips_sorted(
scene, channels, seqbase, timeline_frame, 0);
/* Iterate over rendered strips. */
for (Strip *strip : rendered_strips) {
if (strip->type == STRIP_TYPE_META &&
seq_prefetch_scene_strip_is_rendered(
scene, &strip->channels, &strip->seqbase, scene_strips, timeline_frame, state))
{
return true;
}
/* Recursive "sequencer-type" scene strip detected, no point in attempting to render it. */
if (state.strips_in_progress.contains(strip)) {
return true;
}
if (strip->type == STRIP_TYPE_SCENE && (strip->flag & SEQ_SCENE_STRIPS) != 0 &&
strip->scene != nullptr && editing_get(strip->scene))
{
state.strips_in_progress.add(strip);
const Scene *target_scene = strip->scene;
Editing *target_ed = editing_get(target_scene);
if (target_ed == nullptr) {
continue;
}
VectorSet<Strip *> target_scene_strips = query_scene_strips(target_ed);
int target_timeline_frame = give_frame_index(scene, strip, timeline_frame) +
target_scene->r.sfra;
if (seq_prefetch_scene_strip_is_rendered(target_scene,
target_ed->current_channels(),
target_ed->current_strips(),
target_scene_strips,
target_timeline_frame,
state))
{
return true;
}
}
for (Strip *strip_scene : scene_strips) {
/* Check if the strip is an effect of the scene strip or uses it as modifier.
* This also checks if `strip == strip_scene`. */
if (relations_render_loop_check(strip, strip_scene)) {
return true;
}
/* Adjustment strips with 'replace' blending can use scene strips in channels below it.
* See #151629. */
if (strip->type == STRIP_TYPE_ADJUSTMENT && strip->blend_mode == STRIP_BLEND_REPLACE &&
strip_scene->intersects_frame(scene, timeline_frame) &&
strip_scene->channel < strip->channel)
{
return true;
}
}
}
return false;
}
/* Prefetch must avoid rendering scene strips, because rendering in background locks UI and can
* make it unresponsive for long time periods. */
static bool seq_prefetch_must_skip_frame(PrefetchJob *pfjob,
ListBaseT<SeqTimelineChannel> *channels,
ListBaseT<Strip> *seqbase)
{
/* Pass in state to check for infinite recursion of "sequencer-type" scene strips. */
SeqRenderState state = {};
VectorSet<Strip *> scene_strips = query_scene_strips(editing_get(pfjob->scene_eval));
return seq_prefetch_scene_strip_is_rendered(
pfjob->scene_eval, channels, seqbase, scene_strips, seq_prefetch_cfra(pfjob), state);
}
static bool seq_prefetch_need_suspend(PrefetchJob *pfjob)
{
return seq_prefetch_is_cache_full(pfjob->scene) || pfjob->is_scrubbing ||
(pfjob->num_frames_prefetched >= pfjob->timeline_length);
}
static void seq_prefetch_do_suspend(PrefetchJob *pfjob)
{
BLI_mutex_lock(&pfjob->prefetch_suspend_mutex);
while (seq_prefetch_need_suspend(pfjob) &&
(pfjob->scene->ed->cache_flag & SEQ_CACHE_PREFETCH_ENABLE) && !pfjob->stop)
{
pfjob->waiting = true;
BLI_condition_wait(&pfjob->prefetch_suspend_cond, &pfjob->prefetch_suspend_mutex);
seq_prefetch_update_area(pfjob);
}
pfjob->waiting = false;
BLI_mutex_unlock(&pfjob->prefetch_suspend_mutex);
}
static void *seq_prefetch_frames(void *job)
{
PrefetchJob *pfjob = static_cast<PrefetchJob *>(job);
while (true) {
if (pfjob->cfra < pfjob->timeline_start || pfjob->cfra > pfjob->timeline_end) {
/* Don't try to prefetch anything when we are outside of the timeline range. */
break;
}
pfjob->scene_eval->ed->runtime->prefetch_job = nullptr;
seq_prefetch_update_depsgraph(pfjob);
AnimData *adt = BKE_animdata_from_id(&pfjob->context_cpy.scene->id);
AnimationEvalContext anim_eval_context = seq_prefetch_anim_eval_context(pfjob);
BKE_animsys_evaluate_animdata(
&pfjob->context_cpy.scene->id, adt, &anim_eval_context, ADT_RECALC_ALL, false);
/* This is quite hacky solution:
* We need cross-reference original scene with copy for cache.
* However depsgraph must not have this data, because it will try to kill this job.
* Scene copy don't reference original scene. Perhaps, this could be done by depsgraph.
* Set to nullptr before return!
*/
pfjob->scene_eval->ed->runtime->prefetch_job = pfjob;
ListBaseT<Strip> *seqbase = active_seqbase_get(editing_get(pfjob->scene_eval));
ListBaseT<SeqTimelineChannel> *channels = channels_displayed_get(
editing_get(pfjob->scene_eval));
if (seq_prefetch_must_skip_frame(pfjob, channels, seqbase)) {
pfjob->num_frames_prefetched++;
/* Break instead of keep looping if the job should be terminated. */
if (!(pfjob->scene->ed->cache_flag & SEQ_CACHE_PREFETCH_ENABLE) ||
!(pfjob->scene->ed->cache_flag & SEQ_CACHE_ALL_TYPES) || pfjob->stop)
{
break;
}
continue;
}
ImBuf *ibuf = render_give_ibuf(&pfjob->context_cpy, seq_prefetch_cfra(pfjob), 0);
pfjob->num_frames_prefetched++;
IMB_freeImBuf(ibuf);
/* Suspend thread if there is nothing to be prefetched. */
seq_prefetch_do_suspend(pfjob);
if (!(pfjob->scene->ed->cache_flag & SEQ_CACHE_PREFETCH_ENABLE) ||
!(pfjob->scene->ed->cache_flag & SEQ_CACHE_ALL_TYPES) || pfjob->stop)
{
break;
}
seq_prefetch_update_area(pfjob);
}
pfjob->running = false;
pfjob->scene_eval->ed->runtime->prefetch_job = nullptr;
return nullptr;
}
static PrefetchJob *seq_prefetch_start_ex(const RenderData *context, float cfra)
{
PrefetchJob *pfjob = seq_prefetch_job_get(context->scene);
if (!pfjob) {
if (!context->scene->ed) {
return nullptr;
}
pfjob = MEM_new<PrefetchJob>("PrefetchJob");
context->scene->ed->runtime->prefetch_job = pfjob;
BLI_threadpool_init(&pfjob->threads, seq_prefetch_frames, 1);
BLI_mutex_init(&pfjob->prefetch_suspend_mutex);
BLI_condition_init(&pfjob->prefetch_suspend_cond);
pfjob->bmain_eval = BKE_main_new();
pfjob->scene = context->scene;
pfjob->init_depsgraph();
pfjob->init_gpu();
}
pfjob->bmain = context->bmain;
Scene *scene = pfjob->scene;
const ScenePlaybackRange playback_range = BKE_scene_get_playback_range(pfjob->scene);
pfjob->timeline_start = playback_range.start_frame;
pfjob->timeline_end = playback_range.end_frame;
pfjob->timeline_length = playback_range.end_frame - playback_range.start_frame;
pfjob->cfra = math::max(int(cfra - before_playhead_frames), pfjob->timeline_start);
pfjob->num_frames_prefetched = 0;
pfjob->cache_flags = scene->ed->cache_flag;
pfjob->waiting = false;
pfjob->stop = false;
pfjob->running = true;
seq_prefetch_update_scene(context->scene);
seq_prefetch_update_context(context);
seq_prefetch_update_active_seqbase(pfjob);
BLI_threadpool_remove(&pfjob->threads, pfjob);
BLI_threadpool_insert(&pfjob->threads, pfjob);
return pfjob;
}
void seq_prefetch_start(const RenderData *context, float timeline_frame)
{
Scene *scene = context->scene;
Editing *ed = scene->ed;
bool has_strips = bool(ed->current_strips()->first);
if (!context->is_prefetch_render) {
bool playing = context->is_playing;
bool scrubbing = context->is_scrubbing;
bool running = seq_prefetch_job_is_running(scene);
seq_prefetch_job_scrubbing_set(scene, scrubbing);
seq_prefetch_resume(scene);
/* conditions to start:
* prefetch enabled, prefetch not running, not scrubbing, not playing,
* cache storage enabled, has strips to render, not rendering, not doing modal transform -
* important, see D7820. */
if ((ed->cache_flag & SEQ_CACHE_PREFETCH_ENABLE) && !running && !scrubbing && !playing &&
(ed->cache_flag & SEQ_CACHE_ALL_TYPES) && has_strips && !G.is_rendering && !G.moving)
{
seq_prefetch_start_ex(context, timeline_frame);
}
}
}
bool prefetch_need_redraw(const bContext *C, Scene *scene)
{
bScreen *screen = CTX_wm_screen(C);
bool playing = screen->animtimer != nullptr;
bool scrubbing = screen->scrubbing;
bool running = seq_prefetch_job_is_running(scene);
bool suspended = seq_prefetch_job_is_waiting(scene);
SpaceSeq *sseq = CTX_wm_space_seq(C);
bool showing_cache = sseq->cache_overlay.flag & SEQ_CACHE_SHOW;
/* force redraw, when prefetching and using cache view. */
if (running && !playing && !suspended && showing_cache) {
return true;
}
/* Sometimes scrubbing flag is set when not scrubbing. In that case I want to catch "event" of
* stopping scrubbing */
if (scrubbing) {
return true;
}
return false;
}
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,32 @@
/* SPDX-FileCopyrightText: 2004 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender {
/** \file
* \ingroup sequencer
*/
struct Scene;
struct Strip;
namespace seq {
struct RenderData;
/**
* Start or resume prefetching.
*/
void seq_prefetch_start(const RenderData *context, float timeline_frame);
void seq_prefetch_free(Scene *scene);
bool seq_prefetch_job_is_running(Scene *scene);
void seq_prefetch_get_time_range(Scene *scene, int *r_start, int *r_end);
Scene *prefetch_get_original_scene(const RenderData *context);
Scene *prefetch_get_original_scene_and_strip(const RenderData *context, const Strip *&strip);
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,719 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2026 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "MEM_guardedalloc.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BLI_fileops.h"
#include "BLI_math_base.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#ifdef WIN32
# include "BLI_winstuff.h"
#else
# include <unistd.h>
#endif
#include "BKE_global.hh"
#include "BKE_image.hh"
#include "BKE_main.hh"
#include "BKE_scene.hh"
#include "WM_types.hh"
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"
#include "MOV_read.hh"
#include "SEQ_proxy.hh"
#include "SEQ_relations.hh"
#include "SEQ_render.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_time.hh"
#include "cache/intra_frame_cache.hh"
#include "multiview.hh"
#include "proxy.hh"
#include "render.hh"
#include "sequencer.hh"
#include "utils.hh"
namespace blender::seq {
struct ProxyBuildContext {
MovieProxyBuilder *movie_proxy_builder = nullptr;
int size_flags = 0;
int quality = 0;
bool overwrite = false;
int view_id = 0;
Main *bmain = nullptr;
Scene *scene = nullptr;
Strip *strip = nullptr;
};
IMB_Proxy_Size rendersize_to_proxysize(eSpaceSeq_Proxy_RenderSize render_size)
{
switch (render_size) {
case SEQ_RENDER_SIZE_PROXY_25:
return IMB_PROXY_25;
case SEQ_RENDER_SIZE_PROXY_50:
return IMB_PROXY_50;
case SEQ_RENDER_SIZE_PROXY_75:
return IMB_PROXY_75;
case SEQ_RENDER_SIZE_PROXY_100:
return IMB_PROXY_100;
default:
return IMB_PROXY_NONE;
}
}
float rendersize_to_scale_factor(eSpaceSeq_Proxy_RenderSize render_size)
{
switch (render_size) {
case SEQ_RENDER_SIZE_PROXY_25:
return 0.25f;
case SEQ_RENDER_SIZE_PROXY_50:
return 0.5f;
case SEQ_RENDER_SIZE_PROXY_75:
return 0.75f;
default:
return 1.0f;
}
}
bool seq_proxy_get_custom_file_filepath(const Strip *strip, char *filepath, const int view_id)
{
/* Ideally this would be #PROXY_MAXFILE however BLI_path_abs clamps to #FILE_MAX. */
char filepath_temp[FILE_MAX];
char suffix[24];
StripProxy *proxy = strip->data->proxy;
if (proxy == nullptr) {
return false;
}
BLI_path_join(filepath_temp, sizeof(filepath_temp), proxy->dirpath, proxy->filename);
BLI_path_abs(filepath_temp, BKE_main_blendfile_path_from_global());
if (view_id > 0) {
SNPRINTF(suffix, "_%d", view_id);
/* This will actually append suffix after extension
* which is weird but how was originally coded in multi-view branch. */
BLI_snprintf(filepath, PROXY_MAXFILE, "%s_%s", filepath_temp, suffix);
}
else {
BLI_strncpy(filepath, filepath_temp, PROXY_MAXFILE);
}
return true;
}
static bool seq_proxy_get_filepath_for_elem(const Scene *scene,
const Strip &strip,
const StripElem &strip_elem,
eSpaceSeq_Proxy_RenderSize render_size,
char *filepath,
const int view_id)
{
char dirpath[PROXY_MAXFILE];
char suffix[24] = {'\0'};
Editing *ed = editing_get(scene);
StripProxy *proxy = strip.data->proxy;
if (proxy == nullptr) {
return false;
}
/* Multi-view suffix. */
if (view_id > 0) {
SNPRINTF(suffix, "_%d", view_id);
}
/* Per strip with Custom file situation is handled separately. */
if (proxy->storage & SEQ_STORAGE_PROXY_CUSTOM_FILE &&
ed->proxy_storage != SEQ_EDIT_PROXY_DIR_STORAGE)
{
if (seq_proxy_get_custom_file_filepath(&strip, filepath, view_id)) {
return true;
}
}
if (ed->proxy_storage == SEQ_EDIT_PROXY_DIR_STORAGE) {
/* Per project default. */
if (ed->proxy_dir[0] == 0) {
STRNCPY(dirpath, "//BL_proxy");
}
else { /* Per project with custom dirpath. */
STRNCPY(dirpath, ed->proxy_dir);
}
BLI_path_abs(filepath, BKE_main_blendfile_path_from_global());
}
else {
/* Pre strip with custom dir. */
if (proxy->storage & SEQ_STORAGE_PROXY_CUSTOM_DIR) {
STRNCPY(dirpath, strip.data->proxy->dirpath);
}
else { /* Per strip default. */
SNPRINTF(dirpath, "%s" SEP_STR "BL_proxy", strip.data->dirpath);
}
}
/* Proxy size number to be used in path. */
int proxy_size_number = rendersize_to_scale_factor(render_size) * 100;
BLI_snprintf(filepath,
PROXY_MAXFILE,
"%s" SEP_STR "images" SEP_STR "%d" SEP_STR "%s_proxy%s.jpg",
dirpath,
proxy_size_number,
strip_elem.filename,
suffix);
BLI_path_abs(filepath, BKE_main_blendfile_path_from_global());
return true;
}
static bool seq_proxy_get_filepath(Scene *scene,
Strip *strip,
int timeline_frame,
eSpaceSeq_Proxy_RenderSize render_size,
char *filepath,
const int view_id)
{
if (strip->data->proxy == nullptr) {
return false;
}
return seq_proxy_get_filepath_for_elem(scene,
*strip,
*render_give_stripelem(scene, strip, timeline_frame),
render_size,
filepath,
view_id);
}
bool can_use_proxy(const RenderData *context, const Strip *strip, IMB_Proxy_Size psize)
{
if (strip->data->proxy == nullptr || !context->use_proxies) {
return false;
}
short size_flags = strip->data->proxy->build_size_flags;
return (strip->flag & SEQ_USE_PROXY) != 0 && psize != IMB_PROXY_NONE &&
(size_flags & psize) != 0;
}
ImBuf *seq_proxy_fetch(const RenderData *context, Strip *strip, int timeline_frame)
{
char filepath[PROXY_MAXFILE];
StripProxy *proxy = strip->data->proxy;
const eSpaceSeq_Proxy_RenderSize psize = eSpaceSeq_Proxy_RenderSize(
context->preview_render_size);
/* only use proxies, if they are enabled (even if present!) */
if (!can_use_proxy(context, strip, rendersize_to_proxysize(psize))) {
return nullptr;
}
if (proxy->storage & SEQ_STORAGE_PROXY_CUSTOM_FILE) {
int frameno = round_fl_to_int(give_frame_index(context->scene, strip, timeline_frame)) +
strip->anim_startofs;
if (proxy->anim == nullptr) {
if (seq_proxy_get_filepath(
context->scene, strip, timeline_frame, psize, filepath, context->view_id) == 0)
{
return nullptr;
}
/* Sequencer takes care of colorspace conversion of the result. The input is the best to be
* kept unchanged for the performance reasons. */
proxy->anim = openanim(
filepath, ImBufFlags::Zero, 0, true, strip->data->colorspace_settings.name);
}
if (proxy->anim == nullptr) {
return nullptr;
}
strip_open_anim_file(context->scene, strip, true);
return MOV_decode_frame(proxy->anim, frameno, IMB_PROXY_NONE);
}
if (seq_proxy_get_filepath(
context->scene, strip, timeline_frame, psize, filepath, context->view_id) == 0)
{
return nullptr;
}
if (BLI_exists(filepath)) {
/* Proxies are already be in the sequencer colorspace for fast loading, don't perform
* conversion of float to scene linear that would usually be done. */
char colorspace[IMA_MAX_SPACE];
STRNCPY(colorspace, context->scene->sequencer_colorspace_settings.name);
return IMB_load_image_from_filepath(filepath,
ImBufFlags::ByteData | ImBufFlags::Metadata |
ImBufFlags::NoColorspaceConvert,
colorspace);
}
return nullptr;
}
/**
* Cache the result of #BKE_scene_multiview_view_prefix_get.
*/
struct MultiViewPrefixVars {
char prefix[FILE_MAX];
char ext[FILE_MAXFILE];
};
/**
* Returns whether the file this context would read from even exist,
* if not, don't create the context.
*
* \param prefix_vars: Stores prefix variables for reuse,
* these variables are for internal use, the caller must not depend on them.
*
* \note This function must first a `view_id` of zero, to initialize `prefix_vars`
* for use with other views.
*/
static bool seq_proxy_multiview_context_invalid(Strip *strip,
Scene *scene,
const int view_id,
MultiViewPrefixVars *prefix_vars)
{
if ((scene->r.scemode & R_MULTIVIEW) == 0) {
return false;
}
if ((strip->type == STRIP_TYPE_IMAGE) && (strip->views_format == R_IMF_VIEWS_INDIVIDUAL)) {
if (view_id == 0) {
/* Clear on first use. */
prefix_vars->prefix[0] = '\0';
prefix_vars->ext[0] = '\0';
char filepath[FILE_MAX];
BLI_path_join(
filepath, sizeof(filepath), strip->data->dirpath, strip->data->stripdata->filename);
BLI_path_abs(filepath, ID_BLEND_PATH_FROM_GLOBAL(&scene->id));
const char *ext_ptr = nullptr;
BKE_scene_multiview_view_prefix_get(scene, filepath, prefix_vars->prefix, &ext_ptr);
if (ext_ptr != nullptr) {
STRNCPY(prefix_vars->ext, ext_ptr);
}
}
if (prefix_vars->prefix[0] == '\0') {
return view_id != 0;
}
char filepath[FILE_MAX];
seq_multiview_name(scene, view_id, prefix_vars->prefix, prefix_vars->ext, filepath, FILE_MAX);
if (BLI_access(filepath, R_OK) == 0) {
return false;
}
return view_id != 0;
}
return false;
}
/**
* This returns the maximum possible number of required contexts
*/
static int seq_proxy_context_count(Strip *strip, Scene *scene)
{
int num_views = 1;
if ((scene->r.scemode & R_MULTIVIEW) == 0) {
return 1;
}
switch (strip->type) {
case STRIP_TYPE_MOVIE: {
num_views = int(strip->runtime->movie_readers.size());
break;
}
case STRIP_TYPE_IMAGE: {
switch (strip->views_format) {
case R_IMF_VIEWS_INDIVIDUAL:
num_views = BKE_scene_multiview_num_views_get(&scene->r);
break;
case R_IMF_VIEWS_STEREO_3D:
num_views = 2;
break;
case R_IMF_VIEWS_MULTIVIEW:
/* not supported at the moment */
/* pass through */
default:
num_views = 1;
}
break;
}
default:
break;
}
return num_views;
}
static bool seq_proxy_need_rebuild(Strip *strip, MovieReader *anim)
{
if ((strip->data->proxy->build_flags & SEQ_PROXY_SKIP_EXISTING) == 0) {
return true;
}
IMB_Proxy_Size required_proxies = IMB_Proxy_Size(strip->data->proxy->build_size_flags);
int built_proxies = MOV_get_existing_proxies(anim);
return (required_proxies & built_proxies) != required_proxies;
}
bool proxy_build_start(Main *bmain,
Scene *scene,
Strip *strip,
Set<std::string> *processed_paths,
bool build_only_on_bad_performance,
Vector<ProxyBuildContext *> &r_queue)
{
if (!strip->data || !strip->data->proxy) {
return true;
}
if (!(strip->flag & SEQ_USE_PROXY)) {
return true;
}
int num_files = seq_proxy_context_count(strip, scene);
MultiViewPrefixVars prefix_vars; /* Initialized by #seq_proxy_multiview_context_invalid. */
for (int i = 0; i < num_files; i++) {
if (seq_proxy_multiview_context_invalid(strip, scene, i, &prefix_vars)) {
continue;
}
/* Check if proxies are already built here, because actually opening anims takes a lot of
* time. */
strip_open_anim_file(scene, strip, false);
MovieReader *anim = strip->runtime->movie_reader_get(i);
if (anim && !seq_proxy_need_rebuild(strip, anim)) {
continue;
}
strip_free_movie_readers(strip);
ProxyBuildContext *context = MEM_new<ProxyBuildContext>("strip proxy rebuild context");
Strip *strip_new = strip_duplicate_recursive(
bmain, scene, scene, nullptr, strip, StripDuplicate::Selected);
context->size_flags = strip_new->data->proxy->build_size_flags;
context->quality = strip_new->data->proxy->quality;
context->overwrite = (strip_new->data->proxy->build_flags & SEQ_PROXY_SKIP_EXISTING) == 0;
context->bmain = bmain;
context->scene = scene;
context->strip = strip_new;
context->view_id = i; /* only for images */
if (strip_new->type == STRIP_TYPE_MOVIE) {
strip_open_anim_file(scene, strip_new, true);
anim = strip_new->runtime->movie_reader_get(i);
if (anim) {
context->movie_proxy_builder = MOV_proxy_builder_start(anim,
context->size_flags,
context->quality,
context->overwrite,
processed_paths,
build_only_on_bad_performance);
}
if (!context->movie_proxy_builder) {
MEM_delete(context);
return false;
}
}
r_queue.append(context);
}
return true;
}
static void seq_proxy_build_frame(const Scene *scene,
const int view_id,
ImBuf *ibuf_full,
const Strip &strip,
const StripElem &strip_elem,
int proxy_render_size,
const bool overwrite)
{
char filepath[PROXY_MAXFILE];
if (!seq_proxy_get_filepath_for_elem(scene,
strip,
strip_elem,
eSpaceSeq_Proxy_RenderSize(proxy_render_size),
filepath,
view_id))
{
return;
}
if (!overwrite && BLI_exists(filepath)) {
return;
}
const int rectx = (proxy_render_size * ibuf_full->x) / 100;
const int recty = (proxy_render_size * ibuf_full->y) / 100;
ImBuf *ibuf = ibuf_full;
if (ibuf_full->x != rectx || ibuf_full->y != recty) {
ibuf = IMB_scale_into_new(ibuf_full, rectx, recty, IMBScaleFilter::Nearest, true);
}
const int quality = strip.data->proxy->quality;
const bool save_float = ibuf->float_data() != nullptr;
ibuf->foptions.quality = quality;
if (save_float) {
/* Float image: save as EXR with FP16 data and DWAA compression. */
ibuf->ftype = IMB_FTYPE_OPENEXR;
ibuf->foptions.flag = OPENEXR_HALF | R_IMF_EXR_CODEC_DWAA;
}
else {
/* Byte image: save as JPG. */
ibuf->ftype = IMB_FTYPE_JPG;
if (ibuf->can_contain_alpha()) {
ibuf->color_mode = ImColorMode::RGB; /* JPGs do not support alpha. */
}
}
BLI_file_ensure_parent_dir_exists(filepath);
const bool ok = IMB_save_image(
ibuf, filepath, save_float ? ImBufFlags::FloatData : ImBufFlags::ByteData);
if (ok == false) {
perror(filepath);
}
if (ibuf != ibuf_full) {
IMB_freeImBuf(ibuf);
}
}
static ImBuf *render_image_strip_frame(const ProxyBuildContext &context,
const Strip &strip,
char *filepath,
char *prefix,
const char *ext,
int view_id)
{
ImBuf *ibuf = nullptr;
ImBufFlags flag = ImBufFlags::ByteData | ImBufFlags::Metadata | ImBufFlags::MultiLayer;
if (strip.alpha_mode == SEQ_ALPHA_PREMUL) {
flag |= ImBufFlags::AlphaPremul;
}
if (prefix[0] == '\0') {
ibuf = IMB_load_image_from_filepath(filepath, flag, strip.data->colorspace_settings.name);
}
else {
char filepath_view[FILE_MAX];
BKE_scene_multiview_view_prefix_get(context.scene, filepath, prefix, &ext);
seq_multiview_name(context.scene, view_id, prefix, ext, filepath_view, FILE_MAX);
ibuf = IMB_load_image_from_filepath(filepath_view, flag, strip.data->colorspace_settings.name);
}
if (ibuf == nullptr) {
return nullptr;
}
convert_multilayer_ibuf(ibuf);
if (ibuf->float_data() != nullptr && ibuf->byte_data() != nullptr) {
IMB_free_byte_pixels(ibuf); /* If both float & byte exist, free byte buffer. */
}
ensure_ibuf_is_sequencer_space(context.scene, ibuf, false);
return ibuf;
}
static void image_proxy_builder_process(ProxyBuildContext &context,
const bool *job_stop,
bool *job_update_ui,
const FunctionRef<void(float progress)> set_progress_fn)
{
if (!context.strip || !context.strip->data) {
return;
}
const Strip &strip = *context.strip;
if (!(strip.flag & SEQ_USE_PROXY)) {
return;
}
/* If proxy is set to user-specified custom file, no need to rebuild anything. */
/* that's why it is called custom... */
if (strip.data->proxy && (strip.data->proxy->storage & SEQ_STORAGE_PROXY_CUSTOM_FILE)) {
return;
}
const char *base_path = ID_BLEND_PATH_FROM_GLOBAL(&context.scene->id);
const int tot_views = BKE_scene_multiview_num_views_get(&context.scene->r);
for (int elem_index = 0; elem_index < strip.len; elem_index++) {
const StripElem &s_elem = strip.data->stripdata[elem_index];
char filepath[FILE_MAX];
const char *ext = nullptr;
char prefix[FILE_MAX];
ImBuf *ibuf = nullptr;
BLI_path_join(filepath, sizeof(filepath), strip.data->dirpath, s_elem.filename);
BLI_path_abs(filepath, base_path);
const int totfiles = seq_num_files(context.scene, strip.views_format, true);
bool is_multiview_render = seq_image_strip_is_multiview_render(
context.scene, &strip, totfiles, filepath, prefix, ext);
if (is_multiview_render) {
Array<ImBuf *> ibufs_arr(tot_views, nullptr);
for (int view_id = 0; view_id < totfiles; view_id++) {
ibufs_arr[view_id] = render_image_strip_frame(
context, strip, filepath, prefix, ext, view_id);
}
if (ibufs_arr[0] != nullptr) {
if (strip.views_format == R_IMF_VIEWS_STEREO_3D) {
IMB_ImBufFromStereo3d(strip.stereo3d_format, ibufs_arr[0], &ibufs_arr[0], &ibufs_arr[1]);
}
/* Return the requested image; release the others. */
ibuf = ibufs_arr[context.view_id];
for (ImBuf *ib : ibufs_arr) {
if (ib != ibuf) {
IMB_freeImBuf(ib);
}
}
if (ibuf) {
seq_imbuf_assign_spaces(context.scene, ibuf);
}
}
}
else {
ibuf = render_image_strip_frame(context, strip, filepath, prefix, ext, context.view_id);
}
if (ibuf != nullptr) {
if (context.size_flags & IMB_PROXY_25) {
seq_proxy_build_frame(
context.scene, context.view_id, ibuf, strip, s_elem, 25, context.overwrite);
}
if (context.size_flags & IMB_PROXY_50) {
seq_proxy_build_frame(
context.scene, context.view_id, ibuf, strip, s_elem, 50, context.overwrite);
}
if (context.size_flags & IMB_PROXY_75) {
seq_proxy_build_frame(
context.scene, context.view_id, ibuf, strip, s_elem, 75, context.overwrite);
}
if (context.size_flags & IMB_PROXY_100) {
seq_proxy_build_frame(
context.scene, context.view_id, ibuf, strip, s_elem, 100, context.overwrite);
}
if (set_progress_fn) {
float progress = float(elem_index) / float(strip.len);
set_progress_fn(progress);
}
*job_update_ui = true;
IMB_freeImBuf(ibuf);
}
if (*job_stop || G.is_break) {
break;
}
}
}
static void close_movie_proxy_builder(ProxyBuildContext *context, bool stop)
{
if (context->movie_proxy_builder == nullptr) {
return;
}
for (MovieReader *movie : context->strip->runtime->movie_readers) {
MOV_close_proxies(movie);
}
MOV_proxy_builder_finish(context->movie_proxy_builder, stop);
context->movie_proxy_builder = nullptr;
}
void proxy_build_process(ProxyBuildContext *context,
const bool *should_stop,
bool *has_updated,
const FunctionRef<void(float progress)> set_progress_fn)
{
if (context->strip->type == STRIP_TYPE_MOVIE) {
if (context->movie_proxy_builder) {
MOV_proxy_builder_process(
context->movie_proxy_builder, should_stop, has_updated, set_progress_fn);
close_movie_proxy_builder(context, *should_stop);
}
return;
}
if (context->strip->type == STRIP_TYPE_IMAGE) {
image_proxy_builder_process(*context, should_stop, has_updated, set_progress_fn);
return;
}
}
void proxy_build_finish(ProxyBuildContext *context)
{
close_movie_proxy_builder(context, false);
seq_free_strip_recurse(nullptr, context->strip, true);
MEM_delete(context);
}
void proxy_set(Strip *strip, bool value)
{
if (value) {
strip->flag |= SEQ_USE_PROXY;
if (strip->data->proxy == nullptr) {
strip->data->proxy = seq_strip_proxy_alloc();
}
}
else {
strip->flag &= ~SEQ_USE_PROXY;
}
}
void seq_proxy_index_dir_set(MovieReader *anim, const char *base_dir)
{
char dirname[FILE_MAX];
char filename[FILE_MAXFILE];
MOV_get_filename(anim, filename, FILE_MAXFILE);
BLI_path_join(dirname, sizeof(dirname), base_dir, filename);
MOV_set_custom_proxy_dir(anim, dirname);
}
void free_strip_proxy(Strip *strip)
{
if (strip->data && strip->data->proxy && strip->data->proxy->anim) {
MOV_close(strip->data->proxy->anim);
strip->data->proxy->anim = nullptr;
}
}
} // namespace blender::seq

View File

@@ -0,0 +1,28 @@
/* SPDX-FileCopyrightText: 2004 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup sequencer
*/
namespace blender {
struct ImBuf;
struct MovieReader;
struct Strip;
namespace seq {
struct RenderData;
#define PROXY_MAXFILE (2 * FILE_MAXDIR + FILE_MAXFILE)
ImBuf *seq_proxy_fetch(const RenderData *context, Strip *strip, int timeline_frame);
bool seq_proxy_get_custom_file_filepath(const Strip *strip, char *filepath, int view_id);
void free_strip_proxy(Strip *strip);
void seq_proxy_index_dir_set(MovieReader *anim, const char *base_dir);
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,98 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "MEM_guardedalloc.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BLI_listbase.h"
#include "BKE_context.hh"
#include "SEQ_proxy.hh"
#include "SEQ_relations.hh"
#include "SEQ_sequencer.hh"
#include "WM_api.hh"
#include "WM_types.hh"
namespace blender::seq {
static void proxy_freejob(void *pjv)
{
ProxyJob *pj = static_cast<ProxyJob *>(pjv);
MEM_delete(pj);
}
/* Only this runs inside thread. */
static void proxy_startjob(void *pjv, wmJobWorkerStatus *worker_status)
{
ProxyJob *pj = static_cast<ProxyJob *>(pjv);
for (const int i : pj->queue.index_range()) {
ProxyBuildContext *context = pj->queue[i];
proxy_build_process(
context, &worker_status->stop, &worker_status->do_update, [&](const float new_progress) {
/* Remap the progress of the current proxy to the total progress. */
const float total_progress = (i + new_progress) / pj->queue.size();
worker_status->progress = total_progress;
});
if (worker_status->stop) {
pj->stop = true;
fprintf(stderr, "Canceling proxy rebuild on users request...\n");
break;
}
}
}
static void proxy_endjob(void *pjv)
{
ProxyJob *pj = static_cast<ProxyJob *>(pjv);
Editing *ed = editing_get(pj->scene);
for (ProxyBuildContext *context : pj->queue) {
proxy_build_finish(context);
}
pj->queue.clear();
relations_free_imbuf(pj->scene, &ed->seqbase, false);
WM_main_add_notifier(NC_SCENE | ND_SEQUENCER, pj->scene);
}
ProxyJob *ED_seq_proxy_job_get(const bContext *C, wmJob *wm_job)
{
Scene *scene = CTX_data_sequencer_scene(C);
ProxyJob *pj = static_cast<ProxyJob *>(WM_jobs_customdata_get(wm_job));
if (!pj) {
pj = MEM_new<ProxyJob>("proxy rebuild job");
pj->scene = scene;
pj->main = CTX_data_main(C);
WM_jobs_customdata_set(wm_job, pj, proxy_freejob);
WM_jobs_timer(wm_job, 0.1, NC_SCENE | ND_SEQUENCER, NC_SCENE | ND_SEQUENCER);
WM_jobs_callbacks(wm_job, proxy_startjob, nullptr, nullptr, proxy_endjob);
}
return pj;
}
wmJob *ED_seq_proxy_wm_job_get(const bContext *C)
{
Scene *scene = CTX_data_sequencer_scene(C);
wmJob *wm_job = WM_jobs_get(CTX_wm_manager(C),
CTX_wm_window(C),
scene,
"Building proxies...",
WM_JOB_PROGRESS,
WM_JOB_TYPE_SEQ_BUILD_PROXY);
return wm_job;
}
} // namespace blender::seq

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup sequencer
*/
#include "DNA_listBase.h"
#include "BLI_math_vector_types.hh"
#include "BLI_set.hh"
namespace blender {
struct Depsgraph;
struct ImBuf;
struct Mask;
struct RenderData;
struct Scene;
struct SeqTimelineChannel;
struct Strip;
namespace seq {
/* Recursion protection while rendering a single sequencer frame.
* If the same scene or strip is seen, recursion stops. */
struct SeqRenderState {
Set<Scene *> scenes_in_progress;
Set<Strip *> strips_in_progress;
};
/* Strip corner coordinates in screen pixel space. Note that they might not be
* axis aligned when rotation is present. */
struct StripScreenQuad {
float2 v0, v1, v2, v3;
bool is_empty() const
{
return v0 == v1 && v2 == v3 && v0 == v2;
}
};
/**
* Result of rendering a strip: the produced image,
* plus some auxiliary data.
*/
struct SeqResult {
bool is_valid() const
{
return image != nullptr;
}
ImBuf *image = nullptr;
/* How much the resulting image should be translated, in pixels. */
float2 translation = float2(0, 0);
bool is_opaque_before_transform = false;
};
SeqResult seq_render_give_ibuf_seqbase(const RenderData *context,
SeqRenderState *state,
float timeline_frame,
int chan_shown,
ListBaseT<SeqTimelineChannel> *channels,
ListBaseT<Strip> *seqbasep);
SeqResult seq_render_strip(const RenderData *context,
SeqRenderState *state,
Strip *strip,
float timeline_frame);
/* Renders Mask into an image suitable for sequencer:
* RGB channels contain mask intensity; alpha channel is opaque. */
ImBuf *seq_render_mask(Depsgraph *depsgraph,
int width,
int height,
const Mask *mask,
float frame_index,
bool make_float);
/* Converts image to sequencer color space, if needed. */
void ensure_ibuf_is_sequencer_space(const Scene *scene, ImBuf *ibuf, bool make_float);
void seq_imbuf_assign_spaces(const Scene *scene, ImBuf *ibuf);
StripScreenQuad get_strip_screen_quad(const RenderData *context, const Strip *strip);
void convert_multilayer_ibuf(ImBuf *ibuf);
bool seq_image_strip_is_multiview_render(const Scene *scene,
const Strip *strip,
int totfiles,
const char *filepath,
char *r_prefix,
const char *r_ext);
} // namespace seq
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
/* SPDX-FileCopyrightText: 2004 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup sequencer
*/
#include "BLI_span.hh"
namespace blender {
struct Editing;
struct Scene;
struct Strip;
struct StripProxy;
namespace seq {
/**
* Cache must be freed before calling this function
* since it leaves the #Editing::seqbase in an invalid state.
*/
void seq_free_strip_recurse(Scene *scene, Strip *strip, bool do_id_user);
StripProxy *seq_strip_proxy_alloc();
/**
* Find effect strips, that use strip `strip` as one of inputs.
* If lookup hash doesn't exist, it will be created. If hash is tagged as invalid, it will be
* rebuilt.
*
* \param key: pointer to Strip inside of meta strip
*
* \return collection of effect strips
*/
Span<Strip *> SEQ_lookup_effects_by_strip(Editing *ed, const Strip *key);
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,535 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#include <xxhash.h>
#include "MEM_guardedalloc.h"
#include "DNA_curve_types.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "DNA_sound_types.h"
#include "BLI_listbase.h"
#include "BLI_utildefines.h"
#include "BKE_colortools.hh"
#include "BKE_sound.hh"
#include "SEQ_modifier.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_sound.hh"
#include "strip_time.hh"
#ifdef WITH_AUDASPACE
# include <fx/Echo.h>
# ifdef WITH_CONVOLUTION
# include <fx/Equalizer.h>
# endif
# include <fx/TimeStretchPitchScale.h>
# include <util/Buffer.h>
#endif
namespace blender::seq {
/* Unlike _update_sound_ functions,
* these ones take info from audaspace to update sequence length! */
const SoundModifierWorkerInfo workersSoundModifiers[] = {
{eSeqModifierType_SoundEqualizer, sound_equalizermodifier_recreator},
{eSeqModifierType_Pitch, pitchmodifier_recreator},
{eSeqModifierType_Echo, echomodifier_recreator},
{0, nullptr}};
#ifdef WITH_CONVOLUTION
static bool sequencer_refresh_sound_length_recursive(Main *bmain,
Scene *scene,
ListBaseT<Strip> *seqbase)
{
bool changed = false;
for (Strip &strip : *seqbase) {
if (strip.type == STRIP_TYPE_META) {
if (sequencer_refresh_sound_length_recursive(bmain, scene, &strip.seqbase)) {
changed = true;
}
}
else if (strip.type == STRIP_TYPE_SOUND && strip.sound) {
SoundInfo info;
if (!BKE_sound_info_get(bmain, strip.sound, &info)) {
continue;
}
int old = strip.len;
float fac;
strip.len = std::max(
1, int(round((info.length - strip.sound->offset_time) * scene->frames_per_second())));
fac = float(strip.len) / float(old);
old = strip.startofs;
strip.startofs *= fac;
strip.endofs *= fac;
strip.start += (old -
strip.startofs); /* So that visual/"real" start frame does not change! */
changed = true;
}
}
return changed;
}
#endif
void sound_update_length(Main *bmain, Scene *scene)
{
#ifdef WITH_CONVOLUTION
if (scene->ed) {
sequencer_refresh_sound_length_recursive(bmain, scene, &scene->ed->seqbase);
}
#else
UNUSED_VARS(bmain, scene);
#endif
}
void sound_update_bounds_all(Scene *scene)
{
Editing *ed = scene->ed;
if (ed) {
for (Strip &strip : ed->seqbase) {
if (strip.type == STRIP_TYPE_META) {
strip_update_sound_bounds_recursive(scene, &strip);
}
else if (ELEM(strip.type, STRIP_TYPE_SOUND, STRIP_TYPE_SCENE)) {
sound_update_bounds(scene, &strip);
}
}
}
}
void sound_update_bounds(Scene *scene, Strip *strip)
{
if (strip->type == STRIP_TYPE_SCENE) {
if (strip->scene && strip->runtime->scene_sound) {
/* We have to take into account start frame of the sequence's scene! */
int startofs = strip->startofs + strip->anim_startofs + strip->scene->r.sfra;
BKE_sound_move_scene_sound(scene,
strip->runtime->scene_sound,
strip->left_handle(),
strip->right_handle(scene),
startofs,
0.0);
}
}
else {
BKE_sound_move_scene_sound_defaults(scene, strip);
}
/* mute is set in strip_update_muting_recursive */
}
static void strip_update_sound_recursive(Scene *scene, ListBaseT<Strip> *seqbasep, bSound *sound)
{
for (Strip &strip : *seqbasep) {
if (strip.type == STRIP_TYPE_META) {
strip_update_sound_recursive(scene, &strip.seqbase, sound);
}
else if (strip.type == STRIP_TYPE_SOUND) {
if (strip.runtime->scene_sound && sound == strip.sound) {
BKE_sound_update_scene_sound(strip.runtime->scene_sound, sound);
}
}
}
}
void sound_update(Scene *scene, bSound *sound)
{
if (scene->ed) {
strip_update_sound_recursive(scene, &scene->ed->seqbase, sound);
}
}
float sound_pitch_get(const Scene *scene, const Strip *strip)
{
const Strip *meta_parent = lookup_meta_by_strip(scene->ed, strip);
if (meta_parent != nullptr) {
return strip->speed_factor * sound_pitch_get(scene, meta_parent);
}
return strip->speed_factor;
}
EQCurveMappingData *sound_equalizer_add(SoundEqualizerModifierData *semd, float minX, float maxX)
{
EQCurveMappingData *eqcmd;
if (maxX < 0) {
maxX = SOUND_EQUALIZER_DEFAULT_MAX_FREQ;
}
if (minX < 0) {
minX = 0.0;
}
/* It's the same as #BKE_curvemapping_add, but changing the name. */
eqcmd = MEM_new<EQCurveMappingData>("Equalizer");
BKE_curvemapping_set_defaults(&eqcmd->curve_mapping,
1, /* Total. */
minX,
-SOUND_EQUALIZER_DEFAULT_MAX_DB, /* Min x, y */
maxX,
SOUND_EQUALIZER_DEFAULT_MAX_DB, /* Max x, y */
HD_AUTO_ANIM);
eqcmd->curve_mapping.preset = CURVE_PRESET_CONSTANT_MEDIAN;
rctf clipr;
clipr.xmin = minX;
clipr.xmax = maxX;
clipr.ymin = 0.0;
clipr.ymax = 0.0;
BKE_curvemap_reset(&eqcmd->curve_mapping.cm[0],
&clipr,
CURVE_PRESET_CONSTANT_MEDIAN,
CurveMapSlopeType::Negative);
BLI_addtail(&semd->graphics, eqcmd);
return eqcmd;
}
void sound_equalizermodifier_set_graphs(SoundEqualizerModifierData *semd, int number)
{
sound_equalizermodifier_free(reinterpret_cast<StripModifierData *>(semd));
if (number == 1) {
sound_equalizer_add(semd, SOUND_EQUALIZER_DEFAULT_MIN_FREQ, SOUND_EQUALIZER_DEFAULT_MAX_FREQ);
}
else if (number == 2) {
sound_equalizer_add(semd, 30.0, 2000.0);
sound_equalizer_add(semd, 2000.1, 20000.0);
}
else if (number == 3) {
sound_equalizer_add(semd, 30.0, 1000.0);
sound_equalizer_add(semd, 1000.1, 5000.0);
sound_equalizer_add(semd, 5000.1, 20000.0);
}
}
EQCurveMappingData *sound_equalizermodifier_add_graph(SoundEqualizerModifierData *semd,
float min_freq,
float max_freq)
{
if (min_freq < 0.0) {
return nullptr;
}
if (max_freq < 0.0) {
return nullptr;
}
if (max_freq <= min_freq) {
return nullptr;
}
return sound_equalizer_add(semd, min_freq, max_freq);
}
void sound_equalizermodifier_remove_graph(SoundEqualizerModifierData *semd,
EQCurveMappingData *eqcmd)
{
BLI_remlink_safe(&semd->graphics, eqcmd);
MEM_delete(eqcmd);
}
void sound_equalizermodifier_init_data(StripModifierData *smd)
{
SoundEqualizerModifierData *semd = reinterpret_cast<SoundEqualizerModifierData *>(smd);
sound_equalizer_add(semd, SOUND_EQUALIZER_DEFAULT_MIN_FREQ, SOUND_EQUALIZER_DEFAULT_MAX_FREQ);
}
void sound_equalizermodifier_free(StripModifierData *smd)
{
SoundEqualizerModifierData *semd = reinterpret_cast<SoundEqualizerModifierData *>(smd);
for (EQCurveMappingData &eqcmd : semd->graphics.items_mutable()) {
BKE_curvemapping_free_data(&eqcmd.curve_mapping);
MEM_delete(&eqcmd);
}
BLI_listbase_clear(&semd->graphics);
}
void sound_equalizermodifier_copy_data(StripModifierData *target, StripModifierData *smd)
{
SoundEqualizerModifierData *semd = reinterpret_cast<SoundEqualizerModifierData *>(smd);
SoundEqualizerModifierData *semd_target = reinterpret_cast<SoundEqualizerModifierData *>(target);
EQCurveMappingData *eqcmd_n;
BLI_listbase_clear(&semd_target->graphics);
for (EQCurveMappingData &eqcmd : semd->graphics) {
eqcmd_n = MEM_dupalloc(&eqcmd);
BKE_curvemapping_copy_data(&eqcmd_n->curve_mapping, &eqcmd.curve_mapping);
eqcmd_n->next = eqcmd_n->prev = nullptr;
BLI_addtail(&semd_target->graphics, eqcmd_n);
}
}
#ifdef WITH_CONVOLUTION
static uint64_t sound_equalizermodifier_get_params_hash(float *buf)
{
return XXH3_64bits(buf, sizeof(float) * SOUND_EQUALIZER_SIZE_DEFINITION);
}
#endif
AUD_Sound sound_equalizermodifier_recreator(Strip *strip,
StripModifierData *smd,
AUD_Sound sound_in,
bool &needs_update)
{
#ifdef WITH_CONVOLUTION
UNUSED_VARS(strip);
SoundEqualizerModifierData *semd = (SoundEqualizerModifierData *)smd;
/* No equalizer definition. */
if (semd->graphics.is_empty()) {
return sound_in;
}
float *buf = MEM_new_array_zeroed<float>(SOUND_EQUALIZER_SIZE_DEFINITION, "eqrecreator");
CurveMapping *eq_mapping;
CurveMap *cm;
float minX;
float maxX;
float interval = SOUND_EQUALIZER_DEFAULT_MAX_FREQ / float(SOUND_EQUALIZER_SIZE_DEFINITION);
/* Visit all equalizer definitions. */
for (EQCurveMappingData &mapping : semd->graphics) {
eq_mapping = &mapping.curve_mapping;
BKE_curvemapping_init(eq_mapping);
cm = eq_mapping->cm;
minX = eq_mapping->curr.xmin;
maxX = eq_mapping->curr.xmax;
int idx = int(ceil(minX / interval));
int i = idx;
for (; i * interval <= maxX && i < SOUND_EQUALIZER_SIZE_DEFINITION; i++) {
float freq = i * interval;
float val = BKE_curvemap_evaluateF(eq_mapping, cm, freq);
if (fabs(val) > SOUND_EQUALIZER_DEFAULT_MAX_DB) {
val = (val / fabs(val)) * SOUND_EQUALIZER_DEFAULT_MAX_DB;
}
buf[i] = val;
/* To soften lower limit, but not the first position which is the constant value */
if (i == idx && i > 2) {
buf[i - 1] = 0.5 * (buf[i] + buf[i - 1]);
}
}
/* To soften higher limit */
if (i < SOUND_EQUALIZER_SIZE_DEFINITION) {
buf[i] = 0.5 * (buf[i] + buf[i - 1]);
}
}
const uint64_t curr_params_hash = sound_equalizermodifier_get_params_hash(buf);
/* Only make new sound when necessary. It is faster and it prevents audio glitches. */
if (!needs_update && smd->runtime->last_sound_in == sound_in &&
curr_params_hash == smd->runtime->params_hash)
{
MEM_delete(buf);
return smd->runtime->last_sound_out;
}
std::shared_ptr<aud::Buffer> aud_buf = std::shared_ptr<aud::Buffer>(
new aud::Buffer(sizeof(float) * SOUND_EQUALIZER_SIZE_DEFINITION));
std::memcpy(aud_buf->getBuffer(), buf, sizeof(float) * SOUND_EQUALIZER_SIZE_DEFINITION);
AUD_Sound sound_out = AUD_Sound(new aud::Equalizer(sound_in,
aud_buf,
SOUND_EQUALIZER_SIZE_DEFINITION,
SOUND_EQUALIZER_DEFAULT_MAX_FREQ,
SOUND_EQUALIZER_SIZE_CONVERSION));
needs_update = true;
smd->runtime->last_sound_in = sound_in;
smd->runtime->last_sound_out = sound_out;
smd->runtime->params_hash = curr_params_hash;
MEM_delete(buf);
return sound_out;
#else
UNUSED_VARS(strip, smd, sound_in, needs_update);
return nullptr;
#endif
}
static uint64_t pitchmodifier_get_params_hash(PitchModifierData *pmd)
{
XXH3_state_t *state = XXH3_createState();
XXH3_64bits_reset(state);
XXH3_64bits_update(state, &pmd->mode, sizeof(pmd->mode));
XXH3_64bits_update(state, &pmd->quality, sizeof(pmd->quality));
XXH3_64bits_update(state, &pmd->semitones, sizeof(pmd->semitones));
XXH3_64bits_update(state, &pmd->cents, sizeof(pmd->cents));
XXH3_64bits_update(state, &pmd->ratio, sizeof(pmd->ratio));
XXH3_64bits_update(state, &pmd->preserve_formant, sizeof(pmd->preserve_formant));
uint64_t hash = XXH3_64bits_digest(state);
XXH3_freeState(state);
return hash;
}
AUD_Sound pitchmodifier_recreator(Strip * /*strip*/,
StripModifierData *smd,
AUD_Sound sound_in,
bool &needs_update)
{
const uint64_t curr_params_hash = pitchmodifier_get_params_hash((PitchModifierData *)smd);
if (!needs_update && smd->runtime->last_sound_in == sound_in &&
curr_params_hash == smd->runtime->params_hash)
{
return smd->runtime->last_sound_out;
}
#if defined(WITH_AUDASPACE) && defined(WITH_RUBBERBAND)
PitchModifierData *pmd = (PitchModifierData *)smd;
aud::StretcherQuality quality;
switch (pmd->quality) {
case PITCH_QUALITY_HIGH:
quality = aud::StretcherQuality::HIGH;
break;
case PITCH_QUALITY_FAST:
quality = aud::StretcherQuality::FAST;
break;
case PITCH_QUALITY_CONSISTENT:
quality = aud::StretcherQuality::CONSISTENT;
break;
default:
quality = aud::StretcherQuality::HIGH;
}
double pitch_scale = 0;
int mode = pmd->mode;
if (mode == PITCH_MODE_SEMITONES) {
pitch_scale = pow(2.0, (pmd->semitones + (pmd->cents / 100.0)) / 12.0);
}
else if (mode == PITCH_MODE_RATIO) {
pitch_scale = pmd->ratio;
if (pitch_scale <= 0.0) {
pitch_scale = 1.0;
pmd->ratio = 1.0;
}
}
if (pitch_scale == 0) {
if (smd->runtime->last_sound_in == sound_in) {
return smd->runtime->last_sound_out;
}
else {
return sound_in;
}
}
AUD_Sound sound_out = AUD_Sound(
new aud::TimeStretchPitchScale(sound_in, 1, pitch_scale, quality, pmd->preserve_formant));
needs_update = true;
smd->runtime->last_sound_in = sound_in;
smd->runtime->last_sound_out = sound_out;
smd->runtime->params_hash = curr_params_hash;
return sound_out;
#else
if (smd->runtime->last_sound_in == sound_in) {
return smd->runtime->last_sound_out;
}
else {
return sound_in;
}
#endif
}
#ifdef WITH_AUDASPACE
static uint64_t echomodifier_get_params_hash(EchoModifierData *emd)
{
XXH3_state_t *state = XXH3_createState();
XXH3_64bits_reset(state);
XXH3_64bits_update(state, &emd->delay, sizeof(emd->delay));
XXH3_64bits_update(state, &emd->feedback, sizeof(emd->feedback));
XXH3_64bits_update(state, &emd->mix, sizeof(emd->mix));
uint64_t hash = XXH3_64bits_digest(state);
XXH3_freeState(state);
return hash;
}
#endif
AUD_Sound echomodifier_recreator(Strip * /*strip*/,
StripModifierData *smd,
AUD_Sound sound_in,
bool &needs_update)
{
#if defined(WITH_AUDASPACE)
const uint64_t curr_params_hash = echomodifier_get_params_hash((EchoModifierData *)smd);
if (!needs_update && smd->runtime->last_sound_in == sound_in &&
curr_params_hash == smd->runtime->params_hash)
{
return smd->runtime->last_sound_out;
}
EchoModifierData *emd = (EchoModifierData *)smd;
AUD_Sound sound_out = AUD_Sound(
new aud::Echo(sound_in, emd->delay, emd->feedback, emd->mix, true));
needs_update = true;
smd->runtime->last_sound_in = sound_in;
smd->runtime->last_sound_out = sound_out;
smd->runtime->params_hash = curr_params_hash;
return sound_out;
#else
UNUSED_VARS(smd, sound_in, needs_update);
return nullptr;
#endif
}
const SoundModifierWorkerInfo *sound_modifier_worker_info_get(int type)
{
for (int i = 0; workersSoundModifiers[i].type > 0; i++) {
if (workersSoundModifiers[i].type == type) {
return &workersSoundModifiers[i];
}
}
return nullptr;
}
AUD_Sound sound_modifier_recreator(Strip *strip,
StripModifierData *smd,
AUD_Sound sound,
bool &needs_update)
{
/* Check if the modifier mute flag has changed. */
if ((smd->flag & STRIP_MODIFIER_FLAG_MUTE) != (smd->runtime->flag & STRIP_MODIFIER_FLAG_MUTE)) {
eStripModifierFlag runtime_flag = smd->runtime->flag;
/* Update the runtime mute flag and flag the sound handle for update. */
runtime_flag &= ~(STRIP_MODIFIER_FLAG_MUTE); /* Clear the bit. */
runtime_flag |= (smd->flag & STRIP_MODIFIER_FLAG_MUTE); /* Set the bit. */
smd->runtime->flag = runtime_flag;
needs_update = true;
}
if (!(smd->flag & STRIP_MODIFIER_FLAG_MUTE)) {
const SoundModifierWorkerInfo *smwi = sound_modifier_worker_info_get(smd->type);
return smwi->recreator(strip, smd, sound, needs_update);
}
return sound;
}
} // namespace blender::seq

View File

@@ -0,0 +1,735 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <algorithm>
#include <cmath>
#include <cstring>
#include "MEM_guardedalloc.h"
#include "DNA_mask_types.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "DNA_sound_types.h"
#include "BLI_math_base.hh"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BKE_image.hh"
#include "BKE_layer.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_mask.hh"
#include "BKE_movieclip.hh"
#include "BKE_scene.hh"
#include "BKE_sound.hh"
#include "DEG_depsgraph_query.hh"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"
#include "MOV_read.hh"
#include "SEQ_add.hh"
#include "SEQ_edit.hh"
#include "SEQ_relations.hh"
#include "SEQ_render.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_time.hh"
#include "SEQ_transform.hh"
#include "SEQ_utils.hh"
#include "effects/effects.hh"
#include "multiview.hh"
#include "proxy.hh"
#include "strip_time.hh"
namespace blender::seq {
void add_load_data_init(LoadData *load_data,
const char *name,
const char *path,
const int start_frame,
const int channel)
{
memset(load_data, 0, sizeof(LoadData));
if (name != nullptr) {
STRNCPY(load_data->name, name);
}
if (path != nullptr) {
STRNCPY(load_data->path, path);
}
load_data->start_frame = start_frame;
load_data->channel = channel;
}
static void strip_add_generic_update(Scene *scene, Strip *strip)
{
strip_unique_name_set(scene, &scene->ed->seqbase, strip);
/* Set effect time range values before cache invalidation. */
strip_time_effect_range_set(scene, strip);
relations_invalidate_cache(scene, strip);
strip_lookup_invalidate(scene->ed);
time_update_meta_strip_range(scene, lookup_meta_by_strip(scene->ed, strip));
}
static void strip_add_set_name(Scene *scene, Strip *strip, LoadData *load_data)
{
if (load_data->name[0] != '\0') {
edit_strip_name_set(scene, strip, load_data->name);
}
else {
if (strip->type == STRIP_TYPE_SCENE) {
edit_strip_name_set(scene, strip, load_data->scene->id.name + 2);
}
else if (strip->type == STRIP_TYPE_MOVIECLIP) {
edit_strip_name_set(scene, strip, load_data->clip->id.name + 2);
}
else if (strip->type == STRIP_TYPE_MASK) {
edit_strip_name_set(scene, strip, load_data->mask->id.name + 2);
}
else if (strip->is_effect()) {
edit_strip_name_set(scene, strip, strip_give_name(strip));
}
else { /* Image, sound and movie. */
edit_strip_name_set(scene, strip, load_data->name);
}
}
}
static void strip_add_set_view_transform(Scene *scene, Strip *strip, LoadData *load_data)
{
const char *strip_colorspace = strip->data->colorspace_settings.name;
if (load_data->flags & SEQ_LOAD_SET_VIEW_TRANSFORM) {
const char *role_colorspace_byte;
role_colorspace_byte = IMB_colormanagement_role_colorspace_name_get(COLOR_ROLE_DEFAULT_BYTE);
if (STREQ(strip_colorspace, role_colorspace_byte)) {
const ColorManagedDisplay *display = IMB_colormanagement_display_get_named(
scene->display_settings.display_device);
const char *default_view_transform =
IMB_colormanagement_display_get_default_view_transform_name(display);
STRNCPY_UTF8(scene->view_settings.view_transform, default_view_transform);
}
}
}
Strip *add_scene_strip(Scene *scene, ListBaseT<Strip> *seqbase, LoadData *load_data)
{
Strip *strip = strip_alloc(
seqbase, load_data->start_frame, load_data->channel, STRIP_TYPE_SCENE);
strip->scene = load_data->scene;
strip->scene_view_layer_name = BLI_strdup(BKE_view_layer_default_render(strip->scene)->name);
strip->len = load_data->scene->r.efra - load_data->scene->r.sfra + 1;
id_us_ensure_real(id_cast<ID *>(load_data->scene));
strip_add_set_name(scene, strip, load_data);
strip_add_generic_update(scene, strip);
return strip;
}
Strip *add_movieclip_strip(Scene *scene, ListBaseT<Strip> *seqbase, LoadData *load_data)
{
Strip *strip = strip_alloc(
seqbase, load_data->start_frame, load_data->channel, STRIP_TYPE_MOVIECLIP);
strip->clip = load_data->clip;
strip->len = BKE_movieclip_get_duration(load_data->clip);
id_us_ensure_real(id_cast<ID *>(load_data->clip));
strip_add_set_name(scene, strip, load_data);
strip_add_generic_update(scene, strip);
return strip;
}
Strip *add_mask_strip(Scene *scene, ListBaseT<Strip> *seqbase, LoadData *load_data)
{
Strip *strip = strip_alloc(seqbase, load_data->start_frame, load_data->channel, STRIP_TYPE_MASK);
strip->mask = load_data->mask;
strip->len = BKE_mask_get_duration(load_data->mask);
id_us_ensure_real(id_cast<ID *>(load_data->mask));
strip_add_set_name(scene, strip, load_data);
strip_add_generic_update(scene, strip);
return strip;
}
Strip *add_effect_strip(Scene *scene, ListBaseT<Strip> *seqbase, LoadData *load_data)
{
Strip *strip = strip_alloc(
seqbase, load_data->start_frame, load_data->channel, load_data->effect.type);
strip->flag |= SEQ_USE_EFFECT_DEFAULT_FADE;
effect_ensure_initialized(strip);
const int min_inputs = effect_type_get_min_num_inputs(load_data->effect.type);
if (min_inputs != 0 || load_data->effect.type == STRIP_TYPE_COMPOSITOR) {
strip->input1 = load_data->effect.input1;
strip->input2 = load_data->effect.input2;
}
if (min_inputs == 1) {
strip->blend_mode = strip->input1->blend_mode;
strip->blend_opacity = strip->input1->blend_opacity;
}
if (strip->input1 == nullptr) {
strip->len = 1; /* Effect is generator, set non zero length. */
strip->flag |= SEQ_SINGLE_FRAME_CONTENT;
strip->right_handle_set(scene, load_data->start_frame + load_data->effect.length);
}
strip_add_set_name(scene, strip, load_data);
strip_add_generic_update(scene, strip);
return strip;
}
void add_image_set_directory(Strip *strip, const char *dirpath)
{
STRNCPY(strip->data->dirpath, dirpath);
}
void add_image_load_file(Scene *scene, Strip *strip, size_t strip_frame, const char *filename)
{
StripElem *se = render_give_stripelem(scene, strip, strip->content_start() + strip_frame);
STRNCPY(se->filename, filename);
}
void add_image_init_alpha_mode(Main *bmain, Scene *scene, Strip *strip)
{
if (strip->data && strip->data->stripdata) {
char filepath[FILE_MAX];
ImBuf *ibuf;
BLI_path_join(
filepath, sizeof(filepath), strip->data->dirpath, strip->data->stripdata->filename);
BLI_path_abs(filepath, ID_BLEND_PATH(bmain, &scene->id));
/* Initialize input color space. */
if (strip->type == STRIP_TYPE_IMAGE) {
ibuf = IMB_load_image_from_filepath(filepath,
ImBufFlags::Test | ImBufFlags::MultiLayer |
ImBufFlags::AlphaDetect,
strip->data->colorspace_settings.name);
/* Byte images are default to straight alpha, however sequencer
* works in pre-multiply space, so mark strip to be pre-multiplied first. */
strip->alpha_mode = SEQ_ALPHA_STRAIGHT;
if (ibuf) {
if (flag_is_set(ibuf->flags, ImBufFlags::AlphaPremul)) {
strip->alpha_mode = SEQ_ALPHA_PREMUL;
}
IMB_freeImBuf(ibuf);
}
}
}
}
Strip *add_image_strip(Main *bmain, Scene *scene, ListBaseT<Strip> *seqbase, LoadData *load_data)
{
Strip *strip = strip_alloc(
seqbase, load_data->start_frame, load_data->channel, STRIP_TYPE_IMAGE);
strip->len = load_data->image.count;
StripData *data = strip->data;
data->stripdata = MEM_new_array<StripElem>(load_data->image.count, "stripelem");
if (strip->len == 1) {
strip->flag |= SEQ_SINGLE_FRAME_CONTENT;
}
/* Multiview settings. */
if (load_data->use_multiview) {
strip->flag |= SEQ_USE_VIEWS;
strip->views_format = load_data->views_format;
}
if (load_data->stereo3d_format) {
strip->stereo3d_format = MEM_new<Stereo3dFormat>("strip stereo3d format");
*strip->stereo3d_format = *load_data->stereo3d_format;
}
/* Set initial scale based on load_data->fit_method. */
char file_path[FILE_MAX];
STRNCPY(file_path, load_data->path);
BLI_path_abs(file_path, ID_BLEND_PATH(bmain, &scene->id));
ImBuf *ibuf = IMB_load_image_from_filepath(file_path,
ImBufFlags::ByteData | ImBufFlags::MultiLayer,
strip->data->colorspace_settings.name);
if (ibuf != nullptr) {
/* Set image resolution. Assume that all images in sequence are same size. This fields are only
* informative. */
StripElem *strip_elem = data->stripdata;
for (int i = 0; i < load_data->image.count; i++) {
strip_elem->orig_width = ibuf->x;
strip_elem->orig_height = ibuf->y;
strip_elem++;
}
set_scale_to_fit(strip, ibuf->x, ibuf->y, scene->r.xsch, scene->r.ysch, load_data->fit_method);
IMB_freeImBuf(ibuf);
}
/* Adjust starting length of strip from handle to handle.
* Note that this differs from the content `strip->len`, which is always 1 for single images. */
if (seq::transform_single_image_check(strip)) {
strip->right_handle_set(scene, load_data->start_frame + load_data->image.length);
}
strip_add_set_view_transform(scene, strip, load_data);
strip_add_set_name(scene, strip, load_data);
strip_add_generic_update(scene, strip);
return strip;
}
#ifdef WITH_AUDASPACE
Strip *add_sound_strip(Main *bmain, Scene *scene, ListBaseT<Strip> *seqbase, LoadData *load_data)
{
/* Handles relative paths. */
bSound *sound = BKE_sound_new_file_exists(bmain, load_data->path, load_data->stream_index);
SoundInfo info;
bool sound_loaded = BKE_sound_info_get(bmain, sound, &info);
if (!sound_loaded && !load_data->allow_invalid_file) {
BKE_id_free_us(bmain, sound);
return nullptr;
}
if (info.specs.channels == SOUND_CHANNELS_INVALID && !load_data->allow_invalid_file) {
BKE_id_free_us(bmain, sound);
return nullptr;
}
Strip *strip = strip_alloc(
seqbase, load_data->start_frame, load_data->channel, STRIP_TYPE_SOUND);
strip->sound = sound;
strip->streamindex = load_data->stream_index;
/* We round the frame duration as the audio sample lengths usually does not
* line up with the video frames. Therefore we round this number to the
* nearest frame as the audio track usually overshoots or undershoots the
* end frame of the video by a little bit.
* See #47135 for under shoot example. */
strip->len = std::max(
1, int(round((info.length - sound->offset_time) * scene->frames_per_second())));
StripData *data = strip->data;
/* We only need 1 element to store the filename. */
StripElem *se = data->stripdata = MEM_new<StripElem>("stripelem");
BLI_path_split_dir_file(
load_data->path, data->dirpath, sizeof(data->dirpath), se->filename, sizeof(se->filename));
if (strip->sound != nullptr) {
if (load_data->flags & SEQ_LOAD_SOUND_MONO) {
strip->sound->flags |= SOUND_FLAGS_MONO;
}
if (load_data->flags & SEQ_LOAD_SOUND_CACHE) {
if (strip->sound) {
strip->sound->flags |= SOUND_FLAGS_CACHING;
}
}
/* Turn on Display Waveform by default. */
strip->flag |= SEQ_AUDIO_DRAW_WAVEFORM;
/* Turn on Preserve Pitch by default. */
strip->flag |= SEQ_AUDIO_PITCH_CORRECTION;
}
strip_add_set_name(scene, strip, load_data);
strip_add_generic_update(scene, strip);
return strip;
}
#else // WITH_AUDASPACE
Strip *add_sound_strip(Main * /*bmain*/,
Scene * /*scene*/,
ListBaseT<Strip> * /*seqbase*/,
LoadData * /*load_data*/)
{
return nullptr;
}
#endif // WITH_AUDASPACE
Strip *add_meta_strip(Scene *scene, ListBaseT<Strip> *seqbase, LoadData *load_data)
{
/* Allocate strip. */
Strip *strip_meta = strip_alloc(
seqbase, load_data->start_frame, load_data->channel, STRIP_TYPE_META);
/* Set name. */
strip_add_set_name(scene, strip_meta, load_data);
/* Set frames start and length. */
strip_meta->start = load_data->start_frame;
strip_meta->len = 1;
strip_add_generic_update(scene, strip_meta);
return strip_meta;
}
Strip *add_movie_strip(Main *bmain, Scene *scene, ListBaseT<Strip> *seqbase, LoadData *load_data)
{
char filepath[sizeof(load_data->path)];
STRNCPY(filepath, load_data->path);
BLI_path_abs(filepath, ID_BLEND_PATH(bmain, &scene->id));
char colorspace[/*MAX_COLORSPACE_NAME*/ 64] = "\0";
bool is_multiview_loaded = false;
const int totfiles = seq_num_files(scene, load_data->views_format, load_data->use_multiview);
Array<MovieReader *> anim_arr(totfiles, nullptr);
int orig_width = 0;
int orig_height = 0;
if (load_data->use_multiview && (load_data->views_format == R_IMF_VIEWS_INDIVIDUAL)) {
char prefix[FILE_MAX];
const char *ext = nullptr;
size_t j = 0;
BKE_scene_multiview_view_prefix_get(scene, filepath, prefix, &ext);
if (prefix[0] != '\0') {
for (int i = 0; i < totfiles; i++) {
char filepath_view[FILE_MAX];
seq_multiview_name(scene, i, prefix, ext, filepath_view, sizeof(filepath_view));
/* Sequencer takes care of colorspace conversion of the result. The input is the best to be
* kept unchanged for the performance reasons. */
anim_arr[j] = openanim(filepath_view, ImBufFlags::Zero, 0, true, colorspace);
if (anim_arr[j]) {
seq_anim_add_suffix(scene, anim_arr[j], i);
j++;
}
}
is_multiview_loaded = true;
}
}
if (is_multiview_loaded == false) {
/* Sequencer takes care of colorspace conversion of the result. The input is the best to be
* kept unchanged for the performance reasons. */
anim_arr[0] = openanim(filepath, ImBufFlags::Zero, load_data->stream_index, true, colorspace);
}
if (anim_arr[0] == nullptr && !load_data->allow_invalid_file) {
return nullptr;
}
float video_fps = 0.0f;
load_data->video_stream_start = 0.0;
if (anim_arr[0] != nullptr) {
short fps_num;
float fps_denom;
bool have_fps = MOV_get_fps_num_denom(anim_arr[0], fps_num, fps_denom);
if (have_fps) {
video_fps = fps_num / fps_denom;
}
/* Adjust scene's frame rate settings to match. */
if (have_fps && (load_data->flags & SEQ_LOAD_MOVIE_SYNC_FPS)) {
scene->r.frs_sec = fps_num;
scene->r.frs_sec_base = fps_denom;
DEG_id_tag_update(&scene->id, ID_RECALC_AUDIO_FPS | ID_RECALC_SEQUENCER_STRIPS);
}
load_data->video_stream_start = MOV_get_start_offset_seconds(anim_arr[0]);
}
Strip *strip = strip_alloc(
seqbase, load_data->start_frame, load_data->channel, STRIP_TYPE_MOVIE);
strip->streamindex = load_data->stream_index;
/* Multiview settings. */
if (load_data->use_multiview) {
strip->flag |= SEQ_USE_VIEWS;
strip->views_format = load_data->views_format;
}
if (load_data->stereo3d_format) {
strip->stereo3d_format = MEM_new<Stereo3dFormat>("strip stereo3d format");
*strip->stereo3d_format = *load_data->stereo3d_format;
}
BLI_SCOPED_DEFER([&]() {
for (MovieReader *mr : anim_arr) {
if (!mr) {
continue;
}
if (strip->intersects_frame(scene, scene->r.cfra)) {
strip->runtime->movie_readers.append(mr);
}
else {
MOV_close(mr);
}
}
});
if (anim_arr[0] != nullptr) {
strip->len = MOV_get_duration_frames(anim_arr[0]);
MOV_load_metadata(anim_arr[0]);
/* Set initial scale based on load_data->fit_method. */
orig_width = MOV_get_image_width(anim_arr[0]);
orig_height = MOV_get_image_height(anim_arr[0]);
set_scale_to_fit(
strip, orig_width, orig_height, scene->r.xsch, scene->r.ysch, load_data->fit_method);
float fps = MOV_get_fps(anim_arr[0]);
if (fps > 0.0f) {
strip->media_playback_rate = fps;
}
}
strip->len = std::max(1, strip->len);
if (load_data->adjust_playback_rate) {
strip->flag |= SEQ_AUTO_PLAYBACK_RATE;
}
STRNCPY_UTF8(strip->data->colorspace_settings.name, colorspace);
StripData *data = strip->data;
/* We only need 1 element for MOVIE strips. */
StripElem *se;
data->stripdata = se = MEM_new<StripElem>("stripelem");
data->stripdata->orig_width = orig_width;
data->stripdata->orig_height = orig_height;
data->stripdata->orig_fps = video_fps;
BLI_path_split_dir_file(
load_data->path, data->dirpath, sizeof(data->dirpath), se->filename, sizeof(se->filename));
strip_add_set_view_transform(scene, strip, load_data);
strip_add_set_name(scene, strip, load_data);
strip_add_generic_update(scene, strip);
return strip;
}
void add_reload_new_file(Main *bmain, Scene *scene, Strip *strip, const bool lock_range)
{
int prev_start_frame = 0, prev_end_frame = 0;
/* NOTE: don't rename the strip, will break animation curves. */
if (ELEM(strip->type,
STRIP_TYPE_MOVIE,
STRIP_TYPE_IMAGE,
STRIP_TYPE_SOUND,
STRIP_TYPE_SCENE,
STRIP_TYPE_META,
STRIP_TYPE_MOVIECLIP,
STRIP_TYPE_MASK) == 0)
{
return;
}
if (lock_range) {
/* keep so we don't have to move the actual start and end points (only the data) */
prev_start_frame = strip->left_handle();
prev_end_frame = strip->right_handle(scene);
}
switch (strip->type) {
case STRIP_TYPE_IMAGE: {
/* Hack? */
size_t olen = MEM_allocN_len(strip->data->stripdata) / sizeof(StripElem);
strip->len = olen;
strip->len -= strip->anim_startofs;
strip->len -= strip->anim_endofs;
strip->len = std::max(strip->len, 0);
break;
}
case STRIP_TYPE_MOVIE: {
char filepath[FILE_MAX];
bool is_multiview_loaded = false;
const bool is_multiview = (strip->flag & SEQ_USE_VIEWS) != 0 &&
(scene->r.scemode & R_MULTIVIEW) != 0;
BLI_path_join(
filepath, sizeof(filepath), strip->data->dirpath, strip->data->stripdata->filename);
BLI_path_abs(filepath, ID_BLEND_PATH(bmain, &scene->id));
strip_free_movie_readers(strip);
if (is_multiview && (strip->views_format == R_IMF_VIEWS_INDIVIDUAL)) {
char prefix[FILE_MAX];
const char *ext = nullptr;
const int totfiles = seq_num_files(scene, strip->views_format, true);
int i = 0;
BKE_scene_multiview_view_prefix_get(scene, filepath, prefix, &ext);
if (prefix[0] != '\0') {
for (i = 0; i < totfiles; i++) {
char filepath_view[FILE_MAX];
seq_multiview_name(scene, i, prefix, ext, filepath_view, sizeof(filepath_view));
/* Sequencer takes care of colorspace conversion of the result. The input is the best
* to be kept unchanged for the performance reasons. */
MovieReader *anim = openanim(
filepath_view,
(strip->flag & SEQ_DEINTERLACE) ? ImBufFlags::Deinterlace : ImBufFlags::Zero,
strip->streamindex,
true,
strip->data->colorspace_settings.name);
if (anim) {
seq_anim_add_suffix(scene, anim, i);
strip->runtime->movie_readers.append(anim);
}
}
is_multiview_loaded = true;
}
}
if (is_multiview_loaded == false) {
/* Sequencer takes care of colorspace conversion of the result. The input is the best to be
* kept unchanged for the performance reasons. */
MovieReader *anim = openanim(filepath,
(strip->flag & SEQ_DEINTERLACE) ? ImBufFlags::Deinterlace :
ImBufFlags::Zero,
strip->streamindex,
true,
strip->data->colorspace_settings.name);
if (anim) {
strip->runtime->movie_readers.append(anim);
}
}
/* use the first video as reference for everything */
MovieReader *reader = strip->runtime->movie_reader_get();
if (reader == nullptr) {
return;
}
MOV_load_metadata(reader);
strip->len = MOV_get_duration_frames(reader);
strip->len -= strip->anim_startofs;
strip->len -= strip->anim_endofs;
strip->len = std::max(strip->len, 0);
break;
}
case STRIP_TYPE_MOVIECLIP:
if (strip->clip == nullptr) {
return;
}
strip->len = BKE_movieclip_get_duration(strip->clip);
strip->len -= strip->anim_startofs;
strip->len -= strip->anim_endofs;
strip->len = std::max(strip->len, 0);
break;
case STRIP_TYPE_MASK:
if (strip->mask == nullptr) {
return;
}
strip->len = BKE_mask_get_duration(strip->mask);
strip->len -= strip->anim_startofs;
strip->len -= strip->anim_endofs;
strip->len = std::max(strip->len, 0);
break;
case STRIP_TYPE_SOUND:
#ifdef WITH_AUDASPACE
if (!strip->sound) {
return;
}
strip->len = ceil(double(BKE_sound_get_length(bmain, strip->sound)) *
scene->frames_per_second());
strip->len -= strip->anim_startofs;
strip->len -= strip->anim_endofs;
strip->len = std::max(strip->len, 0);
#else
UNUSED_VARS(bmain);
return;
#endif
break;
case STRIP_TYPE_SCENE: {
strip->len = (strip->scene) ? strip->scene->r.efra - strip->scene->r.sfra + 1 : 0;
strip->len -= strip->anim_startofs;
strip->len -= strip->anim_endofs;
strip->len = std::max(strip->len, 0);
break;
}
default:
break;
}
free_strip_proxy(strip);
if (lock_range) {
strip->handles_set(scene, prev_start_frame, prev_end_frame);
}
relations_invalidate_cache_raw(scene, strip);
}
void add_movie_reload_if_needed(
Main *bmain, Scene *scene, Strip *strip, bool *r_was_reloaded, bool *r_can_produce_frames)
{
BLI_assert_msg(strip->type == STRIP_TYPE_MOVIE,
"This function is only implemented for movie strips.");
bool must_reload = false;
if (strip->runtime->movie_readers.is_empty()) {
/* No movie readers open: reload is necessary. */
must_reload = true;
}
else {
for (const MovieReader *reader : strip->runtime->movie_readers) {
if (!MOV_is_initialized_and_valid(reader)) {
/* A movie reader cannot produce frames, try reloading. */
must_reload = true;
break;
}
}
}
if (!must_reload) {
/* All good! */
*r_was_reloaded = false;
*r_can_produce_frames = true;
return;
}
add_reload_new_file(bmain, scene, strip, true);
*r_was_reloaded = true;
if (strip->runtime->movie_readers.is_empty()) {
/* No readers after reload -> can't produce frames. */
*r_can_produce_frames = false;
return;
}
for (const MovieReader *reader : strip->runtime->movie_readers) {
if (!MOV_is_initialized_and_valid(reader)) {
/* There is still a movie that cannot produce frames. */
*r_can_produce_frames = false;
return;
}
}
/* All good after a reload. */
*r_can_produce_frames = true;
}
} // namespace blender::seq

View File

@@ -0,0 +1,157 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "BLI_listbase.h"
#include "DNA_sequence_types.h"
#include "SEQ_connect.hh"
namespace blender::seq {
static void strip_connections_free(Strip *strip)
{
if (strip == nullptr) {
return;
}
ListBaseT<StripConnection> *connections = &strip->connections;
for (StripConnection &con : connections->items_mutable()) {
MEM_delete(&con);
}
connections->clear_no_delete();
}
void connections_duplicate(ListBaseT<StripConnection> *connections_dst,
ListBaseT<StripConnection> *connections_src)
{
for (StripConnection &con : *connections_src) {
StripConnection *con_duplicate = MEM_new<StripConnection>(__func__, con);
BLI_addtail(connections_dst, con_duplicate);
}
}
bool disconnect(Strip *strip)
{
if (strip == nullptr || strip->connections.is_empty()) {
return false;
}
/* Remove `StripConnections` from other strips' `connections` list that point to `strip`. */
for (StripConnection &con_strip : strip->connections) {
Strip *other = con_strip.strip_ref;
for (StripConnection &con_other : other->connections.items_mutable()) {
if (con_other.strip_ref == strip) {
BLI_remlink(&other->connections, &con_other);
MEM_delete(&con_other);
}
}
}
/* Now clear `connections` for `strip` itself. */
strip_connections_free(strip);
return true;
}
bool disconnect(VectorSet<Strip *> &strip_list)
{
bool changed = false;
for (Strip *strip : strip_list) {
changed |= disconnect(strip);
}
return changed;
}
void cut_one_way_connections(Strip *strip)
{
if (strip == nullptr) {
return;
}
for (StripConnection &con_strip : strip->connections.items_mutable()) {
Strip *other = con_strip.strip_ref;
bool is_one_way = true;
for (StripConnection &con_other : other->connections) {
if (con_other.strip_ref == strip) {
/* The `other` sequence has a bidirectional connection with `strip`. */
is_one_way = false;
break;
}
}
if (is_one_way) {
BLI_remlink(&strip->connections, &con_strip);
MEM_delete(&con_strip);
}
}
}
void connect(Strip *strip1, Strip *strip2)
{
if (strip1 == nullptr || strip2 == nullptr) {
return;
}
VectorSet<Strip *> strip_list;
strip_list.add(strip1);
strip_list.add(strip2);
connect(strip_list);
}
void connect(VectorSet<Strip *> &strip_list)
{
strip_list.remove_if([&](Strip *strip) { return strip == nullptr; });
for (Strip *strip1 : strip_list) {
disconnect(strip1);
for (Strip *strip2 : strip_list) {
if (strip1 == strip2) {
continue;
}
StripConnection *con = MEM_new<StripConnection>("stripconnection");
con->strip_ref = strip2;
BLI_addtail(&strip1->connections, con);
}
}
}
VectorSet<Strip *> connected_strips_get(const Strip *strip)
{
VectorSet<Strip *> connections;
if (strip != nullptr) {
for (StripConnection &con : strip->connections) {
connections.add(con.strip_ref);
}
}
return connections;
}
bool is_strip_connected(const Strip *strip)
{
if (strip == nullptr) {
return false;
}
return !strip->connections.is_empty();
}
bool are_strips_connected_together(VectorSet<Strip *> &strip_list)
{
const int expected_connection_num = strip_list.size() - 1;
for (Strip *strip1 : strip_list) {
VectorSet<Strip *> connections = connected_strips_get(strip1);
int found_connection_num = connections.size();
if (found_connection_num != expected_connection_num) {
return false;
}
for (Strip *strip2 : connections) {
if (!strip_list.contains(strip2)) {
return false;
}
}
}
return true;
}
} // namespace blender::seq

View File

@@ -0,0 +1,528 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BLI_listbase.h"
#include "BLI_math_base.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLT_translation.hh"
#include "BKE_sound.hh"
#include "strip_time.hh"
#include "SEQ_add.hh"
#include "SEQ_animation.hh"
#include "SEQ_channels.hh"
#include "SEQ_connect.hh"
#include "SEQ_edit.hh"
#include "SEQ_effects.hh"
#include "SEQ_iterator.hh"
#include "SEQ_relations.hh"
#include "SEQ_render.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_time.hh"
#include "SEQ_transform.hh"
#include "SEQ_utils.hh"
#include <cstring>
namespace blender::seq {
bool edit_strip_swap(Scene *scene, Strip *strip_a, Strip *strip_b, const char **r_error_str)
{
char name[sizeof(strip_a->name)];
if (strip_a->length(scene) != strip_b->length(scene)) {
*r_error_str = N_("Strips must be the same length");
return false;
}
/* type checking, could be more advanced but disallow sound vs non-sound copy */
if (strip_a->type != strip_b->type) {
if (strip_a->type == STRIP_TYPE_SOUND || strip_b->type == STRIP_TYPE_SOUND) {
*r_error_str = N_("Strips were not compatible");
return false;
}
/* disallow effects to swap with non-effects strips */
if (strip_a->is_effect() != strip_b->is_effect()) {
*r_error_str = N_("Strips were not compatible");
return false;
}
if (strip_a->is_effect() && strip_b->is_effect()) {
if (strip_a->effect_num_inputs_get() != strip_b->effect_num_inputs_get()) {
*r_error_str = N_("Strips must have the same number of inputs");
return false;
}
}
}
dna::shallow_swap(*strip_a, *strip_b);
/* swap back names so animation fcurves don't get swapped */
STRNCPY(name, strip_a->name + 2);
BLI_strncpy(strip_a->name + 2, strip_b->name + 2, sizeof(strip_b->name) - 2);
BLI_strncpy(strip_b->name + 2, name, sizeof(strip_b->name) - 2);
/* swap back opacity, and overlay mode */
std::swap(strip_a->blend_mode, strip_b->blend_mode);
std::swap(strip_a->blend_opacity, strip_b->blend_opacity);
std::swap(strip_a->prev, strip_b->prev);
std::swap(strip_a->next, strip_b->next);
std::swap(strip_a->start, strip_b->start);
std::swap(strip_a->startofs, strip_b->startofs);
std::swap(strip_a->endofs, strip_b->endofs);
std::swap(strip_a->channel, strip_b->channel);
strip_time_effect_range_set(scene, strip_a);
strip_time_effect_range_set(scene, strip_b);
strip_lookup_invalidate(editing_get(scene));
return true;
}
static void strip_update_muting_recursive(ListBaseT<SeqTimelineChannel> *channels,
ListBaseT<Strip> *seqbasep,
Strip *strip_meta,
const bool mute)
{
/* For sound we go over full meta tree to update muted state,
* since sound is played outside of evaluating the imbufs. */
for (Strip &strip : *seqbasep) {
bool strip_mute = (mute || render_is_muted(channels, &strip));
if (strip.type == STRIP_TYPE_META) {
/* if this is the current meta-strip, unmute because
* all strips above this were set to mute */
if (&strip == strip_meta) {
strip_mute = false;
}
strip_update_muting_recursive(&strip.channels, &strip.seqbase, strip_meta, strip_mute);
}
else if (ELEM(strip.type, STRIP_TYPE_SOUND, STRIP_TYPE_SCENE)) {
if (strip.runtime->scene_sound) {
BKE_sound_mute_scene_sound(strip.runtime->scene_sound, strip_mute);
}
}
}
}
void edit_update_muting(Editing *ed)
{
if (ed) {
/* mute all sounds up to current metastack list */
MetaStack *ms = static_cast<MetaStack *>(ed->metastack.last);
if (ms) {
strip_update_muting_recursive(&ed->channels, &ed->seqbase, ms->parent_strip, true);
}
else {
strip_update_muting_recursive(&ed->channels, &ed->seqbase, nullptr, false);
}
}
}
static void sequencer_flag_users_for_removal(Scene *scene, ListBaseT<Strip> *seqbase, Strip *strip)
{
for (Strip &user_strip : *seqbase) {
/* Look in meta-strips for usage of strip. */
if (user_strip.type == STRIP_TYPE_META) {
sequencer_flag_users_for_removal(scene, &user_strip.seqbase, strip);
}
/* Clear strip from modifiers. */
for (StripModifierData &smd : user_strip.modifiers) {
if (smd.mask_strip == strip) {
smd.mask_strip = nullptr;
}
}
/* Mark effects for removal that use the strip. */
if (relation_is_effect_of_strip(&user_strip, strip)) {
user_strip.runtime->flag |= StripRuntimeFlag::MarkForDelete;
/* Strips can be used as mask even if not in same seqbase. */
sequencer_flag_users_for_removal(scene, &scene->ed->seqbase, &user_strip);
}
}
}
void edit_flag_for_removal(Scene *scene, ListBaseT<Strip> *seqbase, Strip *strip)
{
if (strip == nullptr || flag_is_set(strip->runtime->flag, StripRuntimeFlag::MarkForDelete)) {
return;
}
/* Flag and remove meta children. */
if (strip->type == STRIP_TYPE_META) {
for (Strip &meta_child : strip->seqbase) {
edit_flag_for_removal(scene, &strip->seqbase, &meta_child);
}
}
strip->runtime->flag |= StripRuntimeFlag::MarkForDelete;
sequencer_flag_users_for_removal(scene, seqbase, strip);
}
void edit_remove_flagged_strips(Scene *scene, ListBaseT<Strip> *seqbase)
{
for (Strip &strip : seqbase->items_mutable()) {
if (flag_is_set(strip.runtime->flag, StripRuntimeFlag::MarkForDelete)) {
if (strip.type == STRIP_TYPE_META) {
edit_remove_flagged_strips(scene, &strip.seqbase);
}
free_animdata(scene, &strip);
BLI_remlink(seqbase, &strip);
strip_free(scene, &strip);
strip_lookup_invalidate(scene->ed);
}
}
}
bool edit_move_strip_to_seqbase(Scene *scene,
ListBaseT<Strip> *seqbase,
Strip *strip,
ListBaseT<Strip> *dst_seqbase)
{
/* Move to meta. */
BLI_remlink(seqbase, strip);
BLI_addtail(dst_seqbase, strip);
relations_invalidate_cache(scene, strip);
/* Update meta. */
if (transform_test_overlap(scene, dst_seqbase, strip)) {
transform_seqbase_shuffle(dst_seqbase, strip, scene);
}
return true;
}
bool edit_move_strip_to_meta(Scene *scene,
Strip *src_strip,
Strip *dst_stripm,
const char **r_error_str)
{
/* Find the appropriate seqbase */
Editing *ed = editing_get(scene);
ListBaseT<Strip> *seqbase = get_seqbase_by_strip(scene, src_strip);
if (dst_stripm->type != STRIP_TYPE_META) {
*r_error_str = N_("Cannot move strip to non-meta strip");
return false;
}
if (src_strip == dst_stripm) {
*r_error_str = N_("Strip cannot be moved into itself");
return false;
}
if (seqbase == &dst_stripm->seqbase) {
*r_error_str = N_("Moved strip is already inside provided meta strip");
return false;
}
if (src_strip->type == STRIP_TYPE_META && exists_in_seqbase(dst_stripm, &src_strip->seqbase)) {
*r_error_str = N_("Moved strip is parent of provided meta strip");
return false;
}
if (!exists_in_seqbase(dst_stripm, &ed->seqbase)) {
*r_error_str = N_("Cannot move strip to different scene");
return false;
}
VectorSet<Strip *> strips;
strips.add(src_strip);
iterator_set_expand(seqbase, strips, query_strip_effect_chain);
for (Strip *strip : strips) {
/* Move to meta. */
edit_move_strip_to_seqbase(scene, seqbase, strip, &dst_stripm->seqbase);
}
time_update_meta_strip_range(scene, dst_stripm);
return true;
}
static void seq_split_set_right_hold_offset(Main *bmain,
Scene *scene,
Strip *strip,
int timeline_frame)
{
const float content_start = strip->content_start();
const float content_end = strip->content_end(scene);
/* Adjust within range of extended still-frames before strip. */
if (timeline_frame < content_start) {
const float offset = content_start + 1 - timeline_frame;
strip->start -= offset;
strip->startofs += offset;
}
/* Adjust within range of strip contents. */
else if ((timeline_frame >= content_start) && (timeline_frame <= content_end)) {
strip->endofs = 0;
const float scene_fps = float(scene->r.frs_sec) / float(scene->r.frs_sec_base);
const float speed_factor = strip->media_playback_rate_factor(scene_fps);
strip->anim_endofs += round_fl_to_int((content_end - timeline_frame) * speed_factor);
}
/* Needed only to set `strip->len`. */
add_reload_new_file(bmain, scene, strip, false);
strip->right_handle_set(scene, timeline_frame);
}
static void seq_split_set_left_hold_offset(Main *bmain,
Scene *scene,
Strip *strip,
int timeline_frame)
{
const float content_start = strip->content_start();
const float content_end = strip->content_end(scene);
/* Adjust within range of strip contents. */
if ((timeline_frame >= content_start) && (timeline_frame <= content_end)) {
const float scene_fps = float(scene->r.frs_sec) / float(scene->r.frs_sec_base);
const float speed_factor = strip->media_playback_rate_factor(scene_fps);
strip->anim_startofs += round_fl_to_int((timeline_frame - content_start) * speed_factor);
strip->start = timeline_frame;
strip->startofs = 0;
}
/* Adjust within range of extended still-frames after strip. */
else if (timeline_frame > content_end) {
const float offset = timeline_frame - content_end + 1;
strip->start += offset;
strip->endofs += offset;
}
/* Needed only to set `strip->len`. */
add_reload_new_file(bmain, scene, strip, false);
strip->left_handle_set(scene, timeline_frame);
}
static bool seq_edit_split_intersect_check(const Scene *scene,
const Strip *strip,
const int timeline_frame)
{
return timeline_frame > strip->left_handle() && timeline_frame < strip->right_handle(scene);
}
static void seq_edit_split_handle_strip_offsets(Main *bmain,
Scene *scene,
Strip *left_strip,
Strip *right_strip,
const int timeline_frame,
const eSplitMethod method)
{
if (seq_edit_split_intersect_check(scene, right_strip, timeline_frame)) {
switch (method) {
case SPLIT_SOFT:
right_strip->left_handle_set(scene, timeline_frame);
break;
case SPLIT_HARD:
seq_split_set_left_hold_offset(bmain, scene, right_strip, timeline_frame);
break;
}
}
if (seq_edit_split_intersect_check(scene, left_strip, timeline_frame)) {
switch (method) {
case SPLIT_SOFT:
left_strip->right_handle_set(scene, timeline_frame);
break;
case SPLIT_HARD:
seq_split_set_right_hold_offset(bmain, scene, left_strip, timeline_frame);
break;
}
}
}
static bool seq_edit_split_effect_inputs_intersect(const Scene *scene,
const Strip *strip,
const int timeline_frame)
{
bool input_does_intersect = false;
if (strip->input1) {
input_does_intersect |= seq_edit_split_intersect_check(scene, strip->input1, timeline_frame);
if (strip->input1->is_effect()) {
input_does_intersect |= seq_edit_split_effect_inputs_intersect(
scene, strip->input1, timeline_frame);
}
}
if (strip->input2) {
input_does_intersect |= seq_edit_split_intersect_check(scene, strip->input2, timeline_frame);
if (strip->input2->is_effect()) {
input_does_intersect |= seq_edit_split_effect_inputs_intersect(
scene, strip->input2, timeline_frame);
}
}
return input_does_intersect;
}
static bool seq_edit_split_operation_permitted_check(const Scene *scene,
Span<Strip *> strips,
const int timeline_frame,
const char **r_error)
{
for (Strip *strip : strips) {
const ListBaseT<SeqTimelineChannel> *channels = channels_displayed_get(editing_get(scene));
if (transform_is_locked(channels, strip)) {
*r_error = "Strip is locked.";
return false;
}
if (!strip->is_effect()) {
continue;
}
if (!seq_edit_split_intersect_check(scene, strip, timeline_frame)) {
continue;
}
if (strip->effect_num_inputs_get() <= 1) {
continue;
}
if (effect_is_transition(strip->type)) {
*r_error = "Splitting transition effect is not permitted.";
return false;
}
if (!seq_edit_split_effect_inputs_intersect(scene, strip, timeline_frame)) {
*r_error = "Effect inputs don't overlap. Can not split such effect.";
return false;
}
}
return true;
}
Strip *edit_strip_split(Main *bmain,
Scene *scene,
ListBaseT<Strip> *seqbase,
Strip *strip,
const int timeline_frame,
const eSplitMethod method,
const bool ignore_connections,
const char **r_error)
{
if (!seq_edit_split_intersect_check(scene, strip, timeline_frame)) {
return nullptr;
}
/* Whole strip effect chain must be duplicated in order to preserve relationships. */
VectorSet<Strip *> strips;
strips.add(strip);
iterator_set_expand(seqbase,
strips,
ignore_connections ? query_strip_effect_chain :
query_strip_connected_and_effect_chain);
if (!seq_edit_split_operation_permitted_check(scene, strips, timeline_frame, r_error)) {
return nullptr;
}
/* Store `F-Curves`, so original ones aren't renamed. */
AnimationBackup animation_backup{};
animation_backup_original(scene, &animation_backup);
ListBaseT<Strip> left_strips = {nullptr, nullptr};
for (Strip *strip_iter : strips) {
/* Move strips in collection from seqbase to new ListBase. */
BLI_remlink(seqbase, strip_iter);
BLI_addtail(&left_strips, strip_iter);
if (ignore_connections) {
disconnect(strip_iter);
}
/* Duplicate curves from backup, so they can be renamed along with split strips. */
animation_duplicate_backup_to_scene(scene, strip_iter, &animation_backup);
}
/* Duplicate ListBase. */
ListBaseT<Strip> right_strips = {nullptr, nullptr};
seqbase_duplicate_recursive(
bmain, scene, scene, &right_strips, &left_strips, StripDuplicate::All, 0);
Strip *left_strip = static_cast<Strip *>(left_strips.first);
Strip *right_strip = static_cast<Strip *>(right_strips.first);
Strip *return_strip = nullptr;
/* Move strips from detached `ListBase`, otherwise they can't be flagged for removal. */
BLI_movelisttolist(seqbase, &left_strips);
BLI_movelisttolist(seqbase, &right_strips);
/* Rename duplicated strips. This has to be done immediately after adding
* strips to seqbase, for lookup cache to work correctly. */
Strip *strip_rename = right_strip;
for (; strip_rename; strip_rename = strip_rename->next) {
ensure_unique_name(strip_rename, scene);
}
/* Split strips. */
while (left_strip && right_strip) {
if (left_strip->left_handle() >= timeline_frame) {
edit_flag_for_removal(scene, seqbase, left_strip);
}
else if (right_strip->right_handle(scene) <= timeline_frame) {
edit_flag_for_removal(scene, seqbase, right_strip);
}
else if (return_strip == nullptr) {
/* Store return value - pointer to strip that will not be removed. */
return_strip = right_strip;
}
seq_edit_split_handle_strip_offsets(
bmain, scene, left_strip, right_strip, timeline_frame, method);
left_strip = left_strip->next;
right_strip = right_strip->next;
}
edit_remove_flagged_strips(scene, seqbase);
animation_restore_original(scene, &animation_backup);
return return_strip;
}
bool edit_remove_gaps(Scene *scene,
ListBaseT<Strip> *seqbase,
const int initial_frame,
const bool remove_all_gaps)
{
GapInfo gap_info = {0};
seq_time_gap_info_get(scene, seqbase, initial_frame, &gap_info);
if (!gap_info.gap_exists) {
return false;
}
if (remove_all_gaps) {
while (gap_info.gap_exists) {
transform_offset_after_frame(scene, seqbase, -gap_info.gap_length, gap_info.gap_start_frame);
seq_time_gap_info_get(scene, seqbase, initial_frame, &gap_info);
}
}
else {
transform_offset_after_frame(scene, seqbase, -gap_info.gap_length, gap_info.gap_start_frame);
}
return true;
}
void edit_strip_name_set(Scene *scene, Strip *strip, const char *new_name)
{
BLI_strncpy_utf8(strip->name + 2, new_name, MAX_NAME - 2);
BLI_str_utf8_invalid_strip(strip->name + 2, strlen(strip->name + 2));
strip_lookup_invalidate(scene->ed);
}
} // namespace blender::seq

View File

@@ -0,0 +1,244 @@
/* SPDX-FileCopyrightText: 2021-2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "SEQ_sequencer.hh"
#include "sequencer.hh"
#include "DNA_listBase.h"
#include "DNA_node_types.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BLI_listbase.h"
#include "BLI_mutex.hh"
#include <cstring>
#include "MEM_guardedalloc.h"
namespace blender::seq {
static Mutex lookup_lock;
struct StripLookup {
Map<std::string, Strip *> strip_by_name;
Map<const Scene *, VectorSet<Strip *>> strips_by_scene;
Map<const bNodeTree *, VectorSet<Strip *>> strips_by_compositor_node_group;
Map<const Strip *, Strip *> meta_by_strip;
Map<const Strip *, VectorSet<Strip *>> effects_by_strip;
Map<const SeqTimelineChannel *, Strip *> owner_by_channel;
bool is_valid = false;
};
static void strip_lookup_append_effect(const Strip *input, Strip *effect, StripLookup *lookup)
{
if (input == nullptr) {
return;
}
VectorSet<Strip *> &effects = lookup->effects_by_strip.lookup_or_add_default(input);
effects.add(effect);
}
static void strip_by_scene_lookup_build(Strip *strip, StripLookup *lookup)
{
if (strip->scene == nullptr) {
return;
}
VectorSet<Strip *> &strips = lookup->strips_by_scene.lookup_or_add_default(strip->scene);
strips.add(strip);
}
static void strip_by_compositor_node_group_lookup_build(Strip *strip, StripLookup *lookup)
{
if (strip->type == STRIP_TYPE_COMPOSITOR && strip->effectdata) {
const CompositorEffectVars *comp_data = static_cast<CompositorEffectVars *>(strip->effectdata);
if (comp_data->node_group) {
VectorSet<Strip *> &strips = lookup->strips_by_compositor_node_group.lookup_or_add_default(
comp_data->node_group);
strips.add(strip);
}
}
for (StripModifierData &modifier : strip->modifiers) {
if (modifier.type != eSeqModifierType_Compositor) {
continue;
}
const SequencerCompositorModifierData *modifier_data =
reinterpret_cast<SequencerCompositorModifierData *>(&modifier);
if (!modifier_data->node_group) {
continue;
}
VectorSet<Strip *> &strips = lookup->strips_by_compositor_node_group.lookup_or_add_default(
modifier_data->node_group);
strips.add(strip);
}
}
static void strip_lookup_build_effect(Strip *strip, StripLookup *lookup)
{
if (!strip->is_effect()) {
return;
}
strip_lookup_append_effect(strip->input1, strip, lookup);
strip_lookup_append_effect(strip->input2, strip, lookup);
}
static void strip_lookup_build_from_seqbase(Strip *parent_meta,
const ListBaseT<Strip> *seqbase,
StripLookup *lookup)
{
if (parent_meta != nullptr) {
for (SeqTimelineChannel &channel : parent_meta->channels) {
lookup->owner_by_channel.add(&channel, parent_meta);
}
}
for (Strip &strip : *seqbase) {
lookup->strip_by_name.add(strip.name + 2, &strip);
lookup->meta_by_strip.add(&strip, parent_meta);
strip_lookup_build_effect(&strip, lookup);
strip_by_scene_lookup_build(&strip, lookup);
strip_by_compositor_node_group_lookup_build(&strip, lookup);
if (strip.type == STRIP_TYPE_META) {
strip_lookup_build_from_seqbase(&strip, &strip.seqbase, lookup);
}
}
}
static void strip_lookup_build(const Editing *ed, StripLookup *lookup)
{
strip_lookup_build_from_seqbase(nullptr, &ed->seqbase, lookup);
lookup->is_valid = true;
}
static StripLookup *strip_lookup_new()
{
StripLookup *lookup = MEM_new<StripLookup>(__func__);
return lookup;
}
static void strip_lookup_free(StripLookup **lookup)
{
MEM_delete(*lookup);
*lookup = nullptr;
}
static void strip_lookup_rebuild(const Editing *ed, StripLookup **lookup)
{
strip_lookup_free(lookup);
*lookup = strip_lookup_new();
strip_lookup_build(ed, *lookup);
}
static void strip_lookup_update_if_needed(const Editing *ed, StripLookup **lookup)
{
if (!ed) {
return;
}
if (*lookup && (*lookup)->is_valid) {
return;
}
strip_lookup_rebuild(ed, lookup);
}
void strip_lookup_free(Editing *ed)
{
BLI_assert(ed != nullptr);
std::lock_guard lock(lookup_lock);
strip_lookup_free(&ed->runtime->strip_lookup);
}
Strip *lookup_strip_by_name(Editing *ed, const char *key)
{
BLI_assert(ed != nullptr);
std::lock_guard lock(lookup_lock);
strip_lookup_update_if_needed(ed, &ed->runtime->strip_lookup);
StripLookup *lookup = ed->runtime->strip_lookup;
return lookup->strip_by_name.lookup_default(key, nullptr);
}
Span<Strip *> lookup_strips_by_scene(Editing *ed, const Scene *key)
{
BLI_assert(ed != nullptr);
std::lock_guard lock(lookup_lock);
strip_lookup_update_if_needed(ed, &ed->runtime->strip_lookup);
StripLookup *lookup = ed->runtime->strip_lookup;
VectorSet<Strip *> &strips = lookup->strips_by_scene.lookup_or_add_default(key);
return strips.as_span();
}
Map<const Scene *, VectorSet<Strip *>> &lookup_strips_by_scene_map_get(Editing *ed)
{
BLI_assert(ed != nullptr);
std::lock_guard lock(lookup_lock);
strip_lookup_update_if_needed(ed, &ed->runtime->strip_lookup);
StripLookup *lookup = ed->runtime->strip_lookup;
return lookup->strips_by_scene;
}
Span<Strip *> lookup_strips_by_compositor_node_group(Editing *ed, const bNodeTree *key)
{
BLI_assert(ed != nullptr);
BLI_assert(key->type == NTREE_COMPOSIT);
std::lock_guard lock(lookup_lock);
strip_lookup_update_if_needed(ed, &ed->runtime->strip_lookup);
StripLookup *lookup = ed->runtime->strip_lookup;
VectorSet<Strip *> &strips = lookup->strips_by_compositor_node_group.lookup_or_add_default(key);
return strips.as_span();
}
Strip *lookup_meta_by_strip(Editing *ed, const Strip *key)
{
BLI_assert(ed != nullptr);
std::lock_guard lock(lookup_lock);
strip_lookup_update_if_needed(ed, &ed->runtime->strip_lookup);
StripLookup *lookup = ed->runtime->strip_lookup;
return lookup->meta_by_strip.lookup_default(key, nullptr);
}
Span<Strip *> SEQ_lookup_effects_by_strip(Editing *ed, const Strip *key)
{
BLI_assert(ed != nullptr);
std::lock_guard lock(lookup_lock);
strip_lookup_update_if_needed(ed, &ed->runtime->strip_lookup);
StripLookup *lookup = ed->runtime->strip_lookup;
VectorSet<Strip *> &effects = lookup->effects_by_strip.lookup_or_add_default(key);
return effects.as_span();
}
Strip *lookup_strip_by_channel_owner(Editing *ed, const SeqTimelineChannel *channel)
{
BLI_assert(ed != nullptr);
std::lock_guard lock(lookup_lock);
strip_lookup_update_if_needed(ed, &ed->runtime->strip_lookup);
StripLookup *lookup = ed->runtime->strip_lookup;
return lookup->owner_by_channel.lookup_default(channel, nullptr);
}
void strip_lookup_invalidate(const Editing *ed)
{
if (ed == nullptr) {
return;
}
std::lock_guard lock(lookup_lock);
StripLookup *lookup = ed->runtime->strip_lookup;
if (lookup != nullptr) {
lookup->is_valid = false;
}
}
} // namespace blender::seq

View File

@@ -0,0 +1,468 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BLI_listbase.h"
#include "BLI_math_base.h"
#include "BLI_session_uid.h"
#include "BLI_string.h"
#include "BKE_layer.hh"
#include "BKE_main.hh"
#include "BKE_report.hh"
#include "DEG_depsgraph.hh"
#include "MOV_read.hh"
#include "SEQ_iterator.hh"
#include "SEQ_prefetch.hh"
#include "SEQ_preview_cache.hh"
#include "SEQ_relations.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_thumbnail_cache.hh"
#include "SEQ_utils.hh"
#include "cache/final_image_cache.hh"
#include "cache/intra_frame_cache.hh"
#include "cache/source_image_cache.hh"
#include "effects/effects.hh"
#include "sequencer.hh"
#include "utils.hh"
namespace blender::seq {
bool relation_is_effect_of_strip(const Strip *effect, const Strip *input)
{
return ELEM(input, effect->input1, effect->input2);
}
void cache_cleanup(Scene *scene, CacheCleanup mode)
{
if (flag_is_set(mode, CacheCleanup::Thumbnails)) {
thumbnail_cache_clear(scene);
}
if (flag_is_set(mode, CacheCleanup::SourceImage)) {
source_image_cache_clear(scene);
}
if (flag_is_set(mode, CacheCleanup::FinalImage)) {
final_image_cache_clear(scene);
}
if (flag_is_set(mode, CacheCleanup::IntraFrame)) {
intra_frame_cache_invalidate(scene);
preview_cache_invalidate(scene);
}
}
void cache_settings_changed(Scene *scene)
{
if (!(scene->ed->cache_flag & SEQ_CACHE_STORE_RAW)) {
/* RAW caches has been disabled, clear them out. */
source_image_cache_clear(scene);
}
if (!(scene->ed->cache_flag & SEQ_CACHE_STORE_FINAL_OUT)) {
/* Final caches has been disabled, clear them out. */
final_image_cache_clear(scene);
}
}
bool is_cache_full(const Scene *scene)
{
size_t cache_limit = size_t(U.memcachelimit) * 1024 * 1024;
return source_image_cache_calc_memory_size(scene) + final_image_cache_calc_memory_size(scene) >
cache_limit;
}
bool evict_caches_if_full(Scene *scene)
{
if (!is_cache_full(scene)) {
/* Cache is not full, we don't have to evict anything. */
return false;
}
/* Cache is full, so we want to remove some images. We always try to remove one final image,
* and some amount of source images for each final image, so that ratio of cached images
* stays the same. Depending on the frame composition complexity, there can be lots of
* source images cached for a single final frame; if we only removed one source image
* we'd eventually have the cache still filled only with source images. */
bool evicted_final = false;
bool evicted_source = false;
do {
const size_t count_final = final_image_cache_get_image_count(scene);
const size_t count_source = source_image_cache_get_image_count(scene);
evicted_final = false;
evicted_source = false;
const bool final_active = scene->ed->cache_flag & SEQ_CACHE_STORE_FINAL_OUT;
/* Evict one final item, and as much from source as needed to maintain ratio. */
if (count_final != 0) {
evicted_final = final_image_cache_evict(scene);
}
/* Only remove source images if there's more of them than final ones. */
if (count_source != 0 && (!final_active || count_source > count_final)) {
evicted_source = source_image_cache_evict(scene);
/* Only try to enforce the ratio when the final cache is active. */
if (evicted_source && final_active) {
const size_t items = divide_ceil_ul(count_source, std::max<size_t>(count_final, 1));
/* Start at "1" to make sure we only try to evict more frames if the ratio is above 1:1. */
for (size_t i = 1; i < items; i++) {
if (!source_image_cache_evict(scene)) {
/* Can't evict any more frames, stop. */
break;
}
}
}
}
} while (is_cache_full(scene) && (evicted_final || evicted_source));
/* Did we evict anything to free up the cache? */
return !(evicted_final || evicted_source);
}
static void update_range_with_effects(const Scene *scene, const Strip *strip, int2 &r_range)
{
r_range.x = std::min(r_range.x, strip->left_handle());
r_range.y = std::max(r_range.y, strip->right_handle(scene) - 1);
Span<Strip *> effects = SEQ_lookup_effects_by_strip(scene->ed, strip);
for (Strip *effect : effects) {
update_range_with_effects(scene, effect, r_range);
}
}
static void invalidate_final_cache_strip_range(Scene *scene, const Strip *strip)
{
int2 range{MAXFRAME, -MAXFRAME};
update_range_with_effects(scene, strip, range);
final_image_cache_invalidate_frame_range(scene, range.x, range.y);
}
static void invalidate_raw_cache_of_parent_meta(Scene *scene, Strip *strip)
{
Strip *meta = lookup_meta_by_strip(editing_get(scene), strip);
if (meta == nullptr) {
return;
}
relations_invalidate_cache_raw(scene, meta);
}
void relations_invalidate_cache_raw(Scene *scene, Strip *strip)
{
source_image_cache_invalidate_strip(scene, strip);
media_presence_invalidate_strip(scene, strip);
relations_invalidate_cache(scene, strip);
}
void relations_invalidate_cache(Scene *scene, Strip *strip)
{
if (strip->effectdata && strip->type == STRIP_TYPE_SPEED) {
strip_effect_speed_rebuild_map(scene, strip);
}
/* Zero-input compositor effect source caches also need to be invalidated. */
if (strip->type == STRIP_TYPE_COMPOSITOR && !strip->is_effect_with_inputs()) {
source_image_cache_invalidate_strip(scene, strip);
}
invalidate_final_cache_strip_range(scene, strip);
intra_frame_cache_invalidate(scene, strip);
preview_cache_invalidate(scene);
invalidate_raw_cache_of_parent_meta(scene, strip);
/* Needed to update VSE sound. */
DEG_id_tag_update(&scene->id, ID_RECALC_SEQUENCER_STRIPS);
prefetch_stop(scene);
}
void relations_invalidate_scene_strips(const Main *bmain, const Scene *scene_target)
{
for (Scene &scene : bmain->scenes) {
if (scene.ed != nullptr) {
for (Strip *strip : lookup_strips_by_scene(editing_get(&scene), scene_target)) {
relations_invalidate_cache_raw(&scene, strip);
}
}
}
}
void relations_update_view_layer_scene_strips(Main *bmain,
Scene *scene,
const char *old_name,
const char *new_name)
{
for (Scene &scene_iter : bmain->scenes) {
Editing *ed = seq::editing_get(&scene_iter);
if (ed == nullptr) {
continue;
}
for (Strip *strip : seq::lookup_strips_by_scene(ed, scene)) {
BLI_assert(strip->scene_view_layer_name != nullptr);
if (!STREQ(strip->scene_view_layer_name, old_name)) {
continue;
}
MEM_delete(strip->scene_view_layer_name);
strip->scene_view_layer_name = new_name ?
BLI_strdup(new_name) :
BLI_strdup(
BKE_view_layer_default_render(strip->scene)->name);
if (new_name == nullptr) {
/* View layer was deleted. */
seq::relations_invalidate_cache_raw(&scene_iter, strip);
}
}
}
}
void relations_invalidate_compositor_users(const Main *bmain, const bNodeTree *node_tree)
{
for (Scene &scene : bmain->scenes) {
if (scene.ed != nullptr) {
for (Strip *strip : lookup_strips_by_compositor_node_group(editing_get(&scene), node_tree)) {
relations_invalidate_cache(&scene, strip);
}
}
}
}
static void invalidate_movieclip_strips(Scene *scene,
MovieClip *clip_target,
ListBaseT<Strip> *seqbase)
{
for (Strip *strip = static_cast<Strip *>(seqbase->first); strip != nullptr; strip = strip->next)
{
if (strip->clip == clip_target) {
relations_invalidate_cache_raw(scene, strip);
}
if (strip->seqbase.first != nullptr) {
invalidate_movieclip_strips(scene, clip_target, &strip->seqbase);
}
}
}
void relations_invalidate_movieclip_strips(Main *bmain, MovieClip *clip_target)
{
for (Scene *scene = static_cast<Scene *>(bmain->scenes.first); scene != nullptr;
scene = static_cast<Scene *>(scene->id.next))
{
if (scene->ed != nullptr) {
invalidate_movieclip_strips(scene, clip_target, &scene->ed->seqbase);
}
}
}
void relations_free_imbuf(Scene *scene, ListBaseT<Strip> *seqbase, bool for_render)
{
if (scene->ed == nullptr) {
return;
}
prefetch_stop(scene);
for (Strip &strip : *seqbase) {
if (for_render && strip.intersects_frame(scene, scene->r.cfra)) {
continue;
}
if (strip.data) {
if (strip.type == STRIP_TYPE_MOVIE) {
strip_free_movie_readers(&strip);
}
if (strip.type == STRIP_TYPE_SPEED) {
strip_effect_speed_rebuild_map(scene, &strip);
}
}
if (strip.type == STRIP_TYPE_META) {
relations_free_imbuf(scene, &strip.seqbase, for_render);
}
if (strip.type == STRIP_TYPE_SCENE) {
/* FIXME: recurse downwards,
* but do recurse protection somehow! */
}
}
}
static void sequencer_all_free_anim_ibufs(const Scene *scene,
ListBaseT<Strip> *seqbase,
int timeline_frame,
const int frame_range[2])
{
Editing *ed = editing_get(scene);
for (Strip *strip = static_cast<Strip *>(seqbase->first); strip != nullptr; strip = strip->next)
{
if (!strip->intersects_frame(scene, timeline_frame) ||
!((frame_range[0] <= timeline_frame) && (frame_range[1] > timeline_frame)))
{
strip_free_movie_readers(strip);
}
if (strip->type == STRIP_TYPE_META) {
int meta_range[2];
MetaStack *ms = meta_stack_active_get(ed);
if (ms != nullptr && ms->parent_strip == strip) {
meta_range[0] = -MAXFRAME;
meta_range[1] = MAXFRAME;
}
else {
/* Limit frame range to meta strip. */
meta_range[0] = max_ii(frame_range[0], strip->left_handle());
meta_range[1] = min_ii(frame_range[1], strip->right_handle(scene));
}
sequencer_all_free_anim_ibufs(scene, &strip->seqbase, timeline_frame, meta_range);
}
}
}
void relations_free_all_anim_ibufs(Scene *scene, int timeline_frame)
{
Editing *ed = editing_get(scene);
if (ed == nullptr) {
return;
}
const int frame_range[2] = {-MAXFRAME, MAXFRAME};
sequencer_all_free_anim_ibufs(scene, &ed->seqbase, timeline_frame, frame_range);
}
static Strip *sequencer_check_scene_recursion(Scene *scene, ListBaseT<Strip> *seqbase)
{
for (Strip &strip : *seqbase) {
if (strip.type == STRIP_TYPE_SCENE && strip.scene == scene) {
return &strip;
}
if (strip.type == STRIP_TYPE_SCENE && (strip.flag & SEQ_SCENE_STRIPS)) {
if (strip.scene && strip.scene->ed &&
sequencer_check_scene_recursion(scene, &strip.scene->ed->seqbase))
{
return &strip;
}
}
if (strip.type == STRIP_TYPE_META && sequencer_check_scene_recursion(scene, &strip.seqbase)) {
return &strip;
}
}
return nullptr;
}
bool relations_check_scene_recursion(Scene *scene, ReportList *reports)
{
Editing *ed = editing_get(scene);
if (ed == nullptr) {
return false;
}
Strip *recursive_seq = sequencer_check_scene_recursion(scene, &ed->seqbase);
if (recursive_seq != nullptr) {
BKE_reportf(reports,
RPT_WARNING,
"Recursion detected in video sequencer. Strip %s at frame %d will not be rendered",
recursive_seq->name + 2,
recursive_seq->left_handle());
for (Strip &strip : ed->seqbase) {
if (strip.type != STRIP_TYPE_SCENE && sequencer_strip_generates_image(&strip)) {
/* There are other strips to render, so render them. */
return false;
}
}
/* No other strips to render - cancel operator. */
return true;
}
return false;
}
bool relations_render_loop_check(Strip *strip_main, Strip *strip)
{
if (strip_main == nullptr || strip == nullptr) {
return false;
}
if (strip_main == strip) {
return true;
}
if ((strip_main->input1 && relations_render_loop_check(strip_main->input1, strip)) ||
(strip_main->input2 && relations_render_loop_check(strip_main->input2, strip)))
{
return true;
}
for (StripModifierData &smd : strip_main->modifiers) {
if (smd.mask_strip && relations_render_loop_check(smd.mask_strip, strip)) {
return true;
}
}
return false;
}
void strip_free_movie_readers(Strip *strip)
{
for (MovieReader *anim : strip->runtime->movie_readers) {
MOV_close(anim);
}
strip->runtime->movie_readers.clear();
}
void relations_session_uid_generate(Strip *strip)
{
strip->runtime->session_uid = BLI_session_uid_generate();
}
static bool get_uids_cb(Strip *strip, void *user_data)
{
Set<SessionUID> &used_uids = *static_cast<Set<SessionUID> *>(user_data);
const SessionUID &session_uid = strip->runtime->session_uid;
if (!BLI_session_uid_is_generated(&session_uid)) {
printf("Sequence %s does not have UID generated.\n", strip->name);
return true;
}
if (used_uids.contains(session_uid)) {
printf("Sequence %s has duplicate UID generated.\n", strip->name);
return true;
}
used_uids.add(session_uid);
return true;
}
void relations_check_uids_unique_and_report(const Scene *scene)
{
if (scene->ed == nullptr) {
return;
}
Set<SessionUID> used_uids;
foreach_strip(&scene->ed->seqbase, get_uids_cb, &used_uids);
}
bool exists_in_seqbase(const Strip *strip, const ListBaseT<Strip> *seqbase)
{
for (Strip &strip_test : *seqbase) {
if (strip_test.type == STRIP_TYPE_META && exists_in_seqbase(strip, &strip_test.seqbase)) {
return true;
}
if (&strip_test == strip) {
return true;
}
}
return false;
}
} // namespace blender::seq

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BLI_listbase.h"
#include "SEQ_select.hh"
#include "SEQ_sequencer.hh"
namespace blender::seq {
Strip *select_active_get(const Scene *scene)
{
const Editing *ed = editing_get(scene);
if (ed == nullptr) {
return nullptr;
}
return ed->act_strip;
}
void select_active_set(Scene *scene, Strip *strip)
{
Editing *ed = editing_get(scene);
if (ed == nullptr) {
return;
}
ed->act_strip = strip;
}
bool select_active_get_pair(Scene *scene, Strip **r_strip_act, Strip **r_strip_other)
{
Editing *ed = editing_get(scene);
*r_strip_act = select_active_get(scene);
if (*r_strip_act == nullptr) {
return false;
}
*r_strip_other = nullptr;
for (Strip &strip : *ed->current_strips()) {
if (strip.flag & SEQ_SELECT && (&strip != (*r_strip_act))) {
if (*r_strip_other) {
return false;
}
*r_strip_other = &strip;
}
}
return (*r_strip_other != nullptr);
}
bool select_has_any(const Scene *scene)
{
Editing *ed = editing_get(scene);
if (ed != nullptr) {
for (Strip &strip : *ed->current_strips()) {
if (strip.flag & SEQ_SELECT) {
return true;
}
}
}
return false;
}
} // namespace blender::seq

View File

@@ -0,0 +1,614 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <algorithm>
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BLI_listbase.h"
#include "BLI_math_base.h"
#include "BKE_movieclip.hh"
#include "BKE_sound.hh"
#include "DNA_sound_types.h"
#include "MOV_read.hh"
#include "SEQ_animation.hh"
#include "SEQ_channels.hh"
#include "SEQ_iterator.hh"
#include "SEQ_render.hh"
#include "SEQ_retiming.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_time.hh"
#include "SEQ_transform.hh"
#include "sequencer.hh"
#include "strip_time.hh"
#include "utils.hh"
namespace blender {
namespace seq {
float give_frame_index(const Scene *scene, const Strip *strip, float timeline_frame)
{
float frame_index;
float sta = strip->content_start();
float end = strip->is_effect() ? strip->right_handle(scene) : strip->content_end(scene) - 1;
if (end < sta) {
return -1;
}
if (strip->type == STRIP_TYPE_IMAGE && transform_single_image_check(strip)) {
return 0;
}
if (strip->flag & SEQ_REVERSE_FRAMES) {
frame_index = end - timeline_frame;
}
else {
frame_index = timeline_frame - sta;
}
frame_index = max_ff(frame_index, 0);
const float scene_fps = float(scene->r.frs_sec) / float(scene->r.frs_sec_base);
frame_index *= strip->media_playback_rate_factor(scene_fps);
if (retiming_has_keys(strip)) {
const float retiming_factor = strip_retiming_evaluate(strip, frame_index);
/* Retiming maps frame index from 0 up to `strip->len`, because key is positioned at the end of
* last frame. Otherwise the last frame could not be retimed. */
frame_index = retiming_factor * strip->len;
}
/* Clamp frame index to strip content frame range. */
float frame_index_max = strip->is_effect() ? end - sta : strip->len - 1;
frame_index = clamp_f(frame_index, 0, frame_index_max);
if (strip->strobe > 1.0f) {
frame_index -= fmodf(double(frame_index), double(strip->strobe));
}
return frame_index;
}
static int metastrip_start_get(Strip *strip_meta)
{
return strip_meta->start + strip_meta->startofs;
}
static int metastrip_end_get(Strip *strip_meta)
{
return strip_meta->start + strip_meta->len - strip_meta->endofs;
}
static void strip_update_sound_bounds_recursive_impl(const Scene *scene,
Strip *strip_meta,
int start,
int end)
{
/* For sound we go over full meta tree to update bounds of the sound strips,
* since sound is played outside of evaluating the image-buffers (#ImBuf). */
for (Strip &strip : strip_meta->seqbase) {
if (strip.type == STRIP_TYPE_META) {
strip_update_sound_bounds_recursive_impl(scene,
&strip,
max_ii(start, metastrip_start_get(&strip)),
min_ii(end, metastrip_end_get(&strip)));
}
else if (ELEM(strip.type, STRIP_TYPE_SOUND, STRIP_TYPE_SCENE)) {
if (strip.runtime->scene_sound) {
int startofs = strip.startofs;
int endofs = strip.endofs;
if (strip.startofs + strip.start < start) {
startofs = start - strip.start;
}
if (strip.start + strip.len - strip.endofs > end) {
endofs = strip.start + strip.len - end;
}
double offset_time = 0.0f;
if (strip.sound != nullptr) {
offset_time = strip.sound->offset_time + strip.sound_offset;
}
BKE_sound_move_scene_sound(scene,
strip.runtime->scene_sound,
strip.start + startofs,
strip.start + strip.len - endofs,
startofs + strip.anim_startofs,
offset_time);
}
}
}
}
void strip_update_sound_bounds_recursive(const Scene *scene, Strip *strip_meta)
{
strip_update_sound_bounds_recursive_impl(
scene, strip_meta, metastrip_start_get(strip_meta), metastrip_end_get(strip_meta));
}
void time_update_meta_strip_range(const Scene *scene, Strip *strip_meta)
{
if (strip_meta == nullptr) {
return;
}
if (strip_meta->seqbase.is_empty()) {
return;
}
const int strip_start = strip_meta->left_handle();
const int strip_end = strip_meta->right_handle(scene);
int min = MAXFRAME * 2;
int max = -MAXFRAME * 2;
for (Strip &strip : strip_meta->seqbase) {
min = min_ii(strip.left_handle(), min);
max = max_ii(strip.right_handle(scene), max);
}
strip_meta->start = min + strip_meta->anim_startofs;
strip_meta->len = max - strip_meta->anim_endofs - strip_meta->start;
/* Functions `SEQ_time_*_handle_frame_set()` can not be used here, because they are clamped, so
* change must be done at once. */
strip_meta->startofs = strip_start - strip_meta->start;
strip_meta->startdisp = strip_start; /* Only to make files usable in older versions. */
strip_meta->endofs = strip_meta->start + strip_meta->length(scene) - strip_end;
strip_meta->enddisp = strip_end; /* Only to make files usable in older versions. */
strip_update_sound_bounds_recursive(scene, strip_meta);
Span<Strip *> effects = SEQ_lookup_effects_by_strip(scene->ed, strip_meta);
strip_time_update_effects_strip_range(scene, effects);
time_update_meta_strip_range(scene, lookup_meta_by_strip(scene->ed, strip_meta));
}
void strip_time_effect_range_set(const Scene *scene, Strip *strip)
{
if (strip->input1 == nullptr && strip->input2 == nullptr) {
return;
}
if (strip->input1 && strip->input2) { /* 2 - input effect. */
strip->startdisp = max_ii(strip->input1->left_handle(), strip->input2->left_handle());
strip->enddisp = min_ii(strip->input1->right_handle(scene),
strip->input2->right_handle(scene));
}
else if (strip->input1) { /* Single input effect. */
strip->startdisp = strip->input1->right_handle(scene);
strip->enddisp = strip->input1->left_handle();
}
else if (strip->input2) { /* Strip may be missing one of inputs. */
strip->startdisp = strip->input2->right_handle(scene);
strip->enddisp = strip->input2->left_handle();
}
if (strip->startdisp > strip->enddisp) {
std::swap(strip->startdisp, strip->enddisp);
}
/* Values unusable for effects, these should be always 0. */
strip->startofs = strip->endofs = strip->anim_startofs = strip->anim_endofs = 0;
strip->start = strip->startdisp;
strip->len = strip->enddisp - strip->startdisp;
}
void strip_time_update_effects_strip_range(const Scene *scene, const Span<Strip *> effects)
{
/* First pass: Update length of immediate effects. */
for (Strip *strip : effects) {
strip_time_effect_range_set(scene, strip);
}
/* Second pass: Recursive call to update effects in chain and in order, so they inherit length
* correctly. */
for (Strip *strip : effects) {
Span<Strip *> effects_recurse = SEQ_lookup_effects_by_strip(scene->ed, strip);
strip_time_update_effects_strip_range(scene, effects_recurse);
}
}
int time_find_next_prev_edit(Scene *scene,
int timeline_frame,
const short side,
const bool do_skip_mute,
const bool do_center,
const bool do_unselected)
{
Editing *ed = editing_get(scene);
ListBaseT<SeqTimelineChannel> *channels = channels_displayed_get(ed);
int dist, best_dist, best_frame = timeline_frame;
int strip_frames[2], strip_frames_tot;
/* In case where both is passed,
* frame just finds the nearest end while frame_left the nearest start. */
best_dist = MAXFRAME * 2;
if (ed == nullptr) {
return timeline_frame;
}
for (Strip &strip : *ed->current_strips()) {
int i;
if (do_skip_mute && render_is_muted(channels, &strip)) {
continue;
}
if (do_unselected && (strip.flag & SEQ_SELECT)) {
continue;
}
if (do_center) {
strip_frames[0] = (strip.left_handle() + strip.right_handle(scene)) / 2;
strip_frames_tot = 1;
}
else {
strip_frames[0] = strip.left_handle();
strip_frames[1] = strip.right_handle(scene);
strip_frames_tot = 2;
}
for (i = 0; i < strip_frames_tot; i++) {
const int strip_frame = strip_frames[i];
dist = MAXFRAME * 2;
switch (side) {
case SIDE_LEFT:
if (strip_frame < timeline_frame) {
dist = timeline_frame - strip_frame;
}
break;
case SIDE_RIGHT:
if (strip_frame > timeline_frame) {
dist = strip_frame - timeline_frame;
}
break;
case SIDE_BOTH:
dist = abs(strip_frame - timeline_frame);
break;
}
if (dist < best_dist) {
best_frame = strip_frame;
best_dist = dist;
}
}
}
return best_frame;
}
void timeline_init_boundbox(const Scene *scene, rctf *r_rect)
{
r_rect->xmin = scene->r.sfra;
r_rect->xmax = scene->r.efra + 1;
r_rect->ymin = 1.0f; /* The first strip is drawn at y == 1.0f */
r_rect->ymax = 8.0f;
}
void timeline_expand_boundbox(const Scene *scene, const ListBaseT<Strip> *seqbase, rctf *rect)
{
if (seqbase == nullptr) {
return;
}
for (Strip &strip : *seqbase) {
rect->xmin = std::min<float>(rect->xmin, strip.left_handle() - 1);
rect->xmax = std::max<float>(rect->xmax, strip.right_handle(scene) + 1);
/* We do +1 here to account for the channel thickness. Channel n has range of <n, n+1>. */
rect->ymax = std::max(rect->ymax, strip.channel + 1.0f);
}
}
void timeline_boundbox(const Scene *scene, const ListBaseT<Strip> *seqbase, rctf *r_rect)
{
timeline_init_boundbox(scene, r_rect);
timeline_expand_boundbox(scene, seqbase, r_rect);
}
static bool strip_exists_at_frame(const Scene *scene,
Span<Strip *> strips,
const int timeline_frame)
{
for (Strip *strip : strips) {
if (strip->intersects_frame(scene, timeline_frame)) {
return true;
}
}
return false;
}
void seq_time_gap_info_get(const Scene *scene,
ListBaseT<Strip> *seqbase,
const int initial_frame,
GapInfo *r_gap_info)
{
rctf rectf;
/* Get first and last frame. */
timeline_boundbox(scene, seqbase, &rectf);
const int sfra = int(rectf.xmin);
const int efra = int(rectf.xmax);
int timeline_frame = initial_frame;
r_gap_info->gap_exists = false;
VectorSet strips = query_all_strips(seqbase);
if (!strip_exists_at_frame(scene, strips, initial_frame)) {
/* Search backward for gap_start_frame. */
for (; timeline_frame >= sfra; timeline_frame--) {
if (strip_exists_at_frame(scene, strips, timeline_frame)) {
break;
}
}
r_gap_info->gap_start_frame = timeline_frame + 1;
timeline_frame = initial_frame;
}
else {
/* Search forward for gap_start_frame. */
for (; timeline_frame <= efra; timeline_frame++) {
if (!strip_exists_at_frame(scene, strips, timeline_frame)) {
r_gap_info->gap_start_frame = timeline_frame;
break;
}
}
}
/* Search forward for gap_end_frame. */
for (; timeline_frame <= efra; timeline_frame++) {
if (strip_exists_at_frame(scene, strips, timeline_frame)) {
const int gap_end_frame = timeline_frame;
r_gap_info->gap_length = gap_end_frame - r_gap_info->gap_start_frame;
r_gap_info->gap_exists = true;
break;
}
}
}
static void strip_time_slip_strip_ex(const Scene *scene,
Strip *strip,
int delta,
float subframe_delta,
bool slip_keyframes,
bool recursed)
{
if (strip->type == STRIP_TYPE_SOUND && subframe_delta != 0.0f) {
strip->sound_offset += subframe_delta / scene->frames_per_second();
}
if (delta == 0 && (!slip_keyframes || subframe_delta == 0.0f)) {
return;
}
/* Skip effect strips where the length is dependent on another strip,
* as they are calculated with #strip_time_update_effects_strip_range. */
if (strip->input1 != nullptr || strip->input2 != nullptr) {
return;
}
/* Effects only have a start frame and a length, so unless we're inside
* a meta strip, there's no need to do anything. */
if (!recursed && strip->is_effect()) {
return;
}
/* Move strips inside meta strip. */
if (strip->type == STRIP_TYPE_META) {
/* If the meta strip has no contents, don't do anything. */
if (strip->seqbase.is_empty()) {
return;
}
for (Strip &strip_child : strip->seqbase) {
/* The keyframes of strips inside meta strips should always be moved. */
strip_time_slip_strip_ex(scene, &strip_child, delta, subframe_delta, true, true);
}
}
strip->start = strip->start + delta;
if (slip_keyframes) {
float anim_offset = delta;
if (strip->type == STRIP_TYPE_SOUND) {
anim_offset += subframe_delta;
}
offset_animdata(scene, strip, anim_offset);
}
if (!recursed) {
strip->startofs = strip->startofs - delta;
strip->endofs = strip->endofs + delta;
}
/* Only to make files usable in older versions. */
strip->startdisp = strip->left_handle();
strip->enddisp = strip->right_handle(scene);
Span<Strip *> effects = SEQ_lookup_effects_by_strip(scene->ed, strip);
strip_time_update_effects_strip_range(scene, effects);
}
void time_slip_strip(
const Scene *scene, Strip *strip, int frame_delta, float subframe_delta, bool slip_keyframes)
{
strip_time_slip_strip_ex(scene, strip, frame_delta, subframe_delta, slip_keyframes, false);
}
} // namespace seq
float Strip::media_playback_rate_factor(float scene_fps) const
{
if ((this->flag & SEQ_AUTO_PLAYBACK_RATE) == 0) {
return 1.0f;
}
if (this->media_playback_rate == 0.0f) {
return 1.0f;
}
return this->media_playback_rate / scene_fps;
}
float Strip::media_fps(Scene *scene)
{
switch (this->type) {
case STRIP_TYPE_MOVIE: {
seq::strip_open_anim_file(scene, this, true);
const MovieReader *anim = this->runtime->movie_reader_get();
if (anim == nullptr) {
return 0.0f;
}
return MOV_get_fps(anim);
}
case STRIP_TYPE_MOVIECLIP:
if (this->clip != nullptr) {
return BKE_movieclip_get_fps(this->clip);
}
break;
case STRIP_TYPE_SCENE:
if (this->scene != nullptr) {
return float(this->scene->r.frs_sec) / this->scene->r.frs_sec_base;
}
break;
default:
break;
}
return 0.0f;
}
float Strip::content_start() const
{
return this->start;
}
void Strip::content_start_set(const Scene *scene, int timeline_frame)
{
this->start = timeline_frame;
Span<Strip *> effects = seq::SEQ_lookup_effects_by_strip(scene->ed, this);
seq::strip_time_update_effects_strip_range(scene, effects);
seq::time_update_meta_strip_range(scene, seq::lookup_meta_by_strip(scene->ed, this));
}
float Strip::content_end(const Scene *scene) const
{
return this->content_start() + this->length(scene);
}
int Strip::length(const Scene *scene) const
{
const float scene_fps = float(scene->r.frs_sec) / float(scene->r.frs_sec_base);
if (seq::retiming_has_keys(this)) {
const int last_key_frame = seq::retiming_key_frame_get(
scene, this, seq::retiming_last_key_get(this));
/* Last key is mapped to last frame index. Numbering starts from 0. */
const int sound_offset = this->rounded_sound_offset(scene_fps);
return last_key_frame - this->content_start() - sound_offset;
}
return this->len / this->media_playback_rate_factor(scene_fps);
}
int Strip::rounded_sound_offset(float scene_fps) const
{
if (this->type == STRIP_TYPE_SOUND && this->sound != nullptr) {
return round_fl_to_int((this->sound->offset_time + this->sound_offset) * scene_fps);
}
return 0;
}
int Strip::left_handle() const
{
if (this->input1 || this->input2) {
return this->startdisp;
}
return this->start + this->startofs;
}
int Strip::right_handle(const Scene *scene) const
{
if (this->input1 || this->input2) {
return this->enddisp;
}
return this->content_end(scene) - this->endofs;
}
void Strip::left_handle_set(const Scene *scene, int timeline_frame)
{
const float right_handle_orig_frame = this->right_handle(scene);
if (timeline_frame >= right_handle_orig_frame) {
timeline_frame = right_handle_orig_frame - 1;
}
float offset = timeline_frame - this->content_start();
if (seq::transform_single_image_check(this)) {
/* This strip has only 1 frame of content that is always stretched to the whole strip length.
* Move strip start left and adjust end offset to be negative (rightwards past the 1 frame). */
this->content_start_set(scene, timeline_frame);
this->endofs += offset;
}
else {
this->startofs = offset;
}
this->startdisp = timeline_frame; /* Only to make files usable in older versions. */
Span<Strip *> effects = seq::SEQ_lookup_effects_by_strip(scene->ed, this);
seq::strip_time_update_effects_strip_range(scene, effects);
seq::time_update_meta_strip_range(scene, seq::lookup_meta_by_strip(scene->ed, this));
}
void Strip::right_handle_set(const Scene *scene, int timeline_frame)
{
const float left_handle_orig_frame = this->left_handle();
if (timeline_frame <= left_handle_orig_frame) {
timeline_frame = left_handle_orig_frame + 1;
}
this->endofs = this->content_end(scene) - timeline_frame;
this->enddisp = timeline_frame; /* Only to make files usable in older versions. */
Span<Strip *> effects = seq::SEQ_lookup_effects_by_strip(scene->ed, this);
seq::strip_time_update_effects_strip_range(scene, effects);
seq::time_update_meta_strip_range(scene, seq::lookup_meta_by_strip(scene->ed, this));
}
void Strip::handles_set(const Scene *scene, int left_frame, int right_frame)
{
BLI_assert(left_frame < right_frame);
if (left_frame >= this->right_handle(scene)) {
/* Move right handle first to avoid clamping. */
this->right_handle_set(scene, right_frame);
this->left_handle_set(scene, left_frame);
}
else {
this->left_handle_set(scene, left_frame);
this->right_handle_set(scene, right_frame);
}
}
bool Strip::intersects_frame(const Scene *scene, const int timeline_frame) const
{
return (this->left_handle() <= timeline_frame) && (this->right_handle(scene) > timeline_frame);
}
} // namespace blender

View File

@@ -0,0 +1,51 @@
/* SPDX-FileCopyrightText: 2004 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "DNA_listBase.h"
#include "BLI_span.hh"
namespace blender {
/** \file
* \ingroup sequencer
*/
struct Scene;
struct Strip;
namespace seq {
void strip_update_sound_bounds_recursive(const Scene *scene, Strip *strip_meta);
/* Describes gap between strips in timeline. */
struct GapInfo {
int gap_start_frame; /* Start frame of the gap. */
int gap_length; /* Length of the gap. */
bool gap_exists; /* False if there are no gaps. */
};
/**
* Find first gap between strips after initial_frame and describe it by filling data of r_gap_info
*
* \param scene: Scene in which strips are located.
* \param seqbase: List in which strips are located.
* \param initial_frame: frame on timeline from where gaps are searched for.
* \param r_gap_info: data structure describing gap, that will be filled in by this function.
*/
void seq_time_gap_info_get(const Scene *scene,
ListBaseT<Strip> *seqbase,
int initial_frame,
GapInfo *r_gap_info);
void strip_time_effect_range_set(const Scene *scene, Strip *strip);
/**
* Update strip `startdisp` and `enddisp` (n-input effects have no length to calculate these).
*/
void strip_time_update_effects_strip_range(const Scene *scene, Span<Strip *> effects);
float strip_retiming_evaluate(const Strip *strip, const float frame_index);
} // namespace seq
} // namespace blender

View File

@@ -0,0 +1,784 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include "DNA_movieclip_types.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BLI_bounds.hh"
#include "BLI_listbase.h"
#include "BLI_math_base.h"
#include "BLI_math_base.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_rect.h"
#include "BLF_api.hh"
#include "SEQ_animation.hh"
#include "SEQ_channels.hh"
#include "SEQ_edit.hh"
#include "SEQ_iterator.hh"
#include "SEQ_relations.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_time.hh"
#include "SEQ_transform.hh"
#include "effects/effects.hh"
#include "sequencer.hh"
#include "strip_time.hh"
namespace blender::seq {
/* -------------------------------------------------------------------- */
/** \name Transform Utilities
* \{ */
bool transform_single_image_check(const Strip *strip)
{
return (strip->flag & SEQ_SINGLE_FRAME_CONTENT) != 0;
}
bool transform_is_locked(const ListBaseT<SeqTimelineChannel> *channels, const Strip *strip)
{
const SeqTimelineChannel *channel = channel_get_by_index(channels, strip->channel);
return strip->flag & SEQ_LOCK ||
(channel->is_locked() &&
!flag_is_set(strip->runtime->flag, StripRuntimeFlag::IgnoreChannelLock));
}
bool transform_strip_can_be_translated(const Strip *strip)
{
return !strip->is_effect_with_inputs();
}
bool transform_test_overlap(const Scene *scene, Strip *strip1, Strip *strip2)
{
return (strip1 != strip2 && strip1->channel == strip2->channel &&
((strip1->right_handle(scene) <= strip2->left_handle()) ||
(strip1->left_handle() >= strip2->right_handle(scene))) == 0);
}
bool transform_test_overlap(const Scene *scene, ListBaseT<Strip> *seqbasep, Strip *test)
{
Strip *strip;
strip = static_cast<Strip *>(seqbasep->first);
while (strip) {
if (transform_test_overlap(scene, test, strip)) {
return true;
}
strip = strip->next;
}
return false;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Timeline Strip Transform
* \{ */
void transform_translate_strip(Scene *evil_scene, Strip *strip, int delta)
{
if (delta == 0) {
return;
}
/* Meta strips requires their content is to be translated, and then frame range of the meta is
* updated based on nested strips. This won't work for empty meta-strips,
* so they can be treated as normal strip. */
if (strip->type == STRIP_TYPE_META && !strip->seqbase.is_empty()) {
for (Strip &strip_child : strip->seqbase) {
transform_translate_strip(evil_scene, &strip_child, delta);
}
/* Move meta start/end points. */
const int left_handle = strip->left_handle();
const int right_handle = strip->right_handle(evil_scene);
strip->handles_set(evil_scene, left_handle + delta, right_handle + delta);
}
else if (strip->input1 == nullptr && strip->input2 == nullptr) { /* All other strip types. */
strip->start += delta;
/* Only to make files usable in older versions. */
strip->startdisp = strip->left_handle();
strip->enddisp = strip->right_handle(evil_scene);
}
offset_animdata(evil_scene, strip, delta);
Span<Strip *> effects = SEQ_lookup_effects_by_strip(evil_scene->ed, strip);
strip_time_update_effects_strip_range(evil_scene, effects);
time_update_meta_strip_range(evil_scene, lookup_meta_by_strip(evil_scene->ed, strip));
}
bool transform_seqbase_shuffle_ex(ListBaseT<Strip> *seqbasep,
Strip *test,
Scene *evil_scene,
int channel_delta)
{
const int orig_channel = test->channel;
BLI_assert(ELEM(channel_delta, -1, 1));
strip_channel_set(test, test->channel + channel_delta);
const ListBaseT<SeqTimelineChannel> *channels = channels_displayed_get(editing_get(evil_scene));
SeqTimelineChannel *channel = channel_get_by_index(channels, test->channel);
bool use_fallback_translation = false;
while (transform_test_overlap(evil_scene, seqbasep, test) || channel->is_muted() ||
channel->is_locked())
{
if ((channel_delta > 0) ? (test->channel + channel_delta >= MAX_CHANNELS) :
(test->channel + channel_delta < 1))
{
use_fallback_translation = true;
break;
}
strip_channel_set(test, test->channel + channel_delta);
channel = channel_get_by_index(channels, test->channel);
}
/* Strip can not be moved to next free channel, translate it instead. */
if (use_fallback_translation) {
int new_frame = test->right_handle(evil_scene);
for (Strip &strip : *seqbasep) {
if (strip.channel == orig_channel) {
new_frame = max_ii(new_frame, strip.right_handle(evil_scene));
}
}
strip_channel_set(test, orig_channel);
new_frame = new_frame + (test->start - test->left_handle()); /* adjust by the startdisp */
transform_translate_strip(evil_scene, test, new_frame - test->start);
return false;
}
return true;
}
bool transform_seqbase_shuffle(ListBaseT<Strip> *seqbasep, Strip *test, Scene *evil_scene)
{
return transform_seqbase_shuffle_ex(seqbasep, test, evil_scene, 1);
}
static bool shuffle_strip_test_overlap(const Scene *scene,
const Strip *strip1,
const Strip *strip2,
const int offset)
{
BLI_assert(strip1 != strip2);
return (strip1->channel == strip2->channel &&
((strip1->right_handle(scene) + offset <= strip2->left_handle()) ||
(strip1->left_handle() + offset >= strip2->right_handle(scene))) == 0);
}
static int shuffle_strip_time_offset_get(const Scene *scene,
Span<Strip *> strips_to_shuffle,
ListBaseT<Strip> *seqbasep,
char dir)
{
int offset = 0;
bool all_conflicts_resolved = false;
while (!all_conflicts_resolved) {
all_conflicts_resolved = true;
for (Strip *strip : strips_to_shuffle) {
for (Strip &strip_other : *seqbasep) {
if (strips_to_shuffle.contains(&strip_other)) {
continue;
}
if (relation_is_effect_of_strip(&strip_other, strip)) {
continue;
}
if (!shuffle_strip_test_overlap(scene, strip, &strip_other, offset)) {
continue;
}
all_conflicts_resolved = false;
if (dir == 'L') {
offset = min_ii(offset, strip_other.left_handle() - strip->right_handle(scene));
}
else {
offset = max_ii(offset, strip_other.right_handle(scene) - strip->left_handle());
}
}
}
}
return offset;
}
bool transform_seqbase_shuffle_time(Span<Strip *> strips_to_shuffle,
ListBaseT<Strip> *seqbasep,
Scene *evil_scene,
ListBaseT<TimeMarker> *markers,
const bool use_sync_markers)
{
VectorSet<Strip *> empty_set;
return transform_seqbase_shuffle_time(
strips_to_shuffle, empty_set, seqbasep, evil_scene, markers, use_sync_markers);
}
bool transform_seqbase_shuffle_time(Span<Strip *> strips_to_shuffle,
Span<Strip *> time_dependent_strips,
ListBaseT<Strip> *seqbasep,
Scene *evil_scene,
ListBaseT<TimeMarker> *markers,
const bool use_sync_markers)
{
int offset_l = shuffle_strip_time_offset_get(evil_scene, strips_to_shuffle, seqbasep, 'L');
int offset_r = shuffle_strip_time_offset_get(evil_scene, strips_to_shuffle, seqbasep, 'R');
int offset = (-offset_l < offset_r) ? offset_l : offset_r;
if (offset) {
for (Strip *strip : strips_to_shuffle) {
transform_translate_strip(evil_scene, strip, offset);
strip->runtime->flag &= ~StripRuntimeFlag::Overlap;
}
if (!time_dependent_strips.is_empty()) {
for (Strip *strip : time_dependent_strips) {
offset_animdata(evil_scene, strip, offset);
}
}
if (use_sync_markers && !(evil_scene->toolsettings->lock_markers) && (markers != nullptr)) {
/* affect selected markers - it's unlikely that we will want to affect all in this way? */
for (TimeMarker &marker : *markers) {
if (marker.flag & SELECT) {
marker.frame += offset;
}
}
}
}
return offset ? false : true;
}
static VectorSet<Strip *> extract_standalone_strips(Span<Strip *> transformed_strips)
{
VectorSet<Strip *> standalone_strips;
for (Strip *strip : transformed_strips) {
if (!strip->is_effect() || strip->input1 == nullptr) {
standalone_strips.add(strip);
}
}
return standalone_strips;
}
/* Query strips positioned after left edge of transformed strips bound-box. */
static VectorSet<Strip *> query_right_side_strips(ListBaseT<Strip> *seqbase,
Span<Strip *> transformed_strips,
Span<Strip *> time_dependent_strips)
{
int minframe = MAXFRAME;
{
for (Strip *strip : transformed_strips) {
minframe = min_ii(minframe, strip->left_handle());
}
}
VectorSet<Strip *> right_side_strips;
for (Strip &strip : *seqbase) {
if (!time_dependent_strips.is_empty() && time_dependent_strips.contains(&strip)) {
continue;
}
if (transformed_strips.contains(&strip)) {
continue;
}
if ((strip.flag & SEQ_SELECT) == 0 && strip.left_handle() >= minframe) {
right_side_strips.add(&strip);
}
}
return right_side_strips;
}
/* Offset all strips positioned after left edge of transformed strips bound-box by amount equal
* to overlap of transformed strips. */
static void strip_transform_handle_expand_to_fit(Scene *scene,
ListBaseT<Strip> *seqbasep,
Span<Strip *> transformed_strips,
Span<Strip *> time_dependent_strips,
bool use_sync_markers)
{
ListBaseT<TimeMarker> *markers = &scene->markers;
VectorSet right_side_strips = query_right_side_strips(
seqbasep, transformed_strips, time_dependent_strips);
/* Temporarily move right side strips beyond timeline boundary. */
for (Strip *strip : right_side_strips) {
strip->channel += MAX_CHANNELS * 2;
}
/* Shuffle transformed standalone strips. This is because transformed strips can overlap with
* strips on left side. */
VectorSet standalone_strips = extract_standalone_strips(transformed_strips);
transform_seqbase_shuffle_time(
standalone_strips, time_dependent_strips, seqbasep, scene, markers, use_sync_markers);
/* Move temporarily moved strips back to their original place and tag for shuffling. */
for (Strip *strip : right_side_strips) {
strip->channel -= MAX_CHANNELS * 2;
}
/* Shuffle again to displace strips on right side. Final effect shuffling is done in
* SEQ_transform_handle_overlap. */
transform_seqbase_shuffle_time(right_side_strips, seqbasep, scene, markers, use_sync_markers);
}
static VectorSet<Strip *> query_overwrite_targets(const Scene *scene,
ListBaseT<Strip> *seqbasep,
Span<Strip *> transformed_strips)
{
VectorSet<Strip *> overwrite_targets = query_unselected_strips(seqbasep);
/* Effects of transformed strips can be unselected. These must not be included. */
overwrite_targets.remove_if([&](Strip *strip) { return transformed_strips.contains(strip); });
overwrite_targets.remove_if([&](Strip *strip) {
bool does_overlap = false;
for (Strip *strip_transformed : transformed_strips) {
if (transform_test_overlap(scene, strip, strip_transformed)) {
does_overlap = true;
}
}
return !does_overlap;
});
return overwrite_targets;
}
enum eOvelapDescrition {
/* No overlap. */
STRIP_OVERLAP_NONE,
/* Overlapping strip covers overlapped completely. */
STRIP_OVERLAP_IS_FULL,
/* Overlapping strip is inside overlapped. */
STRIP_OVERLAP_IS_INSIDE,
/* Partial overlap between 2 strips. */
STRIP_OVERLAP_LEFT_SIDE,
STRIP_OVERLAP_RIGHT_SIDE,
};
static eOvelapDescrition overlap_description_get(const Scene *scene,
const Strip *transformed,
const Strip *target)
{
if (transformed->left_handle() <= target->left_handle() &&
transformed->right_handle(scene) >= target->right_handle(scene))
{
return STRIP_OVERLAP_IS_FULL;
}
if (transformed->left_handle() > target->left_handle() &&
transformed->right_handle(scene) < target->right_handle(scene))
{
return STRIP_OVERLAP_IS_INSIDE;
}
if (transformed->left_handle() <= target->left_handle() &&
target->left_handle() <= transformed->right_handle(scene))
{
return STRIP_OVERLAP_LEFT_SIDE;
}
if (transformed->left_handle() <= target->right_handle(scene) &&
target->right_handle(scene) <= transformed->right_handle(scene))
{
return STRIP_OVERLAP_RIGHT_SIDE;
}
return STRIP_OVERLAP_NONE;
}
/* Split strip in 3 parts, remove middle part and fit transformed inside. */
static void strip_transform_handle_overwrite_split(Scene *scene,
ListBaseT<Strip> *seqbasep,
const Strip *transformed,
Strip *target)
{
/* Because we are doing a soft split, bmain is not used in SEQ_edit_strip_split, so we can
* pass nullptr here. */
Main *bmain = nullptr;
const char *error_msg = nullptr;
Strip *split_strip = edit_strip_split(
bmain, scene, seqbasep, target, transformed->left_handle(), SPLIT_SOFT, true, &error_msg);
if (split_strip == nullptr) {
return;
}
error_msg = nullptr;
if (edit_strip_split(bmain,
scene,
seqbasep,
split_strip,
transformed->right_handle(scene),
SPLIT_SOFT,
true,
&error_msg) == nullptr)
{
return;
}
edit_flag_for_removal(scene, seqbasep, split_strip);
edit_remove_flagged_strips(scene, seqbasep);
}
/* Trim strips by adjusting handle position.
* This is bit more complicated in case overlap happens on effect. */
static void strip_transform_handle_overwrite_trim(Scene *scene,
ListBaseT<Strip> *seqbasep,
const Strip *transformed,
Strip *target,
const eOvelapDescrition overlap)
{
VectorSet targets = query_by_reference(target, seqbasep, query_strip_effect_chain);
/* Expand collection by adding all target's children, effects and their children. */
if (target->is_effect()) {
iterator_set_expand(seqbasep, targets, query_strip_effect_chain);
}
/* Trim all non effects, that have influence on effect length which is overlapping. */
for (Strip *strip : targets) {
if (strip->is_effect_with_inputs()) {
continue;
}
if (overlap == STRIP_OVERLAP_LEFT_SIDE) {
strip->left_handle_set(scene, transformed->right_handle(scene));
}
else {
BLI_assert(overlap == STRIP_OVERLAP_RIGHT_SIDE);
strip->right_handle_set(scene, transformed->left_handle());
}
}
}
static void strip_transform_handle_overwrite(Scene *scene,
ListBaseT<Strip> *seqbasep,
Span<Strip *> transformed_strips)
{
VectorSet targets = query_overwrite_targets(scene, seqbasep, transformed_strips);
VectorSet<Strip *> strips_to_delete;
const ListBaseT<SeqTimelineChannel> *channels = channels_displayed_get(editing_get(scene));
for (Strip *target : targets) {
for (Strip *transformed : transformed_strips) {
if (transformed->channel != target->channel) {
continue;
}
/* Do not allow overwriting/trimming/deleting locked strips. */
if (transform_is_locked(channels, target)) {
continue;
}
const eOvelapDescrition overlap = overlap_description_get(scene, transformed, target);
if (overlap == STRIP_OVERLAP_IS_FULL) {
strips_to_delete.add(target);
}
else if (overlap == STRIP_OVERLAP_IS_INSIDE) {
strip_transform_handle_overwrite_split(scene, seqbasep, transformed, target);
}
else if (ELEM(overlap, STRIP_OVERLAP_LEFT_SIDE, STRIP_OVERLAP_RIGHT_SIDE)) {
strip_transform_handle_overwrite_trim(scene, seqbasep, transformed, target, overlap);
}
}
}
/* Remove covered strips. This must be done in separate loop, because
* `SEQ_edit_strip_split()` also uses `SEQ_edit_remove_flagged_sequences()`. See #91096. */
if (!strips_to_delete.is_empty()) {
for (Strip *strip : strips_to_delete) {
edit_flag_for_removal(scene, seqbasep, strip);
}
edit_remove_flagged_strips(scene, seqbasep);
}
}
static void strip_transform_handle_overlap_shuffle(Scene *scene,
ListBaseT<Strip> *seqbasep,
Span<Strip *> transformed_strips,
Span<Strip *> time_dependent_strips,
bool use_sync_markers)
{
ListBaseT<TimeMarker> *markers = &scene->markers;
/* Shuffle non strips with no effects attached. */
VectorSet standalone_strips = extract_standalone_strips(transformed_strips);
transform_seqbase_shuffle_time(
standalone_strips, time_dependent_strips, seqbasep, scene, markers, use_sync_markers);
}
void transform_handle_overlap(Scene *scene,
ListBaseT<Strip> *seqbasep,
Span<Strip *> transformed_strips,
bool use_sync_markers)
{
VectorSet<Strip *> empty_set;
transform_handle_overlap(scene, seqbasep, transformed_strips, empty_set, use_sync_markers);
}
void transform_handle_overlap(Scene *scene,
ListBaseT<Strip> *seqbasep,
Span<Strip *> transformed_strips,
Span<Strip *> time_dependent_strips,
bool use_sync_markers)
{
const eSeqOverlapMode overlap_mode = tool_settings_overlap_mode_get(scene);
switch (overlap_mode) {
case SEQ_OVERLAP_EXPAND:
strip_transform_handle_expand_to_fit(
scene, seqbasep, transformed_strips, time_dependent_strips, use_sync_markers);
break;
case SEQ_OVERLAP_OVERWRITE:
strip_transform_handle_overwrite(scene, seqbasep, transformed_strips);
break;
case SEQ_OVERLAP_SHUFFLE:
strip_transform_handle_overlap_shuffle(
scene, seqbasep, transformed_strips, time_dependent_strips, use_sync_markers);
break;
}
/* If any effects still overlap, we need to move them up.
* In some cases other strips can be overlapping still, see #90646. */
for (Strip *strip : transformed_strips) {
if (transform_test_overlap(scene, seqbasep, strip)) {
transform_seqbase_shuffle(seqbasep, strip, scene);
}
strip->runtime->flag &= ~StripRuntimeFlag::Overlap;
}
}
void transform_offset_after_frame(Scene *scene,
ListBaseT<Strip> *seqbase,
const int delta,
const int timeline_frame)
{
for (Strip &strip : *seqbase) {
if (strip.left_handle() >= timeline_frame) {
transform_translate_strip(scene, &strip, delta);
relations_invalidate_cache(scene, &strip);
}
}
if (!scene->toolsettings->lock_markers) {
for (TimeMarker &marker : scene->markers) {
if (marker.frame >= timeline_frame) {
marker.frame += delta;
}
}
}
}
void strip_channel_set(Strip *strip, int channel)
{
strip->channel = math::clamp(channel, 1, MAX_CHANNELS);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Preview Image Transform
* \{ */
float2 image_transform_mirror_factor_get(const Strip *strip)
{
float2 mirror(1.0f, 1.0f);
if ((strip->flag & SEQ_FLIPX) != 0) {
mirror.x = -1.0f;
}
if ((strip->flag & SEQ_FLIPY) != 0) {
mirror.y = -1.0f;
}
return mirror;
}
float2 image_transform_raw_size_get(const Scene *scene, const Strip *strip)
{
float2 scene_render_size(scene->r.xsch, scene->r.ysch);
if (ELEM(strip->type, STRIP_TYPE_MOVIE, STRIP_TYPE_IMAGE)) {
const StripElem *selem = strip->data->stripdata;
return {float(selem->orig_width), float(selem->orig_height)};
}
if (strip->type == STRIP_TYPE_MOVIECLIP) {
const MovieClip *clip = strip->clip;
if (clip != nullptr && clip->lastsize[0] != 0 && clip->lastsize[1] != 0) {
return {float(clip->lastsize[0]), float(clip->lastsize[1])};
}
}
if (strip->type == STRIP_TYPE_COLOR) {
const SolidColorVars *data = static_cast<const SolidColorVars *>(strip->effectdata);
return {float(data->width), float(data->height)};
}
if (strip->type == STRIP_TYPE_TEXT) {
TextVars *data = static_cast<TextVars *>(strip->effectdata);
std::scoped_lock runtime_lock(text_runtime_mutex_get());
text_effect_update_runtime(nullptr, *data, int2(scene_render_size));
BLF_disable(data->runtime->font, BLF_BOLD | BLF_ITALIC);
const float2 text_size(float(BLI_rcti_size_x(&data->runtime->text_boundbox)),
float(BLI_rcti_size_y(&data->runtime->text_boundbox)));
return text_size;
}
return scene_render_size;
}
/* Convert origin from a 0->1 range (where (0,0) is the bottom left of the image)
* to the offset in view-space pixels from an image's center. */
static float2 convert_origin_to_image_offset(const Scene *scene, const Strip *strip, float2 origin)
{
const float2 image_size = image_transform_raw_size_get(scene, strip);
return image_size * origin - (image_size / 2.0f);
}
float2 image_transform_origin_get(const Scene *scene, const Strip *strip)
{
const StripTransform *tr = strip->data->transform;
if (strip->type != STRIP_TYPE_TEXT) {
return tr->origin;
}
/* Text strips 'fake' smaller bounds but their true image size is the size of the render. We must
* convert from an origin relative to the text box -> an origin relative to the whole render. */
const float2 text_size = image_transform_raw_size_get(scene, strip);
const float2 render_size(scene->r.xsch, scene->r.ysch);
/* Before we scale the text origin down to produce the render origin, we must offset the origin
* so that (0,0) corresponds to the center instead of (0.5, 0.5) for correct math. */
const float2 offset_text_origin = float2(tr->origin) - float2(0.5f);
const float2 render_origin = float2(0.5f) + offset_text_origin * (text_size / render_size);
return render_origin;
}
float2 image_transform_origin_preview_offset_get(const Scene *scene, const Strip *strip)
{
const StripTransform *tr = strip->data->transform;
const float2 origin_offset = convert_origin_to_image_offset(scene, strip, tr->origin);
const float2 viewport_pixel_aspect(scene->r.xasp / scene->r.yasp, 1.0f);
const float2 mirror = image_transform_mirror_factor_get(strip);
return (origin_offset + float2(tr->xofs, tr->yofs)) * mirror * viewport_pixel_aspect;
}
float3x3 image_transform_matrix_get(const Scene *scene, const Strip *strip)
{
const StripTransform *tr = strip->data->transform;
const float3x3 matrix = math::from_loc_rot_scale<float3x3>(
float2(tr->xofs, tr->yofs), tr->rotation, float2(tr->scale_x, tr->scale_y));
const float2 origin_offset = convert_origin_to_image_offset(scene, strip, tr->origin);
return math::from_origin_transform(matrix, origin_offset);
}
Array<float2> image_transform_quad_get(const Scene *scene, const Strip *strip)
{
constexpr int num_corners = 4;
const float2 image_size = image_transform_raw_size_get(scene, strip);
/* Raw quad before any rotation/scaling or text anchoring is applied.
*
* NOTE: For text strips, crops should only affect their visible result and not their bounding
* box. Text effects can stray outside, so crop works on the full render buffer. */
const StripCrop no_crop{};
const StripCrop *crop = (strip->type == STRIP_TYPE_TEXT) ? &no_crop : strip->data->crop;
float2 quad[num_corners]{
{(image_size.x / 2) - crop->right, (image_size.y / 2) - crop->top}, /* Top right. */
{(image_size.x / 2) - crop->right, (-image_size.y / 2) + crop->bottom}, /* Bottom right. */
{(-image_size.x / 2) + crop->left, (-image_size.y / 2) + crop->bottom}, /* Bottom left. */
{(-image_size.x / 2) + crop->left, (image_size.y / 2) - crop->top}, /* Top left. */
};
if (strip->type == STRIP_TYPE_TEXT) {
const TextVars *data = static_cast<TextVars *>(strip->effectdata);
float2 offset(0, 0);
switch (data->anchor_x) {
case SEQ_TEXT_ANCHOR_X_LEFT:
offset.x += image_size.x / 2.0f;
break;
case SEQ_TEXT_ANCHOR_X_CENTER:
break;
case SEQ_TEXT_ANCHOR_X_RIGHT:
offset.x += -image_size.x / 2.0f;
break;
default:
break;
}
switch (data->anchor_y) {
case SEQ_TEXT_ANCHOR_Y_BOTTOM:
offset.y += image_size.y / 2.0f;
break;
case SEQ_TEXT_ANCHOR_Y_CENTER:
break;
case SEQ_TEXT_ANCHOR_Y_TOP:
offset.y += -image_size.y / 2.0f;
break;
default:
break;
}
for (float2 &corner : quad) {
corner += offset;
}
}
const float3x3 matrix = image_transform_matrix_get(scene, strip);
const float2 viewport_pixel_aspect(scene->r.xasp / scene->r.yasp, 1.0f);
const float2 mirror = image_transform_mirror_factor_get(strip);
Array<float2> quad_final(num_corners);
for (const int i : IndexRange(num_corners)) {
const float2 point = math::transform_point(matrix, quad[i]);
quad_final[i] = point * mirror * viewport_pixel_aspect;
}
return quad_final;
}
float2 image_preview_unit_to_px(const Scene *scene, const float2 co_src)
{
return {co_src.x * scene->r.xsch, co_src.y * scene->r.ysch};
}
float2 image_preview_unit_from_px(const Scene *scene, const float2 co_src)
{
return {co_src.x / scene->r.xsch, co_src.y / scene->r.ysch};
}
static Bounds<float2> negative_bounds()
{
return {float2(std::numeric_limits<float>::max()), float2(std::numeric_limits<float>::lowest())};
}
Bounds<float2> image_transform_bounding_box_from_strips_get(Scene *scene, Span<Strip *> strips)
{
Bounds<float2> box = negative_bounds();
for (Strip *strip : strips) {
const Array<float2> quad = image_transform_quad_get(scene, strip);
const Bounds<float2> strip_box = *bounds::min_max(quad.as_span());
box = bounds::merge(box, strip_box);
}
return box;
}
/** \} */
} // namespace blender::seq

View File

@@ -0,0 +1,523 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
* SPDX-FileCopyrightText: 2003-2009 Blender Authors
* SPDX-FileCopyrightText: 2005-2006 Peter Schlaile <peter [at] schlaile [dot] de>
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup sequencer
*/
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include "MEM_guardedalloc.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "BLI_listbase.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLI_string_utils.hh"
#include "BLT_translation.hh"
#include "BKE_animsys.h"
#include "BKE_image.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BKE_scene.hh"
#include "SEQ_channels.hh"
#include "SEQ_edit.hh"
#include "SEQ_iterator.hh"
#include "SEQ_relations.hh"
#include "SEQ_render.hh"
#include "SEQ_select.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_utils.hh"
#include "IMB_imbuf_types.hh"
#include "MOV_read.hh"
#include "multiview.hh"
#include "proxy.hh"
#include "utils.hh"
namespace blender::seq {
struct StripUniqueInfo {
Strip *strip;
char name_src[STRIP_NAME_MAXSTR];
char name_dest[STRIP_NAME_MAXSTR];
int count;
int match;
};
static void seqbase_unique_name(ListBaseT<Strip> *seqbasep, StripUniqueInfo *sui)
{
for (Strip &strip : *seqbasep) {
if ((sui->strip != &strip) && STREQ(sui->name_dest, strip.name + 2)) {
/* STRIP_NAME_MAXSTR -4 for the number, -1 for \0, - 2 for r_prefix */
SNPRINTF(
sui->name_dest, "%.*s.%03d", STRIP_NAME_MAXSTR - 4 - 1 - 2, sui->name_src, sui->count++);
sui->match = 1; /* be sure to re-scan */
}
}
}
static bool seqbase_unique_name_recursive_fn(Strip *strip, void *arg_pt)
{
if (strip->seqbase.first) {
seqbase_unique_name(&strip->seqbase, static_cast<StripUniqueInfo *>(arg_pt));
}
return true;
}
void strip_unique_name_set(Scene *scene, ListBaseT<Strip> *seqbasep, Strip *strip)
{
StripUniqueInfo sui;
char *dot;
sui.strip = strip;
STRNCPY(sui.name_src, strip->name + 2);
STRNCPY(sui.name_dest, sui.name_src);
sui.count = 1;
sui.match = 1; /* assume the worst to start the loop */
/* Strip off the suffix only if it is purely numeric. */
if ((dot = strrchr(sui.name_src, '.'))) {
char *suffix = dot + 1;
if (BLI_string_is_decimal(suffix)) {
*dot = '\0';
sui.count = atoi(suffix) + 1;
}
}
while (sui.match) {
sui.match = 0;
seqbase_unique_name(seqbasep, &sui);
foreach_strip(seqbasep, seqbase_unique_name_recursive_fn, &sui);
}
edit_strip_name_set(scene, strip, sui.name_dest);
}
const char *get_default_stripname_by_type(int type)
{
switch (type) {
case STRIP_TYPE_META:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Meta");
case STRIP_TYPE_IMAGE:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Image");
case STRIP_TYPE_SCENE:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Scene");
case STRIP_TYPE_MOVIE:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Movie");
case STRIP_TYPE_MOVIECLIP:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Clip");
case STRIP_TYPE_MASK:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Mask");
case STRIP_TYPE_SOUND:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Audio");
case STRIP_TYPE_CROSS:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Crossfade");
case STRIP_TYPE_GAMCROSS:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Gamma Crossfade");
case STRIP_TYPE_COMPOSITOR:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Compositor");
case STRIP_TYPE_ADD:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Add");
case STRIP_TYPE_SUB:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Subtract");
case STRIP_TYPE_MUL:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Multiply");
case STRIP_TYPE_ALPHAOVER:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Alpha Over");
case STRIP_TYPE_ALPHAUNDER:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Alpha Under");
case STRIP_TYPE_COLORMIX:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Color Mix");
case STRIP_TYPE_WIPE:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Wipe");
case STRIP_TYPE_GLOW:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Glow");
case STRIP_TYPE_COLOR:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Color");
case STRIP_TYPE_MULTICAM:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Multicam");
case STRIP_TYPE_ADJUSTMENT:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Adjustment");
case STRIP_TYPE_SPEED:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Speed");
case STRIP_TYPE_GAUSSIAN_BLUR:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Gaussian Blur");
case STRIP_TYPE_TEXT:
return CTX_DATA_(BLT_I18NCONTEXT_ID_SEQUENCE, "Text");
default:
return nullptr;
}
}
const char *strip_give_name(const Strip *strip)
{
const char *name = get_default_stripname_by_type(strip->type);
if (!name) {
if (!strip->is_effect()) {
return strip->data->dirpath;
}
return DATA_("Effect");
}
return name;
}
ListBaseT<Strip> *get_seqbase_from_strip(Strip *strip,
ListBaseT<SeqTimelineChannel> **r_channels,
int *r_offset)
{
ListBaseT<Strip> *seqbase = nullptr;
switch (strip->type) {
case STRIP_TYPE_META: {
seqbase = &strip->seqbase;
*r_channels = &strip->channels;
*r_offset = strip->content_start();
break;
}
case STRIP_TYPE_SCENE: {
if (strip->flag & SEQ_SCENE_STRIPS && strip->scene) {
Editing *ed = editing_get(strip->scene);
if (ed) {
seqbase = &ed->seqbase;
*r_channels = &ed->channels;
*r_offset = strip->scene->r.sfra;
}
}
break;
}
default:
break;
}
return seqbase;
}
static MovieReader *open_anim_filepath(Strip *strip, const char *filepath, bool openfile)
{
/* Sequencer takes care of colorspace conversion of the result. The input is the best to be
* kept unchanged for the performance reasons. */
if (openfile) {
return openanim(filepath,
(strip->flag & SEQ_DEINTERLACE) ? ImBufFlags::Deinterlace : ImBufFlags::Zero,
strip->streamindex,
true,
strip->data->colorspace_settings.name);
}
return openanim_noload(filepath,
(strip->flag & SEQ_DEINTERLACE) ? ImBufFlags::Deinterlace :
ImBufFlags::Zero,
strip->streamindex,
true,
strip->data->colorspace_settings.name);
}
static bool use_proxy(Editing *ed, Strip *strip)
{
StripProxy *proxy = strip->data->proxy;
return proxy && ((proxy->storage & SEQ_STORAGE_PROXY_CUSTOM_DIR) != 0 ||
(ed->proxy_storage == SEQ_EDIT_PROXY_DIR_STORAGE));
}
static void proxy_dir_get(Editing *ed, Strip *strip, char r_proxy_dirpath[FILE_MAX])
{
if (use_proxy(ed, strip)) {
if (ed->proxy_storage == SEQ_EDIT_PROXY_DIR_STORAGE) {
if (ed->proxy_dir[0] == 0) {
BLI_strncpy(r_proxy_dirpath, "//BL_proxy", FILE_MAX);
}
else {
BLI_strncpy(r_proxy_dirpath, ed->proxy_dir, FILE_MAX);
}
}
else {
BLI_strncpy(r_proxy_dirpath, strip->data->proxy->dirpath, FILE_MAX);
}
BLI_path_abs(r_proxy_dirpath, BKE_main_blendfile_path_from_global());
}
}
static void index_dir_set(Editing *ed, Strip *strip, MovieReader *reader)
{
if (reader == nullptr || !use_proxy(ed, strip)) {
return;
}
char proxy_dirpath[FILE_MAX];
proxy_dir_get(ed, strip, proxy_dirpath);
seq_proxy_index_dir_set(reader, proxy_dirpath);
}
static bool open_anim_file_multiview(Scene *scene, Strip *strip, const char *filepath)
{
char prefix[FILE_MAX];
const char *ext = nullptr;
BKE_scene_multiview_view_prefix_get(scene, filepath, prefix, &ext);
if (strip->views_format != R_IMF_VIEWS_INDIVIDUAL || prefix[0] == '\0') {
return false;
}
Editing *ed = scene->ed;
bool is_multiview_loaded = false;
int totfiles = seq_num_files(scene, strip->views_format, true);
for (int i = 0; i < totfiles; i++) {
const char *suffix = BKE_scene_multiview_view_id_suffix_get(&scene->r, i);
char filepath_view[FILE_MAX];
SNPRINTF(filepath_view, "%s%s%s", prefix, suffix, ext);
/* Multiview files must be loaded, otherwise it is not possible to detect failure. */
MovieReader *reader = open_anim_filepath(strip, filepath_view, true);
if (reader == nullptr) {
strip_free_movie_readers(strip);
return false; /* Multiview render failed. */
}
index_dir_set(ed, strip, reader);
strip->runtime->movie_readers.append(reader);
MOV_set_multiview_suffix(reader, suffix);
is_multiview_loaded = true;
}
return is_multiview_loaded;
}
void strip_open_anim_file(Scene *scene, Strip *strip, bool openfile)
{
if (!openfile && strip->runtime->movie_reader_get() != nullptr) {
return;
}
/* Reset all the previously created anims. */
strip_free_movie_readers(strip);
Editing *ed = scene->ed;
char filepath[FILE_MAX];
BLI_path_join(
filepath, sizeof(filepath), strip->data->dirpath, strip->data->stripdata->filename);
BLI_path_abs(filepath, ID_BLEND_PATH_FROM_GLOBAL(&scene->id));
bool is_multiview = (strip->flag & SEQ_USE_VIEWS) != 0 && (scene->r.scemode & R_MULTIVIEW) != 0;
bool multiview_is_loaded = false;
if (is_multiview) {
multiview_is_loaded = open_anim_file_multiview(scene, strip, filepath);
}
if (!is_multiview || !multiview_is_loaded) {
MovieReader *reader = open_anim_filepath(strip, filepath, openfile);
strip->runtime->movie_readers.append(reader);
index_dir_set(ed, strip, reader);
}
}
const Strip *strip_topmost_get(const Scene *scene, int frame)
{
Editing *ed = scene->ed;
if (!ed) {
return nullptr;
}
ListBaseT<SeqTimelineChannel> *channels = channels_displayed_get(ed);
const Strip *best_strip = nullptr;
int best_channel = -1;
for (const Strip &strip : *ed->current_strips()) {
if (render_is_muted(channels, &strip) || !strip.intersects_frame(scene, frame)) {
continue;
}
/* Only use strips that generate an image, not ones that combine
* other strips or apply some effect. */
if (ELEM(strip.type,
STRIP_TYPE_IMAGE,
STRIP_TYPE_META,
STRIP_TYPE_SCENE,
STRIP_TYPE_MOVIE,
STRIP_TYPE_COLOR,
STRIP_TYPE_TEXT))
{
if (strip.channel > best_channel) {
best_strip = &strip;
best_channel = strip.channel;
}
}
}
return best_strip;
}
ListBaseT<Strip> *get_seqbase_by_strip(const Scene *scene, Strip *strip)
{
Editing *ed = editing_get(scene);
ListBaseT<Strip> *main_seqbase = &ed->seqbase;
Strip *strip_meta = lookup_meta_by_strip(ed, strip);
if (strip_meta != nullptr) {
return &strip_meta->seqbase;
}
if (BLI_findindex(main_seqbase, strip) != -1) {
return main_seqbase;
}
return nullptr;
}
Strip *strip_from_strip_elem(ListBaseT<Strip> *seqbase, StripElem *se)
{
Strip *istrip;
for (istrip = static_cast<Strip *>(seqbase->first); istrip; istrip = istrip->next) {
Strip *strip_found;
if ((istrip->data && istrip->data->stripdata) &&
ARRAY_HAS_ITEM(se, istrip->data->stripdata, istrip->len))
{
break;
}
if ((strip_found = strip_from_strip_elem(&istrip->seqbase, se))) {
istrip = strip_found;
break;
}
}
return istrip;
}
Strip *get_strip_by_name(ListBaseT<Strip> *seqbase, const char *name, bool recursive)
{
for (Strip &istrip : *seqbase) {
if (STREQ(name, istrip.name + 2)) {
return &istrip;
}
if (recursive && !istrip.seqbase.is_empty()) {
Strip *rseq = get_strip_by_name(&istrip.seqbase, name, true);
if (rseq != nullptr) {
return rseq;
}
}
}
return nullptr;
}
Mask *active_mask_get(Scene *scene)
{
Strip *strip_act = select_active_get(scene);
if (strip_act && strip_act->type == STRIP_TYPE_MASK) {
return strip_act->mask;
}
return nullptr;
}
void alpha_mode_from_file_extension(Strip *strip)
{
if (strip->data && strip->data->stripdata) {
const char *filename = strip->data->stripdata->filename;
strip->alpha_mode = eStripAlphaMode(BKE_image_alpha_mode_from_extension_ex(filename));
}
}
bool strip_has_valid_data(const Strip *strip)
{
switch (strip->type) {
case STRIP_TYPE_MASK:
return (strip->mask != nullptr);
case STRIP_TYPE_MOVIECLIP:
return (strip->clip != nullptr);
case STRIP_TYPE_SCENE:
return (strip->scene != nullptr);
case STRIP_TYPE_SOUND:
return (strip->sound != nullptr);
default:
return true;
}
}
bool sequencer_strip_generates_image(Strip *strip)
{
switch (strip->type) {
case STRIP_TYPE_IMAGE:
case STRIP_TYPE_SCENE:
case STRIP_TYPE_MOVIE:
case STRIP_TYPE_MOVIECLIP:
case STRIP_TYPE_MASK:
case STRIP_TYPE_COLOR:
case STRIP_TYPE_TEXT:
return true;
default:
return false;
}
}
void set_scale_to_fit(const Strip *strip,
const int image_width,
const int image_height,
const int preview_width,
const int preview_height,
const eSeqImageFitMethod fit_method)
{
StripTransform *transform = strip->data->transform;
switch (fit_method) {
case SEQ_SCALE_TO_FIT:
transform->scale_x = transform->scale_y = std::min(
float(preview_width) / float(image_width), float(preview_height) / float(image_height));
break;
case SEQ_SCALE_TO_FILL:
transform->scale_x = transform->scale_y = std::max(
float(preview_width) / float(image_width), float(preview_height) / float(image_height));
break;
case SEQ_STRETCH_TO_FILL:
transform->scale_x = float(preview_width) / float(image_width);
transform->scale_y = float(preview_height) / float(image_height);
break;
case SEQ_USE_ORIGINAL_SIZE:
transform->scale_x = 1.0f;
transform->scale_y = 1.0f;
break;
}
}
void ensure_unique_name(Strip *strip, Scene *scene)
{
char name[STRIP_NAME_MAXSTR];
STRNCPY_UTF8(name, strip->name + 2);
strip_unique_name_set(scene, &scene->ed->seqbase, strip);
BKE_animdata_fix_paths_rename(&scene->id,
scene->adt,
nullptr,
"sequence_editor.strips_all",
name,
strip->name + 2,
0,
0,
/*verify_paths=*/false,
/*infix_is_name=*/true);
if (strip->type == STRIP_TYPE_META) {
for (Strip &strip_child : strip->seqbase) {
ensure_unique_name(&strip_child, scene);
}
}
}
} // namespace blender::seq

View File

@@ -0,0 +1,22 @@
/* SPDX-FileCopyrightText: 2004 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup sequencer
*/
namespace blender {
struct Scene;
struct Strip;
namespace seq {
bool sequencer_strip_generates_image(Strip *strip);
void strip_open_anim_file(Scene *scene, Strip *strip, bool openfile);
} // namespace seq
} // namespace blender