Geometry Nodes: support caching imported files

Currently, the import nodes always reimport on each evaluation. This patch adds
support for caching the loaded geometries. This is integrated with
`BLI_memory_cache.hh` and thus also takes the cache size limit into account. If
an imported file is modified on disk, the cache is invalidated. However,
Geometry Nodes will not automatically reevaluate when a file changes, so the
user would have to trigger the evaluation in some other way.

This is an alternative solution to #124369. The main benefits are that the cache
invalidation happens automatically and that the cache system is more general and
does not have to know about e.g. the different file types.

Caching speeds up node setups that heavily rely on import nodes significantly.

Pull Request: https://projects.blender.org/blender/blender/pulls/138425
This commit is contained in:
Jacques Lucke
2025-05-05 19:25:05 +02:00
parent 3cd15d70b2
commit 1b61e419a6
11 changed files with 440 additions and 107 deletions

View File

@@ -0,0 +1,48 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_generic_key.hh"
#include "BLI_string_ref.hh"
#include "BLI_struct_equality_utils.hh"
#include "BLI_utility_mixins.hh"
namespace blender {
/** Utility class that to easy create a #GenericKey from a string. */
class GenericStringKey : public GenericKey, NonMovable {
private:
std::string value_;
/** This may reference the string stored in value_. */
StringRef value_ref_;
public:
GenericStringKey(StringRef value) : value_ref_(value) {}
uint64_t hash() const override
{
return get_default_hash(value_ref_);
}
BLI_STRUCT_EQUALITY_OPERATORS_1(GenericStringKey, value_ref_)
bool equal_to(const GenericKey &other) const override
{
if (const auto *other_typed = dynamic_cast<const GenericStringKey *>(&other)) {
return value_ref_ == other_typed->value_ref_;
}
return false;
}
std::unique_ptr<GenericKey> to_storable() const override
{
auto storable_key = std::make_unique<GenericStringKey>("");
storable_key->value_ = value_ref_;
storable_key->value_ref_ = storable_key->value_;
return storable_key;
}
};
} // namespace blender

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_memory_cache.hh"
#include "BLI_string_ref.hh"
namespace blender::memory_cache {
/**
* Call the given loader function if its result has not been cached yet. The cache key is a
* combination of loader_key and file_paths. load_fn is responsible for still producing a valid
* cache value even if a file is not found.
*/
template<typename T>
std::shared_ptr<const T> get_loaded(const GenericKey &loader_key,
Span<StringRefNull> file_paths,
FunctionRef<std::unique_ptr<T>()> load_fn);
std::shared_ptr<CachedValue> get_loaded_base(const GenericKey &loader_key,
Span<StringRefNull> file_paths,
FunctionRef<std::unique_ptr<CachedValue>()> load_fn);
template<typename T>
inline std::shared_ptr<const T> get_loaded(const GenericKey &loader_key,
Span<StringRefNull> file_paths,
FunctionRef<std::unique_ptr<T>()> load_fn)
{
return std::dynamic_pointer_cast<const T>(get_loaded_base(loader_key, file_paths, load_fn));
}
} // namespace blender::memory_cache

View File

