Paint: Ensure brushes are loaded when requested while in background mode

Depending on internal details of how Blender is run, attempting to load
elements from the asset library may either execute as synchronous &
blocking or asynchronously.

When executing a script in background mode, prior to this commit,
operators that are dependent on the asset system will not execute
correctly due to the loading not being complete.

Busy-waiting for this by repeatedly calling the operator over and over
again in python does not resolve. To match behavior of other operators
when called from python scripts such as the quadriflow remesh, this
commit changes the `brush.asset_activate` operator and dependent code to
force a blocking call instead of optionally using the wmJob background
abstraction system.

Related to #117399

Pull Request: https://projects.blender.org/blender/blender/pulls/134203
This commit is contained in:
Sean Kim
2025-04-10 22:09:06 +02:00
committed by Sean Kim
parent 001b8912ff
commit 898e6f3687
7 changed files with 99 additions and 2 deletions

View File

@@ -61,10 +61,17 @@ void iterate(const AssetLibraryReference &library_reference, AssetListIterFn fn)
* Invoke asset list reading, potentially in a parallel job. Won't wait until the job is done,
* and may return earlier.
*
* \see: #storage_fetch_blocking for a blocking version.
* \warning: Asset list reading involves an #AS_asset_library_load() call which may reload asset
* library data like catalogs (invalidating pointers). Refer to its warning for details.
*/
void storage_fetch(const AssetLibraryReference *library_reference, const bContext *C);
/**
* Invoke asset list reading, guaranteed to execute on the same thread.
*
* \see #storage_fetch for an async version.
*/
void storage_fetch_blocking(const AssetLibraryReference &library_reference, const bContext &C);
bool is_loaded(const AssetLibraryReference *library_reference);
/**
* Clears this asset library and the "All" asset library for reload in both the static asset list

View File

@@ -94,6 +94,7 @@ class AssetList : NonCopyable {
void setup();
void fetch(const bContext &C);
void ensure_blocking(const bContext &C);
void clear(wmWindowManager *wm);
void clear_current_file_assets(wmWindowManager *wm);
@@ -163,6 +164,22 @@ void AssetList::fetch(const bContext &C)
filelist_filter(files);
}
void AssetList::ensure_blocking(const bContext &C)
{
FileList *files = filelist_;
if (filelist_needs_force_reset(files)) {
filelist_clear_from_reset_tag(files);
}
if (filelist_needs_reading(files)) {
filelist_readjob_blocking_run(files, NC_ASSET | ND_ASSET_LIST_READING, &C);
}
filelist_sort(files);
filelist_filter(files);
}
bool AssetList::needs_refetch() const
{
return filelist_needs_force_reset(filelist_) || filelist_needs_reading(filelist_);
@@ -422,6 +439,21 @@ void storage_fetch(const AssetLibraryReference *library_reference, const bContex
}
}
void storage_fetch_blocking(const AssetLibraryReference &library_reference, const bContext &C)
{
std::optional filesel_type = asset_library_reference_to_fileselect_type(library_reference);
if (!filesel_type) {
/* TODO: Warn? */
return;
}
auto [list, is_new] = ensure_list_storage(library_reference, *filesel_type);
if (is_new || list.needs_refetch()) {
list.setup();
list.ensure_blocking(C);
}
}
bool is_loaded(const AssetLibraryReference *library_reference)
{
AssetList *list = lookup_list(*library_reference);

View File

@@ -17,6 +17,7 @@
#include "BKE_blendfile.hh"
#include "BKE_brush.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_paint.hh"
#include "BKE_preferences.h"
#include "BKE_preview_image.hh"
@@ -54,6 +55,14 @@ static wmOperatorStatus brush_asset_activate_exec(bContext *C, wmOperator *op)
* used for the asset-view template. Once the asset list design is used by the Asset Browser,
* this can be simplified to just that case. */
Main *bmain = CTX_data_main(C);
if (G.background) {
/* As asset loading can take upwards of a few minutes on production libraries, we typically
* do not want this to execute in a blocking fashion. However, for testing / profiling
* purposes, this is an acceptable workaround for now until a proper python API is created
* for this usecase. */
asset::list::storage_fetch_blocking(asset_system::all_library_reference(), *C);
}
const asset_system::AssetRepresentation *asset =
asset::operator_asset_reference_props_get_asset_from_all_library(*C, *op->ptr, op->reports);
if (!asset) {

View File

@@ -4239,7 +4239,10 @@ static void assetlibrary_readjob_startjob(void *flrjv, wmJobWorkerStatus *worker
filelist_readjob_startjob(flrjv, worker_status);
}
void filelist_readjob_start(FileList *filelist, const int space_notifier, const bContext *C)
static void filelist_readjob_start_ex(FileList *filelist,
const int space_notifier,
const bContext *C,
const bool force_blocking_read)
{
Main *bmain = CTX_data_main(C);
wmJob *wm_job;
@@ -4276,7 +4279,7 @@ void filelist_readjob_start(FileList *filelist, const int space_notifier, const
* main data changed may need access to the ID files (see #93691). */
const bool no_threads = (filelist->tags & FILELIST_TAGS_NO_THREADS) || flrj->only_main_data;
if (no_threads) {
if (force_blocking_read || no_threads) {
/* Single threaded execution. Just directly call the callbacks. */
wmJobWorkerStatus worker_status = {};
filelist_readjob_startjob(flrj, &worker_status);
@@ -4307,6 +4310,16 @@ void filelist_readjob_start(FileList *filelist, const int space_notifier, const
WM_jobs_start(CTX_wm_manager(C), wm_job);
}
void filelist_readjob_start(FileList *filelist, const int space_notifier, const bContext *C)
{
filelist_readjob_start_ex(filelist, space_notifier, C, false);
}
void filelist_readjob_blocking_run(FileList *filelist, int space_notifier, const bContext *C)
{
filelist_readjob_start_ex(filelist, space_notifier, C, true);
}
void filelist_readjob_stop(FileList *filelist, wmWindowManager *wm)
{
WM_jobs_kill_type(wm, filelist, filelist_jobtype_get(filelist));

View File

@@ -231,6 +231,10 @@ void filelist_freelib(FileList *filelist);
*/
int filelist_files_num_entries(FileList *filelist);
/** Forcibly run the job as a blocking task on the main thread. */
void filelist_readjob_blocking_run(FileList *filelist, int space_notifier, const bContext *C);
/** May run the job in either the main thread or asynchronously. */
void filelist_readjob_start(FileList *filelist, int space_notifier, const bContext *C);
void filelist_readjob_stop(FileList *filelist, wmWindowManager *wm);
int filelist_readjob_running(FileList *filelist, wmWindowManager *wm);

View File

@@ -574,6 +574,13 @@ if(TEST_SRC_DIR_EXISTS)
)
endif()
# ------------------------------------------------------------------------------
# BRUSH TESTS
add_blender_test(
bl_brush
--python ${CMAKE_CURRENT_LIST_DIR}/bl_brush_test.py
)
# ------------------------------------------------------------------------------
# NODE GROUP TESTS
# ------------------------------------------------------------------------------

View File

@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2025 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import sys
import unittest
import bpy
class AssetActivateTest(unittest.TestCase):
def setUp(self):
# Test case isn't specific to Sculpt Mode, but we need a paint mode in general.
bpy.ops.object.mode_set(mode='SCULPT')
def test_loads_essential_asset(self):
result = bpy.ops.brush.asset_activate(
asset_library_type='ESSENTIALS',
relative_asset_identifier='brushes/essentials_brushes-mesh_sculpt.blend/Brush/Smooth')
self.assertEqual({'FINISHED'}, result)
if __name__ == "__main__":
# Drop all arguments before "--", or everything if the delimiter is absent. Keep the executable path.
unittest.main(argv=sys.argv[:1] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []))