BLI: new basic CacheMutex
This patch introduces a new `CacheMutex` which makes it easy to implement lazily computed caches in e.g. `Curves`. For more details see `BLI_cache_mutex.hh`. Differential Revision: https://developer.blender.org/D16419
This commit is contained in:
parent
1d71f82033
commit
c73ae711bf
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include <mutex>
|
||||
|
||||
#include "BLI_cache_mutex.hh"
|
||||
#include "BLI_float3x3.hh"
|
||||
#include "BLI_float4x4.hh"
|
||||
#include "BLI_generic_virtual_array.hh"
|
||||
|
@ -80,17 +81,14 @@ class CurvesGeometryRuntime {
|
|||
*/
|
||||
mutable Vector<int> evaluated_offsets_cache;
|
||||
mutable Vector<int> bezier_evaluated_offsets;
|
||||
mutable std::mutex offsets_cache_mutex;
|
||||
mutable bool offsets_cache_dirty = true;
|
||||
mutable CacheMutex offsets_cache_mutex;
|
||||
|
||||
mutable Vector<curves::nurbs::BasisCache> nurbs_basis_cache;
|
||||
mutable std::mutex nurbs_basis_cache_mutex;
|
||||
mutable bool nurbs_basis_cache_dirty = true;
|
||||
mutable CacheMutex nurbs_basis_cache_mutex;
|
||||
|
||||
/** Cache of evaluated positions. */
|
||||
mutable Vector<float3> evaluated_position_cache;
|
||||
mutable std::mutex position_cache_mutex;
|
||||
mutable bool position_cache_dirty = true;
|
||||
mutable CacheMutex position_cache_mutex;
|
||||
/**
|
||||
* The evaluated positions result, using a separate span in case all curves are poly curves,
|
||||
* in which case a separate array of evaluated positions is unnecessary.
|
||||
|
@ -103,18 +101,15 @@ class CurvesGeometryRuntime {
|
|||
* make slicing this array for a curve fast, an extra float is stored for every curve.
|
||||
*/
|
||||
mutable Vector<float> evaluated_length_cache;
|
||||
mutable std::mutex length_cache_mutex;
|
||||
mutable bool length_cache_dirty = true;
|
||||
mutable CacheMutex length_cache_mutex;
|
||||
|
||||
/** Direction of the curve at each evaluated point. */
|
||||
mutable Vector<float3> evaluated_tangent_cache;
|
||||
mutable std::mutex tangent_cache_mutex;
|
||||
mutable bool tangent_cache_dirty = true;
|
||||
mutable CacheMutex tangent_cache_mutex;
|
||||
|
||||
/** Normal direction vectors for each evaluated point. */
|
||||
mutable Vector<float3> evaluated_normal_cache;
|
||||
mutable std::mutex normal_cache_mutex;
|
||||
mutable bool normal_cache_dirty = true;
|
||||
mutable CacheMutex normal_cache_mutex;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -909,13 +904,13 @@ inline int CurvesGeometry::evaluated_points_num() const
|
|||
|
||||
inline IndexRange CurvesGeometry::evaluated_points_for_curve(int index) const
|
||||
{
|
||||
BLI_assert(!this->runtime->offsets_cache_dirty);
|
||||
BLI_assert(this->runtime->offsets_cache_mutex.is_cached());
|
||||
return offsets_to_range(this->runtime->evaluated_offsets_cache.as_span(), index);
|
||||
}
|
||||
|
||||
inline IndexRange CurvesGeometry::evaluated_points_for_curves(const IndexRange curves) const
|
||||
{
|
||||
BLI_assert(!this->runtime->offsets_cache_dirty);
|
||||
BLI_assert(this->runtime->offsets_cache_mutex.is_cached());
|
||||
BLI_assert(this->curve_num > 0);
|
||||
const int offset = this->runtime->evaluated_offsets_cache[curves.start()];
|
||||
const int offset_next = this->runtime->evaluated_offsets_cache[curves.one_after_last()];
|
||||
|
@ -940,7 +935,7 @@ inline IndexRange CurvesGeometry::lengths_range_for_curve(const int curve_index,
|
|||
inline Span<float> CurvesGeometry::evaluated_lengths_for_curve(const int curve_index,
|
||||
const bool cyclic) const
|
||||
{
|
||||
BLI_assert(!this->runtime->length_cache_dirty);
|
||||
BLI_assert(this->runtime->length_cache_mutex.is_cached());
|
||||
const IndexRange range = this->lengths_range_for_curve(curve_index, cyclic);
|
||||
return this->runtime->evaluated_length_cache.as_span().slice(range);
|
||||
}
|
||||
|
|
|
@ -511,17 +511,7 @@ static void calculate_evaluated_offsets(const CurvesGeometry &curves,
|
|||
|
||||
void CurvesGeometry::ensure_evaluated_offsets() const
|
||||
{
|
||||
if (!this->runtime->offsets_cache_dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* A double checked lock. */
|
||||
std::scoped_lock lock{this->runtime->offsets_cache_mutex};
|
||||
if (!this->runtime->offsets_cache_dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
threading::isolate_task([&]() {
|
||||
this->runtime->offsets_cache_mutex.ensure([&]() {
|
||||
this->runtime->evaluated_offsets_cache.resize(this->curves_num() + 1);
|
||||
|
||||
if (this->has_curve_with_type(CURVE_TYPE_BEZIER)) {
|
||||
|
@ -534,8 +524,6 @@ void CurvesGeometry::ensure_evaluated_offsets() const
|
|||
calculate_evaluated_offsets(
|
||||
*this, this->runtime->evaluated_offsets_cache, this->runtime->bezier_evaluated_offsets);
|
||||
});
|
||||
|
||||
this->runtime->offsets_cache_dirty = false;
|
||||
}
|
||||
|
||||
Span<int> CurvesGeometry::evaluated_offsets() const
|
||||
|
@ -569,17 +557,7 @@ Array<int> CurvesGeometry::point_to_curve_map() const
|
|||
|
||||
void CurvesGeometry::ensure_nurbs_basis_cache() const
|
||||
{
|
||||
if (!this->runtime->nurbs_basis_cache_dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* A double checked lock. */
|
||||
std::scoped_lock lock{this->runtime->nurbs_basis_cache_mutex};
|
||||
if (!this->runtime->nurbs_basis_cache_dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
threading::isolate_task([&]() {
|
||||
this->runtime->nurbs_basis_cache_mutex.ensure([&]() {
|
||||
Vector<int64_t> nurbs_indices;
|
||||
const IndexMask nurbs_mask = this->indices_for_curve_type(CURVE_TYPE_NURBS, nurbs_indices);
|
||||
if (nurbs_mask.is_empty()) {
|
||||
|
@ -619,23 +597,11 @@ void CurvesGeometry::ensure_nurbs_basis_cache() const
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
this->runtime->nurbs_basis_cache_dirty = false;
|
||||
}
|
||||
|
||||
Span<float3> CurvesGeometry::evaluated_positions() const
|
||||
{
|
||||
if (!this->runtime->position_cache_dirty) {
|
||||
return this->runtime->evaluated_positions_span;
|
||||
}
|
||||
|
||||
/* A double checked lock. */
|
||||
std::scoped_lock lock{this->runtime->position_cache_mutex};
|
||||
if (!this->runtime->position_cache_dirty) {
|
||||
return this->runtime->evaluated_positions_span;
|
||||
}
|
||||
|
||||
threading::isolate_task([&]() {
|
||||
this->runtime->position_cache_mutex.ensure([&]() {
|
||||
if (this->is_single_type(CURVE_TYPE_POLY)) {
|
||||
this->runtime->evaluated_positions_span = this->positions();
|
||||
this->runtime->evaluated_position_cache.clear_and_make_inline();
|
||||
|
@ -699,24 +665,12 @@ Span<float3> CurvesGeometry::evaluated_positions() const
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
this->runtime->position_cache_dirty = false;
|
||||
return this->runtime->evaluated_positions_span;
|
||||
}
|
||||
|
||||
Span<float3> CurvesGeometry::evaluated_tangents() const
|
||||
{
|
||||
if (!this->runtime->tangent_cache_dirty) {
|
||||
return this->runtime->evaluated_tangent_cache;
|
||||
}
|
||||
|
||||
/* A double checked lock. */
|
||||
std::scoped_lock lock{this->runtime->tangent_cache_mutex};
|
||||
if (!this->runtime->tangent_cache_dirty) {
|
||||
return this->runtime->evaluated_tangent_cache;
|
||||
}
|
||||
|
||||
threading::isolate_task([&]() {
|
||||
this->runtime->tangent_cache_mutex.ensure([&]() {
|
||||
const Span<float3> evaluated_positions = this->evaluated_positions();
|
||||
const VArray<bool> cyclic = this->cyclic();
|
||||
|
||||
|
@ -732,9 +686,9 @@ Span<float3> CurvesGeometry::evaluated_tangents() const
|
|||
}
|
||||
});
|
||||
|
||||
/* Correct the first and last tangents of non-cyclic Bezier curves so that they align with the
|
||||
* inner handles. This is a separate loop to avoid the cost when Bezier type curves are not
|
||||
* used. */
|
||||
/* Correct the first and last tangents of non-cyclic Bezier curves so that they align with
|
||||
* the inner handles. This is a separate loop to avoid the cost when Bezier type curves are
|
||||
* not used. */
|
||||
Vector<int64_t> bezier_indices;
|
||||
const IndexMask bezier_mask = this->indices_for_curve_type(CURVE_TYPE_BEZIER, bezier_indices);
|
||||
if (!bezier_mask.is_empty()) {
|
||||
|
@ -765,8 +719,6 @@ Span<float3> CurvesGeometry::evaluated_tangents() const
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
this->runtime->tangent_cache_dirty = false;
|
||||
return this->runtime->evaluated_tangent_cache;
|
||||
}
|
||||
|
||||
|
@ -781,17 +733,7 @@ static void rotate_directions_around_axes(MutableSpan<float3> directions,
|
|||
|
||||
Span<float3> CurvesGeometry::evaluated_normals() const
|
||||
{
|
||||
if (!this->runtime->normal_cache_dirty) {
|
||||
return this->runtime->evaluated_normal_cache;
|
||||
}
|
||||
|
||||
/* A double checked lock. */
|
||||
std::scoped_lock lock{this->runtime->normal_cache_mutex};
|
||||
if (!this->runtime->normal_cache_dirty) {
|
||||
return this->runtime->evaluated_normal_cache;
|
||||
}
|
||||
|
||||
threading::isolate_task([&]() {
|
||||
this->runtime->normal_cache_mutex.ensure([&]() {
|
||||
const Span<float3> evaluated_tangents = this->evaluated_tangents();
|
||||
const VArray<bool> cyclic = this->cyclic();
|
||||
const VArray<int8_t> normal_mode = this->normal_mode();
|
||||
|
@ -842,8 +784,6 @@ Span<float3> CurvesGeometry::evaluated_normals() const
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
this->runtime->normal_cache_dirty = false;
|
||||
return this->runtime->evaluated_normal_cache;
|
||||
}
|
||||
|
||||
|
@ -851,8 +791,8 @@ void CurvesGeometry::interpolate_to_evaluated(const int curve_index,
|
|||
const GSpan src,
|
||||
GMutableSpan dst) const
|
||||
{
|
||||
BLI_assert(!this->runtime->offsets_cache_dirty);
|
||||
BLI_assert(!this->runtime->nurbs_basis_cache_dirty);
|
||||
BLI_assert(this->runtime->offsets_cache_mutex.is_cached());
|
||||
BLI_assert(this->runtime->nurbs_basis_cache_mutex.is_cached());
|
||||
const IndexRange points = this->points_for_curve(curve_index);
|
||||
BLI_assert(src.size() == points.size());
|
||||
BLI_assert(dst.size() == this->evaluated_points_for_curve(curve_index).size());
|
||||
|
@ -881,8 +821,8 @@ void CurvesGeometry::interpolate_to_evaluated(const int curve_index,
|
|||
|
||||
void CurvesGeometry::interpolate_to_evaluated(const GSpan src, GMutableSpan dst) const
|
||||
{
|
||||
BLI_assert(!this->runtime->offsets_cache_dirty);
|
||||
BLI_assert(!this->runtime->nurbs_basis_cache_dirty);
|
||||
BLI_assert(this->runtime->offsets_cache_mutex.is_cached());
|
||||
BLI_assert(this->runtime->nurbs_basis_cache_mutex.is_cached());
|
||||
const VArray<int8_t> types = this->curve_types();
|
||||
const VArray<int> resolution = this->resolution();
|
||||
const VArray<bool> cyclic = this->cyclic();
|
||||
|
@ -923,17 +863,7 @@ void CurvesGeometry::interpolate_to_evaluated(const GSpan src, GMutableSpan dst)
|
|||
|
||||
void CurvesGeometry::ensure_evaluated_lengths() const
|
||||
{
|
||||
if (!this->runtime->length_cache_dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* A double checked lock. */
|
||||
std::scoped_lock lock{this->runtime->length_cache_mutex};
|
||||
if (!this->runtime->length_cache_dirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
threading::isolate_task([&]() {
|
||||
this->runtime->length_cache_mutex.ensure([&]() {
|
||||
/* Use an extra length value for the final cyclic segment for a consistent size
|
||||
* (see comment on #evaluated_length_cache). */
|
||||
const int total_num = this->evaluated_points_num() + this->curves_num();
|
||||
|
@ -954,8 +884,6 @@ void CurvesGeometry::ensure_evaluated_lengths() const
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
this->runtime->length_cache_dirty = false;
|
||||
}
|
||||
|
||||
void CurvesGeometry::ensure_can_interpolate_to_evaluated() const
|
||||
|
@ -986,23 +914,23 @@ void CurvesGeometry::resize(const int points_num, const int curves_num)
|
|||
|
||||
void CurvesGeometry::tag_positions_changed()
|
||||
{
|
||||
this->runtime->position_cache_dirty = true;
|
||||
this->runtime->tangent_cache_dirty = true;
|
||||
this->runtime->normal_cache_dirty = true;
|
||||
this->runtime->length_cache_dirty = true;
|
||||
this->runtime->position_cache_mutex.tag_dirty();
|
||||
this->runtime->tangent_cache_mutex.tag_dirty();
|
||||
this->runtime->normal_cache_mutex.tag_dirty();
|
||||
this->runtime->length_cache_mutex.tag_dirty();
|
||||
}
|
||||
void CurvesGeometry::tag_topology_changed()
|
||||
{
|
||||
this->runtime->position_cache_dirty = true;
|
||||
this->runtime->tangent_cache_dirty = true;
|
||||
this->runtime->normal_cache_dirty = true;
|
||||
this->runtime->offsets_cache_dirty = true;
|
||||
this->runtime->nurbs_basis_cache_dirty = true;
|
||||
this->runtime->length_cache_dirty = true;
|
||||
this->runtime->position_cache_mutex.tag_dirty();
|
||||
this->runtime->tangent_cache_mutex.tag_dirty();
|
||||
this->runtime->normal_cache_mutex.tag_dirty();
|
||||
this->runtime->offsets_cache_mutex.tag_dirty();
|
||||
this->runtime->nurbs_basis_cache_mutex.tag_dirty();
|
||||
this->runtime->length_cache_mutex.tag_dirty();
|
||||
}
|
||||
void CurvesGeometry::tag_normals_changed()
|
||||
{
|
||||
this->runtime->normal_cache_dirty = true;
|
||||
this->runtime->normal_cache_mutex.tag_dirty();
|
||||
}
|
||||
|
||||
static void translate_positions(MutableSpan<float3> positions, const float3 &translation)
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* A #CacheMutex is used to protect a lazily computed cache from being computed more than once.
|
||||
* Using #CacheMutex instead of a "raw mutex" to protect a cache has some benefits:
|
||||
* - Avoid common pitfalls like forgetting to use task isolation or a double checked lock.
|
||||
* - Cleaner and less redundant code because the same locking patterns don't have to be repeated
|
||||
* everywhere.
|
||||
* - One can benefit from potential future improvements to #CacheMutex of which there are a few
|
||||
* mentioned below.
|
||||
*
|
||||
* The data protected by #CacheMutex is not part of #CacheMutex. Instead, the #CacheMutex and its
|
||||
* protected data should generally be placed next to each other.
|
||||
*
|
||||
* Each #CacheMutex protects exactly one cache, so multiple cache mutexes have to be used when a
|
||||
* class has multiple caches. That is contrary to a "custom" solution using `std::mutex` where one
|
||||
* mutex could protect multiple caches at the cost of higher lock contention.
|
||||
*
|
||||
* To make sure the cache is up to date, call `CacheMutex::ensure` and pass in the function that
|
||||
* computes the cache.
|
||||
*
|
||||
* To tell the #CacheMutex that the cache is invalidated and to be re-evaluated upon next access
|
||||
* use `CacheMutex::tag_dirty`.
|
||||
*
|
||||
* This example shows how one could implement a lazily computed average vertex position in an
|
||||
* imaginary `Mesh` data structure:
|
||||
*
|
||||
* \code{.cpp}
|
||||
* class Mesh {
|
||||
* private:
|
||||
* mutable CacheMutex average_position_cache_mutex_;
|
||||
* mutable float3 average_position_cache_;
|
||||
*
|
||||
* public:
|
||||
* const float3 &average_position() const
|
||||
* {
|
||||
* average_position_cache_mutex_.ensure([&]() {
|
||||
* average_position_cache_ = actually_compute_average_position();
|
||||
* });
|
||||
* return average_position_cache_;
|
||||
* }
|
||||
*
|
||||
* void tag_positions_changed()
|
||||
* {
|
||||
* average_position_cache_mutex_.tag_dirty();
|
||||
* }
|
||||
* };
|
||||
* \endcode
|
||||
*
|
||||
* Possible future improvements:
|
||||
* - Avoid task isolation when we know that the cache computation does not use threading.
|
||||
* - Try to use a smaller mutex. The mutex does not have to be fair for this use case.
|
||||
* - Try to join the cache computation instead of blocking if another thread is computing the cache
|
||||
* already.
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#include "BLI_function_ref.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
class CacheMutex {
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
std::atomic<bool> cache_valid_ = false;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Make sure the cache exists and is up to date. This calls `compute_cache` once to update the
|
||||
* cache (which is stored outside of this class) if it is dirty, otherwise it does nothing.
|
||||
*
|
||||
* This function is thread-safe under the assumption that the same parameters are passed from
|
||||
* every thread.
|
||||
*/
|
||||
void ensure(FunctionRef<void()> compute_cache);
|
||||
|
||||
/**
|
||||
* Reset the cache. The next time #ensure is called, it will recompute that code.
|
||||
*/
|
||||
void tag_dirty()
|
||||
{
|
||||
cache_valid_.store(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the cache currently does not exist or has been invalidated.
|
||||
*/
|
||||
bool is_dirty() const
|
||||
{
|
||||
return !this->is_cached();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the cache exists and is valid.
|
||||
*/
|
||||
bool is_cached() const
|
||||
{
|
||||
return cache_valid_.load(std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace blender
|
|
@ -54,6 +54,7 @@ set(SRC
|
|||
intern/bitmap_draw_2d.c
|
||||
intern/boxpack_2d.c
|
||||
intern/buffer.c
|
||||
intern/cache_mutex.cc
|
||||
intern/compute_context.cc
|
||||
intern/convexhull_2d.c
|
||||
intern/cpp_type.cc
|
||||
|
@ -178,6 +179,7 @@ set(SRC
|
|||
BLI_bounds.hh
|
||||
BLI_boxpack_2d.h
|
||||
BLI_buffer.h
|
||||
BLI_cache_mutex.hh
|
||||
BLI_color.hh
|
||||
BLI_color_mix.hh
|
||||
BLI_compiler_attrs.h
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BLI_cache_mutex.hh"
|
||||
#include "BLI_task.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
void CacheMutex::ensure(const FunctionRef<void()> compute_cache)
|
||||
{
|
||||
if (cache_valid_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock{mutex_};
|
||||
/* Double checked lock. */
|
||||
if (cache_valid_.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
/* Use task isolation because a mutex is locked and the cache computation might use
|
||||
* multi-threading. */
|
||||
threading::isolate_task(compute_cache);
|
||||
|
||||
cache_valid_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
} // namespace blender
|
Loading…
Reference in New Issue