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
173 lines
5.5 KiB
C++
173 lines
5.5 KiB
C++
/* SPDX-FileCopyrightText: 2023 Blender Foundation
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
#pragma once
|
|
|
|
#include "BKE_simulation_state.hh"
|
|
|
|
#include "BLI_serialize.hh"
|
|
|
|
struct Main;
|
|
struct ModifierData;
|
|
|
|
namespace blender {
|
|
class fstream;
|
|
}
|
|
|
|
namespace blender::bke::sim {
|
|
|
|
using DictionaryValue = io::serialize::DictionaryValue;
|
|
using DictionaryValuePtr = std::shared_ptr<DictionaryValue>;
|
|
|
|
/**
|
|
* Reference to a slice of memory typically stored on disk.
|
|
*/
|
|
struct BDataSlice {
|
|
std::string name;
|
|
IndexRange range;
|
|
|
|
DictionaryValuePtr serialize() const;
|
|
static std::optional<BDataSlice> deserialize(const io::serialize::DictionaryValue &io_slice);
|
|
};
|
|
|
|
/**
|
|
* Abstract base class for loading binary data.
|
|
*/
|
|
class BDataReader {
|
|
public:
|
|
/**
|
|
* Read the data from the given slice into the provided memory buffer.
|
|
* \return True on success, otherwise false.
|
|
*/
|
|
[[nodiscard]] virtual bool read(const BDataSlice &slice, void *r_data) const = 0;
|
|
};
|
|
|
|
/**
|
|
* Abstract base class for writing binary data.
|
|
*/
|
|
class BDataWriter {
|
|
public:
|
|
/**
|
|
* Write the provided binary data.
|
|
* \return Slice where the data has been written to.
|
|
*/
|
|
virtual BDataSlice write(const void *data, int64_t size) = 0;
|
|
};
|
|
|
|
/**
|
|
* Allows for simple data deduplication when writing or reading data by making use of implicit
|
|
* sharing.
|
|
*/
|
|
class BDataSharing {
|
|
private:
|
|
struct StoredByRuntimeValue {
|
|
/**
|
|
* Version of the shared data that was written before. This is needed because the data might
|
|
* be changed later without changing the #ImplicitSharingInfo pointer.
|
|
*/
|
|
int64_t sharing_info_version;
|
|
/**
|
|
* Identifier of the stored data. This includes information for where the data is stored (a
|
|
* #BDataSlice) and optionally information for how it is loaded (e.g. endian information).
|
|
*/
|
|
DictionaryValuePtr io_data;
|
|
};
|
|
|
|
/**
|
|
* Map used to detect when some data has already been written. It keeps a weak reference to
|
|
* #ImplicitSharingInfo, allowing it to check for equality of two arrays just by comparing the
|
|
* sharing info's pointer and version.
|
|
*/
|
|
Map<const ImplicitSharingInfo *, StoredByRuntimeValue> stored_by_runtime_;
|
|
|
|
/**
|
|
* Use a mutex so that #read_shared can be implemented in a thread-safe way.
|
|
*/
|
|
mutable std::mutex mutex_;
|
|
/**
|
|
* Map used to detect when some data has been previously loaded. This keeps strong
|
|
* references to #ImplicitSharingInfo.
|
|
*/
|
|
mutable Map<std::string, ImplicitSharingInfoAndData> runtime_by_stored_;
|
|
|
|
public:
|
|
~BDataSharing();
|
|
|
|
/**
|
|
* Check if the data referenced by `sharing_info` has been written before. If yes, return the
|
|
* identifier for the previously written data. Otherwise, write the data now and store the
|
|
* identifier for later use.
|
|
* \return Identifier that indicates from where the data has been written.
|
|
*/
|
|
[[nodiscard]] DictionaryValuePtr write_shared(const ImplicitSharingInfo *sharing_info,
|
|
FunctionRef<DictionaryValuePtr()> write_fn);
|
|
|
|
/**
|
|
* Check if the data identified by `io_data` has been read before or load it now.
|
|
* \return Shared ownership to the read data, or none if there was an error.
|
|
*/
|
|
[[nodiscard]] std::optional<ImplicitSharingInfoAndData> read_shared(
|
|
const DictionaryValue &io_data,
|
|
FunctionRef<std::optional<ImplicitSharingInfoAndData>()> read_fn) const;
|
|
};
|
|
|
|
/**
|
|
* A specific #BDataReader that reads from disk.
|
|
*/
|
|
class DiskBDataReader : public BDataReader {
|
|
private:
|
|
const std::string bdata_dir_;
|
|
mutable std::mutex mutex_;
|
|
mutable Map<std::string, std::unique_ptr<fstream>> open_input_streams_;
|
|
|
|
public:
|
|
DiskBDataReader(std::string bdata_dir);
|
|
[[nodiscard]] bool read(const BDataSlice &slice, void *r_data) const override;
|
|
};
|
|
|
|
/**
|
|
* A specific #BDataWriter that writes to a file on disk.
|
|
*/
|
|
class DiskBDataWriter : public BDataWriter {
|
|
private:
|
|
/** Name of the file that data is written to. */
|
|
std::string bdata_name_;
|
|
/** File handle. */
|
|
std::ostream &bdata_file_;
|
|
/** Current position in the file. */
|
|
int64_t current_offset_;
|
|
|
|
public:
|
|
DiskBDataWriter(std::string bdata_name, std::ostream &bdata_file, int64_t current_offset);
|
|
|
|
BDataSlice write(const void *data, int64_t size) override;
|
|
};
|
|
|
|
/**
|
|
* Get the directory that contains all baked simulation data for the given modifier.
|
|
*/
|
|
std::string get_default_modifier_bake_directory(const Main &bmain,
|
|
const Object &object,
|
|
const ModifierData &md);
|
|
|
|
/**
|
|
* Encode the simulation state in a #DictionaryValue which also contains references to external
|
|
* binary data that has been written using #bdata_writer.
|
|
*/
|
|
void serialize_modifier_simulation_state(const ModifierSimulationState &state,
|
|
BDataWriter &bdata_writer,
|
|
BDataSharing &bdata_sharing,
|
|
DictionaryValue &r_io_root);
|
|
/**
|
|
* Fill the simulation state by parsing the provided #DictionaryValue which also contains
|
|
* references to external binary data that is read using #bdata_reader.
|
|
*/
|
|
void deserialize_modifier_simulation_state(const bNodeTree &ntree,
|
|
const DictionaryValue &io_root,
|
|
const BDataReader &bdata_reader,
|
|
const BDataSharing &bdata_sharing,
|
|
ModifierSimulationState &r_state);
|
|
|
|
} // namespace blender::bke::sim
|