@@ -116,6 +116,7 @@ set(SRC
intern/math_vector.cc
intern/math_vector_inline.cc
intern/memory_cache.cc
intern/memory_cache_file_load.cc
intern/memory_counter.cc
intern/memory_utils.cc
intern/mesh_boolean.cc
@@ -239,6 +240,7 @@ set(SRC
BLI_function_ref.hh
BLI_generic_array.hh
BLI_generic_key.hh
BLI_generic_key_string.hh
BLI_generic_pointer.hh
BLI_generic_span.hh
BLI_generic_value_map.hh
@@ -325,6 +327,7 @@ set(SRC
BLI_memblock.h
BLI_memiter.h
BLI_memory_cache.hh
BLI_memory_cache_file_load.hh
BLI_memory_counter.hh
BLI_memory_counter_fwd.hh
BLI_memory_utils.h

View File

@@ -0,0 +1,143 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <mutex>
#include <optional>
#include "BLI_fileops.hh"
#include "BLI_hash.hh"
#include "BLI_map.hh"
#include "BLI_memory_cache_file_load.hh"
#include "BLI_task.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
namespace blender::memory_cache {
/**
* A key used to identify data loaded from one or more files.
*/
class LoadFileKey : public GenericKey {
private:
/** The files to load from. */
Vector<std::string> file_paths_;
/**
* The key used to identify the loader. The same files might be loaded with different loaders
* which can result in different data that needs to be cached separately.
*/
std::shared_ptr<const GenericKey> loader_key_;
public:
LoadFileKey(Vector<std::string> file_paths, std::shared_ptr<const GenericKey> loader_key)
: file_paths_(std::move(file_paths)), loader_key_(std::move(loader_key))
{
}
Span<std::string> file_paths() const
{
return file_paths_;
}
uint64_t hash() const override
{
return get_default_hash(file_paths_, *loader_key_);
}
friend bool operator==(const LoadFileKey &a, const LoadFileKey &b)
{
return a.file_paths_ == b.file_paths_ && *a.loader_key_ == *b.loader_key_;
}
bool equal_to(const GenericKey &other) const override
{
if (const auto *other_typed = dynamic_cast<const LoadFileKey *>(&other)) {
return *this == *other_typed;
}
return false;
}
std::unique_ptr<GenericKey> to_storable() const override
{
/* Currently #LoadFileKey is always storable, i.e. it owns all the data it references. A
* potential future optimization could be to support just referencing the paths and loader key,
* but that causes some boilerplate now that is not worth it. */
return std::make_unique<LoadFileKey>(*this);
}
};
static std::optional<int64_t> get_file_modification_time(const StringRefNull path)
{
BLI_stat_t stat;
if (BLI_stat(path.c_str(), &stat) == -1) {
return std::nullopt;
}
return stat.st_mtime;
}
struct FileStatMap {
std::mutex mutex;
Map<std::string, std::optional<int64_t>> map;
};
static FileStatMap &get_file_stat_map()
{
static FileStatMap file_stat_map;
return file_stat_map;
}
static void invalidate_outdated_caches_if_necessary(const Span<StringRefNull> file_paths)
{
FileStatMap &file_stat_map = get_file_stat_map();
/* Retrieve the file modification times before the lock because there is no need for the lock
* yet. While not guaranteed, retrieving the modification time is often optimized by the OS so
* that no actual access to the hard drive is necessary. */
Array<std::optional<int64_t>> new_times(file_paths.size());
for (const int i : file_paths.index_range()) {
new_times[i] = get_file_modification_time(file_paths[i]);
}
std::lock_guard lock{file_stat_map.mutex};
/* Find all paths that have changed on disk. */
VectorSet<StringRefNull> outdated_paths;
for (const int i : file_paths.index_range()) {
const StringRefNull path = file_paths[i];
const std::optional<int64_t> new_time = new_times[i];
std::optional<int64_t> &old_time = file_stat_map.map.lookup_or_add_as(path, new_time);
if (old_time != new_time) {
outdated_paths.add(path);
old_time = new_time;
}
}
/* If any referenced file was changed, invalidate the caches that use it. */
if (!outdated_paths.is_empty()) {
/* Isolate because a mutex is locked. */
threading::isolate_task([&]() {
/* Invalidation is done while the mutex is locked so that other threads won't see the old
* cached value anymore after we've detected that it's oudated. */
memory_cache::remove_if([&](const GenericKey &other_key) {
if (const auto *other_key_typed = dynamic_cast<const LoadFileKey *>(&other_key)) {
const Span<std::string> other_key_paths = other_key_typed->file_paths();
return std::any_of(
other_key_paths.begin(), other_key_paths.end(), [&](const StringRefNull path) {
return outdated_paths.contains(path);
});
}
return false;
});
});
}
}
std::shared_ptr<CachedValue> get_loaded_base(const GenericKey &loader_key,
Span<StringRefNull> file_paths,
FunctionRef<std::unique_ptr<CachedValue>()> load_fn)
{
invalidate_outdated_caches_if_necessary(file_paths);
const LoadFileKey key{file_paths, loader_key.to_storable()};
return memory_cache::get_base(key, load_fn);
}
} // namespace blender::memory_cache

View File

@@ -50,6 +50,7 @@
struct SpaceNode;
struct NodesModifierData;
struct Report;
namespace blender::nodes::geo_eval_log {
@@ -69,6 +70,9 @@ struct NodeWarning {
NodeWarningType type;
std::string message;
NodeWarning(NodeWarningType type, StringRef message) : type(type), message(message) {}
NodeWarning(const Report &report);
uint64_t hash() const
{
return get_default_hash(this->type, this->message);

View File

@@ -2,7 +2,11 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <fmt/format.h>
#include "BLI_generic_key_string.hh"
#include "BLI_listbase.h"
#include "BLI_memory_cache_file_load.hh"
#include "BLI_string.h"
#include "BKE_report.hh"
@@ -25,6 +29,17 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Geometry>("Point Cloud");
}
class LoadCsvCache : public memory_cache::CachedValue {
public:
GeometrySet geometry;
Vector<geo_eval_log::NodeWarning> warnings;
void count_memory(MemoryCounter &counter) const override
{
this->geometry.count_memory(counter);
}
};
static void node_geo_exec(GeoNodeExecParams params)
{
#ifdef WITH_IO_CSV
@@ -47,31 +62,35 @@ static void node_geo_exec(GeoNodeExecParams params)
return;
}
blender::io::csv::CSVImportParams import_params{};
import_params.delimiter = delimiter[0];
STRNCPY(import_params.filepath, path->c_str());
/* Encode delimiter in key because it affects the result. */
const std::string loader_key = fmt::format("import_csv_node_{}", delimiter[0]);
std::shared_ptr<const LoadCsvCache> cached_value = memory_cache::get_loaded<LoadCsvCache>(
GenericStringKey{loader_key}, {StringRefNull(*path)}, [&]() {
blender::io::csv::CSVImportParams import_params{};
import_params.delimiter = delimiter[0];
STRNCPY(import_params.filepath, path->c_str());
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
BLI_SCOPED_DEFER([&]() { BKE_reports_free(&reports); })
import_params.reports = &reports;
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
BLI_SCOPED_DEFER([&]() { BKE_reports_free(&reports); });
import_params.reports = &reports;
PointCloud *pointcloud = blender::io::csv::import_csv_as_pointcloud(import_params);
PointCloud *pointcloud = blender::io::csv::import_csv_as_pointcloud(import_params);
LISTBASE_FOREACH (Report *, report, &(import_params.reports)->list) {
NodeWarningType type;
switch (report->type) {
case RPT_ERROR:
type = NodeWarningType::Error;
break;
default:
type = NodeWarningType::Info;
break;
}
params.error_message_add(type, TIP_(report->message));
auto cached_value = std::make_unique<LoadCsvCache>();
cached_value->geometry = GeometrySet::from_pointcloud(pointcloud);
LISTBASE_FOREACH (Report *, report, &(import_params.reports)->list) {
cached_value->warnings.append_as(*report);
}
return cached_value;
});
for (const geo_eval_log::NodeWarning &warning : cached_value->warnings) {
params.error_message_add(warning.type, warning.message);
}
params.set_output("Point Cloud", GeometrySet::from_pointcloud(pointcloud));
params.set_output("Point Cloud", cached_value->geometry);
#else
params.error_message_add(NodeWarningType::Error,
TIP_("Disabled, Blender was compiled without CSV I/O"));

View File

@@ -4,7 +4,9 @@
#include "node_geometry_util.hh"
#include "BLI_generic_key_string.hh"
#include "BLI_listbase.h"
#include "BLI_memory_cache_file_load.hh"
#include "BLI_string.h"
#include "BKE_instances.hh"
@@ -25,6 +27,17 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Geometry>("Instances");
}
class LoadObjCache : public memory_cache::CachedValue {
public:
GeometrySet geometry;
Vector<geo_eval_log::NodeWarning> warnings;
void count_memory(MemoryCounter &counter) const override
{
this->geometry.count_memory(counter);
}
};
static void node_geo_exec(GeoNodeExecParams params)
{
#ifdef WITH_IO_WAVEFRONT_OBJ
@@ -35,42 +48,40 @@ static void node_geo_exec(GeoNodeExecParams params)
return;
}
OBJImportParams import_params;
STRNCPY(import_params.filepath, path->c_str());
std::shared_ptr<const LoadObjCache> cached_value = memory_cache::get_loaded<LoadObjCache>(
GenericStringKey{"import_obj_node"}, {StringRefNull(*path)}, [&]() {
OBJImportParams import_params;
STRNCPY(import_params.filepath, path->c_str());
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
BLI_SCOPED_DEFER([&]() { BKE_reports_free(&reports); });
import_params.reports = &reports;
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
BLI_SCOPED_DEFER([&]() { BKE_reports_free(&reports); });
import_params.reports = &reports;
Vector<bke::GeometrySet> geometries;
OBJ_import_geometries(&import_params, geometries);
Vector<bke::GeometrySet> geometries;
OBJ_import_geometries(&import_params, geometries);
LISTBASE_FOREACH (Report *, report, &(import_params.reports)->list) {
NodeWarningType type;
switch (report->type) {
case RPT_ERROR:
type = NodeWarningType::Error;
break;
default:
type = NodeWarningType::Info;
break;
}
params.error_message_add(type, TIP_(report->message));
bke::Instances *instances = new bke::Instances();
for (GeometrySet geometry : geometries) {
const int handle = instances->add_reference(bke::InstanceReference{std::move(geometry)});
instances->add_instance(handle, float4x4::identity());
}
auto cached_value = std::make_unique<LoadObjCache>();
cached_value->geometry = GeometrySet::from_instances(instances);
LISTBASE_FOREACH (Report *, report, &(import_params.reports)->list) {
cached_value->warnings.append_as(*report);
}
return cached_value;
});
for (const geo_eval_log::NodeWarning &warning : cached_value->warnings) {
params.error_message_add(warning.type, warning.message);
}
if (geometries.is_empty()) {
params.set_default_remaining_outputs();
return;
}
bke::Instances *instances = new bke::Instances();
for (GeometrySet geometry : geometries) {
const int handle = instances->add_reference(bke::InstanceReference{std::move(geometry)});
instances->add_instance(handle, float4x4::identity());
}
params.set_output("Instances", GeometrySet::from_instances(instances));
params.set_output("Instances", cached_value->geometry);
#else
params.error_message_add(NodeWarningType::Error,
TIP_("Disabled, Blender was compiled without OBJ I/O"));

View File

@@ -4,7 +4,9 @@
#include "node_geometry_util.hh"
#include "BLI_generic_key_string.hh"
#include "BLI_listbase.h"
#include "BLI_memory_cache_file_load.hh"
#include "BLI_string.h"
#include "BKE_report.hh"
@@ -24,6 +26,17 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Geometry>("Mesh");
}
class LoadPlyCache : public memory_cache::CachedValue {
public:
GeometrySet geometry;
Vector<geo_eval_log::NodeWarning> warnings;
void count_memory(MemoryCounter &counter) const override
{
this->geometry.count_memory(counter);
}
};
static void node_geo_exec(GeoNodeExecParams params)
{
#ifdef WITH_IO_PLY
@@ -34,31 +47,33 @@ static void node_geo_exec(GeoNodeExecParams params)
return;
}
PLYImportParams import_params;
STRNCPY(import_params.filepath, path->c_str());
import_params.import_attributes = true;
std::shared_ptr<const LoadPlyCache> cached_value = memory_cache::get_loaded<LoadPlyCache>(
GenericStringKey{"import_ply_node"}, {StringRefNull(*path)}, [&]() {
PLYImportParams import_params;
STRNCPY(import_params.filepath, path->c_str());
import_params.import_attributes = true;
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
BLI_SCOPED_DEFER([&]() { BKE_reports_free(&reports); })
import_params.reports = &reports;
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
BLI_SCOPED_DEFER([&]() { BKE_reports_free(&reports); });
import_params.reports = &reports;
Mesh *mesh = PLY_import_mesh(import_params);
Mesh *mesh = PLY_import_mesh(import_params);
LISTBASE_FOREACH (Report *, report, &(import_params.reports)->list) {
NodeWarningType type;
switch (report->type) {
case RPT_ERROR:
type = NodeWarningType::Error;
break;
default:
type = NodeWarningType::Info;
break;
}
params.error_message_add(type, TIP_(report->message));
auto cached_value = std::make_unique<LoadPlyCache>();
cached_value->geometry = GeometrySet::from_mesh(mesh);
LISTBASE_FOREACH (Report *, report, &(import_params.reports)->list) {
cached_value->warnings.append_as(*report);
}
return cached_value;
});
for (const geo_eval_log::NodeWarning &warning : cached_value->warnings) {
params.error_message_add(warning.type, warning.message);
}
params.set_output("Mesh", GeometrySet::from_mesh(mesh));
params.set_output("Mesh", cached_value->geometry);
#else
params.error_message_add(NodeWarningType::Error,

View File

@@ -4,9 +4,13 @@
#include "node_geometry_util.hh"
#include "BLI_generic_key_string.hh"
#include "BLI_listbase.h"
#include "BLI_memory_cache_file_load.hh"
#include "BLI_string.h"
#include "DNA_mesh_types.h"
#include "BKE_report.hh"
#include "IO_stl.hh"
@@ -24,6 +28,17 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::Geometry>("Mesh");
}
class LoadStlCache : public memory_cache::CachedValue {
public:
GeometrySet geometry;
Vector<geo_eval_log::NodeWarning> warnings;
void count_memory(MemoryCounter &counter) const override
{
this->geometry.count_memory(counter);
}
};
static void node_geo_exec(GeoNodeExecParams params)
{
#ifdef WITH_IO_STL
@@ -34,33 +49,36 @@ static void node_geo_exec(GeoNodeExecParams params)
return;
}
STLImportParams import_params;
STRNCPY(import_params.filepath, path->c_str());
std::shared_ptr<const LoadStlCache> cached_value = memory_cache::get_loaded<LoadStlCache>(
GenericStringKey{"import_stl_node"}, {StringRefNull(*path)}, [&]() {
STLImportParams import_params;
STRNCPY(import_params.filepath, path->c_str());
import_params.forward_axis = IO_AXIS_NEGATIVE_Z;
import_params.up_axis = IO_AXIS_Y;
import_params.forward_axis = IO_AXIS_NEGATIVE_Z;
import_params.up_axis = IO_AXIS_Y;
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
BLI_SCOPED_DEFER([&]() { BKE_reports_free(&reports); })
import_params.reports = &reports;
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
BLI_SCOPED_DEFER([&]() { BKE_reports_free(&reports); })
import_params.reports = &reports;
Mesh *mesh = STL_import_mesh(&import_params);
Mesh *mesh = STL_import_mesh(&import_params);
LISTBASE_FOREACH (Report *, report, &(import_params.reports)->list) {
NodeWarningType type;
switch (report->type) {
case RPT_ERROR:
type = NodeWarningType::Error;
break;
default:
type = NodeWarningType::Info;
break;
}
params.error_message_add(type, TIP_(report->message));
auto cached_value = std::make_unique<LoadStlCache>();
cached_value->geometry = GeometrySet::from_mesh(mesh);
LISTBASE_FOREACH (Report *, report, &(import_params.reports)->list) {
cached_value->warnings.append_as(*report);
}
return cached_value;
});
for (const geo_eval_log::NodeWarning &warning : cached_value->warnings) {
params.error_message_add(warning.type, warning.message);
}
params.set_output("Mesh", GeometrySet::from_mesh(mesh));
params.set_output("Mesh", cached_value->geometry);
#else
params.error_message_add(NodeWarningType::Error,

View File

@@ -3,6 +3,9 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_fileops.h"
#include "BLI_generic_key_string.hh"
#include "BLI_memory_cache_file_load.hh"
#include "BLI_memory_counter.hh"
#include "BLI_string_utf8.h"
#include "node_geometry_util.hh"
@@ -22,6 +25,17 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_output<decl::String>("String");
}
class LoadTextCache : public memory_cache::CachedValue {
public:
std::string text;
Vector<geo_eval_log::NodeWarning> warnings;
void count_memory(MemoryCounter &counter) const override
{
counter.add(this->text.size());
}
};
static void node_geo_exec(GeoNodeExecParams params)
{
const std::optional<std::string> path = params.ensure_absolute_path(
@@ -31,23 +45,33 @@ static void node_geo_exec(GeoNodeExecParams params)
return;
}
size_t buffer_len;
void *buffer = BLI_file_read_text_as_mem(path->c_str(), 0, &buffer_len);
if (!buffer) {
const std::string message = fmt::format(fmt::runtime(TIP_("Cannot open file: {}")), *path);
params.error_message_add(NodeWarningType::Error, message);
params.set_default_remaining_outputs();
return;
}
BLI_SCOPED_DEFER([&]() { MEM_freeN(buffer); });
if (BLI_str_utf8_invalid_byte(static_cast<const char *>(buffer), buffer_len) != -1) {
params.error_message_add(NodeWarningType::Error,
TIP_("File contains invalid UTF-8 characters"));
params.set_default_remaining_outputs();
return;
std::shared_ptr<const LoadTextCache> cached_value = memory_cache::get_loaded<LoadTextCache>(
GenericStringKey{"import_text_node"}, {StringRefNull(*path)}, [&]() {
auto cached_value = std::make_unique<LoadTextCache>();
size_t buffer_len;
void *buffer = BLI_file_read_text_as_mem(path->c_str(), 0, &buffer_len);
if (!buffer) {
const std::string message = fmt::format(fmt::runtime(TIP_("Cannot open file: {}")),
*path);
cached_value->warnings.append({NodeWarningType::Error, message});
return cached_value;
}
BLI_SCOPED_DEFER([&]() { MEM_freeN(buffer); });
if (BLI_str_utf8_invalid_byte(static_cast<const char *>(buffer), buffer_len) != -1) {
cached_value->warnings.append(
{NodeWarningType::Error, TIP_("File contains invalid UTF-8 characters")});
return cached_value;
}
cached_value->text = std::string(static_cast<char *>(buffer), buffer_len);
return cached_value;
});
for (const geo_eval_log::NodeWarning &warning : cached_value->warnings) {
params.error_message_add(warning.type, warning.message);
}
params.set_output("String", std::string(static_cast<char *>(buffer), buffer_len));
params.set_output("String", cached_value->text);
}
static void node_register()

View File

@@ -2,6 +2,7 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_windowmanager_types.h"
#include "NOD_geometry_nodes_bundle.hh"
#include "NOD_geometry_nodes_closure.hh"
#include "NOD_geometry_nodes_log.hh"
@@ -246,6 +247,19 @@ ClosureValueLog::ClosureValueLog(Vector<Item> inputs,
}
}
NodeWarning::NodeWarning(const Report &report)
{
switch (report.type) {
case RPT_ERROR:
this->type = NodeWarningType::Error;
break;
default:
this->type = NodeWarningType::Info;
break;
}
this->message = report.message;
}
/* Avoid generating these in every translation unit. */
GeoModifierLog::GeoModifierLog() = default;
GeoModifierLog::~GeoModifierLog() = default;