/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved. * SPDX-FileCopyrightText: 2003-2026 Blender Authors * SPDX-FileCopyrightText: 2005-2006 Peter Schlaile * * 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 #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 *processed_paths, bool build_only_on_bad_performance, Vector &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("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 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 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 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