Volumes: improve file cache and unloading
This changes how the lazy-loading and unloading of volume grids works. With that it should also fix #124164. The cache is now moved to a deeper and more global level. This allows reloadable volume grids to be unloaded automatically when a memory limit is reached. The previous system for automatically unloading grids only worked in fairly specific cases and also did not work all that well with caching (parts of) volume sequences. At its core, this patch adds a general cache system in `BLI_memory_cache.hh`. It has a simple interface of the form `get(key, compute_if_not_cached_fn) -> value`. To avoid growing the cache indefinitly, it uses the new `BLI_memory_counter.hh` API to detect when the cache size limit is reached. In this case it can automatically free some cached values. Currently, this uses an LRU system, where the items that have not been used in a while are removed first. Other heuristics can be implemented too, but especially for caches for loading files from disk this works well already. The new memory cache is internally used by `volume_grid_file_cache.cc` for loading individual volume grids and their simplified variants. It could potentially also be used to cache which grids are stored in a file. Additionally, it can potentially also be used as caching layer in more places like loading bakes or in import geometry nodes. It's not clear yet whether this will need an extension to the API which currently is fairly minimal. To allow different systems to use the same memory cache, it has to support arbitrary identifiers for the cached data. Therefore, this patch also introduces `GenericKey`, which is an abstract base class for any kind of key that is comparable, hashable and copyable. The implementation of the cache currently relies on a new `ConcurrentMap` data-structure which is a thin wrapper around `tbb::concurrent_hash_map` with a fallback implementation for when `tbb` is not available. This data structure allows concurrent reads and writes to the cache. Note that adding data to the cache is still serialized because of the memory counting. The size of the cache depends on the `memory_cache_limit` property that's already shown in the user preferences. While it has a generic name, it's currently only used by the VSE which is currently using the `MEM_CacheLimiter` API which has a similar purpose but seems to be less automatic, thread-safe and also has no idea of implicit-sharing. It also seems to be designed in a way where one is expected to create multiple "cache limiters" each of which has its own limit. Longer term, we should probably strive towards unifying these systems, which seems feasible but a bit out of scope right now. While it's not ideal that these cache systems don't use a shared memory limit, it's essentially what we already have for all cache systems in Blender, so it's nothing new. Some tests for lazy-loading had to be removed because this behavior is more implicit now and is not as easily observable from the outside. Pull Request: https://projects.blender.org/blender/blender/pulls/126411
This commit is contained in:
@@ -26,6 +26,20 @@
|
||||
|
||||
namespace blender::bke::volume_grid {
|
||||
|
||||
/**
|
||||
* A grid or tree may be loaded lazily when it's accessed. This is especially useful for grids that
|
||||
* are loaded from disk. Those may even be unloaded temporarily to avoid using too much memory.
|
||||
*/
|
||||
struct LazyLoadedGrid {
|
||||
/**
|
||||
* The newly loaded grid. In some cases, only the tree if this is used. The referenced tree is
|
||||
* expected to be either uniquely owned or implicitly-shared. In the latter case, the
|
||||
* implicit-sharing info for the tree has to be available too.
|
||||
*/
|
||||
std::shared_ptr<openvdb::GridBase> grid;
|
||||
ImplicitSharingPtr<> tree_sharing_info;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main volume grid data structure. It wraps an OpenVDB grid and adds some features on top of it.
|
||||
*
|
||||
@@ -55,9 +69,13 @@ namespace blender::bke::volume_grid {
|
||||
class VolumeGridData : public ImplicitSharingMixin {
|
||||
private:
|
||||
/**
|
||||
* Empty struct that exists so that it can be used as token in #VolumeTreeAccessToken.
|
||||
* Exists so that it can be used as token in #VolumeTreeAccessToken.
|
||||
*/
|
||||
struct AccessToken {};
|
||||
struct AccessToken {
|
||||
const VolumeGridData &grid;
|
||||
|
||||
AccessToken(const VolumeGridData &grid) : grid(grid) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* A mutex that needs to be locked whenever working with the data members below.
|
||||
@@ -94,7 +112,7 @@ class VolumeGridData : public ImplicitSharingMixin {
|
||||
/**
|
||||
* A function that can load the full grid or also just the tree lazily.
|
||||
*/
|
||||
std::function<std::shared_ptr<openvdb::GridBase>()> lazy_load_grid_;
|
||||
std::function<LazyLoadedGrid()> lazy_load_grid_;
|
||||
/**
|
||||
* An error produced while trying to lazily load the grid.
|
||||
*/
|
||||
@@ -134,7 +152,7 @@ class VolumeGridData : public ImplicitSharingMixin {
|
||||
* might come from e.g. #readAllGridMetadata. This allows working with the transform and
|
||||
* meta-data without actually loading the tree.
|
||||
*/
|
||||
explicit VolumeGridData(std::function<std::shared_ptr<openvdb::GridBase>()> lazy_load_grid,
|
||||
explicit VolumeGridData(std::function<LazyLoadedGrid()> lazy_load_grid,
|
||||
std::shared_ptr<openvdb::GridBase> meta_data_and_transform_grid = {});
|
||||
|
||||
~VolumeGridData();
|
||||
@@ -220,16 +238,34 @@ class VolumeGridData : public ImplicitSharingMixin {
|
||||
*/
|
||||
bool is_reloadable() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Unloads the tree data if it's reloadable and no one is using it right now.
|
||||
*/
|
||||
void unload_tree_if_possible() const;
|
||||
|
||||
private:
|
||||
void ensure_grid_loaded() const;
|
||||
void delete_self();
|
||||
};
|
||||
|
||||
/**
|
||||
* Multiple #VolumeDataGrid can implicitly share the same underlying tree with different
|
||||
* meta-data/transforms. Note that this is different from using a `shared_ptr`, because that is
|
||||
* only used to please different APIs and does not enforce that shared data is immutable.
|
||||
*/
|
||||
class OpenvdbTreeSharingInfo : public ImplicitSharingInfo {
|
||||
private:
|
||||
std::shared_ptr<openvdb::tree::TreeBase> tree_;
|
||||
|
||||
public:
|
||||
OpenvdbTreeSharingInfo(std::shared_ptr<openvdb::tree::TreeBase> tree);
|
||||
|
||||
static ImplicitSharingPtr<> make(std::shared_ptr<openvdb::tree::TreeBase> tree);
|
||||
|
||||
void delete_self_with_data() override;
|
||||
void delete_data_only() override;
|
||||
};
|
||||
|
||||
class VolumeTreeAccessToken {
|
||||
private:
|
||||
std::shared_ptr<VolumeGridData::AccessToken> token_;
|
||||
@@ -237,6 +273,18 @@ class VolumeTreeAccessToken {
|
||||
friend VolumeGridData;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Defaulting everything but the destructor is fine because the default behavior works
|
||||
* correctly on the single data member. The destructor has a special implementation because it
|
||||
* may automatically unload the volume tree if it's not used anymore to conserve memory.
|
||||
*/
|
||||
VolumeTreeAccessToken() = default;
|
||||
VolumeTreeAccessToken(const VolumeTreeAccessToken &) = default;
|
||||
VolumeTreeAccessToken(VolumeTreeAccessToken &&) = default;
|
||||
VolumeTreeAccessToken &operator=(const VolumeTreeAccessToken &) = default;
|
||||
VolumeTreeAccessToken &operator=(VolumeTreeAccessToken &&) = default;
|
||||
~VolumeTreeAccessToken();
|
||||
|
||||
/** True if the access token can be used with the given grid. */
|
||||
bool valid_for(const VolumeGridData &grid) const;
|
||||
|
||||
|
||||
@@ -68,11 +68,6 @@ VolumeGridType get_type(const VolumeGridData &grid);
|
||||
*/
|
||||
int get_channels_num(VolumeGridType type);
|
||||
|
||||
/**
|
||||
* Unloads the tree data if no one is using it right now and it could be reloaded later on.
|
||||
*/
|
||||
void unload_tree_if_possible(const VolumeGridData &grid);
|
||||
|
||||
/**
|
||||
* Get the transform of the grid as an affine matrix.
|
||||
*/
|
||||
|
||||
@@ -16,31 +16,9 @@ namespace blender::bke::volume_grid {
|
||||
|
||||
#ifdef WITH_OPENVDB
|
||||
|
||||
/**
|
||||
* Multiple #VolumeDataGrid can implicitly share the same underlying tree with different
|
||||
* meta-data/transforms.
|
||||
*/
|
||||
class OpenvdbTreeSharingInfo : public ImplicitSharingInfo {
|
||||
private:
|
||||
std::shared_ptr<openvdb::tree::TreeBase> tree_;
|
||||
|
||||
public:
|
||||
OpenvdbTreeSharingInfo(std::shared_ptr<openvdb::tree::TreeBase> tree) : tree_(std::move(tree)) {}
|
||||
|
||||
void delete_self_with_data() override
|
||||
{
|
||||
MEM_delete(this);
|
||||
}
|
||||
|
||||
void delete_data_only() override
|
||||
{
|
||||
tree_.reset();
|
||||
}
|
||||
};
|
||||
|
||||
VolumeGridData::VolumeGridData()
|
||||
{
|
||||
tree_access_token_ = std::make_shared<AccessToken>();
|
||||
tree_access_token_ = std::make_shared<AccessToken>(*this);
|
||||
}
|
||||
|
||||
struct CreateGridOp {
|
||||
@@ -67,12 +45,11 @@ VolumeGridData::VolumeGridData(std::shared_ptr<openvdb::GridBase> grid)
|
||||
BLI_assert(grid_.use_count() == 1);
|
||||
BLI_assert(grid_->isTreeUnique());
|
||||
|
||||
tree_sharing_info_ = ImplicitSharingPtr<>(
|
||||
MEM_new<OpenvdbTreeSharingInfo>(__func__, grid_->baseTreePtr()));
|
||||
tree_access_token_ = std::make_shared<AccessToken>();
|
||||
tree_sharing_info_ = OpenvdbTreeSharingInfo::make(grid_->baseTreePtr());
|
||||
tree_access_token_ = std::make_shared<AccessToken>(*this);
|
||||
}
|
||||
|
||||
VolumeGridData::VolumeGridData(std::function<std::shared_ptr<openvdb::GridBase>()> lazy_load_grid,
|
||||
VolumeGridData::VolumeGridData(std::function<LazyLoadedGrid()> lazy_load_grid,
|
||||
std::shared_ptr<openvdb::GridBase> meta_data_and_transform_grid)
|
||||
: grid_(std::move(meta_data_and_transform_grid)), lazy_load_grid_(std::move(lazy_load_grid))
|
||||
{
|
||||
@@ -80,7 +57,7 @@ VolumeGridData::VolumeGridData(std::function<std::shared_ptr<openvdb::GridBase>(
|
||||
transform_loaded_ = true;
|
||||
meta_data_loaded_ = true;
|
||||
}
|
||||
tree_access_token_ = std::make_shared<AccessToken>();
|
||||
tree_access_token_ = std::make_shared<AccessToken>(*this);
|
||||
}
|
||||
|
||||
VolumeGridData::~VolumeGridData() = default;
|
||||
@@ -122,8 +99,7 @@ std::shared_ptr<openvdb::GridBase> VolumeGridData::grid_ptr_for_write(
|
||||
else {
|
||||
auto tree_copy = grid_->baseTree().copy();
|
||||
grid_->setTree(tree_copy);
|
||||
tree_sharing_info_ = ImplicitSharingPtr<>(
|
||||
MEM_new<OpenvdbTreeSharingInfo>(__func__, std::move(tree_copy)));
|
||||
tree_sharing_info_ = OpenvdbTreeSharingInfo::make(std::move(tree_copy));
|
||||
}
|
||||
/* Can't reload the grid anymore if it has been changed. */
|
||||
lazy_load_grid_ = {};
|
||||
@@ -268,7 +244,7 @@ void VolumeGridData::ensure_grid_loaded() const
|
||||
return;
|
||||
}
|
||||
BLI_assert(lazy_load_grid_);
|
||||
std::shared_ptr<openvdb::GridBase> loaded_grid;
|
||||
LazyLoadedGrid loaded_grid;
|
||||
/* Isolate because the a mutex is locked. */
|
||||
threading::isolate_task([&]() {
|
||||
error_message_.clear();
|
||||
@@ -282,39 +258,44 @@ void VolumeGridData::ensure_grid_loaded() const
|
||||
error_message_ = "Unknown error reading VDB file";
|
||||
}
|
||||
});
|
||||
if (!loaded_grid) {
|
||||
if (!loaded_grid.grid) {
|
||||
BLI_assert(!loaded_grid.tree_sharing_info);
|
||||
if (grid_) {
|
||||
const openvdb::Name &grid_type = grid_->type();
|
||||
if (openvdb::GridBase::isRegistered(grid_type)) {
|
||||
/* Create a dummy grid of the expected type. */
|
||||
loaded_grid = openvdb::GridBase::createGrid(grid_type);
|
||||
loaded_grid.grid = openvdb::GridBase::createGrid(grid_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!loaded_grid) {
|
||||
if (!loaded_grid.grid) {
|
||||
/* Create a dummy grid. We can't really know the expected data type here. */
|
||||
loaded_grid = openvdb::FloatGrid::create();
|
||||
loaded_grid.grid = openvdb::FloatGrid::create();
|
||||
}
|
||||
BLI_assert(loaded_grid.grid);
|
||||
BLI_assert(loaded_grid.grid.unique());
|
||||
|
||||
if (!loaded_grid.tree_sharing_info) {
|
||||
BLI_assert(loaded_grid.grid->isTreeUnique());
|
||||
loaded_grid.tree_sharing_info = OpenvdbTreeSharingInfo::make(loaded_grid.grid->baseTreePtr());
|
||||
}
|
||||
BLI_assert(loaded_grid);
|
||||
BLI_assert(loaded_grid.use_count() == 1);
|
||||
BLI_assert(loaded_grid->isTreeUnique());
|
||||
|
||||
if (grid_) {
|
||||
/* Keep the existing grid pointer and just insert the newly loaded data. */
|
||||
BLI_assert(!tree_loaded_);
|
||||
BLI_assert(meta_data_loaded_);
|
||||
grid_->setTree(loaded_grid->baseTreePtr());
|
||||
grid_->setTree(loaded_grid.grid->baseTreePtr());
|
||||
if (!transform_loaded_) {
|
||||
grid_->setTransform(loaded_grid->transformPtr());
|
||||
grid_->setTransform(loaded_grid.grid->transformPtr());
|
||||
}
|
||||
}
|
||||
else {
|
||||
grid_ = std::move(loaded_grid);
|
||||
grid_ = std::move(loaded_grid.grid);
|
||||
}
|
||||
|
||||
BLI_assert(tree_sharing_info_ == nullptr);
|
||||
tree_sharing_info_ = ImplicitSharingPtr<>(
|
||||
MEM_new<OpenvdbTreeSharingInfo>(__func__, grid_->baseTreePtr()));
|
||||
BLI_assert(!tree_sharing_info_);
|
||||
BLI_assert(loaded_grid.tree_sharing_info);
|
||||
tree_sharing_info_ = std::move(loaded_grid.tree_sharing_info);
|
||||
|
||||
tree_loaded_ = true;
|
||||
transform_loaded_ = true;
|
||||
@@ -378,6 +359,37 @@ VolumeGridType get_type(const openvdb::GridBase &grid)
|
||||
return VOLUME_GRID_UNKNOWN;
|
||||
}
|
||||
|
||||
ImplicitSharingPtr<> OpenvdbTreeSharingInfo::make(std::shared_ptr<openvdb::tree::TreeBase> tree)
|
||||
{
|
||||
return ImplicitSharingPtr<>{MEM_new<OpenvdbTreeSharingInfo>(__func__, std::move(tree))};
|
||||
}
|
||||
|
||||
OpenvdbTreeSharingInfo::OpenvdbTreeSharingInfo(std::shared_ptr<openvdb::tree::TreeBase> tree)
|
||||
: tree_(std::move(tree))
|
||||
{
|
||||
}
|
||||
|
||||
void OpenvdbTreeSharingInfo::delete_self_with_data()
|
||||
{
|
||||
MEM_delete(this);
|
||||
}
|
||||
|
||||
void OpenvdbTreeSharingInfo::delete_data_only()
|
||||
{
|
||||
tree_.reset();
|
||||
}
|
||||
|
||||
VolumeTreeAccessToken::~VolumeTreeAccessToken()
|
||||
{
|
||||
const VolumeGridData *grid = token_ ? &token_->grid : nullptr;
|
||||
token_.reset();
|
||||
if (grid) {
|
||||
/* Unload immediately when the value is not used anymore. However, the tree may still be cached
|
||||
* at a deeper level and thus usually does not have to be loaded from disk again.*/
|
||||
grid->unload_tree_if_possible();
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* WITH_OPENVDB */
|
||||
|
||||
std::string get_name(const VolumeGridData &volume_grid)
|
||||
@@ -421,15 +433,6 @@ int get_channels_num(const VolumeGridType type)
|
||||
return 0;
|
||||
}
|
||||
|
||||
void unload_tree_if_possible(const VolumeGridData &grid)
|
||||
{
|
||||
#ifdef WITH_OPENVDB
|
||||
grid.unload_tree_if_possible();
|
||||
#else
|
||||
UNUSED_VARS(grid);
|
||||
#endif
|
||||
}
|
||||
|
||||
float4x4 get_transform_matrix(const VolumeGridData &grid)
|
||||
{
|
||||
#ifdef WITH_OPENVDB
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
# include "BKE_volume_openvdb.hh"
|
||||
|
||||
# include "BLI_map.hh"
|
||||
# include "BLI_memory_cache.hh"
|
||||
# include "BLI_memory_counter.hh"
|
||||
|
||||
# include <openvdb/openvdb.h>
|
||||
|
||||
@@ -123,6 +125,54 @@ static FileCache &get_file_cache(const StringRef file_path)
|
||||
[&]() { return create_file_cache(file_path); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies a grid in the global memory cache.
|
||||
*/
|
||||
class GridReadKey : public GenericKey {
|
||||
public:
|
||||
std::string file_path;
|
||||
std::string grid_name;
|
||||
int simplify_level;
|
||||
|
||||
uint64_t hash() const override
|
||||
{
|
||||
return get_default_hash(this->file_path, this->grid_name, this->simplify_level);
|
||||
}
|
||||
|
||||
BLI_STRUCT_EQUALITY_OPERATORS_3(GridReadKey, file_path, grid_name, simplify_level)
|
||||
|
||||
bool equal_to(const GenericKey &other) const override
|
||||
{
|
||||
if (const auto *other_typed = dynamic_cast<const GridReadKey *>(&other)) {
|
||||
return *this == *other_typed;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<GenericKey> to_storable() const override
|
||||
{
|
||||
return std::make_unique<GridReadKey>(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class GridReadValue : public memory_cache::CachedValue {
|
||||
private:
|
||||
mutable std::atomic<int64_t> bytes_ = 0;
|
||||
|
||||
public:
|
||||
ImplicitSharingPtr<> tree_sharing_info;
|
||||
openvdb::GridBase::Ptr grid;
|
||||
|
||||
void count_memory(MemoryCounter &memory) const override
|
||||
{
|
||||
/* Avoid computing the amount of memory from scratch every time. */
|
||||
if (bytes_ == 0) {
|
||||
this->bytes_ = grid->baseTree().memUsage();
|
||||
}
|
||||
memory.add(bytes_);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load a single grid by name from a file. This loads the full grid including meta-data, transforms
|
||||
* and the tree.
|
||||
@@ -140,6 +190,49 @@ static openvdb::GridBase::Ptr load_single_grid_from_disk(const StringRef file_pa
|
||||
return file.readGrid(grid_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a single grid by name from a file. This loads the full grid including meta-data, transforms
|
||||
* and the tree.
|
||||
*/
|
||||
static LazyLoadedGrid load_single_grid_from_disk_cached(const StringRef file_path,
|
||||
const StringRef grid_name,
|
||||
const int simplify_level)
|
||||
{
|
||||
GridReadKey key;
|
||||
key.file_path = file_path;
|
||||
key.grid_name = grid_name;
|
||||
key.simplify_level = simplify_level;
|
||||
|
||||
std::shared_ptr<const GridReadValue> value = memory_cache::get<GridReadValue>(
|
||||
std::move(key), [&key]() {
|
||||
openvdb::GridBase::Ptr grid;
|
||||
if (key.simplify_level == 0) {
|
||||
grid = load_single_grid_from_disk(key.file_path, key.grid_name);
|
||||
}
|
||||
else {
|
||||
/* Build the simplified grid from the main grid. */
|
||||
const GVolumeGrid main_grid = get_grid_from_file(key.file_path, key.grid_name, 0);
|
||||
const VolumeGridType grid_type = main_grid->grid_type();
|
||||
const float resolution_factor = 1.0f / (1 << key.simplify_level);
|
||||
VolumeTreeAccessToken tree_token;
|
||||
grid = BKE_volume_grid_create_with_changed_resolution(
|
||||
grid_type, main_grid->grid(tree_token), resolution_factor);
|
||||
}
|
||||
auto value = std::make_unique<GridReadValue>();
|
||||
value->grid = std::move(grid);
|
||||
value->tree_sharing_info = OpenvdbTreeSharingInfo::make(value->grid->baseTreePtr());
|
||||
return value;
|
||||
});
|
||||
if (!value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/* Copy the grid so that it has a single owner. Note that the tree is still shared. */
|
||||
openvdb::GridBase::Ptr grid = value->grid->copyGrid();
|
||||
grid->setTransform(grid->transform().copy());
|
||||
return {grid, value->tree_sharing_info};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is already a cached grid for the parameters and creates it otherwise. This does
|
||||
* not load the tree, because that is done on-demand.
|
||||
@@ -154,17 +247,8 @@ static GVolumeGrid get_cached_grid(const StringRef file_path,
|
||||
/* A callback that actually loads the full grid including the tree when it's accessed. */
|
||||
auto load_grid_fn = [file_path = std::string(file_path),
|
||||
grid_name = std::string(grid_cache.meta_data_grid->getName()),
|
||||
simplify_level]() {
|
||||
if (simplify_level == 0) {
|
||||
return load_single_grid_from_disk(file_path, grid_name);
|
||||
}
|
||||
/* Build the simplified grid from the main grid. */
|
||||
const GVolumeGrid main_grid = get_grid_from_file(file_path, grid_name, 0);
|
||||
const VolumeGridType grid_type = main_grid->grid_type();
|
||||
const float resolution_factor = 1.0f / (1 << simplify_level);
|
||||
VolumeTreeAccessToken tree_token;
|
||||
return BKE_volume_grid_create_with_changed_resolution(
|
||||
grid_type, main_grid->grid(tree_token), resolution_factor);
|
||||
simplify_level]() -> LazyLoadedGrid {
|
||||
return load_single_grid_from_disk_cached(file_path, grid_name, simplify_level);
|
||||
};
|
||||
/* This allows the returned grid to already contain meta-data and transforms, even if the tree is
|
||||
* not loaded yet. */
|
||||
|
||||
@@ -75,58 +75,6 @@ TEST_F(VolumeTest, add_grid_in_two_volumes)
|
||||
BKE_id_free(bmain, volume_b);
|
||||
}
|
||||
|
||||
TEST_F(VolumeTest, lazy_load_grid)
|
||||
{
|
||||
int load_counter = 0;
|
||||
auto load_grid = [&]() {
|
||||
load_counter++;
|
||||
return openvdb::FloatGrid::create(10.0f);
|
||||
};
|
||||
VolumeGrid<float> volume_grid{MEM_new<VolumeGridData>(__func__, load_grid)};
|
||||
EXPECT_EQ(load_counter, 0);
|
||||
EXPECT_FALSE(volume_grid->is_loaded());
|
||||
VolumeTreeAccessToken tree_token;
|
||||
EXPECT_EQ(volume_grid.grid(tree_token).background(), 10.0f);
|
||||
EXPECT_EQ(load_counter, 1);
|
||||
EXPECT_TRUE(volume_grid->is_loaded());
|
||||
EXPECT_TRUE(volume_grid->is_reloadable());
|
||||
EXPECT_EQ(volume_grid.grid(tree_token).background(), 10.0f);
|
||||
EXPECT_EQ(load_counter, 1);
|
||||
volume_grid->unload_tree_if_possible();
|
||||
EXPECT_TRUE(volume_grid->is_loaded());
|
||||
tree_token.reset();
|
||||
volume_grid->unload_tree_if_possible();
|
||||
EXPECT_FALSE(volume_grid->is_loaded());
|
||||
EXPECT_EQ(volume_grid.grid(tree_token).background(), 10.0f);
|
||||
EXPECT_TRUE(volume_grid->is_loaded());
|
||||
EXPECT_EQ(load_counter, 2);
|
||||
volume_grid.grid_for_write(tree_token).getAccessor().setValue({0, 0, 0}, 1.0f);
|
||||
EXPECT_EQ(volume_grid.grid(tree_token).getAccessor().getValue({0, 0, 0}), 1.0f);
|
||||
EXPECT_FALSE(volume_grid->is_reloadable());
|
||||
}
|
||||
|
||||
TEST_F(VolumeTest, lazy_load_tree_only)
|
||||
{
|
||||
bool load_run = false;
|
||||
auto load_grid = [&]() {
|
||||
load_run = true;
|
||||
return openvdb::FloatGrid::create(10.0f);
|
||||
};
|
||||
VolumeGrid<float> volume_grid{
|
||||
MEM_new<VolumeGridData>(__func__, load_grid, openvdb::FloatGrid::create(0.0f))};
|
||||
EXPECT_FALSE(volume_grid->is_loaded());
|
||||
EXPECT_EQ(volume_grid->name(), "");
|
||||
EXPECT_FALSE(load_run);
|
||||
volume_grid.get_for_write().set_name("Test");
|
||||
EXPECT_FALSE(load_run);
|
||||
EXPECT_EQ(volume_grid->name(), "Test");
|
||||
VolumeTreeAccessToken tree_token;
|
||||
volume_grid.grid_for_write(tree_token);
|
||||
EXPECT_TRUE(load_run);
|
||||
EXPECT_EQ(volume_grid->name(), "Test");
|
||||
EXPECT_EQ(volume_grid.grid(tree_token).background(), 10.0f);
|
||||
}
|
||||
|
||||
} // namespace blender::bke::tests
|
||||
|
||||
#endif /* WITH_OPENVDB */
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup bli
|
||||
*/
|
||||
|
||||
#ifdef WITH_TBB
|
||||
/* Quiet top level deprecation message, unrelated to API usage here. */
|
||||
# if defined(WIN32) && !defined(NOMINMAX)
|
||||
/* TBB includes Windows.h which will define min/max macros causing issues
|
||||
* when we try to use std::min and std::max later on. */
|
||||
# define NOMINMAX
|
||||
# define TBB_MIN_MAX_CLEANUP
|
||||
# endif
|
||||
# include <tbb/concurrent_hash_map.h>
|
||||
# ifdef WIN32
|
||||
/* We cannot keep this defined, since other parts of the code deal with this on their own, leading
|
||||
* to multiple define warnings unless we un-define this, however we can only undefine this if we
|
||||
* were the ones that made the definition earlier. */
|
||||
# ifdef TBB_MIN_MAX_CLEANUP
|
||||
# undef NOMINMAX
|
||||
# endif
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "BLI_hash.hh"
|
||||
#include "BLI_hash_tables.hh"
|
||||
#include "BLI_set.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
/**
|
||||
* A #ConcurrentMap allows adding, removing and looking up values from multiple threads
|
||||
* concurrently. It has higher memory and performance overhead than a simple #Map when not used
|
||||
* concurrently though.
|
||||
*
|
||||
* For thread-safety, one always has the use an accessor to retrieve or update values. This makes
|
||||
* sure that only one thread can modify a value at a time. Multiple threads may read from the same
|
||||
* key at the same time though.
|
||||
*
|
||||
* \note: #ConcurrentMap does not support iteration over all values.
|
||||
*
|
||||
* This is a thin wrapper around tbb::concurrent_hash_map that also has a fallback implemention if
|
||||
* TBB is not available. The fallback implementation is not optimized for performance. It mainly
|
||||
* intends to be a simple implementation that can compile whenever the TBB variant can compile.
|
||||
*/
|
||||
template<typename Key,
|
||||
typename Value,
|
||||
typename Hash = DefaultHash<Key>,
|
||||
typename IsEqual = DefaultEquality<Key>>
|
||||
class ConcurrentMap {
|
||||
public:
|
||||
using size_type = int64_t;
|
||||
|
||||
#ifdef WITH_TBB
|
||||
private:
|
||||
struct Hasher {
|
||||
template<typename T> size_t hash(const T &value) const
|
||||
{
|
||||
return Hash{}(value);
|
||||
}
|
||||
|
||||
template<typename T1, typename T2> bool equal(const T1 &a, const T2 &b) const
|
||||
{
|
||||
return IsEqual{}(a, b);
|
||||
}
|
||||
};
|
||||
|
||||
using TBBMap = tbb::concurrent_hash_map<Key, Value, Hasher>;
|
||||
TBBMap map_;
|
||||
|
||||
public:
|
||||
using MutableAccessor = typename TBBMap::accessor;
|
||||
using ConstAccessor = typename TBBMap::const_accessor;
|
||||
|
||||
/**
|
||||
* Try to find the key-value-pair for the given key and get write-access to it. Only one thread
|
||||
* may have write access to it at a time. The looked up value can be accessed through the
|
||||
* accessor.
|
||||
*
|
||||
* \return True if the lookup was successfull.
|
||||
*/
|
||||
bool lookup(MutableAccessor &accessor, const Key &key)
|
||||
{
|
||||
return map_.find(accessor, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as above, but only retrieves read-access which multiple threads can have at the same
|
||||
* time.
|
||||
*/
|
||||
bool lookup(ConstAccessor &accessor, const Key &key)
|
||||
{
|
||||
return map_.find(accessor, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the key to the map if it does not exist yet. The value is default initialized and can be
|
||||
* updated through the accessor.
|
||||
*/
|
||||
bool add(MutableAccessor &accessor, const Key &key)
|
||||
{
|
||||
return map_.insert(accessor, key);
|
||||
}
|
||||
|
||||
bool add(ConstAccessor &accessor, const Key &key)
|
||||
{
|
||||
return map_.insert(accessor, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the key-value-pair that corresponds to this key. This waits until no one else is using
|
||||
* it anymore.
|
||||
*/
|
||||
bool remove(const Key &key)
|
||||
{
|
||||
return map_.erase(key);
|
||||
}
|
||||
|
||||
#else
|
||||
private:
|
||||
/**
|
||||
* In the fallback implementation, we actually use a #Set, because the API expects the key and
|
||||
* value to be stored in a `std::pair`. #Set can support this use case too.
|
||||
*/
|
||||
struct SetKey {
|
||||
std::pair<Key, Value> item;
|
||||
|
||||
SetKey(Key key) : item(std::move(key), Value()) {}
|
||||
|
||||
uint64_t hash() const
|
||||
{
|
||||
return Hash{}(this->item.first);
|
||||
}
|
||||
|
||||
static uint64_t hash_as(const Key &key)
|
||||
{
|
||||
return Hash{}(key);
|
||||
}
|
||||
|
||||
friend bool operator==(const SetKey &a, const SetKey &b)
|
||||
{
|
||||
return IsEqual{}(a.item.first, b.item.first);
|
||||
}
|
||||
|
||||
friend bool operator==(const Key &a, const SetKey &b)
|
||||
{
|
||||
return IsEqual{}(a, b.item.first);
|
||||
}
|
||||
|
||||
friend bool operator==(const SetKey &a, const Key &b)
|
||||
{
|
||||
return IsEqual{}(a.item.first, b);
|
||||
}
|
||||
};
|
||||
|
||||
using UsedSet = Set<SetKey>;
|
||||
|
||||
struct Accessor {
|
||||
std::unique_lock<std::mutex> mutex;
|
||||
std::pair<Key, Value> *data = nullptr;
|
||||
|
||||
std::pair<Key, Value> *operator->()
|
||||
{
|
||||
return this->data;
|
||||
}
|
||||
};
|
||||
|
||||
std::mutex mutex_;
|
||||
UsedSet set_;
|
||||
|
||||
public:
|
||||
using MutableAccessor = Accessor;
|
||||
using ConstAccessor = Accessor;
|
||||
|
||||
bool lookup(Accessor &accessor, const Key &key)
|
||||
{
|
||||
accessor.mutex = std::unique_lock(mutex_);
|
||||
SetKey *stored_key = const_cast<SetKey *>(set_.lookup_key_ptr_as(key));
|
||||
if (!stored_key) {
|
||||
return false;
|
||||
}
|
||||
accessor.data = &stored_key->item;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool add(Accessor &accessor, const Key &key)
|
||||
{
|
||||
accessor.mutex = std::unique_lock(mutex_);
|
||||
const bool newly_added = !set_.contains_as(key);
|
||||
SetKey &stored_key = const_cast<SetKey &>(set_.lookup_key_or_add_as(key));
|
||||
accessor.data = &stored_key.item;
|
||||
return newly_added;
|
||||
}
|
||||
|
||||
bool remove(const Key &key)
|
||||
{
|
||||
std::unique_lock lock(mutex_);
|
||||
return set_.remove_as(key);
|
||||
}
|
||||
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,59 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
/** \file
|
||||
* \ingroup bli
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "BLI_utildefines.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
/**
|
||||
* A #GenericKey allows different kinds of keys to be used in the same data-structure like a #Set
|
||||
* or #Map.
|
||||
*
|
||||
* Typically, the key is stored as `std::reference_wrapper<const GenericKey>` in the
|
||||
* data-structure. That implies that one has to make sure that the key is not destructed while it's
|
||||
* still in use.
|
||||
*/
|
||||
class GenericKey {
|
||||
public:
|
||||
virtual ~GenericKey() = default;
|
||||
|
||||
/** The hash function has to be implemented by the non-abstract subclass. */
|
||||
virtual uint64_t hash() const = 0;
|
||||
|
||||
/**
|
||||
* Check if the other key is equal to this one. Usually that involves a dynamic_cast to check if
|
||||
* it has the same type.
|
||||
*/
|
||||
virtual bool equal_to(const GenericKey &other) const = 0;
|
||||
|
||||
/**
|
||||
* For efficieny, it can be good to not always allocate the key if it's just used for lookup.
|
||||
* This method allows the key to be converted into a heap-allocated version that can be stored
|
||||
* safely if necessary.
|
||||
*/
|
||||
virtual std::unique_ptr<GenericKey> to_storable() const = 0;
|
||||
|
||||
friend bool operator==(const GenericKey &a, const GenericKey &b)
|
||||
{
|
||||
const bool are_equal = a.equal_to(b);
|
||||
/* Ensure that equality check is symmetric. */
|
||||
BLI_assert(are_equal == b.equal_to(a));
|
||||
return are_equal;
|
||||
}
|
||||
|
||||
friend bool operator!=(const GenericKey &a, const GenericKey &b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,69 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_function_ref.hh"
|
||||
#include "BLI_generic_key.hh"
|
||||
#include "BLI_memory_counter_fwd.hh"
|
||||
|
||||
namespace blender::memory_cache {
|
||||
|
||||
/**
|
||||
* A value that is stored in the cache. It may be freed automatically when the cache is full. This
|
||||
* is expected to be subclassed by users of the memory cache.
|
||||
*/
|
||||
class CachedValue {
|
||||
public:
|
||||
virtual ~CachedValue() = default;
|
||||
|
||||
/**
|
||||
* Gather the memory used by this value. This allows the cache system to determine when it is
|
||||
* full.
|
||||
*/
|
||||
virtual void count_memory(MemoryCounter &memory) const = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the value that corresponds to the given key. If it's not cached yet, #compute_fn is
|
||||
* called and its result is cached for the next time.
|
||||
*
|
||||
* If the cache is full, older values may be freed.
|
||||
*/
|
||||
template<typename T>
|
||||
std::shared_ptr<const T> get(const GenericKey &key, FunctionRef<std::unique_ptr<T>()> compute_fn);
|
||||
|
||||
/**
|
||||
* A non-templated version of the main entry point above.
|
||||
*/
|
||||
std::shared_ptr<CachedValue> get_base(const GenericKey &key,
|
||||
FunctionRef<std::unique_ptr<CachedValue>()> compute_fn);
|
||||
|
||||
/**
|
||||
* Set how much memory the cache is allowed to use. This is only an approximation because counting
|
||||
* the memory is not 100% accurate, and for some types the memory usage may even change over time.
|
||||
*/
|
||||
void set_approximate_size_limit(int64_t limit_in_bytes);
|
||||
|
||||
/**
|
||||
* Remove all elements from the cache. Note that this does not guarantee that no elements are in
|
||||
* the cache after the function returned. This is because another thread may have added a new
|
||||
* element right after the clearing.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Inline Functions
|
||||
* \{ */
|
||||
|
||||
template<typename T>
|
||||
inline std::shared_ptr<const T> get(const GenericKey &key,
|
||||
FunctionRef<std::unique_ptr<T>()> compute_fn)
|
||||
{
|
||||
return std::dynamic_pointer_cast<const T>(get_base(key, compute_fn));
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
} // namespace blender::memory_cache
|
||||
@@ -116,6 +116,7 @@ set(SRC
|
||||
intern/math_vec.cc
|
||||
intern/math_vector.c
|
||||
intern/math_vector_inline.c
|
||||
intern/memory_cache.cc
|
||||
intern/memory_counter.cc
|
||||
intern/memory_utils.c
|
||||
intern/mesh_boolean.cc
|
||||
@@ -238,6 +239,7 @@ set(SRC
|
||||
BLI_fnmatch.h
|
||||
BLI_function_ref.hh
|
||||
BLI_generic_array.hh
|
||||
BLI_generic_key.hh
|
||||
BLI_generic_pointer.hh
|
||||
BLI_generic_span.hh
|
||||
BLI_generic_value_map.hh
|
||||
@@ -318,6 +320,7 @@ set(SRC
|
||||
BLI_memarena.h
|
||||
BLI_memblock.h
|
||||
BLI_memiter.h
|
||||
BLI_memory_cache.hh
|
||||
BLI_memory_counter.hh
|
||||
BLI_memory_utils.h
|
||||
BLI_memory_utils.hh
|
||||
@@ -559,6 +562,7 @@ if(WITH_GTESTS)
|
||||
tests/BLI_math_vector_test.cc
|
||||
tests/BLI_math_vector_types_test.cc
|
||||
tests/BLI_memiter_test.cc
|
||||
tests/BLI_memory_cache_test.cc
|
||||
tests/BLI_memory_counter_test.cc
|
||||
tests/BLI_memory_utils_test.cc
|
||||
tests/BLI_mesh_boolean_test.cc
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup bli
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#include "BLI_concurrent_map.hh"
|
||||
#include "BLI_memory_cache.hh"
|
||||
#include "BLI_memory_counter.hh"
|
||||
#include "BLI_task.hh"
|
||||
|
||||
namespace blender::memory_cache {
|
||||
|
||||
struct StoredValue {
|
||||
/**
|
||||
* The corresponding key. It's stored here, because only a reference to it is used as key in the
|
||||
* hash table.
|
||||
*/
|
||||
std::unique_ptr<const GenericKey> key;
|
||||
/** The user-provided value. */
|
||||
std::shared_ptr<CachedValue> value;
|
||||
/** A logical time that indicates when the value was last used. Lower values are older. */
|
||||
int64_t last_use_time = 0;
|
||||
};
|
||||
|
||||
using CacheMap = ConcurrentMap<std::reference_wrapper<const GenericKey>, StoredValue>;
|
||||
|
||||
struct Cache {
|
||||
CacheMap map;
|
||||
|
||||
std::atomic<int64_t> logical_time = 0;
|
||||
std::atomic<int64_t> approximate_limit = 1024 * 1024 * 1024;
|
||||
/**
|
||||
* This is derived from `memory` below, but is atomic for safe access when the global mutex is
|
||||
* not locked.
|
||||
*/
|
||||
std::atomic<int64_t> size_in_bytes = 0;
|
||||
|
||||
std::mutex global_mutex;
|
||||
/** Amount of memory currently used in the cache. */
|
||||
MemoryCount memory;
|
||||
/**
|
||||
* Keys currently cached. This is stored separately from the map, because the map does not allow
|
||||
* thread-safe iteration.
|
||||
*/
|
||||
Vector<const GenericKey *> keys;
|
||||
};
|
||||
|
||||
static Cache &get_cache()
|
||||
{
|
||||
static Cache cache;
|
||||
return cache;
|
||||
}
|
||||
|
||||
static void try_enforce_limit();
|
||||
|
||||
static void set_new_logical_time(const StoredValue &stored_value, const int64_t new_time)
|
||||
{
|
||||
/* Don't want to use `std::atomic` directly in the struct, because that makes it
|
||||
* non-movable. Could also use a non-const accessor, but that may degrade performance more.
|
||||
* It's not necessary for correctness that the time is exactly the right value. */
|
||||
reinterpret_cast<std::atomic<int64_t> *>(const_cast<int64_t *>(&stored_value.last_use_time))
|
||||
->store(new_time, std::memory_order_relaxed);
|
||||
static_assert(sizeof(int64_t) == sizeof(std::atomic<int64_t>));
|
||||
}
|
||||
|
||||
std::shared_ptr<CachedValue> get_base(const GenericKey &key,
|
||||
const FunctionRef<std::unique_ptr<CachedValue>()> compute_fn)
|
||||
{
|
||||
Cache &cache = get_cache();
|
||||
/* "Touch" the cached value so that we know that it is still used. This makes it less likely that
|
||||
* it is removed. */
|
||||
const int64_t new_time = cache.logical_time.fetch_add(1, std::memory_order_relaxed);
|
||||
{
|
||||
/* Fast path when the value is already cached. */
|
||||
CacheMap::ConstAccessor accessor;
|
||||
if (cache.map.lookup(accessor, std::ref(key))) {
|
||||
set_new_logical_time(accessor->second, new_time);
|
||||
return accessor->second.value;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compute value while no locks are held to avoid potential for dead-locks. Not using a lock also
|
||||
* means that the value may be computed more than once, but that's still better than locking all
|
||||
* the time. It may be possible to implement something smarter in the future. */
|
||||
std::shared_ptr<CachedValue> result = compute_fn();
|
||||
/* Result should be valid. Use exception to propagate error if necessary. */
|
||||
BLI_assert(result);
|
||||
|
||||
{
|
||||
CacheMap::MutableAccessor accessor;
|
||||
const bool newly_inserted = cache.map.add(accessor, std::ref(key));
|
||||
if (!newly_inserted) {
|
||||
/* The value is available already. It was computed unnecessarily. Use the value created by
|
||||
* the other thread instead. */
|
||||
return accessor->second.value;
|
||||
}
|
||||
/* We want to store the key in the map, but the reference we got passed in may go out of scope.
|
||||
* So make a storable copy of it that we use in the map. */
|
||||
accessor->second.key = key.to_storable();
|
||||
/* Modifying the key should be fine because the new key is equal to the original key. */
|
||||
const_cast<std::reference_wrapper<const GenericKey> &>(accessor->first) = std::ref(
|
||||
*accessor->second.key);
|
||||
|
||||
/* Store the value. Don't move, because we still want to return the value from the function. */
|
||||
accessor->second.value = result;
|
||||
/* Set initial logical time for the new cached entry. */
|
||||
set_new_logical_time(accessor->second, new_time);
|
||||
|
||||
{
|
||||
/* Update global data of the cache. */
|
||||
std::lock_guard lock{cache.global_mutex};
|
||||
memory_counter::MemoryCounter memory_counter{cache.memory};
|
||||
accessor->second.value->count_memory(memory_counter);
|
||||
cache.keys.append(&accessor->first.get());
|
||||
cache.size_in_bytes = cache.memory.total_bytes;
|
||||
}
|
||||
}
|
||||
/* Potentially free elements from the cache. Note, even if this would free the value we just
|
||||
* added, it would still work correctly, because we already have a shared_ptr to it. */
|
||||
try_enforce_limit();
|
||||
return result;
|
||||
}
|
||||
|
||||
void set_approximate_size_limit(const int64_t limit_in_bytes)
|
||||
{
|
||||
Cache &cache = get_cache();
|
||||
cache.approximate_limit = limit_in_bytes;
|
||||
try_enforce_limit();
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
Cache &cache = get_cache();
|
||||
std::lock_guard lock{cache.global_mutex};
|
||||
|
||||
/* It's not possible to just call a map.clear() method because that is not thread-safe. */
|
||||
for (const GenericKey *key : cache.keys) {
|
||||
const bool success = cache.map.remove(*key);
|
||||
BLI_assert(success);
|
||||
UNUSED_VARS_NDEBUG(success);
|
||||
}
|
||||
cache.keys.clear();
|
||||
cache.size_in_bytes = 0;
|
||||
cache.memory.reset();
|
||||
}
|
||||
|
||||
static void try_enforce_limit()
|
||||
{
|
||||
Cache &cache = get_cache();
|
||||
const int64_t old_size = cache.size_in_bytes.load(std::memory_order_relaxed);
|
||||
const int64_t approximate_limit = cache.approximate_limit.load(std::memory_order_relaxed);
|
||||
if (old_size < approximate_limit) {
|
||||
/* Nothing to do, the current cache size is still within the right limits. */
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock{cache.global_mutex};
|
||||
|
||||
/* Gather all the keys with their latest usage times. */
|
||||
Vector<std::pair<int64_t, const GenericKey *>> keys_with_time;
|
||||
for (const GenericKey *key : cache.keys) {
|
||||
CacheMap::ConstAccessor accessor;
|
||||
if (!cache.map.lookup(accessor, *key)) {
|
||||
continue;
|
||||
}
|
||||
keys_with_time.append({accessor->second.last_use_time, key});
|
||||
}
|
||||
/* Sort the items so that the newest keys come first. */
|
||||
std::sort(keys_with_time.begin(), keys_with_time.end());
|
||||
std::reverse(keys_with_time.begin(), keys_with_time.end());
|
||||
|
||||
/* Count used memory starting at the most recently touched element. Stop at the element when the
|
||||
* amount became larger than the capacity. */
|
||||
cache.memory.reset();
|
||||
MemoryCounter memory_counter{cache.memory};
|
||||
std::optional<int> first_bad_index;
|
||||
for (const int i : keys_with_time.index_range()) {
|
||||
const GenericKey &key = *keys_with_time[i].second;
|
||||
CacheMap::ConstAccessor accessor;
|
||||
if (!cache.map.lookup(accessor, key)) {
|
||||
continue;
|
||||
}
|
||||
accessor->second.value->count_memory(memory_counter);
|
||||
/* Undershoot a little bit. This typically results in more things being freed that have not
|
||||
* been used in a while. The benefit is that we have to do the decision what to free less
|
||||
* often than if we were always just freeing the minimum amount necessary. */
|
||||
if (cache.memory.total_bytes <= approximate_limit * 0.75) {
|
||||
continue;
|
||||
}
|
||||
first_bad_index = i;
|
||||
break;
|
||||
}
|
||||
if (!first_bad_index) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Avoid recounting memory if the last item is not way too large and the overshoot is still ok.
|
||||
* The alternative would be to subtract the last item from the counted memory again, but removing
|
||||
* from #MemoryCount is not implemented yet. */
|
||||
bool need_memory_recount = false;
|
||||
if (cache.memory.total_bytes < approximate_limit * 1.1) {
|
||||
*first_bad_index += 1;
|
||||
if (*first_bad_index == keys_with_time.size()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
need_memory_recount = true;
|
||||
}
|
||||
|
||||
/* Remove elements that don't fit anymore. */
|
||||
for (const int i : keys_with_time.index_range().drop_front(*first_bad_index)) {
|
||||
const GenericKey &key = *keys_with_time[i].second;
|
||||
cache.map.remove(key);
|
||||
}
|
||||
|
||||
/* Update keys vector. */
|
||||
cache.keys.clear();
|
||||
for (const int i : keys_with_time.index_range().take_front(*first_bad_index)) {
|
||||
cache.keys.append(keys_with_time[i].second);
|
||||
}
|
||||
|
||||
if (need_memory_recount) {
|
||||
/* Recount memory another time, because the last count does not accurately represent the actual
|
||||
* value. */
|
||||
cache.memory.reset();
|
||||
MemoryCounter memory_counter{cache.memory};
|
||||
for (const int i : keys_with_time.index_range().take_front(*first_bad_index)) {
|
||||
const GenericKey &key = *keys_with_time[i].second;
|
||||
CacheMap::ConstAccessor accessor;
|
||||
if (!cache.map.lookup(accessor, key)) {
|
||||
continue;
|
||||
}
|
||||
accessor->second.value->count_memory(memory_counter);
|
||||
}
|
||||
}
|
||||
cache.size_in_bytes = cache.memory.total_bytes;
|
||||
}
|
||||
|
||||
} // namespace blender::memory_cache
|
||||
@@ -0,0 +1,83 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 */
|
||||
|
||||
#include "BLI_hash.hh"
|
||||
#include "BLI_memory_cache.hh"
|
||||
#include "BLI_memory_counter.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
#include "BLI_strict_flags.h" /* Keep last. */
|
||||
|
||||
namespace blender::memory_cache::tests {
|
||||
|
||||
class GenericIntKey : public GenericKey {
|
||||
private:
|
||||
int value_;
|
||||
|
||||
public:
|
||||
GenericIntKey(int value) : value_(value) {}
|
||||
|
||||
uint64_t hash() const override
|
||||
{
|
||||
return get_default_hash(value_);
|
||||
}
|
||||
|
||||
bool equal_to(const GenericKey &other) const override
|
||||
{
|
||||
if (const auto *other_typed = dynamic_cast<const GenericIntKey *>(&other)) {
|
||||
return other_typed->value_ == value_;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<GenericKey> to_storable() const override
|
||||
{
|
||||
return std::make_unique<GenericIntKey>(*this);
|
||||
}
|
||||
};
|
||||
|
||||
class CachedInt : public memory_cache::CachedValue {
|
||||
public:
|
||||
int value;
|
||||
|
||||
CachedInt(int initial_value) : value(initial_value) {}
|
||||
|
||||
void count_memory(MemoryCounter &memory) const override
|
||||
{
|
||||
memory.add(sizeof(int));
|
||||
}
|
||||
};
|
||||
|
||||
TEST(memory_cache, Simple)
|
||||
{
|
||||
memory_cache::clear();
|
||||
{
|
||||
bool newly_computed = false;
|
||||
EXPECT_EQ(4, memory_cache::get<CachedInt>(GenericIntKey(0), [&]() {
|
||||
newly_computed = true;
|
||||
return std::make_unique<CachedInt>(4);
|
||||
})->value);
|
||||
EXPECT_TRUE(newly_computed);
|
||||
}
|
||||
{
|
||||
bool newly_computed = false;
|
||||
EXPECT_EQ(4, memory_cache::get<CachedInt>(GenericIntKey(0), [&]() {
|
||||
newly_computed = true;
|
||||
return std::make_unique<CachedInt>(4);
|
||||
})->value);
|
||||
EXPECT_FALSE(newly_computed);
|
||||
}
|
||||
memory_cache::clear();
|
||||
{
|
||||
bool newly_computed = false;
|
||||
EXPECT_EQ(4, memory_cache::get<CachedInt>(GenericIntKey(0), [&]() {
|
||||
newly_computed = true;
|
||||
return std::make_unique<CachedInt>(4);
|
||||
})->value);
|
||||
EXPECT_TRUE(newly_computed);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::memory_cache::tests
|
||||
@@ -301,8 +301,6 @@ static DRWVolumeGrid *volume_grid_cache_get(const Volume *volume,
|
||||
return cache_grid;
|
||||
}
|
||||
|
||||
const bool was_loaded = bke::volume_grid::is_loaded(*grid);
|
||||
|
||||
DenseFloatVolumeGrid dense_grid;
|
||||
if (BKE_volume_grid_dense_floats(volume, grid, &dense_grid)) {
|
||||
cache_grid->texture_to_object = float4x4(dense_grid.texture_to_object);
|
||||
@@ -329,11 +327,6 @@ static DRWVolumeGrid *volume_grid_cache_get(const Volume *volume,
|
||||
}
|
||||
}
|
||||
|
||||
/* Free grid from memory if it wasn't previously loaded. */
|
||||
if (!was_loaded) {
|
||||
bke::volume_grid::unload_tree_if_possible(*grid);
|
||||
}
|
||||
|
||||
return cache_grid;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "BLI_math_base.h"
|
||||
#include "BLI_math_rotation.h"
|
||||
#include "BLI_memory_cache.hh"
|
||||
#include "BLI_string_utf8.h"
|
||||
#include "BLI_string_utf8_symbols.h"
|
||||
#include "BLI_utildefines.h"
|
||||
@@ -894,7 +895,9 @@ static void rna_UserDef_audio_update(Main *bmain, Scene * /*scene*/, PointerRNA
|
||||
|
||||
static void rna_Userdef_memcache_update(Main * /*bmain*/, Scene * /*scene*/, PointerRNA * /*ptr*/)
|
||||
{
|
||||
MEM_CacheLimiter_set_maximum(size_t(U.memcachelimit) * 1024 * 1024);
|
||||
const int64_t new_limit = int64_t(U.memcachelimit) * 1024 * 1024;
|
||||
MEM_CacheLimiter_set_maximum(new_limit);
|
||||
blender::memory_cache::set_approximate_size_limit(new_limit);
|
||||
USERDEF_TAG_DIRTY;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,10 +145,10 @@ static bool rna_VolumeGrid_load(ID * /*id*/, DummyVolumeGridData *dummy_grid)
|
||||
return blender::bke::volume_grid::error_message_from_load(*grid).empty();
|
||||
}
|
||||
|
||||
static void rna_VolumeGrid_unload(ID * /*id*/, DummyVolumeGridData *dummy_grid)
|
||||
static void rna_VolumeGrid_unload(ID * /*id*/, DummyVolumeGridData * /*dummy_grid*/)
|
||||
{
|
||||
auto *grid = reinterpret_cast<const blender::bke::VolumeGridData *>(dummy_grid);
|
||||
blender::bke::volume_grid::unload_tree_if_possible(*grid);
|
||||
/* This is handled transparently. The grid is unloaded automatically if it's not used and the
|
||||
* memory cache is full. */
|
||||
}
|
||||
|
||||
/* Grids Iterator */
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "BLI_filereader.h"
|
||||
#include "BLI_linklist.h"
|
||||
#include "BLI_math_time.h"
|
||||
#include "BLI_memory_cache.hh"
|
||||
#include "BLI_system.h"
|
||||
#include "BLI_threads.h"
|
||||
#include "BLI_time.h"
|
||||
@@ -512,7 +513,10 @@ static void wm_init_userdef(Main *bmain)
|
||||
SET_FLAG_FROM_TEST(G.f, U.flag & USER_INTERNET_ALLOW, G_FLAG_INTERNET_ALLOW);
|
||||
}
|
||||
|
||||
MEM_CacheLimiter_set_maximum(size_t(U.memcachelimit) * 1024 * 1024);
|
||||
const int64_t cache_limit = int64_t(U.memcachelimit) * 1024 * 1024;
|
||||
MEM_CacheLimiter_set_maximum(cache_limit);
|
||||
blender::memory_cache::set_approximate_size_limit(cache_limit);
|
||||
|
||||
BKE_sound_init(bmain);
|
||||
|
||||
/* Update the temporary directory from the preferences or fallback to the system default. */
|
||||
|
||||
Reference in New Issue
Block a user