The simulation state used by simulation nodes is owned by the modifier. Since a geometry nodes setup can contain an arbitrary number of simulations, the modifier has a mapping from `SimulationZoneID` to `SimulationZoneState`. This patch changes what is used as `SimulationZoneID`. Previously, the `SimulationZoneID` contained a list of `bNode::identifier` that described the path from the root node tree to the simulation output node. This works ok in many cases, but also has a significant problem: The `SimulationZoneID` changes when moving the simulation zone into or out of a node group. This implies that any of these operations loses the mapping from zone to simulation state, invalidating the cache or even baked data. The goal of this patch is to introduce a single-integer ID that identifies a (nested) simulation zone and is stable even when grouping and un-grouping. The ID should be stable even if the node group containing the (nested) simulation zone is in a separate linked .blend file and that linked file is changed. In the future, the same kind of ID can be used to store e.g. checkpoint/baked/frozen data in the modifier. To achieve the described goal, node trees can now store an arbitrary number of nested node references (an array of `bNestedNodeRef`). Each nested node reference has an ID that is unique within the current node tree. The node tree does not store the entire path to the nested node. Instead it only know which group node the nested node is in, and what the nested node ID of the node is within that group. Grouping and un-grouping operations have to update the nested node references to keep the IDs stable. Importantly though, these operations only have to care about the two node groups that are affected. IDs in higher level node groups remain unchanged by design. A consequence of this design is that every `bNodeTree` now has a `bNestedNodeRef` for every (nested) simulation zone. Two instances of the same simulation zone (because a node group is reused) are referenced by two separate `bNestedNodeRef`. This is important to keep in mind, because it also means that this solution doesn't scale well if we wanted to use it to keep stable references to *all* nested nodes. I can't think of a solution that fulfills the described requirements but scales better with more nodes. For that reason, this solution should only be used when we want to store data for each referenced nested node at the top level (like we do for simulations). This is not a replacement for `ViewerPath` which can store a path to data in a node tree without changing the node tree. Also `ViewerPath` can contain information like the loop iteration that should be viewed (#109164). `bNestedNodeRef` can't differentiate between different iterations of a loop. This also means that simulations can't be used inside of a loop (loops inside of a simulation work fine though). When baking, the new stable ID is now written to disk, which means that baked data is not invalidated by grouping/un-grouping operations. Backward compatibility for baked data is provided, but only works as long as the simulation zone has not been moved to a different node group yet. Forward compatibility for the baked data is not provided (so older versions can't load the data baked with a newer version of Blender). Pull Request: https://projects.blender.org/blender/blender/pulls/109444
205 lines
5.1 KiB
C++
205 lines
5.1 KiB
C++
/* SPDX-FileCopyrightText: 2023 Blender Foundation
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
#pragma once
|
|
|
|
#include "BKE_geometry_set.hh"
|
|
|
|
#include "BLI_map.hh"
|
|
#include "BLI_sub_frame.hh"
|
|
|
|
struct bNodeTree;
|
|
|
|
namespace blender::bke::sim {
|
|
|
|
class BDataSharing;
|
|
class ModifierSimulationCache;
|
|
|
|
class SimulationStateItem {
|
|
public:
|
|
virtual ~SimulationStateItem() = default;
|
|
};
|
|
|
|
class GeometrySimulationStateItem : public SimulationStateItem {
|
|
public:
|
|
GeometrySimulationStateItem(GeometrySet geometry);
|
|
GeometrySet geometry;
|
|
};
|
|
|
|
/**
|
|
* References a field input/output that becomes an attribute as part of the simulation state.
|
|
* The attribute is actually stored in a #GeometrySimulationStateItem, so this just references
|
|
* the attribute's name.
|
|
*/
|
|
class AttributeSimulationStateItem : public SimulationStateItem {
|
|
private:
|
|
std::string name_;
|
|
|
|
public:
|
|
AttributeSimulationStateItem(std::string name) : name_(std::move(name)) {}
|
|
|
|
StringRefNull name() const
|
|
{
|
|
return name_;
|
|
}
|
|
};
|
|
|
|
/** Storage for a single value of a trivial type like `float`, `int`, etc. */
|
|
class PrimitiveSimulationStateItem : public SimulationStateItem {
|
|
private:
|
|
const CPPType &type_;
|
|
void *value_;
|
|
|
|
public:
|
|
PrimitiveSimulationStateItem(const CPPType &type, const void *value);
|
|
~PrimitiveSimulationStateItem();
|
|
|
|
const void *value() const
|
|
{
|
|
return value_;
|
|
}
|
|
|
|
const CPPType &type() const
|
|
{
|
|
return type_;
|
|
}
|
|
};
|
|
|
|
class StringSimulationStateItem : public SimulationStateItem {
|
|
private:
|
|
std::string value_;
|
|
|
|
public:
|
|
StringSimulationStateItem(std::string value);
|
|
|
|
StringRefNull value() const
|
|
{
|
|
return value_;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Storage of values for a single simulation input and output node pair.
|
|
* Used as a cache to allow random access in time, and as an intermediate form before data is
|
|
* baked.
|
|
*/
|
|
class SimulationZoneState {
|
|
public:
|
|
Map<int, std::unique_ptr<SimulationStateItem>> item_by_identifier;
|
|
};
|
|
|
|
/** Identifies a simulation zone (input and output node pair) used by a modifier. */
|
|
struct SimulationZoneID {
|
|
/** ID of the #bNestedNodeRef that references the output node of the zone. */
|
|
int32_t nested_node_id;
|
|
|
|
uint64_t hash() const
|
|
{
|
|
return this->nested_node_id;
|
|
}
|
|
|
|
friend bool operator==(const SimulationZoneID &a, const SimulationZoneID &b)
|
|
{
|
|
return a.nested_node_id == b.nested_node_id;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Stores a single frame of simulation states for all simulation zones in a modifier's node
|
|
* hierarchy.
|
|
*/
|
|
class ModifierSimulationState {
|
|
private:
|
|
mutable bool bake_loaded_;
|
|
|
|
public:
|
|
ModifierSimulationCache *owner_;
|
|
mutable std::mutex mutex_;
|
|
Map<SimulationZoneID, std::unique_ptr<SimulationZoneState>> zone_states_;
|
|
/** File path to folder containing baked meta-data. */
|
|
std::optional<std::string> meta_path_;
|
|
/** File path to folder containing baked data. */
|
|
std::optional<std::string> bdata_dir_;
|
|
|
|
const SimulationZoneState *get_zone_state(const SimulationZoneID &zone_id) const;
|
|
SimulationZoneState &get_zone_state_for_write(const SimulationZoneID &zone_id);
|
|
void ensure_bake_loaded(const bNodeTree &ntree) const;
|
|
};
|
|
|
|
struct ModifierSimulationStateAtFrame {
|
|
SubFrame frame;
|
|
ModifierSimulationState state;
|
|
};
|
|
|
|
enum class CacheState {
|
|
/** The cache is up-to-date with the inputs. */
|
|
Valid,
|
|
/**
|
|
* Nodes or input values have changed since the cache was created, i.e. the output would be
|
|
* different if the simulation was run again.
|
|
*/
|
|
Invalid,
|
|
/** The cache has been baked and will not be invalidated by changing inputs. */
|
|
Baked,
|
|
};
|
|
|
|
struct StatesAroundFrame {
|
|
const ModifierSimulationStateAtFrame *prev = nullptr;
|
|
const ModifierSimulationStateAtFrame *current = nullptr;
|
|
const ModifierSimulationStateAtFrame *next = nullptr;
|
|
};
|
|
|
|
class ModifierSimulationCache {
|
|
private:
|
|
mutable std::mutex states_at_frames_mutex_;
|
|
/**
|
|
* All simulation states, sorted by frame.
|
|
*/
|
|
Vector<std::unique_ptr<ModifierSimulationStateAtFrame>> states_at_frames_;
|
|
/**
|
|
* Used for baking to deduplicate arrays when writing and writing from storage. Sharing info
|
|
* must be kept alive for multiple frames to detect if each data array's version has changed.
|
|
*/
|
|
std::unique_ptr<BDataSharing> bdata_sharing_;
|
|
|
|
friend ModifierSimulationState;
|
|
|
|
public:
|
|
CacheState cache_state_ = CacheState::Valid;
|
|
bool failed_finding_bake_ = false;
|
|
|
|
float last_fps_ = 0.0f;
|
|
|
|
void try_discover_bake(StringRefNull absolute_bake_dir);
|
|
|
|
bool has_state_at_frame(const SubFrame &frame) const;
|
|
bool has_states() const;
|
|
const ModifierSimulationState *get_state_at_exact_frame(const SubFrame &frame) const;
|
|
ModifierSimulationState &get_state_at_frame_for_write(const SubFrame &frame);
|
|
StatesAroundFrame get_states_around_frame(const SubFrame &frame) const;
|
|
|
|
void invalidate()
|
|
{
|
|
cache_state_ = CacheState::Invalid;
|
|
}
|
|
|
|
CacheState cache_state() const
|
|
{
|
|
return cache_state_;
|
|
}
|
|
|
|
void reset();
|
|
void clear_prev_states();
|
|
};
|
|
|
|
/**
|
|
* Wrap simulation cache in `std::shared_ptr` so that it can be owned by evaluated modifier even if
|
|
* the original modifier has been deleted.
|
|
*/
|
|
struct ModifierSimulationCachePtr {
|
|
std::shared_ptr<ModifierSimulationCache> ptr;
|
|
};
|
|
|
|
} // namespace blender::bke::sim
|