Files
test/source/blender/blenkernel/intern/blendfile.cc
Germano Cavalcante 3ab65cff04 Windows: show popup after crash
Implements a crash dialog for Windows.

The crash popup provides the following actions:
- Restart: reopen Blender from the last saved or auto-saved time
- Report a Bug: forward to Blender bug tracker
- View Crash Log: open the .txt file with the crash log
- Close: Closes without any further action

Pull Request: https://projects.blender.org/blender/blender/pulls/129974
2025-04-04 18:38:53 +02:00

2239 lines
80 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bke
*
* High level `.blend` file read/write,
* and functions for writing *partial* files (only selected data-blocks).
*/
#include <cstdlib>
#include <cstring>
#include <optional>
#include "CLG_log.h"
#include "MEM_guardedalloc.h"
#include "DNA_brush_types.h"
#include "DNA_scene_types.h"
#include "DNA_screen_types.h"
#include "DNA_space_types.h"
#include "BLI_fileops.h"
#include "BLI_function_ref.hh"
#include "BLI_listbase.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_system.h"
#include "BLI_time.h"
#include "BLI_utildefines.h"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
#include "BLT_translation.hh"
#include "IMB_colormanagement.hh"
#include "BKE_addon.h"
#include "BKE_appdir.hh"
#include "BKE_blender.hh"
#include "BKE_blender_version.h"
#include "BKE_blendfile.hh"
#include "BKE_bpath.hh"
#include "BKE_colorband.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_idtype.hh"
#include "BKE_layer.hh"
#include "BKE_lib_id.hh"
#include "BKE_lib_override.hh"
#include "BKE_lib_query.hh"
#include "BKE_lib_remap.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BKE_main_idmap.hh"
#include "BKE_main_namemap.hh"
#include "BKE_preferences.h"
#include "BKE_report.hh"
#include "BKE_scene.hh"
#include "BKE_screen.hh"
#include "BKE_studiolight.h"
#include "BKE_undo_system.hh"
#include "BKE_workspace.hh"
#include "BLO_read_write.hh"
#include "BLO_readfile.hh"
#include "BLO_userdef_default.h"
#include "BLO_writefile.hh"
#include "RE_pipeline.h"
#ifdef WITH_PYTHON
# include "BPY_extern.hh"
#endif
using namespace blender::bke;
/* -------------------------------------------------------------------- */
/** \name Blend/Library Paths
* \{ */
bool BKE_blendfile_extension_check(const char *str)
{
const char *ext_test[4] = {".blend", ".ble", ".blend.gz", nullptr};
return BLI_path_extension_check_array(str, ext_test);
}
bool BKE_blendfile_library_path_explode(const char *path,
char *r_dir,
char **r_group,
char **r_name)
{
/* We might get some data names with slashes,
* so we have to go up in path until we find blend file itself,
* then we know next path item is group, and everything else is data name. */
char *slash = nullptr, *prev_slash = nullptr, c = '\0';
r_dir[0] = '\0';
if (r_group) {
*r_group = nullptr;
}
if (r_name) {
*r_name = nullptr;
}
/* if path leads to an existing directory, we can be sure we're not (in) a library */
if (BLI_is_dir(path)) {
return false;
}
BLI_strncpy(r_dir, path, FILE_MAX_LIBEXTRA);
while ((slash = (char *)BLI_path_slash_rfind(r_dir))) {
char tc = *slash;
*slash = '\0';
if (BKE_blendfile_extension_check(r_dir) && BLI_is_file(r_dir)) {
break;
}
if (STREQ(r_dir, BLO_EMBEDDED_STARTUP_BLEND)) {
break;
}
if (prev_slash) {
*prev_slash = c;
}
prev_slash = slash;
c = tc;
}
if (!slash) {
return false;
}
if (slash[1] != '\0') {
BLI_assert(strlen(slash + 1) < BLO_GROUP_MAX);
if (r_group) {
*r_group = slash + 1;
}
}
if (prev_slash && (prev_slash[1] != '\0')) {
BLI_assert(strlen(prev_slash + 1) < MAX_ID_NAME - 2);
if (r_name) {
*r_name = prev_slash + 1;
}
}
return true;
}
bool BKE_blendfile_is_readable(const char *path, ReportList *reports)
{
BlendFileReadReport readfile_reports;
readfile_reports.reports = reports;
BlendHandle *bh = BLO_blendhandle_from_file(path, &readfile_reports);
if (bh != nullptr) {
BLO_blendhandle_close(bh);
return true;
}
return false;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Blend File IO (High Level)
* \{ */
static bool foreach_path_clean_cb(BPathForeachPathData * /*bpath_data*/,
char *path_dst,
size_t path_dst_maxncpy,
const char *path_src)
{
BLI_strncpy(path_dst, path_src, path_dst_maxncpy);
BLI_path_slash_native(path_dst);
return !STREQ(path_dst, path_src);
}
/* make sure path names are correct for OS */
static void clean_paths(Main *bmain)
{
BPathForeachPathData foreach_path_data{};
foreach_path_data.bmain = bmain;
foreach_path_data.callback_function = foreach_path_clean_cb;
foreach_path_data.flag = BKE_BPATH_FOREACH_PATH_SKIP_MULTIFILE;
foreach_path_data.user_data = nullptr;
BKE_bpath_foreach_path_main(&foreach_path_data);
LISTBASE_FOREACH (Scene *, scene, &bmain->scenes) {
BLI_path_slash_native(scene->r.pic);
}
}
static bool wm_scene_is_visible(wmWindowManager *wm, Scene *scene)
{
LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
if (win->scene == scene) {
return true;
}
}
return false;
}
static void setup_app_userdef(BlendFileData *bfd)
{
if (bfd->user) {
/* only here free userdef themes... */
BKE_blender_userdef_data_set_and_free(bfd->user);
bfd->user = nullptr;
/* Security issue: any blend file could include a #BLO_CODE_USER block.
*
* Preferences are loaded from #BLENDER_STARTUP_FILE and later on load #BLENDER_USERPREF_FILE,
* to load the preferences defined in the users home directory.
*
* This means we will never accidentally (or maliciously)
* enable scripts auto-execution by loading a `.blend` file. */
U.flag |= USER_SCRIPT_AUTOEXEC_DISABLE;
}
}
/**
* Helper struct to manage IDs that are re-used across blend-file loading (i.e. moved from the old
* Main the new one).
*
* NOTE: this is only used when actually loading a real `.blend` file,
* loading of memfile undo steps does not need it.
*/
struct ReuseOldBMainData {
Main *new_bmain;
Main *old_bmain;
/** Data generated and used by calling WM code to handle keeping WM and UI IDs as best as
* possible across file reading.
*
* \note May be null in undo (memfile) case. */
BlendFileReadWMSetupData *wm_setup_data;
/** Storage for all remapping rules (old_id -> new_id) required by the preservation of old IDs
* into the new Main. */
id::IDRemapper *remapper;
bool is_libraries_remapped;
/** Used to find matching IDs by name/lib in new main, to remap ID usages of data ported over
* from old main. */
IDNameLib_Map *id_map;
};
/**
* Search for all libraries in `old_bmain` that are also in `new_bmain` (i.e. different Library
* IDs having the same absolute filepath), and create a remapping rule for these.
*
* NOTE: The case where the `old_bmain` would be a library in the newly read one is not handled
* here, as it does not create explicit issues. The local data from `old_bmain` is either
* discarded, or added to the `new_bmain` as local data as well. Worst case, there will be a
* double of a linked data as a local one, without any known relationships between them. In
* practice, this latter case is not expected to commonly happen.
*/
static id::IDRemapper &reuse_bmain_data_remapper_ensure(ReuseOldBMainData *reuse_data)
{
if (reuse_data->is_libraries_remapped) {
return *reuse_data->remapper;
}
if (reuse_data->remapper == nullptr) {
reuse_data->remapper = MEM_new<id::IDRemapper>(__func__);
}
Main *new_bmain = reuse_data->new_bmain;
Main *old_bmain = reuse_data->old_bmain;
id::IDRemapper &remapper = *reuse_data->remapper;
LISTBASE_FOREACH (Library *, old_lib_iter, &old_bmain->libraries) {
/* In case newly opened `new_bmain` is a library of the `old_bmain`, remap it to null, since a
* file should never ever have linked data from itself. */
if (STREQ(old_lib_iter->runtime->filepath_abs, new_bmain->filepath)) {
remapper.add(&old_lib_iter->id, nullptr);
continue;
}
/* NOTE: Although this is quadratic complexity, it is not expected to be an issue in practice:
* - Files using more than a few tens of libraries are extremely rare.
* - This code is only executed once for every file reading (not on undos).
*/
LISTBASE_FOREACH (Library *, new_lib_iter, &new_bmain->libraries) {
if (!STREQ(old_lib_iter->runtime->filepath_abs, new_lib_iter->runtime->filepath_abs)) {
continue;
}
remapper.add(&old_lib_iter->id, &new_lib_iter->id);
break;
}
}
reuse_data->is_libraries_remapped = true;
return *reuse_data->remapper;
}
static bool reuse_bmain_data_remapper_is_id_remapped(id::IDRemapper &remapper, ID *id)
{
IDRemapperApplyResult result = remapper.get_mapping_result(id, ID_REMAP_APPLY_DEFAULT, nullptr);
if (ELEM(result, ID_REMAP_RESULT_SOURCE_REMAPPED, ID_REMAP_RESULT_SOURCE_UNASSIGNED)) {
/* ID is already remapped to its matching ID in the new main, or explicitly remapped to null,
* nothing else to do here. */
return true;
}
BLI_assert_msg(result != ID_REMAP_RESULT_SOURCE_NOT_MAPPABLE,
"There should never be a non-mappable (i.e. null) input here.");
BLI_assert(result == ID_REMAP_RESULT_SOURCE_UNAVAILABLE);
return false;
}
static bool reuse_bmain_move_id(ReuseOldBMainData *reuse_data,
ID *id,
Library *lib,
const bool reuse_existing)
{
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
/* Nothing to move for embedded ID. */
if (id->flag & ID_FLAG_EMBEDDED_DATA) {
remapper.add(id, id);
return true;
}
Main *new_bmain = reuse_data->new_bmain;
Main *old_bmain = reuse_data->old_bmain;
ListBase *new_lb = which_libbase(new_bmain, GS(id->name));
ListBase *old_lb = which_libbase(old_bmain, GS(id->name));
if (reuse_existing) {
/* A 'new' version of the same data may already exist in new_bmain, in the rare case
* that the same asset blend file was linked explicitly into the blend file we are loading.
* Don't move the old linked ID, but remap its usages to the new one instead. */
LISTBASE_FOREACH_BACKWARD (ID *, id_iter, new_lb) {
if (!ELEM(id_iter->lib, id->lib, lib)) {
continue;
}
if (!STREQ(id_iter->name + 2, id->name + 2)) {
continue;
}
remapper.add(id, id_iter);
return false;
}
}
/* If ID is already in the new_bmain, this should not have been called. */
BLI_assert(BLI_findindex(new_lb, id) < 0);
BLI_assert(BLI_findindex(old_lb, id) >= 0);
/* Move from one list to another, and ensure name is valid. */
BLI_remlink_safe(old_lb, id);
/* In case the ID is linked and its library ID is re-used from the old Main, it is not possible
* to handle name_map (and ensure name uniqueness).
* This is because IDs are moved one by one from old Main's lists to new ones, while the re-used
* library's name_map would be built only from IDs in the new list, leading to incomplete/invalid
* states.
* Currently, such name uniqueness checks should not be needed, as no new name would be expected
* in the re-used library. Should this prove to be wrong at some point, the name check will have
* to happen at the end of #reuse_editable_asset_bmain_data_for_blendfile, in a separate loop
* over Main IDs.
*/
const bool handle_name_map_updates = !ID_IS_LINKED(id) || id->lib != lib;
if (handle_name_map_updates) {
BKE_main_namemap_remove_id(*old_bmain, *id);
}
id->lib = lib;
BLI_addtail(new_lb, id);
if (handle_name_map_updates) {
BKE_id_new_name_validate(
*new_bmain, *new_lb, *id, nullptr, IDNewNameMode::RenameExistingNever, true);
}
else {
id_sort_by_name(new_lb, id, nullptr);
}
BKE_lib_libblock_session_uid_renew(id);
/* Remap to itself, to avoid re-processing this ID again. */
remapper.add(id, id);
return true;
}
static Library *reuse_bmain_data_dependencies_new_library_get(ReuseOldBMainData *reuse_data,
Library *old_lib)
{
const id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
Library *new_lib = old_lib;
IDRemapperApplyResult result = remapper.apply(reinterpret_cast<ID **>(&new_lib),
ID_REMAP_APPLY_DEFAULT);
switch (result) {
case ID_REMAP_RESULT_SOURCE_UNAVAILABLE: {
/* Move library to new bmain.
* There should be no filepath conflicts, as #reuse_bmain_data_remapper_ensure has
* already remapped existing libraries with matching filepath. */
reuse_bmain_move_id(reuse_data, &old_lib->id, nullptr, false);
/* Clear the name_map of the library, as not all of its IDs are guaranteed reused. The name
* map cannot be used/kept in valid state while some IDs are moved from old to new main. See
* also #reuse_bmain_move_id code. */
BKE_main_namemap_destroy(&old_lib->runtime->name_map);
return old_lib;
}
case ID_REMAP_RESULT_SOURCE_NOT_MAPPABLE: {
BLI_assert_unreachable();
return nullptr;
}
case ID_REMAP_RESULT_SOURCE_REMAPPED: {
/* Already in new bmain, only transfer flags. */
new_lib->runtime->tag |= old_lib->runtime->tag &
(LIBRARY_ASSET_EDITABLE | LIBRARY_ASSET_FILE_WRITABLE);
return new_lib;
}
case ID_REMAP_RESULT_SOURCE_UNASSIGNED: {
/* Happens when the library is the newly opened blend file. */
return nullptr;
}
}
BLI_assert_unreachable();
return nullptr;
}
static int reuse_editable_asset_bmain_data_dependencies_process_cb(
LibraryIDLinkCallbackData *cb_data)
{
ID *id = *cb_data->id_pointer;
if (id == nullptr) {
return IDWALK_RET_NOP;
}
if (GS(id->name) == ID_LI) {
/* Libraries are handled separately. */
return IDWALK_RET_STOP_RECURSION;
}
ReuseOldBMainData *reuse_data = static_cast<ReuseOldBMainData *>(cb_data->user_data);
/* First check if it has already been remapped. */
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
if (reuse_bmain_data_remapper_is_id_remapped(remapper, id)) {
return IDWALK_RET_STOP_RECURSION;
}
if (id->lib == nullptr) {
/* There should be no links to local datablocks from linked editable data. */
remapper.add(id, nullptr);
BLI_assert_unreachable();
return IDWALK_RET_STOP_RECURSION;
}
/* Only preserve specific datablock types. */
if (!ID_TYPE_SUPPORTS_ASSET_EDITABLE(GS(id->name))) {
remapper.add(id, nullptr);
return IDWALK_RET_STOP_RECURSION;
}
/* There may be a new library pointer in new_bmain, matching a library in old_bmain, even
* though pointer values are not the same. So we need to check new linked IDs in new_bmain
* against both potential library pointers. */
Library *old_id_new_lib = reuse_bmain_data_dependencies_new_library_get(reuse_data, id->lib);
/* Happens when the library is the newly opened blend file. */
if (old_id_new_lib == nullptr) {
remapper.add(id, nullptr);
return IDWALK_RET_STOP_RECURSION;
}
/* Move to new main database. */
return reuse_bmain_move_id(reuse_data, id, old_id_new_lib, true) ? IDWALK_RET_STOP_RECURSION :
IDWALK_RET_NOP;
}
static bool reuse_editable_asset_needed(ReuseOldBMainData *reuse_data)
{
Main *old_bmain = reuse_data->old_bmain;
LISTBASE_FOREACH (Library *, lib, &old_bmain->libraries) {
if (lib->runtime->tag & LIBRARY_ASSET_EDITABLE) {
return true;
}
}
return false;
}
/**
* Selectively 'import' data from old Main into new Main, provided it does not conflict with data
* already present in the new Main (name-wise and library-wise).
*
* Dependencies from moved over old data are also imported into the new Main, (unless, in case of
* linked data, a matching linked ID is already available in new Main).
*
* When a conflict is found, usages of the conflicted ID by the old data are stored in the
* `remapper` of `ReuseOldBMainData` to be remapped to the matching data in the new Main later.
*
* NOTE: This function will never remove any original new data from the new Main, it only moves
* (some of) the old data to the new Main.
*/
static void reuse_editable_asset_bmain_data_for_blendfile(ReuseOldBMainData *reuse_data,
const short idcode)
{
Main *new_bmain = reuse_data->new_bmain;
Main *old_bmain = reuse_data->old_bmain;
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
ListBase *old_lb = which_libbase(old_bmain, idcode);
ID *old_id_iter;
FOREACH_MAIN_LISTBASE_ID_BEGIN (old_lb, old_id_iter) {
/* Keep any datablocks from libraries marked as LIBRARY_ASSET_EDITABLE. */
if (!(ID_IS_LINKED(old_id_iter) && old_id_iter->lib->runtime->tag & LIBRARY_ASSET_EDITABLE)) {
continue;
}
Library *old_id_new_lib = reuse_bmain_data_dependencies_new_library_get(reuse_data,
old_id_iter->lib);
/* Happens when the library is the newly opened blend file. */
if (old_id_new_lib == nullptr) {
remapper.add(old_id_iter, nullptr);
continue;
}
if (reuse_bmain_move_id(reuse_data, old_id_iter, old_id_new_lib, true)) {
/* Port over dependencies of re-used ID, unless matching already existing ones in
* new_bmain can be found.
*
* NOTE : No pointers are remapped here, this code only moves dependencies from old_bmain
* to new_bmain if needed, and add necessary remapping rules to the reuse_data.remapper. */
BKE_library_foreach_ID_link(new_bmain,
old_id_iter,
reuse_editable_asset_bmain_data_dependencies_process_cb,
reuse_data,
IDWALK_RECURSE | IDWALK_DO_LIBRARY_POINTER);
}
}
FOREACH_MAIN_LISTBASE_ID_END;
}
/**
* Grease pencil brushes may have a material pinned that is from the current file. Moving local
* scene data to a different #Main is tricky, so in that case, unpin the material.
*/
static void unpin_file_local_grease_pencil_brush_materials(const ReuseOldBMainData *reuse_data)
{
ID *old_id_iter;
FOREACH_MAIN_LISTBASE_ID_BEGIN (&reuse_data->old_bmain->brushes, old_id_iter) {
const Brush *brush = reinterpret_cast<Brush *>(old_id_iter);
if (brush->gpencil_settings && brush->gpencil_settings->material &&
/* Don't unpin if this material is linked, then it can be preserved for the new file. */
!ID_IS_LINKED(brush->gpencil_settings->material))
{
/* Unpin material and clear pointer. */
brush->gpencil_settings->flag &= ~GP_BRUSH_MATERIAL_PINNED;
brush->gpencil_settings->material = nullptr;
}
}
FOREACH_MAIN_LISTBASE_ID_END;
}
/**
* Does a complete replacement of data in `new_bmain` by data from `old_bmain. Original new data
* are moved to the `old_bmain`, and will be freed together with it.
*
* WARNING: Currently only expects to work on local data, won't work properly if some of the IDs of
* given type are linked.
*
* NOTE: Unlike with #reuse_editable_asset_bmain_data_for_blendfile, there is no support at all for
* potential dependencies of the IDs moved around. This is not expected to be necessary for the
* current use cases (UI-related IDs).
*/
static void swap_old_bmain_data_for_blendfile(ReuseOldBMainData *reuse_data, const short id_code)
{
Main *new_bmain = reuse_data->new_bmain;
Main *old_bmain = reuse_data->old_bmain;
ListBase *new_lb = which_libbase(new_bmain, id_code);
ListBase *old_lb = which_libbase(old_bmain, id_code);
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
/* NOTE: Full swapping is only supported for ID types that are assumed to be only local
* data-blocks (like UI-like ones). Otherwise, the swapping could fail in many funny ways. */
BLI_assert(BLI_listbase_is_empty(old_lb) || !ID_IS_LINKED(old_lb->last));
BLI_assert(BLI_listbase_is_empty(new_lb) || !ID_IS_LINKED(new_lb->last));
std::swap(*new_lb, *old_lb);
/* TODO: Could add per-IDType control over name-maps clearing, if this becomes a performances
* concern. */
BKE_main_namemap_clear(*old_bmain);
BKE_main_namemap_clear(*new_bmain);
/* Original 'new' IDs have been moved into the old listbase and will be discarded (deleted).
* Original 'old' IDs have been moved into the new listbase and are being reused (kept).
* The discarded ones need to be remapped to a matching reused one, based on their names, if
* possible.
*
* Since both lists are ordered, and they are all local, we can do a smart parallel processing of
* both lists here instead of doing complete full list searches. */
ID *discarded_id_iter = static_cast<ID *>(old_lb->first);
ID *reused_id_iter = static_cast<ID *>(new_lb->first);
while (!ELEM(nullptr, discarded_id_iter, reused_id_iter)) {
const int strcmp_result = strcmp(discarded_id_iter->name + 2, reused_id_iter->name + 2);
if (strcmp_result == 0) {
/* Matching IDs, we can remap the discarded 'new' one to the re-used 'old' one. */
remapper.add(discarded_id_iter, reused_id_iter);
discarded_id_iter = static_cast<ID *>(discarded_id_iter->next);
reused_id_iter = static_cast<ID *>(reused_id_iter->next);
}
else if (strcmp_result < 0) {
/* No matching reused 'old' ID for this discarded 'new' one. */
remapper.add(discarded_id_iter, nullptr);
discarded_id_iter = static_cast<ID *>(discarded_id_iter->next);
}
else {
reused_id_iter = static_cast<ID *>(reused_id_iter->next);
}
}
/* Also remap all remaining non-compared discarded 'new' IDs to null. */
for (; discarded_id_iter != nullptr;
discarded_id_iter = static_cast<ID *>(discarded_id_iter->next))
{
remapper.add(discarded_id_iter, nullptr);
}
FOREACH_MAIN_LISTBASE_ID_BEGIN (new_lb, reused_id_iter) {
/* Necessary as all `session_uid` are renewed on blendfile loading. */
BKE_lib_libblock_session_uid_renew(reused_id_iter);
/* Ensure that the reused ID is remapped to itself, since it is known to be in the `new_bmain`.
*/
remapper.add_overwrite(reused_id_iter, reused_id_iter);
}
FOREACH_MAIN_LISTBASE_ID_END;
}
/**
* Similar to #swap_old_bmain_data_for_blendfile, but with special handling for WM ID. Tightly
* related to further WM post-processing from calling WM code (see #WM_file_read and
* #wm_homefile_read_ex).
*/
static void swap_wm_data_for_blendfile(ReuseOldBMainData *reuse_data, const bool load_ui)
{
Main *old_bmain = reuse_data->old_bmain;
Main *new_bmain = reuse_data->new_bmain;
ListBase *old_wm_list = &old_bmain->wm;
ListBase *new_wm_list = &new_bmain->wm;
/* Currently there should never be more than one WM in a main. */
BLI_assert(BLI_listbase_count_at_most(new_wm_list, 2) <= 1);
BLI_assert(BLI_listbase_count_at_most(old_wm_list, 2) <= 1);
wmWindowManager *old_wm = static_cast<wmWindowManager *>(old_wm_list->first);
wmWindowManager *new_wm = static_cast<wmWindowManager *>(new_wm_list->first);
if (old_wm == nullptr) {
/* No current (old) WM. Either (new) WM from file is used, or if none, WM code is responsible
* to add a new default WM. Nothing to do here. */
return;
}
/* Current (old) WM, and (new) WM in file, and loading UI: use WM from file, keep old WM around
* for further processing in WM code. */
if (load_ui && new_wm != nullptr) {
/* Support window-manager ID references being held between file load operations by keeping
* #Main.wm.first memory address in-place, while swapping all of its contents.
*
* This is needed so items such as key-maps can be held by an add-on,
* without it pointing to invalid memory, see: #86431. */
BLI_remlink(old_wm_list, old_wm);
BLI_remlink(new_wm_list, new_wm);
BKE_lib_id_swap_full(nullptr,
&old_wm->id,
&new_wm->id,
true,
(ID_REMAP_SKIP_NEVER_NULL_USAGE | ID_REMAP_SKIP_UPDATE_TAGGING |
ID_REMAP_SKIP_USER_REFCOUNT | ID_REMAP_FORCE_UI_POINTERS));
/* Not strictly necessary, but helps for readability. */
std::swap<wmWindowManager *>(old_wm, new_wm);
BLI_addhead(new_wm_list, new_wm);
/* Do not add old WM back to `old_bmain`, so that it does not get freed when `old_bmain` is
* freed. Calling WM code will need this old WM to restore some windows etc. data into the
* new WM, and is responsible to free it properly. */
reuse_data->wm_setup_data->old_wm = old_wm;
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
remapper.add(&old_wm->id, &new_wm->id);
}
/* Current (old) WM, but no (new) one in file (should only happen when reading pre 2.5 files, no
* WM back then), or not loading UI: Keep current WM. */
else {
swap_old_bmain_data_for_blendfile(reuse_data, ID_WM);
old_wm->init_flag &= ~WM_INIT_FLAG_WINDOW;
reuse_data->wm_setup_data->old_wm = old_wm;
}
}
static int swap_old_bmain_data_for_blendfile_dependencies_process_cb(
LibraryIDLinkCallbackData *cb_data)
{
ID *id = *cb_data->id_pointer;
if (id == nullptr) {
return IDWALK_RET_NOP;
}
ReuseOldBMainData *reuse_data = static_cast<ReuseOldBMainData *>(cb_data->user_data);
/* First check if it has already been remapped. */
id::IDRemapper &remapper = reuse_bmain_data_remapper_ensure(reuse_data);
if (reuse_bmain_data_remapper_is_id_remapped(remapper, id)) {
return IDWALK_RET_NOP;
}
IDNameLib_Map *id_map = reuse_data->id_map;
BLI_assert(id_map != nullptr);
ID *id_new = BKE_main_idmap_lookup_id(id_map, id);
remapper.add(id, id_new);
return IDWALK_RET_NOP;
}
static void swap_old_bmain_data_dependencies_process(ReuseOldBMainData *reuse_data,
const short id_code)
{
Main *new_bmain = reuse_data->new_bmain;
ListBase *new_lb = which_libbase(new_bmain, id_code);
BLI_assert(reuse_data->id_map != nullptr);
ID *new_id_iter;
FOREACH_MAIN_LISTBASE_ID_BEGIN (new_lb, new_id_iter) {
/* Check all ID usages and find a matching new ID to remap them to in `new_bmain` if possible
* (matching by names and libraries).
*
* Note that this call does not do any effective remapping, it only adds required remapping
* operations to the remapper. */
BKE_library_foreach_ID_link(new_bmain,
new_id_iter,
swap_old_bmain_data_for_blendfile_dependencies_process_cb,
reuse_data,
IDWALK_READONLY | IDWALK_INCLUDE_UI | IDWALK_DO_LIBRARY_POINTER);
}
FOREACH_MAIN_LISTBASE_ID_END;
}
static int reuse_bmain_data_invalid_local_usages_fix_cb(LibraryIDLinkCallbackData *cb_data)
{
ID *id = *cb_data->id_pointer;
if (id == nullptr) {
return IDWALK_RET_NOP;
}
/* Embedded data cannot (yet) be fully trusted to have the same lib pointer as their owner ID, so
* for now ignore them. This code should never have anything to fix for them anyway, otherwise
* there is something extremely wrong going on. */
if ((cb_data->cb_flag & (IDWALK_CB_EMBEDDED | IDWALK_CB_EMBEDDED_NOT_OWNING)) != 0) {
return IDWALK_RET_NOP;
}
if (!ID_IS_LINKED(id)) {
ID *owner_id = cb_data->owner_id;
/* Do not allow linked data to use local data. */
if (ID_IS_LINKED(owner_id)) {
if (cb_data->cb_flag & IDWALK_CB_USER) {
id_us_min(id);
}
*cb_data->id_pointer = nullptr;
}
/* Do not allow local liboverride data to use local data as reference. */
else if (ID_IS_OVERRIDE_LIBRARY_REAL(owner_id) &&
&owner_id->override_library->reference == cb_data->id_pointer)
{
if (cb_data->cb_flag & IDWALK_CB_USER) {
id_us_min(id);
}
*cb_data->id_pointer = nullptr;
}
}
return IDWALK_RET_NOP;
}
/**
* Detect and fix invalid usages of locale IDs by linked ones (or as reference of liboverrides).
*/
static void reuse_bmain_data_invalid_local_usages_fix(ReuseOldBMainData *reuse_data)
{
Main *new_bmain = reuse_data->new_bmain;
ID *id_iter;
FOREACH_MAIN_ID_BEGIN (new_bmain, id_iter) {
if (!ID_IS_LINKED(id_iter) && !ID_IS_OVERRIDE_LIBRARY_REAL(id_iter)) {
continue;
}
ID *liboverride_reference = ID_IS_OVERRIDE_LIBRARY_REAL(id_iter) ?
id_iter->override_library->reference :
nullptr;
BKE_library_foreach_ID_link(
new_bmain, id_iter, reuse_bmain_data_invalid_local_usages_fix_cb, reuse_data, IDWALK_NOP);
/* Liboverrides who lost their reference should not be liboverrides anymore, but regular IDs.
*/
if (ID_IS_OVERRIDE_LIBRARY_REAL(id_iter) &&
id_iter->override_library->reference != liboverride_reference)
{
BKE_lib_override_library_free(&id_iter->override_library, true);
}
}
FOREACH_MAIN_ID_END;
}
/* Post-remapping helpers to ensure validity of the UI data. */
static void view3d_data_consistency_ensure(wmWindow *win, Scene *scene, ViewLayer *view_layer)
{
bScreen *screen = BKE_workspace_active_screen_get(win->workspace_hook);
LISTBASE_FOREACH (ScrArea *, area, &screen->areabase) {
LISTBASE_FOREACH (SpaceLink *, sl, &area->spacedata) {
if (sl->spacetype != SPACE_VIEW3D) {
continue;
}
View3D *v3d = reinterpret_cast<View3D *>(sl);
if (v3d->camera == nullptr || v3d->scenelock) {
v3d->camera = scene->camera;
}
if (v3d->localvd == nullptr) {
continue;
}
if (v3d->localvd->camera == nullptr || v3d->scenelock) {
v3d->localvd->camera = v3d->camera;
}
/* Local-view can become invalid during undo/redo steps, exit it when no valid object could
* be found. */
Base *base;
for (base = static_cast<Base *>(view_layer->object_bases.first); base; base = base->next) {
if (base->local_view_bits & v3d->local_view_uid) {
break;
}
}
if (base != nullptr) {
/* The local view3D still has a valid object, nothing else to do. */
continue;
}
/* No valid object found for the local view3D, it has to be cleared off. */
MEM_freeN(v3d->localvd);
v3d->localvd = nullptr;
v3d->local_view_uid = 0;
/* Region-base storage is different depending on whether the space is active or not. */
ListBase *regionbase = (sl == area->spacedata.first) ? &area->regionbase : &sl->regionbase;
LISTBASE_FOREACH (ARegion *, region, regionbase) {
if (region->regiontype != RGN_TYPE_WINDOW) {
continue;
}
RegionView3D *rv3d = static_cast<RegionView3D *>(region->regiondata);
MEM_SAFE_FREE(rv3d->localvd);
}
}
}
}
static void wm_data_consistency_ensure(wmWindowManager *curwm,
Scene *cur_scene,
ViewLayer *cur_view_layer)
{
/* There may not be any available WM (e.g. when reading `userpref.blend`). */
if (curwm == nullptr) {
return;
}
LISTBASE_FOREACH (wmWindow *, win, &curwm->windows) {
if (win->scene == nullptr) {
win->scene = cur_scene;
}
if (BKE_view_layer_find(win->scene, win->view_layer_name) == nullptr) {
STRNCPY(win->view_layer_name, cur_view_layer->name);
}
view3d_data_consistency_ensure(win, win->scene, cur_view_layer);
}
}
/**
* This function runs after loading blend-file data and is responsible
* for setting up the context (the active view-layer, scene & work-space).
*
* When loading a blend-file without it's UI (#G_FILE_NO_UI),
* the existing screen-data needs to point to the newly loaded blend-file data.
*
* \note this is called on undo so any slow conversion functions here
* should be avoided or check `mode != LOAD_UNDO`.
*
* \param bfd: Blend file data, freed by this function on exit.
*/
static void setup_app_data(bContext *C,
BlendFileData *bfd,
const BlendFileReadParams *params,
BlendFileReadWMSetupData *wm_setup_data,
BlendFileReadReport *reports)
{
Main *bmain = G_MAIN;
const bool recover = (G.fileflags & G_FILE_RECOVER_READ) != 0;
enum {
LOAD_UI = 1,
LOAD_UI_OFF,
LOAD_UNDO,
} mode;
if (params->undo_direction != STEP_INVALID) {
BLI_assert(bfd->curscene != nullptr);
mode = LOAD_UNDO;
}
else if (bfd->fileflags & G_FILE_ASSET_EDIT_FILE) {
BKE_report(reports->reports,
RPT_WARNING,
"This file is managed by the asset system, you cannot overwrite it (using \"Save "
"As\" is possible)");
/* From now on the file in memory is a normal file, further saving it will contain a
* window-manager, scene, ... and potentially user created data. Use #Main.is_asset_edit_file
* to detect if saving this file needs extra protections. */
bfd->fileflags &= ~G_FILE_ASSET_EDIT_FILE;
BLI_assert(bfd->main->is_asset_edit_file);
mode = LOAD_UI_OFF;
}
/* May happen with library files, loading undo-data should never have a null `curscene`
* (but may have a null `curscreen`). */
else if (ELEM(nullptr, bfd->curscreen, bfd->curscene)) {
BKE_report(reports->reports, RPT_WARNING, "Library file, loading empty scene");
mode = LOAD_UI_OFF;
}
else if (G.fileflags & G_FILE_NO_UI) {
mode = LOAD_UI_OFF;
}
else {
mode = LOAD_UI;
}
/* Free all render results and interactive compositor renders, without this stale data gets
* displayed after loading files */
if (mode != LOAD_UNDO) {
RE_FreeAllRenderResults();
RE_FreeInteractiveCompositorRenders();
}
/* Only make file-paths compatible when loading for real (not undo). */
if (mode != LOAD_UNDO) {
clean_paths(bfd->main);
}
BLI_assert(BKE_main_namemap_validate(*bfd->main));
/* Temporary data to handle swapping around IDs between old and new mains,
* and accumulate the required remapping accordingly. */
ReuseOldBMainData reuse_data = {nullptr};
reuse_data.new_bmain = bfd->main;
reuse_data.old_bmain = bmain;
reuse_data.wm_setup_data = wm_setup_data;
if (mode != LOAD_UNDO) {
const short ui_id_codes[]{ID_WS, ID_SCR};
/* WM needs special complex handling, regardless of whether UI is kept or loaded from file. */
swap_wm_data_for_blendfile(&reuse_data, mode == LOAD_UI);
if (mode != LOAD_UI) {
/* Re-use UI data from `old_bmain` if keeping existing UI. */
for (auto id_code : ui_id_codes) {
swap_old_bmain_data_for_blendfile(&reuse_data, id_code);
}
}
/* Needs to happen after all data from `old_bmain` has been moved into new one. */
BLI_assert(reuse_data.id_map == nullptr);
reuse_data.id_map = BKE_main_idmap_create(
reuse_data.new_bmain, true, reuse_data.old_bmain, MAIN_IDMAP_TYPE_NAME);
swap_old_bmain_data_dependencies_process(&reuse_data, ID_WM);
if (mode != LOAD_UI) {
for (auto id_code : ui_id_codes) {
swap_old_bmain_data_dependencies_process(&reuse_data, id_code);
}
}
BKE_main_idmap_destroy(reuse_data.id_map);
if (!params->is_factory_settings && reuse_editable_asset_needed(&reuse_data)) {
unpin_file_local_grease_pencil_brush_materials(&reuse_data);
/* Keep linked brush asset data, similar to UI data. Only does a known
* subset know. Could do everything, but that risks dragging along more
* scene data than we want. */
for (short idtype_index = 0; idtype_index < INDEX_ID_MAX; idtype_index++) {
const IDTypeInfo *idtype_info = BKE_idtype_get_info_from_idtype_index(idtype_index);
if (ID_TYPE_SUPPORTS_ASSET_EDITABLE(idtype_info->id_code)) {
reuse_editable_asset_bmain_data_for_blendfile(&reuse_data, idtype_info->id_code);
}
}
}
if (mode != LOAD_UI) {
LISTBASE_FOREACH (bScreen *, screen, &bfd->main->screens) {
BKE_screen_runtime_refresh_for_blendfile(screen);
}
}
}
/* Logic for 'track_undo_scene' is to keep using the scene which the active screen has, as long
* as the scene associated with the undo operation is visible in one of the open windows.
*
* - 'curscreen->scene': Scene the user is currently looking at.
* - 'bfd->curscene': Scene undo-step was created in.
*
* This means that users can have 2 or more windows open and undo in both without screens
* switching. But if they close one of the screens, undo will ensure that the scene being
* operated on will be activated (otherwise we'd be undoing on an off-screen scene which isn't
* acceptable). See: #43424. */
bool track_undo_scene = false;
/* Always use the Scene and ViewLayer pointers from new file, if possible. */
ViewLayer *cur_view_layer = bfd->cur_view_layer;
Scene *curscene = bfd->curscene;
wmWindow *win = nullptr;
bScreen *curscreen = nullptr;
/* Ensure that there is a valid scene and view-layer. */
if (curscene == nullptr) {
curscene = static_cast<Scene *>(bfd->main->scenes.first);
}
/* Empty file, add a scene to make Blender work. */
if (curscene == nullptr) {
curscene = BKE_scene_add(bfd->main, "Empty");
}
if (cur_view_layer == nullptr) {
/* Fallback to the active scene view layer. */
cur_view_layer = BKE_view_layer_default_view(curscene);
}
/* If UI is not loaded when opening actual `.blend` file,
* and always in case of undo MEMFILE reading. */
if (mode != LOAD_UI) {
/* Re-use current window and screen. */
win = CTX_wm_window(C);
curscreen = CTX_wm_screen(C);
track_undo_scene = (mode == LOAD_UNDO && curscreen && curscene && bfd->main->wm.first);
if (track_undo_scene) {
/* Keep the old (to-be-freed) scene, remapping below will ensure it's remapped to the
* matching new scene if available, or null otherwise, in which case
* #wm_data_consistency_ensure will define `curscene` as the active one. */
}
/* Enforce `curscene` to be in current screen. */
else if (win) { /* The window may be null in background-mode. */
win->scene = curscene;
}
}
BLI_assert(BKE_main_namemap_validate(*bfd->main));
/* Apply remapping of ID pointers caused by re-using part of the data from the 'old' main into
* the new one. */
if (reuse_data.remapper != nullptr) {
/* In undo case all "keeping old data" and remapping logic is now handled
* in file reading code itself, so there should never be any remapping to do here. */
BLI_assert(mode != LOAD_UNDO);
/* Handle all pending remapping from swapping old and new IDs around. */
BKE_libblock_remap_multiple_raw(bfd->main,
*reuse_data.remapper,
(ID_REMAP_FORCE_UI_POINTERS | ID_REMAP_SKIP_USER_REFCOUNT |
ID_REMAP_SKIP_UPDATE_TAGGING | ID_REMAP_SKIP_USER_CLEAR));
/* Fix potential invalid usages of now-locale-data created by remapping above. Should never
* be needed in undo case, this is to address cases like:
* "opening a blend-file that was a library of the previous opened blend-file". */
reuse_bmain_data_invalid_local_usages_fix(&reuse_data);
MEM_delete(reuse_data.remapper);
reuse_data.remapper = nullptr;
wm_data_consistency_ensure(CTX_wm_manager(C), curscene, cur_view_layer);
}
if (mode == LOAD_UNDO) {
/* It's possible to undo into a time before the scene existed, in this case the window's scene
* will be null. Since it doesn't make sense to remove the window, set it to the current scene.
*
* NOTE: Redo will restore the active scene to the window so a reasonably consistent state
* is maintained. We could do better by keeping a window/scene map for each undo step.
*
* Another source of potential inconsistency is undoing into a step where the active camera
* object does not exist (see e.g. #125636).
*/
wm_data_consistency_ensure(CTX_wm_manager(C), curscene, cur_view_layer);
}
BLI_assert(BKE_main_namemap_validate(*bfd->main));
if (mode != LOAD_UI) {
if (win) {
curscene = win->scene;
}
if (track_undo_scene) {
wmWindowManager *wm = static_cast<wmWindowManager *>(bfd->main->wm.first);
if (!wm_scene_is_visible(wm, bfd->curscene)) {
curscene = bfd->curscene;
if (win) {
win->scene = curscene;
}
BKE_screen_view3d_scene_sync(curscreen, curscene);
}
}
/* We need to tag this here because events may be handled immediately after.
* only the current screen is important because we won't have to handle
* events from multiple screens at once. */
if (curscreen) {
BKE_screen_gizmo_tag_refresh(curscreen);
}
}
CTX_data_scene_set(C, curscene);
BLI_assert(BKE_main_namemap_validate(*bfd->main));
/* This frees the `old_bmain`. */
BKE_blender_globals_main_replace(bfd->main);
bmain = G_MAIN;
bfd->main = nullptr;
CTX_data_main_set(C, bmain);
BLI_assert(BKE_main_namemap_validate(*bmain));
/* These context data should remain valid if old UI is being re-used. */
if (mode == LOAD_UI) {
/* Setting a window-manger clears all other windowing members (window, screen, area, etc).
* So only do it when effectively loading a new #wmWindowManager
* otherwise just assert that the WM from context is still the same as in `new_bmain`. */
CTX_wm_manager_set(C, static_cast<wmWindowManager *>(bmain->wm.first));
CTX_wm_screen_set(C, bfd->curscreen);
CTX_wm_area_set(C, nullptr);
CTX_wm_region_set(C, nullptr);
CTX_wm_region_popup_set(C, nullptr);
}
BLI_assert(CTX_wm_manager(C) == static_cast<wmWindowManager *>(bmain->wm.first));
/* Keep state from preferences. */
const int fileflags_keep = G_FILE_FLAG_ALL_RUNTIME;
G.fileflags = (G.fileflags & fileflags_keep) | (bfd->fileflags & ~fileflags_keep);
/* Special cases, override any #G_FLAG_ALL_READFILE flags from the blend-file. */
if (G.f != bfd->globalf) {
const int flags_keep = G_FLAG_ALL_RUNTIME;
bfd->globalf &= G_FLAG_ALL_READFILE;
bfd->globalf = (bfd->globalf & ~flags_keep) | (G.f & flags_keep);
}
G.f = bfd->globalf;
#ifdef WITH_PYTHON
/* let python know about new main */
if (CTX_py_init_get(C)) {
BPY_context_update(C);
}
#endif
if (mode != LOAD_UNDO) {
/* Perform complex versioning that involves adding or removing IDs,
* and/or needs to operate over the whole Main data-base
* (versioning done in file reading code only operates on a per-library basis). */
BLO_read_do_version_after_setup(bmain, nullptr, reports);
BLO_readfile_id_runtime_data_free_all(*bmain);
}
bmain->recovered = false;
/* `startup.blend` or recovered startup. */
if (params->is_startup) {
bmain->filepath[0] = '\0';
}
else if (recover) {
/* In case of auto-save or `quit.blend`, use original file-path instead
* (see also #read_global in `readfile.cc`). */
bmain->recovered = true;
STRNCPY(bmain->filepath, bfd->filepath);
}
/* Set the loaded .blend file path for crash recovery. */
STRNCPY(G.filepath_last_blend, bmain->filepath);
/* Base-flags, groups, make depsgraph, etc. */
/* first handle case if other windows have different scenes visible. */
if (mode == LOAD_UI) {
wmWindowManager *wm = static_cast<wmWindowManager *>(bmain->wm.first);
if (wm) {
LISTBASE_FOREACH (wmWindow *, win, &wm->windows) {
if (win->scene && win->scene != curscene) {
BKE_scene_set_background(bmain, win->scene);
}
}
}
}
/* Setting scene might require having a dependency graph, with copy-on-eval
* we need to make sure we ensure scene has correct color management before
* constructing dependency graph. */
if (mode != LOAD_UNDO) {
IMB_colormanagement_check_file_config(bmain);
}
BKE_scene_set_background(bmain, curscene);
if (mode != LOAD_UNDO) {
/* TODO(@sergey): Can this be also move above? */
RE_FreeAllPersistentData();
}
/* Both undo and regular file loading can perform some fairly complex ID manipulation, simpler
* and safer to fully redo reference-counting. This is a relatively cheap process anyway. */
BKE_main_id_refcount_recompute(bmain, false);
BLI_assert(BKE_main_namemap_validate(*bmain));
if (mode != LOAD_UNDO && liboverride::is_auto_resync_enabled()) {
reports->duration.lib_overrides_resync = BLI_time_now_seconds();
BKE_lib_override_library_main_resync(
bmain,
curscene,
bfd->cur_view_layer ? bfd->cur_view_layer : BKE_view_layer_default_view(curscene),
reports);
reports->duration.lib_overrides_resync = BLI_time_now_seconds() -
reports->duration.lib_overrides_resync;
/* We need to rebuild some of the deleted override rules (for UI feedback purpose). */
BKE_lib_override_library_main_operations_create(bmain, true, nullptr);
}
/* Now that liboverrides have been resynced and 'irrelevant' missing linked IDs has been removed,
* report actual missing linked data. */
if (mode != LOAD_UNDO) {
ID *id_iter;
int missing_linked_ids_num = 0;
FOREACH_MAIN_ID_BEGIN (bmain, id_iter) {
if (ID_IS_LINKED(id_iter) && (id_iter->tag & ID_TAG_MISSING)) {
missing_linked_ids_num++;
BLO_reportf_wrap(reports,
RPT_INFO,
RPT_("LIB: %s: '%s' missing from '%s', parent '%s'"),
BKE_idtype_idcode_to_name(GS(id_iter->name)),
id_iter->name + 2,
id_iter->lib->runtime->filepath_abs,
id_iter->lib->runtime->parent ?
id_iter->lib->runtime->parent->runtime->filepath_abs :
"<direct>");
}
}
FOREACH_MAIN_ID_END;
reports->count.missing_linked_id = missing_linked_ids_num;
}
}
static void setup_app_blend_file_data(bContext *C,
BlendFileData *bfd,
const BlendFileReadParams *params,
BlendFileReadWMSetupData *wm_setup_data,
BlendFileReadReport *reports)
{
if ((params->skip_flags & BLO_READ_SKIP_USERDEF) == 0) {
setup_app_userdef(bfd);
}
if ((params->skip_flags & BLO_READ_SKIP_DATA) == 0) {
setup_app_data(C, bfd, params, wm_setup_data, reports);
}
}
static void handle_subversion_warning(Main *main, BlendFileReadReport *reports)
{
if (main->versionfile > BLENDER_FILE_VERSION || (main->versionfile == BLENDER_FILE_VERSION &&
main->subversionfile > BLENDER_FILE_SUBVERSION))
{
BKE_reportf(reports->reports,
RPT_WARNING,
"File written by newer Blender binary (%d.%d), expect loss of data!",
main->versionfile,
main->subversionfile);
}
}
void BKE_blendfile_read_setup_readfile(bContext *C,
BlendFileData *bfd,
const BlendFileReadParams *params,
BlendFileReadWMSetupData *wm_setup_data,
BlendFileReadReport *reports,
/* Extra args. */
const bool startup_update_defaults,
const char *startup_app_template)
{
if (bfd->main->is_read_invalid) {
BKE_reports_prepend(reports->reports,
"File could not be read, critical data corruption detected");
BLO_blendfiledata_free(bfd);
return;
}
if (startup_update_defaults) {
if ((params->skip_flags & BLO_READ_SKIP_DATA) == 0) {
BLO_update_defaults_startup_blend(bfd->main, startup_app_template);
}
}
setup_app_blend_file_data(C, bfd, params, wm_setup_data, reports);
BLO_blendfiledata_free(bfd);
}
void BKE_blendfile_read_setup_undo(bContext *C,
BlendFileData *bfd,
const BlendFileReadParams *params,
BlendFileReadReport *reports)
{
BKE_blendfile_read_setup_readfile(C, bfd, params, nullptr, reports, false, nullptr);
}
BlendFileData *BKE_blendfile_read(const char *filepath,
const BlendFileReadParams *params,
BlendFileReadReport *reports)
{
/* Don't print startup file loading. */
if (params->is_startup == false) {
if (!G.quiet) {
printf("Read blend: \"%s\"\n", filepath);
}
}
BlendFileData *bfd = BLO_read_from_file(filepath, eBLOReadSkip(params->skip_flags), reports);
if (bfd && bfd->main->is_read_invalid) {
BLO_blendfiledata_free(bfd);
bfd = nullptr;
}
if (bfd) {
handle_subversion_warning(bfd->main, reports);
}
else {
BKE_reports_prependf(reports->reports, "Loading \"%s\" failed: ", filepath);
}
return bfd;
}
BlendFileData *BKE_blendfile_read_from_memory(const void *file_buf,
int file_buf_size,
const BlendFileReadParams *params,
ReportList *reports)
{
BlendFileData *bfd = BLO_read_from_memory(
file_buf, file_buf_size, eBLOReadSkip(params->skip_flags), reports);
if (bfd && bfd->main->is_read_invalid) {
BLO_blendfiledata_free(bfd);
bfd = nullptr;
}
if (bfd) {
/* Pass. */
}
else {
BKE_reports_prepend(reports, "Loading failed: ");
}
return bfd;
}
BlendFileData *BKE_blendfile_read_from_memfile(Main *bmain,
MemFile *memfile,
const BlendFileReadParams *params,
ReportList *reports)
{
BlendFileData *bfd = BLO_read_from_memfile(
bmain, BKE_main_blendfile_path(bmain), memfile, params, reports);
if (bfd && bfd->main->is_read_invalid) {
BLO_blendfiledata_free(bfd);
bfd = nullptr;
}
if (bfd == nullptr) {
BKE_reports_prepend(reports, "Loading failed: ");
}
return bfd;
}
void BKE_blendfile_read_make_empty(bContext *C)
{
Main *bmain = CTX_data_main(C);
ListBase *lb;
ID *id;
FOREACH_MAIN_LISTBASE_BEGIN (bmain, lb) {
FOREACH_MAIN_LISTBASE_ID_BEGIN (lb, id) {
if (ELEM(GS(id->name), ID_SCE, ID_SCR, ID_WM, ID_WS)) {
break;
}
BKE_id_delete(bmain, id);
}
FOREACH_MAIN_LISTBASE_ID_END;
}
FOREACH_MAIN_LISTBASE_END;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Blend File IO (Preferences)
*
* Application Templates
* =====================
*
* When using app-templates, both regular & app-template preferences are used.
* Note that "regular" preferences refers to the preferences used with no app-template is active.
*
* - Reading preferences is performed for both the app-template & regular preferences.
*
* The preferences are merged by using some from the app-template and other settings from the
* regular preferences (add-ons from the app-template for example are used),
* undo-memory uses the regular preferences (for e.g.).
*
* - Writing preferences is performed for both the app-template & regular preferences.
*
* Writing unmodified preference (#U) into the regular preferences
* would loose any settings the app-template overrides.
* To keep default settings the regular preferences is read, add-ons etc temporarily swapped
* into #U for writing, then swapped back out so as not to change the run-time preferences.
*
* \note The function #BKE_blender_userdef_app_template_data_swap determines which settings
* the app-template overrides.
* \{ */
UserDef *BKE_blendfile_userdef_read(const char *filepath, ReportList *reports)
{
BlendFileData *bfd;
UserDef *userdef = nullptr;
BlendFileReadReport blend_file_read_reports{};
blend_file_read_reports.reports = reports;
bfd = BLO_read_from_file(
filepath, BLO_READ_SKIP_ALL & ~BLO_READ_SKIP_USERDEF, &blend_file_read_reports);
if (bfd) {
if (bfd->user) {
userdef = bfd->user;
}
BKE_main_free(bfd->main);
MEM_delete(bfd);
}
return userdef;
}
UserDef *BKE_blendfile_userdef_read_from_memory(const void *file_buf,
int file_buf_size,
ReportList *reports)
{
BlendFileData *bfd;
UserDef *userdef = nullptr;
bfd = BLO_read_from_memory(
file_buf, file_buf_size, BLO_READ_SKIP_ALL & ~BLO_READ_SKIP_USERDEF, reports);
if (bfd) {
if (bfd->user) {
userdef = bfd->user;
}
BKE_main_free(bfd->main);
MEM_delete(bfd);
}
else {
BKE_reports_prepend(reports, "Loading failed: ");
}
return userdef;
}
UserDef *BKE_blendfile_userdef_from_defaults()
{
UserDef *userdef = MEM_callocN<UserDef>(__func__);
*userdef = blender::dna::shallow_copy(U_default);
/* Add-ons. */
{
const char *addons[] = {
"io_anim_bvh",
"io_curve_svg",
"io_mesh_uv_layout",
"io_scene_fbx",
"io_scene_gltf2",
"cycles",
"pose_library",
"bl_pkg",
};
for (int i = 0; i < ARRAY_SIZE(addons); i++) {
bAddon *addon = BKE_addon_new();
STRNCPY(addon->module, addons[i]);
BLI_addtail(&userdef->addons, addon);
}
}
/* Theme. */
{
bTheme *btheme = MEM_mallocN<bTheme>(__func__);
memcpy(btheme, &U_theme_default, sizeof(*btheme));
BLI_addtail(&userdef->themes, btheme);
}
#ifdef WITH_PYTHON_SECURITY
/* use alternative setting for security nuts
* otherwise we'd need to patch the binary blob - startup.blend.c */
userdef->flag |= USER_SCRIPT_AUTOEXEC_DISABLE;
#else
userdef->flag &= ~USER_SCRIPT_AUTOEXEC_DISABLE;
#endif
/* System-specific fonts directory.
* NOTE: when not found, leaves as-is (`//` for the blend-file directory). */
if (BKE_appdir_font_folder_default(userdef->fontdir, sizeof(userdef->fontdir))) {
/* Not actually needed, just a convention that directory selection
* adds a trailing slash. */
BLI_path_slash_ensure(userdef->fontdir, sizeof(userdef->fontdir));
}
userdef->memcachelimit = min_ii(BLI_system_memory_max_in_megabytes_int() / 2,
userdef->memcachelimit);
/* Init weight paint range. */
BKE_colorband_init(&userdef->coba_weight, true);
/* Default studio light. */
BKE_studiolight_default(userdef->light_param, userdef->light_ambient);
BKE_preferences_asset_library_default_add(userdef);
BKE_preferences_extension_repo_add_defaults_all(userdef);
{
BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
userdef, "VIEW3D_AST_brush_sculpt", "Brushes/Mesh Sculpt/General");
BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
userdef, "VIEW3D_AST_brush_sculpt", "Brushes/Mesh Sculpt/Paint");
BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
userdef, "VIEW3D_AST_brush_sculpt", "Brushes/Mesh Sculpt/Simulation");
BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
userdef, "VIEW3D_AST_brush_gpencil_paint", "Brushes/Grease Pencil Draw/Draw");
BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
userdef, "VIEW3D_AST_brush_gpencil_paint", "Brushes/Grease Pencil Draw/Erase");
BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
userdef, "VIEW3D_AST_brush_gpencil_paint", "Brushes/Grease Pencil Draw/Utilities");
BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
userdef, "VIEW3D_AST_brush_gpencil_sculpt", "Brushes/Grease Pencil Sculpt/Contrast");
BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
userdef, "VIEW3D_AST_brush_gpencil_sculpt", "Brushes/Grease Pencil Sculpt/Transform");
BKE_preferences_asset_shelf_settings_ensure_catalog_path_enabled(
userdef, "VIEW3D_AST_brush_gpencil_sculpt", "Brushes/Grease Pencil Sculpt/Utilities");
}
return userdef;
}
bool BKE_blendfile_userdef_write(const char *filepath, ReportList *reports)
{
Main *mainb = MEM_callocN<Main>("empty main");
bool ok = false;
BlendFileWriteParams params{};
params.use_userdef = true;
if (BLO_write_file(mainb, filepath, 0, &params, reports)) {
ok = true;
}
MEM_freeN(mainb);
return ok;
}
bool BKE_blendfile_userdef_write_app_template(const char *filepath, ReportList *reports)
{
/* Checking that `filepath` exists is not essential, it just avoids printing a warning that
* the file can't be found. In this case it's not an error - as the file is used if it exists,
* falling back to the defaults.
* If the preferences exists but file reading fails - the file can be assumed corrupt
* so overwriting the file is OK. */
UserDef *userdef_default = BLI_exists(filepath) ? BKE_blendfile_userdef_read(filepath, nullptr) :
nullptr;
if (userdef_default == nullptr) {
userdef_default = BKE_blendfile_userdef_from_defaults();
}
BKE_blender_userdef_app_template_data_swap(&U, userdef_default);
bool ok = BKE_blendfile_userdef_write(filepath, reports);
BKE_blender_userdef_app_template_data_swap(&U, userdef_default);
BKE_blender_userdef_data_free(userdef_default, false);
MEM_freeN(userdef_default);
return ok;
}
bool BKE_blendfile_userdef_write_all(ReportList *reports)
{
char filepath[FILE_MAX];
bool ok = true;
const bool use_template_userpref = BKE_appdir_app_template_has_userpref(U.app_template);
std::optional<std::string> cfgdir = BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, nullptr);
if (cfgdir) {
bool ok_write;
BLI_path_join(filepath, sizeof(filepath), cfgdir->c_str(), BLENDER_USERPREF_FILE);
if (!G.quiet) {
printf("Writing userprefs: \"%s\" ", filepath);
}
if (use_template_userpref) {
ok_write = BKE_blendfile_userdef_write_app_template(filepath, reports);
}
else {
ok_write = BKE_blendfile_userdef_write(filepath, reports);
}
if (ok_write) {
if (!G.quiet) {
printf("ok\n");
}
BKE_report(reports, RPT_INFO, "Preferences saved");
}
else {
if (!G.quiet) {
printf("fail\n");
}
ok = false;
BKE_report(reports, RPT_ERROR, "Saving preferences failed");
}
}
else {
BKE_report(reports, RPT_ERROR, "Unable to create userpref path");
}
if (use_template_userpref) {
cfgdir = BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, U.app_template);
if (cfgdir) {
/* Also save app-template preferences. */
BLI_path_join(filepath, sizeof(filepath), cfgdir->c_str(), BLENDER_USERPREF_FILE);
if (!G.quiet) {
printf("Writing userprefs app-template: \"%s\" ", filepath);
}
if (BKE_blendfile_userdef_write(filepath, reports) != 0) {
if (!G.quiet) {
printf("ok\n");
}
}
else {
if (!G.quiet) {
printf("fail\n");
}
ok = false;
}
}
else {
BKE_report(reports, RPT_ERROR, "Unable to create app-template userpref path");
ok = false;
}
}
if (ok) {
U.runtime.is_dirty = false;
}
return ok;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Blend File IO (WorkSpace)
* \{ */
WorkspaceConfigFileData *BKE_blendfile_workspace_config_read(const char *filepath,
const void *file_buf,
int file_buf_size,
ReportList *reports)
{
BlendFileData *bfd;
WorkspaceConfigFileData *workspace_config = nullptr;
if (filepath) {
BlendFileReadReport blend_file_read_reports{};
blend_file_read_reports.reports = reports;
bfd = BLO_read_from_file(filepath, BLO_READ_SKIP_USERDEF, &blend_file_read_reports);
}
else {
bfd = BLO_read_from_memory(file_buf, file_buf_size, BLO_READ_SKIP_USERDEF, reports);
}
if (bfd) {
workspace_config = MEM_callocN<WorkspaceConfigFileData>(__func__);
workspace_config->main = bfd->main;
/* Only 2.80+ files have actual workspaces, don't try to use screens
* from older versions. */
if (bfd->main->versionfile >= 280) {
workspace_config->workspaces = bfd->main->workspaces;
}
MEM_delete(bfd);
}
return workspace_config;
}
void BKE_blendfile_workspace_config_data_free(WorkspaceConfigFileData *workspace_config)
{
BKE_main_free(workspace_config->main);
MEM_freeN(workspace_config);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Blend File Write (Partial)
* \{ */
static CLG_LogRef LOG_PARTIALWRITE = {"bke.blendfile.partial_write"};
namespace blender::bke::blendfile {
PartialWriteContext::PartialWriteContext(StringRefNull reference_root_filepath)
: reference_root_filepath_(reference_root_filepath)
{
BKE_main_init(this->bmain);
if (!reference_root_filepath_.empty()) {
STRNCPY(this->bmain.filepath, reference_root_filepath_.c_str());
}
/* Only for IDs matching existing data in current G_MAIN. */
matching_uid_map_ = BKE_main_idmap_create(&this->bmain, false, nullptr, MAIN_IDMAP_TYPE_UID);
/* For all IDs existing in the context. */
this->bmain.id_map = BKE_main_idmap_create(
&this->bmain, false, nullptr, MAIN_IDMAP_TYPE_UID | MAIN_IDMAP_TYPE_NAME);
};
PartialWriteContext::~PartialWriteContext()
{
BKE_main_idmap_destroy(matching_uid_map_);
BLI_assert(this->bmain.next == nullptr);
BKE_main_destroy(this->bmain);
};
void PartialWriteContext::preempt_session_uid(ID *ctx_id, uint session_uid)
{
/* If there is already an existing ID in the 'matching' set with that UID, it should be the same
* as the given ctx_id. */
ID *matching_ctx_id = BKE_main_idmap_lookup_uid(matching_uid_map_, session_uid);
if (matching_ctx_id == ctx_id) {
/* That ID has already been added to the context, nothing to do. */
BLI_assert(matching_ctx_id->session_uid == session_uid);
return;
}
if (matching_ctx_id != nullptr) {
/* Another ID in the context, who has a matching ID in current G_MAIN, is sharing the same
* session UID. This marks a critical corruption somewhere! */
CLOG_FATAL(
&LOG_PARTIALWRITE,
"Different matching IDs sharing the same session UID in the partial write context.");
return;
}
/* No ID with this session UID in the context, who's matching a current ID in G_MAIN. Check if a
* non-matching context ID is already using that UID, if yes, regenerate a new one for it, such
* that given `ctx_id` can use the desired UID. */
/* NOTE: In theory, there should never be any session uid collision currently, since these are
* generated session-wide, regardless of the type/source of the IDs. */
matching_ctx_id = BKE_main_idmap_lookup_uid(this->bmain.id_map, session_uid);
BLI_assert(matching_ctx_id != ctx_id);
if (matching_ctx_id) {
CLOG_INFO(&LOG_PARTIALWRITE,
3,
"Non-matching IDs sharing the same session UID in the partial write context.");
BKE_main_idmap_remove_id(this->bmain.id_map, matching_ctx_id);
/* FIXME: Allow #BKE_lib_libblock_session_uid_renew to work with temp IDs? */
matching_ctx_id->tag &= ~ID_TAG_TEMP_MAIN;
BKE_lib_libblock_session_uid_renew(matching_ctx_id);
matching_ctx_id->tag |= ID_TAG_TEMP_MAIN;
BKE_main_idmap_insert_id(this->bmain.id_map, matching_ctx_id);
BLI_assert(BKE_main_idmap_lookup_uid(this->bmain.id_map, session_uid) == nullptr);
}
ctx_id->session_uid = session_uid;
}
void PartialWriteContext::process_added_id(ID *ctx_id,
const PartialWriteContext::IDAddOperations operations)
{
const bool set_fake_user = (operations & SET_FAKE_USER) != 0;
const bool set_clipboard_mark = (operations & SET_CLIPBOARD_MARK) != 0;
if (set_fake_user) {
id_fake_user_set(ctx_id);
}
else {
/* NOTE: Using this tag will ensure that this ID is written on disk in current state (current
* context session). However, reloading the blendfile will clear this tag. */
id_us_ensure_real(ctx_id);
}
if (set_clipboard_mark) {
ctx_id->flag |= ID_FLAG_CLIPBOARD_MARK;
}
}
ID *PartialWriteContext::id_add_copy(const ID *id, const bool regenerate_session_uid)
{
ID *ctx_root_id = nullptr;
BLI_assert(BKE_main_idmap_lookup_uid(matching_uid_map_, id->session_uid) == nullptr);
const int copy_flags = (LIB_ID_CREATE_NO_MAIN | LIB_ID_CREATE_NO_USER_REFCOUNT |
/* NOTE: Could make this an option if needed in the future */
LIB_ID_COPY_ASSET_METADATA);
ctx_root_id = BKE_id_copy_in_lib(nullptr, id->lib, id, std::nullopt, nullptr, copy_flags);
ctx_root_id->tag |= ID_TAG_TEMP_MAIN;
/* Ensure that the newly copied ID has a library in temp local bmain if it was linked.
* While this could be optimized out in case the ID is made local in the context, this adds
* complexity as default ID management code like 'make local' code will create invalid bmain
* namemap data. */
this->ensure_library(ctx_root_id);
if (regenerate_session_uid) {
/* Calling #BKE_lib_libblock_session_uid_renew is not needed here, copying already generated a
* new one. */
BLI_assert(BKE_main_idmap_lookup_uid(matching_uid_map_, id->session_uid) == nullptr);
}
else {
this->preempt_session_uid(ctx_root_id, id->session_uid);
BKE_main_idmap_insert_id(matching_uid_map_, ctx_root_id);
}
BKE_main_idmap_insert_id(this->bmain.id_map, ctx_root_id);
BKE_libblock_management_main_add(&this->bmain, ctx_root_id);
/* Note: remapping of external file relative paths is done as part of the 'write' process. */
return ctx_root_id;
}
void PartialWriteContext::make_local(ID *ctx_id, const int make_local_flags)
{
/* Making an ID local typically resets its session UID, here we want to keep the same value. */
const uint ctx_id_session_uid = ctx_id->session_uid;
BKE_main_idmap_remove_id(this->bmain.id_map, ctx_id);
BKE_main_idmap_remove_id(matching_uid_map_, ctx_id);
if (ID_IS_LINKED(ctx_id)) {
BKE_lib_id_make_local(&this->bmain, ctx_id, make_local_flags);
}
/* NOTE: Cannot rely only on `ID_IS_OVERRIDE_LIBRARY` here, as the reference pointer to the
* linked data may have already been cleared out by dependency management in code above that
* call. */
else if ((ctx_id->override_library || ID_IS_OVERRIDE_LIBRARY(ctx_id)) &&
(make_local_flags & LIB_ID_MAKELOCAL_LIBOVERRIDE_CLEAR) != 0)
{
BKE_lib_override_library_make_local(&this->bmain, ctx_id);
}
this->preempt_session_uid(ctx_id, ctx_id_session_uid);
BKE_main_idmap_insert_id(this->bmain.id_map, ctx_id);
BKE_main_idmap_insert_id(matching_uid_map_, ctx_id);
}
Library *PartialWriteContext::ensure_library(ID *ctx_id)
{
if (!ID_IS_LINKED(ctx_id)) {
return nullptr;
}
blender::StringRefNull lib_path = ctx_id->lib->runtime->filepath_abs;
Library *ctx_lib = this->libraries_map_.lookup_default(lib_path, nullptr);
if (!ctx_lib) {
ctx_lib = reinterpret_cast<Library *>(id_add_copy(&ctx_id->lib->id, true));
this->libraries_map_.add(lib_path, ctx_lib);
}
ctx_id->lib = ctx_lib;
return ctx_lib;
}
Library *PartialWriteContext::ensure_library(blender::StringRefNull library_absolute_path)
{
Library *ctx_lib = this->libraries_map_.lookup_default(library_absolute_path, nullptr);
if (!ctx_lib) {
const char *library_name = BLI_path_basename(library_absolute_path.c_str());
ctx_lib = static_cast<Library *>(
BKE_id_new_in_lib(&this->bmain, nullptr, ID_LI, library_name));
ctx_lib->id.tag |= ID_TAG_TEMP_MAIN;
id_us_min(&ctx_lib->id);
this->libraries_map_.add(library_absolute_path, ctx_lib);
}
return ctx_lib;
}
ID *PartialWriteContext::id_add(
const ID *id,
PartialWriteContext::IDAddOptions options,
blender::FunctionRef<PartialWriteContext::IDAddOperations(
LibraryIDLinkCallbackData *cb_data, PartialWriteContext::IDAddOptions options)>
dependencies_filter_cb)
{
constexpr int make_local_flags = (LIB_ID_MAKELOCAL_INDIRECT | LIB_ID_MAKELOCAL_FORCE_LOCAL |
LIB_ID_MAKELOCAL_LIBOVERRIDE_CLEAR);
const bool add_dependencies = (options.operations & ADD_DEPENDENCIES) != 0;
const bool clear_dependencies = (options.operations & CLEAR_DEPENDENCIES) != 0;
const bool duplicate_dependencies = (options.operations & DUPLICATE_DEPENDENCIES) != 0;
BLI_assert(clear_dependencies || add_dependencies || dependencies_filter_cb);
BLI_assert(!clear_dependencies || !(add_dependencies || duplicate_dependencies));
UNUSED_VARS_NDEBUG(add_dependencies, clear_dependencies, duplicate_dependencies);
/* Do not directly add an embedded ID. Add its owner instead. */
if (id->flag & ID_FLAG_EMBEDDED_DATA) {
id = BKE_id_owner_get(const_cast<ID *>(id), true);
}
/* The given ID may have already been added (either explicitly or as a dependency) before. */
ID *ctx_root_id = BKE_main_idmap_lookup_uid(matching_uid_map_, id->session_uid);
if (ctx_root_id) {
/* If the root orig ID is already in the context, assume all of its dependencies are as well.
*/
BLI_assert(ctx_root_id->session_uid == id->session_uid);
this->process_added_id(ctx_root_id, options.operations);
return ctx_root_id;
}
/* Local mapping, such that even in case dependencies are duplicated for this specific added ID,
* once a dependency has been duplicated, it can be re-used for other ID usages within the
* dependencies of the added ID. */
blender::Map<const ID *, ID *> local_ctx_id_map;
/* A list of IDs to post-process. Only contains IDs that were actually added to the context (not
* the ones that were already there and were re-used). The #IDAddOperations item of the pair
* stores the returned value from the given #dependencies_filter_cb (or given global #options
* parameter otherwise). */
blender::Vector<std::pair<ID *, PartialWriteContext::IDAddOperations>> post_process_ids_todo;
ctx_root_id = id_add_copy(id, false);
BLI_assert(ctx_root_id->session_uid == id->session_uid);
local_ctx_id_map.add(id, ctx_root_id);
post_process_ids_todo.append({ctx_root_id, options.operations});
this->process_added_id(ctx_root_id, options.operations);
blender::VectorSet<ID *> ids_to_process{ctx_root_id};
auto dependencies_cb = [this,
options,
&local_ctx_id_map,
&ids_to_process,
&post_process_ids_todo,
dependencies_filter_cb](LibraryIDLinkCallbackData *cb_data) -> int {
ID **id_ptr = cb_data->id_pointer;
const ID *orig_deps_id = *id_ptr;
if (cb_data->cb_flag & (IDWALK_CB_EMBEDDED | IDWALK_CB_EMBEDDED_NOT_OWNING)) {
return IDWALK_RET_NOP;
}
if (!orig_deps_id) {
return IDWALK_RET_NOP;
}
if (cb_data->cb_flag & IDWALK_CB_INTERNAL) {
/* Cleanup internal ID pointers. */
*id_ptr = nullptr;
return IDWALK_RET_NOP;
}
PartialWriteContext::IDAddOperations operations_final = (options.operations & MASK_INHERITED);
if (dependencies_filter_cb) {
const PartialWriteContext::IDAddOperations operations_per_id = dependencies_filter_cb(
cb_data, options);
operations_final = ((operations_per_id & MASK_PER_ID_USAGE) |
(operations_final & ~MASK_PER_ID_USAGE));
}
const bool add_dependencies = (operations_final & ADD_DEPENDENCIES) != 0;
const bool clear_dependencies = (operations_final & CLEAR_DEPENDENCIES) != 0;
const bool duplicate_dependencies = (operations_final & DUPLICATE_DEPENDENCIES) != 0;
BLI_assert(clear_dependencies || add_dependencies);
BLI_assert(!clear_dependencies || !(add_dependencies || duplicate_dependencies));
UNUSED_VARS_NDEBUG(add_dependencies);
if (clear_dependencies) {
if (cb_data->cb_flag & IDWALK_CB_NEVER_NULL) {
CLOG_WARN(&LOG_PARTIALWRITE,
"Clearing a 'never null' ID usage of '%s' by '%s', this is likely not a "
"desired action",
(*id_ptr)->name,
cb_data->owner_id->name);
}
/* Owner ID should be a 'context-main' duplicate of a real Main ID, as such there should be
* no need to decrease ID usages refcount here. */
*id_ptr = nullptr;
return IDWALK_RET_NOP;
}
/* else if (add_dependencies) */
/* The given ID may have already been added (either explicitly or as a dependency) before. */
ID *ctx_deps_id = nullptr;
if (duplicate_dependencies) {
ctx_deps_id = local_ctx_id_map.lookup(orig_deps_id);
}
else {
ctx_deps_id = BKE_main_idmap_lookup_uid(matching_uid_map_, orig_deps_id->session_uid);
}
if (!ctx_deps_id) {
if (cb_data->cb_flag & IDWALK_CB_LOOPBACK) {
/* Do not follow 'loop back' pointers. */
/* NOTE: Not sure whether this should be considered an error or not. Typically hitting such
* a case is bad practice. On the other hand, some of these pointers are present in
* 'normal' IDs, like e.g. the parent collections ones. This implies that currently, all
* attempt to adding a collection to a partial write context should make usage of a custom
* `dependencies_filter_cb` function to explicitly clear these pointers. */
CLOG_ERROR(&LOG_PARTIALWRITE,
"First dependency to ID '%s' found through a 'loopback' usage from ID '%s', "
"this should never happen",
(*id_ptr)->name,
cb_data->owner_id->name);
*id_ptr = nullptr;
return IDWALK_RET_NOP;
}
ctx_deps_id = this->id_add_copy(orig_deps_id, duplicate_dependencies);
local_ctx_id_map.add(orig_deps_id, ctx_deps_id);
ids_to_process.add(ctx_deps_id);
post_process_ids_todo.append({ctx_deps_id, operations_final});
}
if (duplicate_dependencies) {
BLI_assert(ctx_deps_id->session_uid != orig_deps_id->session_uid);
}
else {
BLI_assert(ctx_deps_id->session_uid == orig_deps_id->session_uid);
}
this->process_added_id(ctx_deps_id, operations_final);
/* In-place remapping. */
*id_ptr = ctx_deps_id;
return IDWALK_RET_NOP;
};
while (!ids_to_process.is_empty()) {
ID *ctx_id = ids_to_process.pop();
BKE_library_foreach_ID_link(
&this->bmain, ctx_id, dependencies_cb, &options, IDWALK_DO_INTERNAL_RUNTIME_POINTERS);
}
/* Post process all newly added IDs in the context:
* - Make them local or ensure that their library reference is also in the context.
*/
for (auto [ctx_id, options_final] : post_process_ids_todo) {
const bool do_make_local = (options_final & MAKE_LOCAL) != 0;
if (do_make_local) {
this->make_local(ctx_id, make_local_flags);
}
}
return ctx_root_id;
}
ID *PartialWriteContext::id_create(const short id_type,
const blender::StringRefNull id_name,
Library *library,
PartialWriteContext::IDAddOptions options)
{
Library *ctx_library = nullptr;
if (library) {
ctx_library = this->ensure_library(library->runtime->filepath_abs);
}
ID *ctx_id = static_cast<ID *>(
BKE_id_new_in_lib(&this->bmain, ctx_library, id_type, id_name.c_str()));
ctx_id->tag |= ID_TAG_TEMP_MAIN;
id_us_min(ctx_id);
this->process_added_id(ctx_id, options.operations);
/* See function doc about why handling of #matching_uid_map_ can be skipped here. */
BKE_main_idmap_insert_id(this->bmain.id_map, ctx_id);
return ctx_id;
}
void PartialWriteContext::id_delete(const ID *id)
{
if (ID *ctx_id = BKE_main_idmap_lookup_uid(matching_uid_map_, id->session_uid)) {
BKE_main_idmap_remove_id(matching_uid_map_, ctx_id);
BKE_id_delete(&this->bmain, ctx_id);
}
}
void PartialWriteContext::remove_unused(const bool clear_extra_user)
{
LibQueryUnusedIDsData parameters;
parameters.do_local_ids = true;
parameters.do_linked_ids = true;
parameters.do_recursive = true;
if (clear_extra_user) {
ID *id_iter;
FOREACH_MAIN_ID_BEGIN (&this->bmain, id_iter) {
id_us_clear_real(id_iter);
}
FOREACH_MAIN_ID_END;
}
BKE_lib_query_unused_ids_tag(&this->bmain, ID_TAG_DOIT, parameters);
CLOG_INFO(&LOG_PARTIALWRITE,
3,
"Removing %d unused IDs from current partial write context",
parameters.num_total[INDEX_ID_NULL]);
ID *id_iter;
FOREACH_MAIN_ID_BEGIN (&this->bmain, id_iter) {
if ((id_iter->tag & ID_TAG_DOIT) != 0) {
BKE_main_idmap_remove_id(matching_uid_map_, id_iter);
}
}
FOREACH_MAIN_ID_END;
BKE_id_multi_tagged_delete(&this->bmain);
}
void PartialWriteContext::clear()
{
BKE_main_idmap_clear(*matching_uid_map_);
BKE_main_clear(this->bmain);
}
bool PartialWriteContext::is_valid()
{
blender::Set<ID *> ids_in_context;
blender::Set<uint> session_uids_in_context;
bool is_valid = true;
ID *id_iter;
/* Fill `ids_in_context`, check uniqueness of session_uid's. */
FOREACH_MAIN_ID_BEGIN (&this->bmain, id_iter) {
ids_in_context.add(id_iter);
if (session_uids_in_context.contains(id_iter->session_uid)) {
CLOG_ERROR(&LOG_PARTIALWRITE, "ID %s does not have a unique session_uid", id_iter->name);
is_valid = false;
}
else {
session_uids_in_context.add(id_iter->session_uid);
}
}
FOREACH_MAIN_ID_END;
/* Check that no ID uses IDs from outside this context. */
auto id_validate_dependencies_cb = [&ids_in_context,
&is_valid](LibraryIDLinkCallbackData *cb_data) -> int {
ID **id_p = cb_data->id_pointer;
ID *owner_id = cb_data->owner_id;
ID *self_id = cb_data->self_id;
/* By definition, embedded IDs are not in Main, so they are not listed in this context either.
*/
if (cb_data->cb_flag & (IDWALK_CB_EMBEDDED | IDWALK_CB_EMBEDDED_NOT_OWNING)) {
return IDWALK_RET_NOP;
}
if (*id_p && !ids_in_context.contains(*id_p)) {
if (owner_id != self_id) {
CLOG_ERROR(
&LOG_PARTIALWRITE,
"ID %s (used by ID '%s', embedded ID '%s') is not in current partial write context",
(*id_p)->name,
owner_id->name,
self_id->name);
}
else {
CLOG_ERROR(&LOG_PARTIALWRITE,
"ID %s (used by ID '%s') is not in current partial write context",
(*id_p)->name,
owner_id->name);
}
is_valid = false;
}
return IDWALK_RET_NOP;
};
FOREACH_MAIN_ID_BEGIN (&this->bmain, id_iter) {
BKE_library_foreach_ID_link(
&this->bmain, id_iter, id_validate_dependencies_cb, nullptr, IDWALK_READONLY);
}
FOREACH_MAIN_ID_END;
return is_valid;
}
bool PartialWriteContext::write(const char *write_filepath,
const int write_flags,
const int remap_mode,
ReportList &reports)
{
BLI_assert_msg(write_filepath != reference_root_filepath_,
"A library blendfile should not overwrite currently edited blendfile");
/* In case the write path is the same as one of the libraries used by this context, make this
* library local, and delete it (and all of its potentially remaining linked data). */
blender::Vector<Library *> make_local_libs;
LISTBASE_FOREACH (Library *, library, &this->bmain.libraries) {
if (STREQ(write_filepath, library->runtime->filepath_abs)) {
make_local_libs.append(library);
}
}
/* Will likely change in the near future (embedded linked IDs, virtual libraries...), but
* currently this should never happen. */
if (make_local_libs.size() > 1) {
CLOG_WARN(&LOG_PARTIALWRITE,
"%d libraries found using the same filepath as destination one ('%s'), should "
"never happen.",
int32_t(make_local_libs.size()),
write_filepath);
}
for (Library *lib : make_local_libs) {
BKE_library_make_local(&this->bmain, lib, nullptr, false, false, false);
BKE_id_delete(&this->bmain, lib);
}
make_local_libs.clear();
BLI_assert(this->is_valid());
BlendFileWriteParams blend_file_write_params{};
blend_file_write_params.remap_mode = eBLO_WritePathRemap(remap_mode);
return BLO_write_file(
&this->bmain, write_filepath, write_flags, &blend_file_write_params, &reports);
}
bool PartialWriteContext::write(const char *write_filepath, ReportList &reports)
{
return this->write(write_filepath, 0, BLO_WRITE_PATH_REMAP_RELATIVE, reports);
}
} // namespace blender::bke::blendfile
/** \} */